Compare commits
146 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1547421074 | ||
|
|
f3b7935ab0 | ||
|
|
2fca83c60a | ||
|
|
c663a3724c | ||
|
|
94e651a50e | ||
|
|
40726d5b9a | ||
|
|
d8b35b5501 | ||
|
|
32d32d3f02 | ||
|
|
3914eb8e11 | ||
|
|
ad9963ade2 | ||
|
|
363f9984f6 | ||
|
|
f5c9edd1fd | ||
|
|
c1b2498009 | ||
|
|
1e381e4940 | ||
|
|
e87aff3801 | ||
|
|
0ed3508d7c | ||
|
|
c1b8f5969e | ||
|
|
24ebe343bd | ||
|
|
d0b633fa17 | ||
|
|
9dfe5ca671 | ||
|
|
bdb448eb94 | ||
|
|
2c91da18ea | ||
|
|
1d630e1e3a | ||
|
|
2c0c24cb86 | ||
|
|
a42fb7ce36 | ||
|
|
f48aea843f | ||
|
|
8430031566 | ||
|
|
fa8ebe30bd | ||
|
|
e6e5728d25 | ||
|
|
5f41539026 | ||
|
|
1b99480b82 | ||
|
|
151b07434e | ||
|
|
11a6e4d1c4 | ||
|
|
cbcbd53cc0 | ||
|
|
d26de6be8e | ||
|
|
b51ae0677d | ||
|
|
5cdb7d9969 | ||
|
|
687f47318b | ||
|
|
1071349bbc | ||
|
|
72ce900cd8 | ||
|
|
ecef05061b | ||
|
|
15915a08b9 | ||
|
|
ad46e065c9 | ||
|
|
f5bbcecade | ||
|
|
3f5643931c | ||
|
|
28fe79ade4 | ||
|
|
5858340d04 | ||
|
|
36e4bccc6a | ||
|
|
1d73ac91e5 | ||
|
|
1f3ad73135 | ||
|
|
4bb190b81a | ||
|
|
723815e4c5 | ||
|
|
4b64d99ac4 | ||
|
|
cb877a2a99 | ||
|
|
a60f527f3c | ||
|
|
fc6e5bccfd | ||
|
|
185f4ab6f3 | ||
|
|
0dc4e8cdf7 | ||
|
|
a628abc128 | ||
|
|
01a00fc98d | ||
|
|
4c2c6a3058 | ||
|
|
4f4a84ebce | ||
|
|
791956daac | ||
|
|
3c009cc46e | ||
|
|
e345bb14d3 | ||
|
|
3637ba9963 | ||
|
|
2155a2a055 | ||
|
|
dd91831481 | ||
|
|
61751664c5 | ||
|
|
06c8853399 | ||
|
|
727603ff46 | ||
|
|
de84964ff1 | ||
|
|
03ccabfb5c | ||
|
|
752d1c9b11 | ||
|
|
af64cda7fb | ||
|
|
d697150982 | ||
|
|
4587de15b0 | ||
|
|
ffd84c017e | ||
|
|
84f6592ecf | ||
|
|
676f1a19ba | ||
|
|
18dccc9e17 | ||
|
|
63bfe21eb7 | ||
|
|
d31739d8d8 | ||
|
|
1cf2787f3a | ||
|
|
ec97bbb3be | ||
|
|
cb6a242fe8 | ||
|
|
d9ba90e6ef | ||
|
|
43a4a5b665 | ||
|
|
612483fbea | ||
|
|
ad93bfa6c6 | ||
|
|
2f1702f5a4 | ||
|
|
1876c4a4a6 | ||
|
|
1fa558e6a0 | ||
|
|
a4c079ac30 | ||
|
|
78bc82873b | ||
|
|
98e65fcefe | ||
|
|
8857eaf144 | ||
|
|
da5f354cb7 | ||
|
|
6f8604d6a3 | ||
|
|
b8c7519519 | ||
|
|
4b0f64131c | ||
|
|
f7d4061b86 | ||
|
|
5bdc55a8b0 | ||
|
|
bd7b595d1c | ||
|
|
afed7edd04 | ||
|
|
23e5efc446 | ||
|
|
ed3570166c | ||
|
|
aa053d04c2 | ||
|
|
bccac8377c | ||
|
|
278e75a7df | ||
|
|
a59b3b95e9 | ||
|
|
63994f9caf | ||
|
|
292f5de9e3 | ||
|
|
a695ce6356 | ||
|
|
ea503dbaa2 | ||
|
|
7e8cdfe042 | ||
|
|
554f50e235 | ||
|
|
bbc3b9bcb7 | ||
|
|
9b2a7ab0ba | ||
|
|
8c6932c193 | ||
|
|
f47e4c2a9a | ||
|
|
53f2425f65 | ||
|
|
00db6c11f2 | ||
|
|
a4a0dfbe5f | ||
|
|
c2b3cd5b2d | ||
|
|
20387f26f3 | ||
|
|
0e7283aece | ||
|
|
937144b45f | ||
|
|
fb600f9fe7 | ||
|
|
845e14869b | ||
|
|
0a8058cd00 | ||
|
|
28e024c611 | ||
|
|
ade7693b9d | ||
|
|
9fc8a87ded | ||
|
|
0df137977d | ||
|
|
bea629bf78 | ||
|
|
1712844313 | ||
|
|
81b32a491e | ||
|
|
179679f215 | ||
|
|
95a5b9e80c | ||
|
|
cc2545ebb2 | ||
|
|
3f0c22342a | ||
|
|
199ed9f8b2 | ||
|
|
2ee516b5fd | ||
|
|
92f35554ab | ||
|
|
3bae44e833 |
67 changed files with 2691 additions and 3096 deletions
68
.github/workflows/release.yml
vendored
68
.github/workflows/release.yml
vendored
|
|
@ -5,15 +5,6 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
release_name:
|
|
||||||
description: 'Custom Release Name (e.g., "Jolly", "Christmas")'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
release_tag:
|
|
||||||
description: 'Release Tag (e.g., "v1.0.0")'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
|
@ -38,38 +29,33 @@ jobs:
|
||||||
run: bun install
|
run: bun install
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Determine Release Info
|
- name: Extract version
|
||||||
id: release_info
|
id: extract
|
||||||
run: |
|
run: |
|
||||||
# For workflow_dispatch, use the provided inputs
|
# Extract major.minor.patch version from scripts/build.mjs
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
# This locates the first quoted three-segment version string in the build script (e.g. "1.1.7")
|
||||||
release_name="${{ github.event.inputs.release_name }}"
|
if [ ! -f scripts/build.mjs ]; then
|
||||||
release_tag="${{ github.event.inputs.release_tag }}"
|
echo "scripts/build.mjs not found" >&2
|
||||||
else
|
exit 1
|
||||||
# For push events, extract version and generate a name
|
fi
|
||||||
if [ ! -f scripts/build.mjs ]; then
|
# Print a snippet of the file for debugging in workflow logs
|
||||||
echo "scripts/build.mjs not found" >&2
|
echo '--- scripts/build.mjs (snippet) ---' >&2
|
||||||
exit 1
|
sed -n '1,200p' scripts/build.mjs >&2 || true
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract version
|
# Primary extraction: find the first quoted version (supports major.minor.patch or major.minor.patch.patch formats, may include prerelease suffix)
|
||||||
raw=$(grep -m1 -oE '\"[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?\"' scripts/build.mjs | tr -d '\"' | head -n1 || true)
|
raw=$(grep -m1 -oE '\"[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z.-]+)?\"' scripts/build.mjs | tr -d '\"' | head -n1 || true)
|
||||||
version=$(printf '%s' "$raw" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
|
||||||
|
|
||||||
if ! printf '%s' "$version" | grep -xE '^[0-9]+\.[0-9]+\.[0-9]+$' >/dev/null; then
|
# Strip any prerelease suffix, keep only major.minor.patch or major.minor.patch.patch
|
||||||
echo "Failed to extract a valid base version from scripts/build.mjs" >&2
|
version=$(printf '%s' "$raw" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?).*/\1/')
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use version-based naming for push events
|
# Validate final result is either major.minor.patch or major.minor.patch.patch
|
||||||
release_name="Release v${version}"
|
if ! printf '%s' "$version" | grep -xE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$' >/dev/null; then
|
||||||
release_tag="v${version}"
|
echo "Failed to extract a valid base version from scripts/build.mjs" >&2
|
||||||
|
sed -n '1,200p' scripts/build.mjs >&2 || true
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "release_name=${release_name}" >> $GITHUB_OUTPUT
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
echo "release_tag=${release_tag}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Using release name: ${release_name}" >&2
|
|
||||||
echo "Using release tag: ${release_tag}" >&2
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Prepare tag
|
- name: Prepare tag
|
||||||
|
|
@ -79,7 +65,9 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
tag="${{ steps.release_info.outputs.release_tag }}"
|
# Use the extracted version directly as the tag for main branch releases
|
||||||
|
version="${{ steps.extract.outputs.version }}"
|
||||||
|
tag="${version}"
|
||||||
|
|
||||||
echo "[debug] computed tag=${tag}" >&2
|
echo "[debug] computed tag=${tag}" >&2
|
||||||
|
|
||||||
|
|
@ -107,10 +95,10 @@ jobs:
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.release_info.outputs.release_tag }}
|
tag_name: ${{ steps.prepare.outputs.tag }}
|
||||||
name: ${{ steps.release_info.outputs.release_name }}
|
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 }}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
{
|
{
|
||||||
"languages": {
|
"format_on_save": "off",
|
||||||
"TypeScript": {
|
|
||||||
"format_on_save": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -1,39 +1,41 @@
|
||||||
# ShiggyCord [](https://discord.gg/nQykFF9Ud6)
|
# 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
|
||||||
54
package.json
54
package.json
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "ShiggyCord",
|
"name": "GoonCord",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": {
|
"author": {
|
||||||
"name": "kmmiio99o",
|
"name": "OTKUSteyler",
|
||||||
"url": "https://github.com/kmmiio99o"
|
"url": "https://github.com/OTKUSteyler"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
|
|
@ -13,44 +13,40 @@
|
||||||
},
|
},
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
"@swc/core": "^1.15.47",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@swc/helpers": "^0.5.23",
|
||||||
"@semantic-release/exec": "^7.1.0",
|
"@types/node": "^26.1.2",
|
||||||
"@semantic-release/git": "^10.0.1",
|
|
||||||
"semantic-release": "^25.0.2",
|
|
||||||
"@swc/core": "^1.13.1",
|
|
||||||
"@swc/helpers": "^0.5.17",
|
|
||||||
"@types/node": "^25.0.3",
|
|
||||||
"@types/yargs-parser": "^21.0.3",
|
"@types/yargs-parser": "^21.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||||
"@typescript-eslint/parser": "^8.50.1",
|
"@typescript-eslint/parser": "^8.65.0",
|
||||||
"@typescript-eslint/typescript-estree": "^8.50.1",
|
"@typescript-eslint/typescript-estree": "^8.65.0",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^6.0.0",
|
||||||
"esbuild": "^0.27.2",
|
"esbuild": "^0.28.1",
|
||||||
"esbuild-plugin-globals": "^0.2.0",
|
"esbuild-plugin-globals": "^0.2.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^10.8.0",
|
||||||
"eslint-plugin-import-alias": "^1.2.0",
|
"eslint-plugin-import-alias": "^1.2.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"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.3.0",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^7.0.2",
|
||||||
"yargs-parser": "^22.0.0"
|
"yargs-parser": "^22.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gullerya/object-observer": "^6.1.4",
|
"@gullerya/object-observer": "^7.0.0",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.101.4",
|
||||||
"@types/chroma-js": "~3.1.2",
|
"@types/chroma-js": "~3.1.2",
|
||||||
"@types/lodash": "~4.17.21",
|
"@types/lodash": "~4.17.24",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.17",
|
||||||
"es-toolkit": "^1.43.0",
|
"es-toolkit": "^1.50.0",
|
||||||
"fuzzysort": "^3.1.0",
|
"fuzzysort": "^3.1.0",
|
||||||
"intl-messageformat": "^11.0.5",
|
"intl-messageformat": "^11.2.12",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"react-devtools": "^7.0.1",
|
"react-devtools": "^7.0.1",
|
||||||
"react-native": "0.83.1",
|
"react-native": "0.86.2",
|
||||||
"react-native-reanimated": "^4.2.1",
|
"react-native-reanimated": "^4.5.3",
|
||||||
|
"react-native-webview": "^14.0.1",
|
||||||
"spitroast": "^2.1.6",
|
"spitroast": "^2.1.6",
|
||||||
"type-fest": "^5.3.1"
|
"type-fest": "^5.8.0"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@swc/core",
|
"@swc/core",
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -50,12 +50,14 @@ const config = {
|
||||||
window: "globalThis",
|
window: "globalThis",
|
||||||
__DEV__: JSON.stringify(releaseBranch !== "main"),
|
__DEV__: JSON.stringify(releaseBranch !== "main"),
|
||||||
},
|
},
|
||||||
inject: ["./shims/asyncIteratorSymbol.js", "./shims/promiseAllSettled.js"],
|
inject: ["./shims/asyncIteratorSymbol.js", "./shims/promiseAllSettled.js", "./shims/weakref.js"],
|
||||||
legalComments: "none",
|
legalComments: "none",
|
||||||
alias: {
|
alias: {
|
||||||
"!ShiggyCord-deps-shim!": "./shims/depsModule.ts",
|
"!ShiggyCord-deps-shim!": "./shims/depsModule.ts",
|
||||||
spitroast: "./node_modules/spitroast",
|
spitroast: "./node_modules/spitroast",
|
||||||
"react/jsx-runtime": "./shims/jsxRuntime",
|
"react/jsx-runtime": "./shims/jsxRuntime",
|
||||||
|
"@gullerya/object-observer":
|
||||||
|
path.resolve("./node_modules/@gullerya/object-observer/dist/object-observer.min.js"),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
globalPlugin({
|
globalPlugin({
|
||||||
|
|
@ -75,7 +77,7 @@ const config = {
|
||||||
constModules: {
|
constModules: {
|
||||||
globals: {
|
globals: {
|
||||||
"bunny-build-info": {
|
"bunny-build-info": {
|
||||||
version: `"Jolly 1.4"`,
|
version: `"1.4.1.8"`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { readFile } from "fs/promises";
|
||||||
import http from "http";
|
import http from "http";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import readline from "readline";
|
import readline from "readline";
|
||||||
import url from "url";
|
import { URL } from "url";
|
||||||
import yargs from "yargs-parser";
|
import yargs from "yargs-parser";
|
||||||
|
|
||||||
import { buildBundle } from "./build.mjs";
|
import { buildBundle } from "./build.mjs";
|
||||||
|
|
@ -15,7 +15,7 @@ const args = yargs(process.argv.slice(2));
|
||||||
export function serve(options) {
|
export function serve(options) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
const { pathname } = url.parse(req.url || "", true);
|
const { pathname } = new URL(req.url || "", `http://localhost:${args.port ?? 4040}`);
|
||||||
if (pathname?.endsWith(".js")) {
|
if (pathname?.endsWith(".js")) {
|
||||||
try {
|
try {
|
||||||
let fileToServe;
|
let fileToServe;
|
||||||
|
|
|
||||||
128
shims/weakref.js
Normal file
128
shims/weakref.js
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* WeakRef / FinalizationRegistry shim
|
||||||
|
*
|
||||||
|
* Purpose:
|
||||||
|
* - Provide a safe fallback when the runtime doesn't implement WeakRef or FinalizationRegistry.
|
||||||
|
* - This fallback avoids a ReferenceError and keeps code paths functional, but it does NOT
|
||||||
|
* emulate true weak-reference GC behavior. Use it only as a compatibility fallback.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
let _shimWarned = false;
|
||||||
|
|
||||||
|
function _warnOnce(msg) {
|
||||||
|
if (!_shimWarned) {
|
||||||
|
// keep this quiet by default; print only when fallback is actually used
|
||||||
|
try {
|
||||||
|
// Prefer console.warn when available
|
||||||
|
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
||||||
|
console.warn(`[weakref-shim] ${msg}`);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
_shimWarned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WeakRef fallback
|
||||||
|
* - Keeps a strong reference to the target (so it will not be GC'd).
|
||||||
|
* - Implements the minimal `deref()` API.
|
||||||
|
*/
|
||||||
|
class WeakRefFallback {
|
||||||
|
constructor(value) {
|
||||||
|
// Keep a strong reference: this is *not* weak.
|
||||||
|
this.__value = value;
|
||||||
|
_warnOnce(
|
||||||
|
"WeakRef is not available in this runtime. Using a strong-reference fallback — memory usage/semantics may differ."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deref() {
|
||||||
|
return this.__value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FinalizationRegistry fallback
|
||||||
|
* - No-op registry: register/unregister do nothing.
|
||||||
|
* - Implement `cleanupSome` as a best-effort sync call when a callback is provided,
|
||||||
|
* but it cannot be tied to GC.
|
||||||
|
*/
|
||||||
|
class FinalizationRegistryFallback {
|
||||||
|
constructor(cleanupCallback) {
|
||||||
|
// store callback to allow optional manual invocation via cleanupSome
|
||||||
|
this.__cleanupCallback = typeof cleanupCallback === "function" ? cleanupCallback : null;
|
||||||
|
_warnOnce(
|
||||||
|
"FinalizationRegistry is not available in this runtime. Using a no-op fallback — finalizers will not run based on GC."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* register(target, heldValue, unregisterToken?)
|
||||||
|
* - No-op in fallback. We keep no strong references here on purpose (can't emulate GC).
|
||||||
|
*/
|
||||||
|
register(_target /*, _heldValue, _unregisterToken */) {
|
||||||
|
// intentionally no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unregister(unregisterToken)
|
||||||
|
* - No-op in fallback.
|
||||||
|
*/
|
||||||
|
unregister(_unregisterToken) {
|
||||||
|
// intentionally no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cleanupSome(callback?)
|
||||||
|
* - If provided a callback or a stored cleanup callback exists, call it immediately.
|
||||||
|
* - This is NOT equivalent to real FinalizationRegistry cleanup; it's only a manual helper.
|
||||||
|
*/
|
||||||
|
cleanupSome(callback) {
|
||||||
|
const cb = typeof callback === "function" ? callback : this.__cleanupCallback;
|
||||||
|
if (typeof cb === "function") {
|
||||||
|
try {
|
||||||
|
cb();
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
if (typeof console !== "undefined" && typeof console.error === "function") {
|
||||||
|
console.error("[weakref-shim] error running cleanupSome callback:", e);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If native implementations exist, prefer them; otherwise install fallbacks.
|
||||||
|
// We assign to globalThis so code using `WeakRef`/`FinalizationRegistry` globals doesn't crash.
|
||||||
|
const NativeWeakRef =
|
||||||
|
typeof globalThis !== "undefined" && typeof globalThis.WeakRef === "function" ? globalThis.WeakRef : undefined;
|
||||||
|
|
||||||
|
const NativeFinalizationRegistry =
|
||||||
|
typeof globalThis !== "undefined" && typeof globalThis.FinalizationRegistry === "function"
|
||||||
|
? globalThis.FinalizationRegistry
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const WeakRefImpl = NativeWeakRef ?? WeakRefFallback;
|
||||||
|
const FinalizationRegistryImpl = NativeFinalizationRegistry ?? FinalizationRegistryFallback;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof globalThis !== "undefined") {
|
||||||
|
if (typeof globalThis.WeakRef === "undefined") {
|
||||||
|
// Install fallback
|
||||||
|
globalThis.WeakRef = WeakRefImpl;
|
||||||
|
}
|
||||||
|
if (typeof globalThis.FinalizationRegistry === "undefined") {
|
||||||
|
globalThis.FinalizationRegistry = FinalizationRegistryImpl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Some embedder environments may forbid writing globals; ignore in that case.
|
||||||
|
_warnOnce("Unable to assign WeakRef/FinalizationRegistry to globalThis: " + (e && e.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the implementations so the inject mechanism (or explicit imports) can reference them.
|
||||||
|
// Export names are sorted to satisfy lint rules that require sorted exports.
|
||||||
|
export { FinalizationRegistryImpl as FinalizationRegistry, WeakRefImpl as WeakRef };
|
||||||
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,24 +18,26 @@ export default () =>
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
execute([ephemeral], ctx) {
|
execute([ephemeral], ctx) {
|
||||||
const info = getDebugInfo();
|
const info = getDebugInfo();
|
||||||
const content = [
|
|
||||||
"**ShiggyCord Debug Info**",
|
|
||||||
`> ShiggyCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
|
|
||||||
`> Discord: ${info.discord.version} (${info.discord.build})`,
|
|
||||||
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
|
|
||||||
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
|
|
||||||
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
|
|
||||||
`> Device: ${info.device.model}`,
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
if (ephemeral?.value) {
|
if(info.device.codename === undefined) { info.device.codename = ""}
|
||||||
messageUtil.sendBotMessage(ctx.channel.id, content);
|
else{ info.device.codename = "(" + info.device.codename + ")" }
|
||||||
} else {
|
|
||||||
const fixNonce = Date.now().toString();
|
const content = [
|
||||||
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {
|
"**💦GoonCord💦 Debug Info**",
|
||||||
nonce: fixNonce,
|
`> GoonCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
|
||||||
});
|
`> Discord: ${info.discord.version} (${info.discord.build})`,
|
||||||
}
|
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
|
||||||
},
|
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
|
||||||
};
|
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
|
||||||
|
`> Device: ${info.device.model} ${info.device.codename}`,
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
if (ephemeral?.value) {
|
||||||
|
messageUtil.sendBotMessage(ctx.channel.id, content);
|
||||||
|
} else {
|
||||||
|
const fixNonce = (BigInt(Date.now() - 1420070400000) << 22n).toString();
|
||||||
|
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {nonce:fixNonce});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ export default () =>
|
||||||
if (ephemeral?.value) {
|
if (ephemeral?.value) {
|
||||||
messageUtil.sendBotMessage(ctx.channel.id, content);
|
messageUtil.sendBotMessage(ctx.channel.id, content);
|
||||||
} else {
|
} else {
|
||||||
const fixNonce = Date.now().toString();
|
const fixNonce = (BigInt(Date.now() - 1420070400000) << 22n).toString();
|
||||||
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {
|
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {
|
||||||
nonce: fixNonce,
|
nonce: fixNonce,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { BundleUpdaterManager } from "@lib/api/native/modules";
|
||||||
|
|
||||||
export default () =>
|
export default () =>
|
||||||
<ApplicationCommand>{
|
<ApplicationCommand>{
|
||||||
name: "reload",
|
name: "ReGoon",
|
||||||
description: Strings.COMMAND_RELOAD_DESC,
|
description: Strings.COMMAND_RELOAD_DESC,
|
||||||
options: [],
|
options: [],
|
||||||
execute() {
|
execute() {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@
|
||||||
"AUTO_DEBUGGER": "Automatically connect to Debugger",
|
"AUTO_DEBUGGER": "Automatically connect to Debugger",
|
||||||
"AUTO_DEVTOOLS": "Automatically connect to Devtools",
|
"AUTO_DEVTOOLS": "Automatically connect to Devtools",
|
||||||
"ASSET_BROWSER": "Asset Browser",
|
"ASSET_BROWSER": "Asset Browser",
|
||||||
|
"ASSET_TYPES": "Asset Types",
|
||||||
|
"IMAGE_FILES": "Image Files",
|
||||||
|
"TEXT_FILES": "Text Files",
|
||||||
"BRAND": "Brand",
|
"BRAND": "Brand",
|
||||||
"SHIGGYCORD": "ShiggyCold",
|
"SHIGGYCORD": "GoonCord",
|
||||||
"SHIGGYCORD_URL": "ShiggyCold URL",
|
"SHIGGYCORD_URL": "GoonCord URL",
|
||||||
"BROWSER": "Browser",
|
"BROWSER": "Browser",
|
||||||
"BYTECODE": "Bytecode",
|
"BYTECODE": "Bytecode",
|
||||||
"CANCEL": "Cancel",
|
"CANCEL": "Cancel",
|
||||||
|
|
@ -21,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)",
|
||||||
|
|
@ -67,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",
|
||||||
|
|
@ -91,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",
|
||||||
|
|
@ -120,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,11 +1,12 @@
|
||||||
|
import { logger } from "@lib/utils/logger";
|
||||||
import { FluxDispatcher } from "@metro/common";
|
import { FluxDispatcher } from "@metro/common";
|
||||||
import { findByNameLazy } from "@metro/wrappers";
|
import { findByNameLazy, findByStoreName } from "@metro/wrappers";
|
||||||
import { PrimitiveType } from "intl-messageformat";
|
import { PrimitiveType } from "intl-messageformat";
|
||||||
|
|
||||||
import langDefault from "./default.json";
|
import langDefault from "./default.json";
|
||||||
|
|
||||||
const IntlMessageFormat = findByNameLazy(
|
const IntlMessageFormat = findByNameLazy(
|
||||||
"MessageFormat",
|
"MessageFormat",
|
||||||
) as typeof import("intl-messageformat").default;
|
) as typeof import("intl-messageformat").default;
|
||||||
|
|
||||||
type I18nKey = keyof typeof langDefault;
|
type I18nKey = keyof typeof langDefault;
|
||||||
|
|
@ -17,61 +18,133 @@ const _loadedLocale = new Set<string>();
|
||||||
const _loadedStrings = {} as Record<string, typeof langDefault>;
|
const _loadedStrings = {} as Record<string, typeof langDefault>;
|
||||||
|
|
||||||
export const Strings = new Proxy(
|
export const Strings = new Proxy(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
get: (_t, prop: keyof typeof langDefault) => {
|
get: (_t, prop: keyof typeof langDefault) => {
|
||||||
if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) {
|
if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) {
|
||||||
return _loadedStrings[_currentLocale]?.[prop];
|
return _loadedStrings[_currentLocale]?.[prop];
|
||||||
}
|
}
|
||||||
return langDefault[prop];
|
return langDefault[prop];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
) as Record<I18nKey, string>;
|
) as Record<I18nKey, string>;
|
||||||
|
|
||||||
export function initFetchI18nStrings() {
|
const languageMap: Record<string, string> = {
|
||||||
const cb = ({ locale }: { locale: string }) => {
|
"ar-SA": "ar",
|
||||||
const languageMap = {
|
"bn-BD": "bn",
|
||||||
"es-ES": "es",
|
"ca-ES": "ca",
|
||||||
"es-419": "es_419",
|
"de-DE": "de",
|
||||||
"zh-TW": "zh-Hant",
|
"es-ES": "es",
|
||||||
"zh-CN": "zh-Hans",
|
"es-419": "es",
|
||||||
"pt-PT": "pt",
|
"fa-IR": "fa",
|
||||||
"pt-BR": "pt_BR",
|
"fi-FI": "fi",
|
||||||
"sv-SE": "sv",
|
"fr-FR": "fr",
|
||||||
} as Record<string, string>;
|
"hi-IN": "hi",
|
||||||
|
"hr-HR": "hr",
|
||||||
|
"hu-HU": "hu",
|
||||||
|
"id-ID": "id",
|
||||||
|
"it-IT": "it",
|
||||||
|
"ja-JP": "ja",
|
||||||
|
"pl-PL": "pl",
|
||||||
|
"pt-BR": "pt_BR",
|
||||||
|
"ru-RU": "ru",
|
||||||
|
"sk-SK": "sk",
|
||||||
|
"sv-SE": "sv",
|
||||||
|
"tr-TR": "tr",
|
||||||
|
"vi-VN": "vi",
|
||||||
|
};
|
||||||
|
|
||||||
|
function fetchLocale(locale: string) {
|
||||||
const resolvedLocale = (_lastSetLocale = languageMap[locale] ?? locale);
|
const resolvedLocale = (_lastSetLocale = languageMap[locale] ?? locale);
|
||||||
|
|
||||||
if (resolvedLocale.startsWith("en-")) {
|
logger.log("[i18n] fetchLocale called:", locale, "->", resolvedLocale);
|
||||||
_currentLocale = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_loadedLocale.has(resolvedLocale)) {
|
if (!_loadedLocale.has(resolvedLocale)) {
|
||||||
_loadedLocale.add(resolvedLocale);
|
_loadedLocale.add(resolvedLocale);
|
||||||
|
|
||||||
fetch(
|
if (resolvedLocale.toLowerCase().startsWith("en")) {
|
||||||
`https://raw.githubusercontent.com/kmmiio99o/i18n/main/resources/${resolvedLocale}/shiggycord.json`,
|
// use local default.json for english
|
||||||
)
|
logger.log("[i18n] Using local default.json for English locale");
|
||||||
.then((r) => r.json())
|
_loadedStrings[resolvedLocale] = langDefault;
|
||||||
.then((strings) => (_loadedStrings[resolvedLocale] = strings))
|
_currentLocale = resolvedLocale;
|
||||||
.then(
|
} else {
|
||||||
() =>
|
fetch(`https://codeberg.org/cocobo1/kettu-i18n/raw/branch/main/base/${resolvedLocale}.json`)
|
||||||
resolvedLocale === _lastSetLocale &&
|
.then(r => r.json())
|
||||||
(_currentLocale = resolvedLocale),
|
.then(strings => {
|
||||||
)
|
logger.log("[i18n] Loaded strings for:", resolvedLocale);
|
||||||
.catch((e) =>
|
_loadedStrings[resolvedLocale] = strings;
|
||||||
console.error(
|
_currentLocale = resolvedLocale;
|
||||||
`An error occured while fetching strings for ${resolvedLocale}: ${e}`,
|
})
|
||||||
),
|
.catch(e =>
|
||||||
);
|
logger.error(`[i18n] Error fetching strings for ${resolvedLocale}: ${e}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_currentLocale = resolvedLocale;
|
_currentLocale = resolvedLocale;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", cb);
|
export function initFetchI18nStrings() {
|
||||||
return () => FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", cb);
|
let attempts = 0;
|
||||||
|
const checkAndFetch = () => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const LocaleStore = findByStoreName("LocaleStore");
|
||||||
|
logger.log("[i18n] Attempt", attempts, "- LocaleStore:", !!LocaleStore);
|
||||||
|
|
||||||
|
if (!LocaleStore) {
|
||||||
|
logger.log("[i18n] LocaleStore not found yet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LocaleStore?._isInitialized !== true) {
|
||||||
|
logger.log("[i18n] LocaleStore not initialized yet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = LocaleStore.locale;
|
||||||
|
|
||||||
|
if (locale) {
|
||||||
|
logger.log("[i18n] Using LocaleStore:", locale);
|
||||||
|
fetchLocale(locale);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.log("[i18n] Error:", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryTimes = () => {
|
||||||
|
if (checkAndFetch()) return;
|
||||||
|
if (attempts < 15) {
|
||||||
|
setTimeout(tryTimes, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tryTimes();
|
||||||
|
|
||||||
|
const cb = (e: any) => {
|
||||||
|
if (e?.settings?.changes?.loading) {
|
||||||
|
logger.log("[i18n] Settings loading, skipping...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = e?.settings?.changes?.protoToSave?.localization?.locale?.value;
|
||||||
|
logger.log("[i18n] Locale changed:", locale);
|
||||||
|
if (locale) {
|
||||||
|
logger.log("[i18n] Found locale in event:", locale);
|
||||||
|
fetchLocale(locale);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FluxDispatcher.subscribe("USER_SETTINGS_PROTO_UPDATE_EDIT_INFO", cb);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
FluxDispatcher.unsubscribe("USER_SETTINGS_PROTO_UPDATE_EDIT_INFO", cb);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormatStringRet<T> = T extends PrimitiveType
|
type FormatStringRet<T> = T extends PrimitiveType
|
||||||
|
|
@ -79,10 +152,20 @@ type FormatStringRet<T> = T extends PrimitiveType
|
||||||
: string | T | (string | T)[];
|
: string | T | (string | T)[];
|
||||||
|
|
||||||
export function formatString<T = void>(
|
export function formatString<T = void>(
|
||||||
key: I18nKey,
|
key: I18nKey,
|
||||||
val: Record<string, T>,
|
val: Record<string, T>,
|
||||||
): FormatStringRet<T> {
|
): FormatStringRet<T> {
|
||||||
const str = Strings[key];
|
const str = Strings[key];
|
||||||
// @ts-ignore
|
if (!str) return str as FormatStringRet<T>;
|
||||||
return new IntlMessageFormat(str).format(val);
|
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>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,201 @@
|
||||||
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 } from "@metro";
|
import { findByNameLazy } from "@metro";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { defineCorePlugin } from "..";
|
import { defineCorePlugin } from "..";
|
||||||
|
import { FluxDispatcher } from "@metro/common";
|
||||||
|
|
||||||
interface BunnyBadge {
|
interface Badge {
|
||||||
label: string;
|
label: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useBadgesModule = findByName("useBadges", false);
|
interface UserBadgeData {
|
||||||
|
roles?: string[];
|
||||||
|
custom?: Badge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BadgeData {
|
||||||
|
[userId: string]: UserBadgeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoleData {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RolesData {
|
||||||
|
[roleName: string]: RoleData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EquicordBadge {
|
||||||
|
tooltip: string;
|
||||||
|
badge: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBadgesModule = findByNameLazy("useBadges", false);
|
||||||
|
|
||||||
|
const badgesCache = new Map<string, Badge[]>();
|
||||||
|
const badgeProps = new Map<string, Record<string, any>>();
|
||||||
|
const pendingRequests = new Set<string>();
|
||||||
|
|
||||||
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() {
|
||||||
let allBadges: { [x: string]: any } | null = null;
|
onJsxCreate("ProfileBadge", (component, ret) => {
|
||||||
const badgeProps = {} as Record<string, any>;
|
if (ret.props.id?.startsWith("rain-")) {
|
||||||
|
const cachedProps = badgeProps.get(ret.props.id);
|
||||||
onJsxCreate("ProfileBadge", (component, ret) => {
|
if (cachedProps) {
|
||||||
if (ret.props.id?.startsWith("bunny-")) {
|
ret.props.source = cachedProps.source;
|
||||||
const cachedProps = badgeProps[ret.props.id];
|
ret.props.label = cachedProps.label;
|
||||||
if (cachedProps) {
|
ret.props.id = cachedProps.id;
|
||||||
ret.props.source = cachedProps.source;
|
}
|
||||||
ret.props.label = cachedProps.label;
|
}
|
||||||
ret.props.id = cachedProps.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onJsxCreate("RenderedBadge", (component, ret) => {
|
|
||||||
if (ret.props.id?.startsWith("bunny-")) {
|
|
||||||
const cachedProps = badgeProps[ret.props.id];
|
|
||||||
if (cachedProps) {
|
|
||||||
Object.assign(ret.props, cachedProps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
after("default", useBadgesModule, ([user], result) => {
|
|
||||||
const [badges, setBadges] = useState<BunnyBadge[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
if (!allBadges) {
|
|
||||||
fetch(
|
|
||||||
"https://codeberg.org/raincord/badges/raw/branch/main/badges.json",
|
|
||||||
)
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((data) => {
|
|
||||||
allBadges = data;
|
|
||||||
//@ts-expect-error
|
|
||||||
setBadges(allBadges[user.userId] || []);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
//user has no badges, but maybe they should get some by contributing
|
|
||||||
setBadges(allBadges[user.userId] || []);
|
|
||||||
}
|
|
||||||
}, [user?.userId]);
|
|
||||||
|
|
||||||
if (user && badges.length > 0) {
|
|
||||||
badges.forEach((badge, i) => {
|
|
||||||
const badgeId = `bunny-${user.userId}-${i}`;
|
|
||||||
|
|
||||||
badgeProps[badgeId] = {
|
|
||||||
id: badgeId,
|
|
||||||
source: { uri: badge.url },
|
|
||||||
label: badge.label,
|
|
||||||
userId: user.userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
id: badgeId,
|
|
||||||
description: badge.label,
|
|
||||||
icon: " _",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
onJsxCreate("RenderedBadge", (component, ret) => {
|
||||||
},
|
if (ret.props.id?.startsWith("rain-")) {
|
||||||
|
const cachedProps = badgeProps.get(ret.props.id);
|
||||||
|
if (cachedProps) {
|
||||||
|
Object.assign(ret.props, cachedProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 [
|
||||||
|
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 allBadges: Badge[] = [];
|
||||||
|
|
||||||
|
// GoonCord and raincord badge/role sets are keyed independently,
|
||||||
|
// so each source is resolved against its own roles file.
|
||||||
|
const badgeSources: Array<{ badges: BadgeData; roles: RolesData }> = [
|
||||||
|
{ badges: goonBadgesData, roles: goonRolesData },
|
||||||
|
{ badges: rainBadgesData, roles: rainRolesData },
|
||||||
|
];
|
||||||
|
|
||||||
|
badgeSources.forEach(({ badges, roles }) => {
|
||||||
|
const userBadgeData = badges[userId];
|
||||||
|
if (!userBadgeData) return;
|
||||||
|
|
||||||
|
// process role badges
|
||||||
|
userBadgeData.roles?.forEach(roleName => {
|
||||||
|
const roleData = roles[roleName];
|
||||||
|
if (roleData) {
|
||||||
|
allBadges.push({
|
||||||
|
label: roleData.label,
|
||||||
|
url: roleData.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// process custom badges
|
||||||
|
if (userBadgeData.custom) {
|
||||||
|
allBadges.push(...userBadgeData.custom);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// process equicord badges
|
||||||
|
const equicordUserBadges = equicordData[userId] ?? [];
|
||||||
|
equicordUserBadges.forEach(b => {
|
||||||
|
allBadges.push({ label: b.tooltip, url: b.badge });
|
||||||
|
});
|
||||||
|
|
||||||
|
badgesCache.set(userId, allBadges);
|
||||||
|
|
||||||
|
allBadges.forEach((badge, i) => {
|
||||||
|
const badgeId = `rain-${userId}-${i}`;
|
||||||
|
badgeProps.set(badgeId, {
|
||||||
|
id: badgeId,
|
||||||
|
source: { uri: badge.url },
|
||||||
|
label: badge.label,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: { id: userId } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[bunny.badges] Failed to fetch/process badges:", err);
|
||||||
|
} finally {
|
||||||
|
pendingRequests.delete(userId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return after("default", useBadgesModule, ([user], result) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const userId = user.userId;
|
||||||
|
const cached = badgesCache.get(userId);
|
||||||
|
|
||||||
|
if (!cached) {
|
||||||
|
if (!pendingRequests.has(userId)) {
|
||||||
|
fetchAndProcessBadges(userId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cached.forEach((badge, i) => {
|
||||||
|
const badgeId = `rain-${userId}-${i}`;
|
||||||
|
|
||||||
|
result.unshift({
|
||||||
|
id: badgeId,
|
||||||
|
description: badge.label,
|
||||||
|
icon: " _",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
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://tiktokez.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
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,43 @@ import { PluginInstanceInternal } from "@lib/addons/plugins/types";
|
||||||
interface CorePlugin {
|
interface CorePlugin {
|
||||||
default: PluginInstanceInternal;
|
default: PluginInstanceInternal;
|
||||||
preenabled: boolean;
|
preenabled: boolean;
|
||||||
|
loader: () => PluginInstanceInternal;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CorePluginRecord = Record<string, CorePlugin>;
|
||||||
|
|
||||||
|
function makeLazyPlugin(loaderFn: () => any, preenabled = true): CorePlugin {
|
||||||
|
const container: any = {
|
||||||
|
loader: () => {
|
||||||
|
// loaderFn should synchronously require the module
|
||||||
|
const mod = loaderFn();
|
||||||
|
// module may be an object with `.default` or the instance directly
|
||||||
|
const inst = mod && (mod.default ?? mod);
|
||||||
|
return inst;
|
||||||
|
},
|
||||||
|
preenabled,
|
||||||
|
get default() {
|
||||||
|
// load and cache the instance on first access
|
||||||
|
const inst = container.loader();
|
||||||
|
Object.defineProperty(container, "default", { value: inst, writable: false, enumerable: true });
|
||||||
|
return inst;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return container as CorePlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from @lib/plugins
|
// Called from @lib/plugins
|
||||||
export const getCorePlugins = (): Record<string, CorePlugin> => ({
|
export const getCorePlugins = (): CorePluginRecord => ({
|
||||||
"bunny.quickinstall": { ...require("./quickinstall"), preenabled: true },
|
"bunny.quickinstall": makeLazyPlugin(() => require("./quickinstall"), true),
|
||||||
"bunny.badges": require("./badges"),
|
"bunny.badges": makeLazyPlugin(() => require("./badges")),
|
||||||
"bunny.notrack": require("./notrack"),
|
"bunny.notrack": makeLazyPlugin(() => require("./notrack")),
|
||||||
"bunny.messagefix": require("./messagefix"),
|
"bunny.fixembed": makeLazyPlugin(() => require("./fixembed")),
|
||||||
"bunny.fixembed": require("./fixembed"),
|
"bunny.favouriteanything": makeLazyPlugin(() => require("./favouriteanything")),
|
||||||
"bunny.enhancements": require("./shiggyenhancements")
|
"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");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import { defineCorePlugin } from "..";
|
|
||||||
import { findByProps } from "@metro";
|
|
||||||
import { logger } from "@lib/utils/logger";
|
|
||||||
|
|
||||||
// Find the MessageActions module
|
|
||||||
const MessageActions = findByProps("sendMessage");
|
|
||||||
let originalSendMessage: Function;
|
|
||||||
|
|
||||||
export default defineCorePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "bunny.messagefix",
|
|
||||||
version: "1.0.0",
|
|
||||||
type: "plugin",
|
|
||||||
spec: 3,
|
|
||||||
main: "",
|
|
||||||
display: {
|
|
||||||
name: "MessageFix",
|
|
||||||
description: "Ensures messages include the required nonce parameter",
|
|
||||||
authors: [{ name: "Win8.1VMUser (plugin by ShiggyCord Team)" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
start() {
|
|
||||||
// Save original function
|
|
||||||
originalSendMessage = MessageActions.sendMessage;
|
|
||||||
|
|
||||||
// Replace with our implementation
|
|
||||||
MessageActions.sendMessage = function (
|
|
||||||
channelId,
|
|
||||||
message,
|
|
||||||
replyRef,
|
|
||||||
options,
|
|
||||||
) {
|
|
||||||
// Ensure options exists and has a nonce
|
|
||||||
options = options || {};
|
|
||||||
options.nonce = options.nonce || Date.now().toString();
|
|
||||||
|
|
||||||
// Call original with fixed parameters
|
|
||||||
return originalSendMessage.call(
|
|
||||||
this,
|
|
||||||
channelId,
|
|
||||||
message,
|
|
||||||
replyRef,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.log("MessageFix: Enabled - adding nonce to all messages");
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
// Restore original function
|
|
||||||
if (originalSendMessage) MessageActions.sendMessage = originalSendMessage;
|
|
||||||
logger.log("MessageFix: Disabled");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
@ -5,13 +5,7 @@ import { installTheme, removeTheme, themes } from "@lib/addons/themes";
|
||||||
import { findAssetId } from "@lib/api/assets";
|
import { findAssetId } from "@lib/api/assets";
|
||||||
import { isThemeSupported } from "@lib/api/native/loader";
|
import { isThemeSupported } from "@lib/api/native/loader";
|
||||||
import { after } from "@lib/api/patcher";
|
import { after } from "@lib/api/patcher";
|
||||||
import {
|
import { HTTP_REGEX_MULTI, KETTU_DISCORD_SERVER_ID, KETTU_PLUGINS_CHANNEL_ID, KETTU_THEMES_CHANNEL_ID, VD_DISCORD_SERVER_ID, VD_PLUGINS_CHANNEL_ID, VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
|
||||||
HTTP_REGEX_MULTI,
|
|
||||||
VD_DISCORD_SERVER_ID,
|
|
||||||
VD_PLUGINS_CHANNEL_ID,
|
|
||||||
VD_PROXY_PREFIX,
|
|
||||||
VD_THEMES_CHANNEL_ID,
|
|
||||||
} from "@lib/utils/constants";
|
|
||||||
import { lazyDestructure } from "@lib/utils/lazy";
|
import { lazyDestructure } from "@lib/utils/lazy";
|
||||||
import { Button } from "@metro/common/components";
|
import { Button } from "@metro/common/components";
|
||||||
import { findByProps, findByPropsLazy } from "@metro/wrappers";
|
import { findByProps, findByPropsLazy } from "@metro/wrappers";
|
||||||
|
|
@ -22,91 +16,74 @@ type PostType = "Plugin" | "Theme";
|
||||||
|
|
||||||
// const ForumPostLongPressActionSheet = findByNameProxy("ForumPostLongPressActionSheet", false);
|
// const ForumPostLongPressActionSheet = findByNameProxy("ForumPostLongPressActionSheet", false);
|
||||||
// const { ActionSheetRow } = findByPropsProxy("ActionSheetRow");
|
// const { ActionSheetRow } = findByPropsProxy("ActionSheetRow");
|
||||||
const { useFirstForumPostMessage } = lazyDestructure(() =>
|
const { useFirstForumPostMessage } = lazyDestructure(() => findByProps("useFirstForumPostMessage"));
|
||||||
findByProps("useFirstForumPostMessage"),
|
|
||||||
);
|
|
||||||
const forumReactions = findByPropsLazy("MostCommonForumPostReaction");
|
const forumReactions = findByPropsLazy("MostCommonForumPostReaction");
|
||||||
|
|
||||||
const postMap = {
|
const postMap = {
|
||||||
Plugin: {
|
Plugin: {
|
||||||
storage: VdPluginManager.plugins,
|
storage: VdPluginManager.plugins,
|
||||||
urlsFilter: (url: string) => url.startsWith(VD_PROXY_PREFIX),
|
urlsFilter: (url: string) => url.startsWith(VD_PROXY_PREFIX),
|
||||||
installOrRemove: (url: string) => {
|
installOrRemove: (url: string) => {
|
||||||
const isInstalled = postMap.Plugin.storage[url];
|
const isInstalled = postMap.Plugin.storage[url];
|
||||||
return isInstalled
|
return isInstalled ? VdPluginManager.removePlugin(url) : VdPluginManager.installPlugin(url);
|
||||||
? VdPluginManager.removePlugin(url)
|
}
|
||||||
: VdPluginManager.installPlugin(url);
|
|
||||||
},
|
},
|
||||||
},
|
Theme: {
|
||||||
Theme: {
|
storage: themes,
|
||||||
storage: themes,
|
urlsFilter: (url: string) => url.endsWith(".json"),
|
||||||
urlsFilter: (url: string) => url.endsWith(".json"),
|
installOrRemove: (url: string) => {
|
||||||
installOrRemove: (url: string) => {
|
const isInstalled = postMap.Theme.storage[url];
|
||||||
const isInstalled = postMap.Theme.storage[url];
|
return isInstalled ? removeTheme(url) : installTheme(url);
|
||||||
return isInstalled ? removeTheme(url) : installTheme(url);
|
},
|
||||||
},
|
}
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function useExtractThreadContent(
|
function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet = false): ([PostType, string]) | void {
|
||||||
thread: any,
|
if (thread.guild_id !== KETTU_DISCORD_SERVER_ID && thread.guild_id !== VD_DISCORD_SERVER_ID) return;
|
||||||
_firstMessage = null,
|
|
||||||
actionSheet = false,
|
|
||||||
): [PostType, string] | void {
|
|
||||||
if (thread.guild_id !== VD_DISCORD_SERVER_ID) return;
|
|
||||||
|
|
||||||
// Determine what type of addon this is.
|
// Determine what type of addon this is.
|
||||||
let postType: PostType;
|
let postType: PostType;
|
||||||
if (thread.parent_id === VD_PLUGINS_CHANNEL_ID) {
|
if (thread.parent_id === KETTU_PLUGINS_CHANNEL_ID || thread.parent_id === VD_PLUGINS_CHANNEL_ID) {
|
||||||
postType = "Plugin";
|
postType = "Plugin";
|
||||||
} else if (thread.parent_id === VD_THEMES_CHANNEL_ID && isThemeSupported()) {
|
} else if ((thread.parent_id === KETTU_THEMES_CHANNEL_ID || thread.parent_id === VD_THEMES_CHANNEL_ID) && isThemeSupported()) {
|
||||||
postType = "Theme";
|
postType = "Theme";
|
||||||
} else return;
|
} else return;
|
||||||
|
|
||||||
const { firstMessage } = actionSheet
|
const { firstMessage } = actionSheet ? useFirstForumPostMessage(thread) : { firstMessage: _firstMessage };
|
||||||
? useFirstForumPostMessage(thread)
|
|
||||||
: { firstMessage: _firstMessage };
|
|
||||||
|
|
||||||
const urls = firstMessage?.content
|
const urls = firstMessage?.content?.match(HTTP_REGEX_MULTI)?.filter(postMap[postType].urlsFilter);
|
||||||
?.match(HTTP_REGEX_MULTI)
|
if (!urls || !urls[0]) return;
|
||||||
?.filter(postMap[postType].urlsFilter);
|
|
||||||
if (!urls || !urls[0]) return;
|
|
||||||
|
|
||||||
// Sync with lib/managers/plugins
|
// Sync with lib/managers/plugins
|
||||||
if (postType === "Plugin" && !urls[0].endsWith("/")) urls[0] += "/";
|
if (postType === "Plugin" && !urls[0].endsWith("/")) urls[0] += "/";
|
||||||
|
|
||||||
return [postType, urls[0]];
|
return [postType, urls[0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useInstaller(
|
function useInstaller(thread: any, firstMessage = null, actionSheet = false): [true] | [false, PostType, boolean, boolean, () => Promise<void>] {
|
||||||
thread: any,
|
const [postType, url] = useExtractThreadContent(thread, firstMessage, actionSheet) ?? [];
|
||||||
firstMessage = null,
|
|
||||||
actionSheet = false,
|
|
||||||
): [true] | [false, PostType, boolean, boolean, () => Promise<void>] {
|
|
||||||
const [postType, url] =
|
|
||||||
useExtractThreadContent(thread, firstMessage, actionSheet) ?? [];
|
|
||||||
|
|
||||||
useProxy(VdPluginManager.plugins);
|
useProxy(VdPluginManager.plugins);
|
||||||
useProxy(themes);
|
useProxy(themes);
|
||||||
|
|
||||||
const [isInstalling, setIsInstalling] = React.useState(false);
|
const [isInstalling, setIsInstalling] = React.useState(false);
|
||||||
|
|
||||||
if (!postType || !url) return [true];
|
if (!postType || !url) return [true];
|
||||||
|
|
||||||
const isInstalled = Boolean(postMap[postType].storage[url]);
|
const isInstalled = Boolean(postMap[postType].storage[url]);
|
||||||
|
|
||||||
const installOrRemove = async () => {
|
const installOrRemove = async () => {
|
||||||
setIsInstalling(true);
|
setIsInstalling(true);
|
||||||
try {
|
try {
|
||||||
await postMap[postType].installOrRemove(url);
|
await postMap[postType].installOrRemove(url);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showToast(e.message, findAssetId("Small"));
|
showToast(e.message, findAssetId("Small"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsInstalling(false);
|
setIsInstalling(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return [false, postType, isInstalled, isInstalling, installOrRemove];
|
return [false, postType, isInstalled, isInstalling, installOrRemove];
|
||||||
}
|
}
|
||||||
|
|
||||||
// // apparently broken???
|
// // apparently broken???
|
||||||
|
|
@ -127,43 +104,33 @@ function useInstaller(
|
||||||
// </ActionsSection>);
|
// </ActionsSection>);
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const installButtonPatch = () =>
|
const installButtonPatch = () => after("MostCommonForumPostReaction", forumReactions, ([{ thread, firstMessage }], res) => {
|
||||||
after(
|
const [shouldReturn, _, installed, loading, installOrRemove] = useInstaller(thread, firstMessage, true);
|
||||||
"MostCommonForumPostReaction",
|
if (shouldReturn) return;
|
||||||
forumReactions,
|
|
||||||
([{ thread, firstMessage }], res) => {
|
|
||||||
const [shouldReturn, _, installed, loading, installOrRemove] =
|
|
||||||
useInstaller(thread, firstMessage, true);
|
|
||||||
if (shouldReturn) return;
|
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<>
|
{res}
|
||||||
{res}
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
// variant={installed ? "destructive" : "primary"} crashes older version because "destructive" was renamed from "danger" and there's no sane way for compat check horror
|
// variant={installed ? "destructive" : "primary"} crashes older version because "destructive" was renamed from "danger" and there's no sane way for compat check horror
|
||||||
variant={installed ? "secondary" : "primary"}
|
variant={installed ? "secondary" : "primary"}
|
||||||
text={installed ? Strings.UNINSTALL : Strings.INSTALL}
|
text={installed ? Strings.UNINSTALL : Strings.INSTALL}
|
||||||
onPress={installOrRemove}
|
onPress={installOrRemove}
|
||||||
icon={findAssetId(
|
icon={findAssetId(installed ? "ic_message_delete" : "DownloadIcon")}
|
||||||
installed ? "ic_message_delete" : "DownloadIcon",
|
style={{ marginLeft: 8 }}
|
||||||
)}
|
|
||||||
style={{ marginLeft: 8 }}
|
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</>
|
</>;
|
||||||
);
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const patches = [
|
const patches = [
|
||||||
// actionSheetPatch(),
|
// actionSheetPatch(),
|
||||||
installButtonPatch(),
|
installButtonPatch()
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => patches.map((p) => p());
|
return () => patches.map(p => p());
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { installTheme } from "@lib/addons/themes";
|
||||||
import { findAssetId } from "@lib/api/assets";
|
import { findAssetId } from "@lib/api/assets";
|
||||||
import { isThemeSupported } from "@lib/api/native/loader";
|
import { isThemeSupported } from "@lib/api/native/loader";
|
||||||
import { after, instead } from "@lib/api/patcher";
|
import { after, instead } from "@lib/api/patcher";
|
||||||
import { VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
|
import { VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID, KETTU_THEMES_CHANNEL_ID } from "@lib/utils/constants";
|
||||||
import { lazyDestructure } from "@lib/utils/lazy";
|
import { lazyDestructure } from "@lib/utils/lazy";
|
||||||
import { channels } from "@metro/common";
|
import { channels } from "@metro/common";
|
||||||
import { byMutableProp } from "@metro/filters";
|
import { byMutableProp } from "@metro/filters";
|
||||||
|
|
@ -72,7 +72,8 @@ export default () => {
|
||||||
// Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed.
|
// Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed.
|
||||||
if (
|
if (
|
||||||
urlType === "theme" &&
|
urlType === "theme" &&
|
||||||
getChannel(getChannelId())?.parent_id !== VD_THEMES_CHANNEL_ID
|
getChannel(getChannelId())?.parent_id !== VD_THEMES_CHANNEL_ID &&
|
||||||
|
getChannel(getChannelId())?.parent_id !== KETTU_THEMES_CHANNEL_ID
|
||||||
)
|
)
|
||||||
return orig.apply(this, args);
|
return orig.apply(this, args);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,301 +0,0 @@
|
||||||
import { defineCorePlugin } from "..";
|
|
||||||
import { findByProps } from "@metro";
|
|
||||||
import { logger } from "@lib/utils/logger";
|
|
||||||
import { settings } from "@lib/api/settings";
|
|
||||||
import { React } from "@metro/common";
|
|
||||||
|
|
||||||
const { TableRowGroup, TableRow, TableSwitchRow, Stack } = findByProps(
|
|
||||||
"TableRowGroup",
|
|
||||||
"TableRow",
|
|
||||||
"TableSwitchRow",
|
|
||||||
"Stack",
|
|
||||||
);
|
|
||||||
const { ScrollView } = require("react-native");
|
|
||||||
|
|
||||||
// Get ReactNative for NativeModules
|
|
||||||
const { ReactNative } = window as any;
|
|
||||||
const { DCDSoundManager } = ReactNative?.NativeModules || {};
|
|
||||||
|
|
||||||
// Sound settings type
|
|
||||||
type SoundSettings = {
|
|
||||||
enabled: boolean;
|
|
||||||
probability: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare module "@lib/api/settings" {
|
|
||||||
interface Settings {
|
|
||||||
startupSound?: SoundSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const soundUrl =
|
|
||||||
"https://github.com/kmmiio99o/kmmiio99o.github.io/raw/refs/heads/main/music/shiggycord.mp3";
|
|
||||||
const soundId = 6971;
|
|
||||||
let soundDuration = -1;
|
|
||||||
let timeoutId: NodeJS.Timeout | null = null;
|
|
||||||
let isPlaying = false;
|
|
||||||
let isPrepared = false;
|
|
||||||
|
|
||||||
// Function to prepare the sound
|
|
||||||
const prepareSound = function (): Promise<any> {
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
if (!DCDSoundManager) {
|
|
||||||
logger.error("DCDSoundManager not available");
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DCDSoundManager.prepare(
|
|
||||||
soundUrl,
|
|
||||||
"music",
|
|
||||||
soundId,
|
|
||||||
function (error: any, sound: any) {
|
|
||||||
if (error) {
|
|
||||||
logger.error("Failed to prepare sound:", error);
|
|
||||||
resolve(null);
|
|
||||||
} else {
|
|
||||||
resolve(sound);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to play the sound
|
|
||||||
async function playSound() {
|
|
||||||
if (!DCDSoundManager) {
|
|
||||||
logger.error("DCDSoundManager not available for playback");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying) {
|
|
||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
|
||||||
DCDSoundManager.stop(soundId);
|
|
||||||
isPlaying = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isPlaying = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await DCDSoundManager.play(soundId);
|
|
||||||
|
|
||||||
if (soundDuration > 0) {
|
|
||||||
timeoutId = setTimeout(function () {
|
|
||||||
isPlaying = false;
|
|
||||||
DCDSoundManager.stop(soundId);
|
|
||||||
timeoutId = null;
|
|
||||||
}, soundDuration);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error playing sound:", error);
|
|
||||||
isPlaying = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we should play based on probability
|
|
||||||
function shouldPlaySound(): boolean {
|
|
||||||
const probability = settings.startupSound?.probability ?? 2;
|
|
||||||
const random = Math.random() * 100;
|
|
||||||
return random <= probability;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineCorePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "bunny.enhancements",
|
|
||||||
version: "0.1.0",
|
|
||||||
type: "plugin",
|
|
||||||
spec: 3,
|
|
||||||
main: "",
|
|
||||||
display: {
|
|
||||||
name: "Shiggy Enhancements",
|
|
||||||
description:
|
|
||||||
"Fixes common discord bugs because discord wont, and adds some features. (Originally Kettu Enhancements)",
|
|
||||||
authors: [{ name: "cocobo1" }, { name: "Shiggy Team" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
SettingsComponent() {
|
|
||||||
const { useState, useEffect } = React;
|
|
||||||
const [config, setConfig] = useState<SoundSettings>({
|
|
||||||
enabled: settings.startupSound?.enabled ?? true,
|
|
||||||
probability: settings.startupSound?.probability ?? 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
settings.startupSound = config;
|
|
||||||
}, [config]);
|
|
||||||
|
|
||||||
const updateConfig = (
|
|
||||||
key: keyof SoundSettings,
|
|
||||||
value: boolean | number,
|
|
||||||
) => {
|
|
||||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get UI components including TextInput
|
|
||||||
const {
|
|
||||||
TableRowGroup: _TRG,
|
|
||||||
TableRow: _TR,
|
|
||||||
TableSwitchRow: _TSR,
|
|
||||||
Stack: _S,
|
|
||||||
TextInput: _TextInput,
|
|
||||||
} = findByProps(
|
|
||||||
"TableRowGroup",
|
|
||||||
"TableRow",
|
|
||||||
"TableSwitchRow",
|
|
||||||
"Stack",
|
|
||||||
"TextInput",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fallback if table components aren't available
|
|
||||||
if (!_TRG || !_TSR || !_TR || !_S) {
|
|
||||||
const FallbackText =
|
|
||||||
"Startup Sound UI unavailable (missing TableRow components).";
|
|
||||||
return React.createElement(
|
|
||||||
ScrollView,
|
|
||||||
{ style: { flex: 1, padding: 12 } },
|
|
||||||
FallbackText,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TableRowGroup = _TRG;
|
|
||||||
const TableRow = _TR;
|
|
||||||
const TableSwitchRow = _TSR;
|
|
||||||
const Stack = _S;
|
|
||||||
const TextInput = _TextInput;
|
|
||||||
|
|
||||||
// Handle probability input change
|
|
||||||
const handleProbabilityChange = (v: string) => {
|
|
||||||
const num = parseInt(v);
|
|
||||||
if (!isNaN(num) && num >= 0 && num <= 100) {
|
|
||||||
updateConfig("probability", num);
|
|
||||||
} else if (v === "") {
|
|
||||||
updateConfig("probability", 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create settings elements
|
|
||||||
const settingsElements = [];
|
|
||||||
|
|
||||||
// Startup Sound toggle with probability display
|
|
||||||
settingsElements.push(
|
|
||||||
React.createElement(
|
|
||||||
TableRowGroup,
|
|
||||||
{ title: "Startup Sound", key: "startup-sound" },
|
|
||||||
React.createElement(TableSwitchRow, {
|
|
||||||
label: "Enable Startup Sound",
|
|
||||||
value: config.enabled,
|
|
||||||
onValueChange: (v: boolean) => updateConfig("enabled", v),
|
|
||||||
}),
|
|
||||||
React.createElement(TableRow, {
|
|
||||||
label: "Play Chance",
|
|
||||||
subLabel: `Current: ${config.probability}% chance to play on startup`,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Probability input in its own invisible group
|
|
||||||
if (TextInput) {
|
|
||||||
settingsElements.push(
|
|
||||||
React.createElement(
|
|
||||||
TableRowGroup,
|
|
||||||
{ title: "", key: "probability-input-group" },
|
|
||||||
React.createElement(TextInput, {
|
|
||||||
placeholder: "Enter probability (0-100)",
|
|
||||||
value: config.probability.toString(),
|
|
||||||
onChange: handleProbabilityChange,
|
|
||||||
key: "probability-input",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// About section
|
|
||||||
settingsElements.push(
|
|
||||||
React.createElement(
|
|
||||||
TableRowGroup,
|
|
||||||
{ title: "About", key: "about" },
|
|
||||||
React.createElement(TableRow, {
|
|
||||||
label: "Description",
|
|
||||||
subLabel:
|
|
||||||
"Shiggy Enhancements, more options, more features. Originally Kettu Enhancements with additinal features.",
|
|
||||||
disabled: true,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
ScrollView,
|
|
||||||
{ style: { flex: 1 } },
|
|
||||||
React.createElement(
|
|
||||||
Stack,
|
|
||||||
{ spacing: 8, style: { padding: 10 } },
|
|
||||||
...settingsElements,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
start() {
|
|
||||||
logger.log("Shiggy Enhancements: Starting plugin");
|
|
||||||
|
|
||||||
// Initialize sound settings
|
|
||||||
settings.startupSound = settings.startupSound || {
|
|
||||||
enabled: false,
|
|
||||||
probability: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if DCDSoundManager is available
|
|
||||||
if (!DCDSoundManager) {
|
|
||||||
logger.error("DCDSoundManager not found - startup sound disabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play startup sound if enabled and probability check passes
|
|
||||||
if (settings.startupSound.enabled && shouldPlaySound()) {
|
|
||||||
logger.log(
|
|
||||||
`Attempting to play startup sound (${settings.startupSound.probability}% chance)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Always prepare the sound fresh each time
|
|
||||||
prepareSound()
|
|
||||||
.then(function (sound) {
|
|
||||||
if (sound) {
|
|
||||||
isPrepared = true;
|
|
||||||
soundDuration = sound.duration || 5000;
|
|
||||||
logger.log(`Sound prepared, duration: ${soundDuration}ms`);
|
|
||||||
playSound();
|
|
||||||
logger.log(
|
|
||||||
`Startup sound played (${settings.startupSound?.probability}% chance)`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.error("Failed to prepare sound");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error("Error in sound preparation:", error);
|
|
||||||
});
|
|
||||||
} else if (settings.startupSound.enabled) {
|
|
||||||
logger.log(
|
|
||||||
`Startup sound skipped due to probability (${settings.startupSound?.probability}% chance)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
logger.log("Shiggy Enhancements: Stopping plugin");
|
|
||||||
|
|
||||||
// Stop and cleanup audio
|
|
||||||
if (DCDSoundManager && isPlaying) {
|
|
||||||
DCDSoundManager.stop(soundId);
|
|
||||||
isPlaying = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
timeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset preparation state so it can play again next time
|
|
||||||
isPrepared = false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -23,51 +23,54 @@ const { showSimpleActionSheet } = lazyDestructure(() =>
|
||||||
|
|
||||||
// TODO: These styles work weirdly. Low DPI Has cramped text. Fix?
|
// TODO: These styles work weirdly. Low DPI Has cramped text. Fix?
|
||||||
const useStyles = createStyles({
|
const useStyles = createStyles({
|
||||||
card: {
|
card: {
|
||||||
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
|
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: "hidden",
|
overflow: "hidden"
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
},
|
},
|
||||||
headerLeading: {
|
headerLeading: {
|
||||||
flexDirection: "column",
|
flex: 1,
|
||||||
justifyContent: "center",
|
flexDirection: "column",
|
||||||
scale: 1.2,
|
justifyContent: "center",
|
||||||
},
|
scale: 1.2
|
||||||
headerTrailing: {
|
},
|
||||||
display: "flex",
|
headerTrailing: {
|
||||||
flexDirection: "row",
|
display: "flex",
|
||||||
gap: 15,
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
gap: 15,
|
||||||
},
|
alignItems: "center"
|
||||||
headerLabel: {
|
},
|
||||||
...TextStyleSheet["heading-md/semibold"],
|
headerLabel: {
|
||||||
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY,
|
...TextStyleSheet["heading-lg/semibold"],
|
||||||
},
|
},
|
||||||
headerSubtitle: {
|
headerLabelCompact: {
|
||||||
...TextStyleSheet["text-md/semibold"],
|
...TextStyleSheet["text-md/semibold"],
|
||||||
color: semanticColors.TEXT_MUTED,
|
},
|
||||||
},
|
headerSubtitle: {
|
||||||
descriptionLabel: {
|
...TextStyleSheet["text-sm/semibold"],
|
||||||
...TextStyleSheet["text-md/semibold"],
|
color: semanticColors.TEXT_MUTED,
|
||||||
color: semanticColors.TEXT_NORMAL,
|
},
|
||||||
},
|
descriptionLabel: {
|
||||||
actions: {
|
...TextStyleSheet["text-md/semibold"],
|
||||||
flexDirection: "row-reverse",
|
color: "text-strong",
|
||||||
alignItems: "center",
|
},
|
||||||
gap: 5,
|
actions: {
|
||||||
},
|
flexDirection: "row-reverse",
|
||||||
iconStyle: {
|
alignItems: "center",
|
||||||
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT,
|
gap: 5
|
||||||
opacity: 0.2,
|
},
|
||||||
height: 64,
|
iconStyle: {
|
||||||
width: 64,
|
tintColor: semanticColors.LOGO_PRIMARY,
|
||||||
left: void 0,
|
opacity: 0.2,
|
||||||
right: "30%",
|
height: 64,
|
||||||
top: "-10%",
|
width: 64,
|
||||||
},
|
left: void 0,
|
||||||
|
right: "30%",
|
||||||
|
top: "-10%"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Action {
|
interface Action {
|
||||||
|
|
@ -108,9 +111,9 @@ export default function AddonCard(props: CardProps) {
|
||||||
<Stack spacing={16}>
|
<Stack spacing={16}>
|
||||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||||
<View style={styles.headerLeading}>
|
<View style={styles.headerLeading}>
|
||||||
<Text style={styles.headerLabel}>{props.headerLabel}</Text>
|
<Text numberOfLines={1} ellipsizeMode="tail" style={styles.headerLabel}>{props.headerLabel}</Text>
|
||||||
{props.headerSublabel && (
|
{props.headerSublabel && (
|
||||||
<Text style={styles.headerSubtitle}>{props.headerSublabel}</Text>
|
<Text numberOfLines={1} ellipsizeMode="tail" style={styles.headerSubtitle}>{props.headerSublabel}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.headerTrailing, { marginLeft: "auto" }]}>
|
<View style={[styles.headerTrailing, { marginLeft: "auto" }]}>
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,6 @@ import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Image, ScrollView, View } from "react-native";
|
import { Image, ScrollView, View } from "react-native";
|
||||||
|
|
||||||
|
|
@ -182,6 +180,27 @@ export default function AddonPage<T extends object>({
|
||||||
const { bottom: bottomInset } = useSafeAreaInsets();
|
const { bottom: bottomInset } = useSafeAreaInsets();
|
||||||
const navigation = NavigationNative.useNavigation();
|
const navigation = NavigationNative.useNavigation();
|
||||||
|
|
||||||
|
// Lazy-load fuzzysort to reduce initial bundle size
|
||||||
|
const fuzzysortRef = React.useRef<any>(null);
|
||||||
|
const [fuzzysortReady, setFuzzysortReady] = React.useState(false);
|
||||||
|
React.useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
import("fuzzysort")
|
||||||
|
.then((mod) => {
|
||||||
|
const f = (mod as any).default ?? mod;
|
||||||
|
if (mounted) {
|
||||||
|
fuzzysortRef.current = f;
|
||||||
|
setFuzzysortReady(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// ignore load errors
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.OptionsActionSheetComponent) {
|
if (props.OptionsActionSheetComponent) {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
|
|
@ -199,27 +218,6 @@ export default function AddonPage<T extends object>({
|
||||||
}
|
}
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
const fuzzysortRef = useRef<any>(null);
|
|
||||||
const [fuzzLoaded, setFuzzLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
if (!fuzzysortRef.current) {
|
|
||||||
import("fuzzysort")
|
|
||||||
.then((m) => {
|
|
||||||
if (cancelled) return;
|
|
||||||
fuzzysortRef.current = m.default || m;
|
|
||||||
setFuzzLoaded(true);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// ignore dynamic import failures; we'll fallback to simple filtering
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const results = useMemo(() => {
|
const results = useMemo(() => {
|
||||||
let values = props.items;
|
let values = props.items;
|
||||||
if (props.resolveItem)
|
if (props.resolveItem)
|
||||||
|
|
@ -227,84 +225,13 @@ export default function AddonPage<T extends object>({
|
||||||
const items = values.filter((i) => isNotNil(i) && typeof i === "object");
|
const items = values.filter((i) => isNotNil(i) && typeof i === "object");
|
||||||
if (!search && sortFn) items.sort(sortFn);
|
if (!search && sortFn) items.sort(sortFn);
|
||||||
|
|
||||||
// Prefer fuzzysort when loaded and a search query exists
|
if (!fuzzysortReady || !fuzzysortRef.current) return [] as any[];
|
||||||
if (fuzzysortRef.current && search) {
|
|
||||||
try {
|
|
||||||
return fuzzysortRef.current.go(search, items, {
|
|
||||||
keys: props.searchKeywords,
|
|
||||||
all: true,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// If fuzzysort throws for some reason, fall back to the simple filter below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no search query, return items shaped similarly to fuzzysort results
|
return fuzzysortRef.current.go(search, items, {
|
||||||
const makeFallbackResult = (obj: any, keys: any[]) => {
|
keys: props.searchKeywords,
|
||||||
const r: any = { obj };
|
all: true,
|
||||||
// current search query in the outer scope; only highlight when there's an active query
|
|
||||||
const q = String(search ?? "")
|
|
||||||
.toLowerCase()
|
|
||||||
.trim();
|
|
||||||
for (let k = 0; k < keys.length; k++) {
|
|
||||||
const key = keys[k];
|
|
||||||
let text = "";
|
|
||||||
try {
|
|
||||||
text =
|
|
||||||
typeof key === "string" ? String(obj[key] ?? "") : String(key(obj));
|
|
||||||
} catch {
|
|
||||||
text = "";
|
|
||||||
}
|
|
||||||
r[k] = {
|
|
||||||
target: text,
|
|
||||||
highlight: (cb: any) => {
|
|
||||||
// Do not highlight anything when there is no active search.
|
|
||||||
if (!q) return [];
|
|
||||||
const lower = text.toLowerCase();
|
|
||||||
const idx = lower.indexOf(q);
|
|
||||||
if (idx === -1) return [];
|
|
||||||
// Split into before/match/after and invoke the callback for each segment so
|
|
||||||
// consumers can render matched substring(s) with highlight styling while
|
|
||||||
// preserving surrounding context.
|
|
||||||
const parts: any[] = [];
|
|
||||||
let i = 0;
|
|
||||||
const before = text.slice(0, idx);
|
|
||||||
const match = text.slice(idx, idx + q.length);
|
|
||||||
const after = text.slice(idx + q.length);
|
|
||||||
if (before) parts.push(cb(before, i++));
|
|
||||||
parts.push(cb(match, i++));
|
|
||||||
if (after) parts.push(cb(after, i++));
|
|
||||||
return parts;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
};
|
|
||||||
if (!search) {
|
|
||||||
return items.map((obj: any) =>
|
|
||||||
makeFallbackResult(obj, props.searchKeywords),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic fallback: case-insensitive substring match across configured keys
|
|
||||||
const q = String(search).toLowerCase();
|
|
||||||
const filtered = items.filter((it: any) => {
|
|
||||||
for (const key of props.searchKeywords) {
|
|
||||||
try {
|
|
||||||
const text =
|
|
||||||
typeof key === "string" ? String(it[key] ?? "") : String(key(it));
|
|
||||||
if (text.toLowerCase().includes(q)) return true;
|
|
||||||
} catch {
|
|
||||||
// ignore any accessor errors and continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
}, [props.items, sortFn, search, fuzzysortReady]);
|
||||||
return filtered.map((obj: any) =>
|
|
||||||
makeFallbackResult(obj, props.searchKeywords),
|
|
||||||
);
|
|
||||||
}, [props.items, sortFn, search, fuzzLoaded]);
|
|
||||||
|
|
||||||
const onInstallPress = useCallback(() => {
|
const onInstallPress = useCallback(() => {
|
||||||
if (!props.installAction) return () => {};
|
if (!props.installAction) return () => {};
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import {
|
||||||
TableSwitchRow,
|
TableSwitchRow,
|
||||||
Stack,
|
Stack,
|
||||||
} from "@metro/common/components";
|
} from "@metro/common/components";
|
||||||
import { semanticColors } from "@ui/color";
|
|
||||||
import { ScrollView, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
|
|
||||||
import ErrorComponentStackCard from "./ErrorComponentStackCard";
|
import ErrorComponentStackCard from "./ErrorComponentStackCard";
|
||||||
|
|
@ -74,8 +73,8 @@ export default function ErrorBoundaryScreen(props: {
|
||||||
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>
|
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>
|
||||||
<Text
|
<Text
|
||||||
variant="heading-md/extrabold"
|
variant="heading-md/extrabold"
|
||||||
|
color="text-default"
|
||||||
style={{
|
style={{
|
||||||
color: semanticColors.HEADER_PRIMARY,
|
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -20,19 +20,10 @@ export default function initSettings() {
|
||||||
render: () => import("@core/ui/settings/pages/General"),
|
render: () => import("@core/ui/settings/pages/General"),
|
||||||
useTrailing: () => `(${version})`,
|
useTrailing: () => `(${version})`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "CORE_PLUGINS",
|
|
||||||
title: () => Strings.PLUGINS,
|
|
||||||
icon: findAssetId("ActivitiesIcon"),
|
|
||||||
render: () => import("@core/ui/settings/pages/Plugins/CorePlugins"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "BUNNY_PLUGINS",
|
key: "BUNNY_PLUGINS",
|
||||||
title: () => "External Plugins",
|
title: () => Strings.PLUGINS,
|
||||||
icon:
|
icon: findAssetId("PuzzlePieceIcon"),
|
||||||
findAssetId("DownloadIcon") ??
|
|
||||||
findAssetId("LinkIcon") ??
|
|
||||||
findAssetId("ActivitiesIcon"),
|
|
||||||
render: () => import("@core/ui/settings/pages/Plugins"),
|
render: () => import("@core/ui/settings/pages/Plugins"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,111 @@
|
||||||
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
|
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
|
||||||
import { iterateAssets } from "@lib/api/assets";
|
import { iterateAssets } from "@lib/api/assets";
|
||||||
import { Text } from "@metro/common/components";
|
import { findAssetId } from "@lib/api/assets";
|
||||||
|
import { Strings } from "@core/i18n";
|
||||||
|
import { IconButton, TableCheckboxRow, TableRowGroup, ActionSheet, BottomSheetTitleHeader } from "@metro/common/components";
|
||||||
import { ErrorBoundary, Search } from "@ui/components";
|
import { ErrorBoundary, Search } from "@ui/components";
|
||||||
import { useMemo } from "react";
|
import { showSheet } from "@lib/ui/sheets";
|
||||||
|
import { useProxy, createProxy } from "@core/vendetta/storage";
|
||||||
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { FlatList, View } from "react-native";
|
import { FlatList, View } from "react-native";
|
||||||
|
|
||||||
|
const IMAGE_FILES = [
|
||||||
|
{ id: "png", label: "PNG", defaultEnabled: true },
|
||||||
|
{ id: "jpg", label: "JPG", defaultEnabled: true },
|
||||||
|
{ id: "jpeg", label: "JPEG", defaultEnabled: true },
|
||||||
|
{ id: "svg", label: "SVG", defaultEnabled: true },
|
||||||
|
{ id: "gif", label: "GIF", defaultEnabled: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TEXT_FILES = [
|
||||||
|
{ id: "jsona", label: "JSONA", defaultEnabled: false },
|
||||||
|
{ id: "json", label: "JSON", defaultEnabled: false },
|
||||||
|
{ id: "lottie", label: "Lottie", defaultEnabled: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ALL_FILE_TYPES = [...IMAGE_FILES, ...TEXT_FILES];
|
||||||
|
|
||||||
|
// Create initial enabled filters state
|
||||||
|
const createInitialFilters = () => {
|
||||||
|
const filters: Record<string, boolean> = {};
|
||||||
|
ALL_FILE_TYPES.forEach(type => {
|
||||||
|
filters[type.id] = type.defaultEnabled;
|
||||||
|
});
|
||||||
|
return filters;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { proxy: assetBrowserStorage } = createProxy({
|
||||||
|
enabledFilters: createInitialFilters()
|
||||||
|
});
|
||||||
|
|
||||||
export default function AssetBrowser() {
|
export default function AssetBrowser() {
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = useState("");
|
||||||
const all = useMemo(() => Array.from(iterateAssets()), []);
|
const [updateTick, setUpdateTick] = useState(0);
|
||||||
|
|
||||||
|
// Use useProxy to make the state reactive
|
||||||
|
useProxy(assetBrowserStorage);
|
||||||
|
|
||||||
|
const getFilteredAssets = useCallback(() => {
|
||||||
|
return Array.from(iterateAssets()).filter((asset) => {
|
||||||
|
const type = String(asset.type ?? "").toLowerCase();
|
||||||
|
return assetBrowserStorage.enabledFilters[type] === true;
|
||||||
|
});
|
||||||
|
}, [updateTick]);
|
||||||
|
|
||||||
|
const all = useMemo(() => getFilteredAssets(), [getFilteredAssets]);
|
||||||
|
|
||||||
|
const toggleFilter = (filterId: string) => {
|
||||||
|
assetBrowserStorage.enabledFilters[filterId] = !assetBrowserStorage.enabledFilters[filterId];
|
||||||
|
setUpdateTick(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterPress = () => {
|
||||||
|
showSheet("AssetBrowserFilter", () => {
|
||||||
|
useProxy(assetBrowserStorage);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionSheet>
|
||||||
|
<BottomSheetTitleHeader title={Strings.ASSET_TYPES} />
|
||||||
|
<TableRowGroup title={Strings.IMAGE_FILES}>
|
||||||
|
{IMAGE_FILES.map(fileType => (
|
||||||
|
<TableCheckboxRow
|
||||||
|
key={fileType.id}
|
||||||
|
label={fileType.label}
|
||||||
|
checked={assetBrowserStorage.enabledFilters[fileType.id] === true}
|
||||||
|
onPress={() => toggleFilter(fileType.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableRowGroup>
|
||||||
|
<TableRowGroup title={Strings.TEXT_FILES}>
|
||||||
|
{TEXT_FILES.map(fileType => (
|
||||||
|
<TableCheckboxRow
|
||||||
|
key={fileType.id}
|
||||||
|
label={fileType.label}
|
||||||
|
checked={assetBrowserStorage.enabledFilters[fileType.id] === true}
|
||||||
|
onPress={() => toggleFilter(fileType.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableRowGroup>
|
||||||
|
</ActionSheet>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Search
|
<View style={{ flexDirection: "row", margin: 10, gap: 8, alignItems: "center" }}>
|
||||||
style={{ margin: 10 }}
|
<Search
|
||||||
onChangeText={(v: string) => setSearch(v)}
|
style={{ flex: 1 }}
|
||||||
/>
|
isRound
|
||||||
|
onChangeText={(v: string) => setSearch(v)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={findAssetId("FileIcon")}
|
||||||
|
variant="tertiary"
|
||||||
|
onPress={handleFilterPress}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -25,18 +115,9 @@ export default function AssetBrowser() {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
|
||||||
variant="text-sm/medium"
|
|
||||||
style={{
|
|
||||||
marginBottom: 16,
|
|
||||||
color: "var(--text-muted)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Some assets types cannot be displayed and will be marked in red.
|
|
||||||
</Text>
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={all.filter(
|
data={all.filter(
|
||||||
(a) => a.name.includes(search) || a.id.toString() === search,
|
(a) => (a.name.includes(search) || a.id.toString() === search),
|
||||||
)}
|
)}
|
||||||
renderItem={({ item }: any) => <AssetDisplay asset={item} />}
|
renderItem={({ item }: any) => <AssetDisplay asset={item} />}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ import {
|
||||||
connectToDebugger,
|
connectToDebugger,
|
||||||
disconnectFromDebugger,
|
disconnectFromDebugger,
|
||||||
isConnectedToDebugger,
|
isConnectedToDebugger,
|
||||||
|
connectRdt,
|
||||||
|
disconnectRdt,
|
||||||
|
useIsRdtConnected,
|
||||||
} from "@lib/api/debug";
|
} from "@lib/api/debug";
|
||||||
import {
|
import {
|
||||||
getReactDevToolsProp,
|
getReactDevToolsProp,
|
||||||
|
|
@ -33,7 +36,7 @@ import { ErrorBoundary } from "@ui/components";
|
||||||
import ErrorBoundaryScreen from "@core/ui/reporter/components/ErrorBoundaryScreen";
|
import ErrorBoundaryScreen from "@core/ui/reporter/components/ErrorBoundaryScreen";
|
||||||
import { createStyles, TextStyleSheet } from "@ui/styles";
|
import { createStyles, TextStyleSheet } from "@ui/styles";
|
||||||
import { NativeModules } from "react-native";
|
import { NativeModules } from "react-native";
|
||||||
import { ScrollView, StyleSheet } from "react-native";
|
import { ScrollView } from "react-native";
|
||||||
import { showToast } from "@ui/toasts";
|
import { showToast } from "@ui/toasts";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
|
@ -72,6 +75,7 @@ export default function Developer() {
|
||||||
const [isDebuggerConnected, setIsDebuggerConnected] = useState(
|
const [isDebuggerConnected, setIsDebuggerConnected] = useState(
|
||||||
isConnectedToDebugger(),
|
isConnectedToDebugger(),
|
||||||
);
|
);
|
||||||
|
const isRdtConnected = useIsRdtConnected();
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const navigation = NavigationNative.useNavigation();
|
const navigation = NavigationNative.useNavigation();
|
||||||
|
|
@ -96,6 +100,18 @@ export default function Developer() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReactDevToolsConnect = () => {
|
||||||
|
if (isRdtConnected) {
|
||||||
|
disconnectRdt();
|
||||||
|
} else {
|
||||||
|
if (!settings.devToolsUrl?.trim()) {
|
||||||
|
showToast("Invalid devTools URL!", findAssetId("Small"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectRdt(settings.devToolsUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|
@ -106,108 +122,90 @@ export default function Developer() {
|
||||||
style={{ paddingVertical: 24, paddingHorizontal: 12 }}
|
style={{ paddingVertical: 24, paddingHorizontal: 12 }}
|
||||||
spacing={24}
|
spacing={24}
|
||||||
>
|
>
|
||||||
<TableRowGroup title={Strings.DEBUGGER_URL}>
|
<Stack spacing={4}>
|
||||||
<TextInput
|
<TableRowGroup title={Strings.DEBUGGER_URL}>
|
||||||
placeholder="127.0.0.1:9090"
|
<Stack spacing={4}>
|
||||||
size="md"
|
<TextInput
|
||||||
leadingIcon={() => (
|
placeholder="127.0.0.1:9090"
|
||||||
<LegacyFormText style={styles.leadingText}>
|
size="md"
|
||||||
ws://
|
leadingIcon={() => (
|
||||||
</LegacyFormText>
|
<LegacyFormText style={styles.leadingText}>
|
||||||
)}
|
ws://
|
||||||
defaultValue={settings.debuggerUrl}
|
</LegacyFormText>
|
||||||
onChange={(v: string) => (settings.debuggerUrl = v)}
|
)}
|
||||||
/>
|
defaultValue={settings.debuggerUrl}
|
||||||
<TableSwitchRow
|
onChange={(v: string) => (settings.debuggerUrl = v)}
|
||||||
label={Strings.AUTO_DEBUGGER}
|
/>
|
||||||
icon={<TableRow.Icon source={findAssetId("copy")} />}
|
</Stack>
|
||||||
value={settings.autoDebugger}
|
</TableRowGroup>
|
||||||
onValueChange={(v: boolean) => {
|
|
||||||
settings.autoDebugger = v;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TableRow
|
|
||||||
label={Strings.CONNECT_TO_DEBUG_WEBSOCKET}
|
|
||||||
subLabel="Connect to Chrome DevTools for debugging"
|
|
||||||
icon={<TableRow.Icon source={findAssetId("WrenchIcon")} />}
|
|
||||||
onPress={() => connectToDebugger(settings.debuggerUrl)}
|
|
||||||
/>
|
|
||||||
</TableRowGroup>
|
|
||||||
|
|
||||||
{isReactDevToolsPreloaded() && (
|
<TableRowGroup>
|
||||||
<TableRowGroup title="React Development">
|
|
||||||
<TextInput
|
|
||||||
placeholder="127.0.0.1:8097"
|
|
||||||
size="md"
|
|
||||||
leadingIcon={() => (
|
|
||||||
<LegacyFormText style={styles.leadingText}>
|
|
||||||
ws://
|
|
||||||
</LegacyFormText>
|
|
||||||
)}
|
|
||||||
defaultValue={settings.devToolsUrl}
|
|
||||||
onChange={(v: string) => (settings.devToolsUrl = v)}
|
|
||||||
/>
|
|
||||||
<TableSwitchRow
|
<TableSwitchRow
|
||||||
label={Strings.AUTO_DEVTOOLS}
|
label={Strings.AUTO_DEBUGGER}
|
||||||
icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />}
|
icon={<TableRow.Icon source={findAssetId("copy")} />}
|
||||||
value={settings.autoDevTools}
|
value={settings.autoDebugger}
|
||||||
onValueChange={(v: boolean) => {
|
onValueChange={(v: boolean) => {
|
||||||
settings.autoDevTools = v;
|
settings.autoDebugger = v;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TableRow
|
<TableRow
|
||||||
label={Strings.CONNECT_TO_REACT_DEVTOOLS}
|
label={Strings.CONNECT_TO_DEBUG_WEBSOCKET}
|
||||||
subLabel="Connect React DevTools for component debugging"
|
subLabel="Connect DevTools for debugging"
|
||||||
icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />}
|
icon={<TableRow.Icon source={findAssetId("WrenchIcon")} />}
|
||||||
onPress={async () => {
|
onPress={handleDebuggerConnect}
|
||||||
if (!settings.devToolsUrl?.trim()) {
|
|
||||||
showToast("Invalid devTools URL!", findAssetId("Small"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const devTools =
|
|
||||||
window[getReactDevToolsProp() || "__vendetta_rdc"];
|
|
||||||
|
|
||||||
if (!devTools?.connectToDevTools) {
|
|
||||||
showToast(
|
|
||||||
"React DevTools not available",
|
|
||||||
findAssetId("Small"),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await devTools.connectToDevTools({
|
|
||||||
host: settings.devToolsUrl.split(":")?.[0],
|
|
||||||
resolveRNStyle: StyleSheet.flatten,
|
|
||||||
});
|
|
||||||
showToast(
|
|
||||||
"Connected to React DevTools!",
|
|
||||||
findAssetId("Check"),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
showToast(
|
|
||||||
"Failed to connect to React DevTools",
|
|
||||||
findAssetId("Small"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</TableRowGroup>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{isLoaderConfigSupported() && isVendettaLoader() && (
|
{isReactDevToolsPreloaded() && (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<TableRowGroup title="React Development">
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<TextInput
|
||||||
|
placeholder="127.0.0.1:8097"
|
||||||
|
size="md"
|
||||||
|
leadingIcon={() => (
|
||||||
|
<LegacyFormText style={styles.leadingText}>
|
||||||
|
ws://
|
||||||
|
</LegacyFormText>
|
||||||
|
)}
|
||||||
|
defaultValue={settings.devToolsUrl}
|
||||||
|
onChange={(v: string) => (settings.devToolsUrl = v)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</TableRowGroup>
|
||||||
|
|
||||||
|
<TableRowGroup>
|
||||||
<TableSwitchRow
|
<TableSwitchRow
|
||||||
label={Strings.LOAD_REACT_DEVTOOLS}
|
label={Strings.AUTO_DEVTOOLS}
|
||||||
subLabel={`${Strings.VERSION}: ${getReactDevToolsVersion()}`}
|
icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />}
|
||||||
icon={
|
value={settings.autoDevTools}
|
||||||
<TableRow.Icon source={findAssetId("ic_badge_staff")} />
|
|
||||||
}
|
|
||||||
value={loaderConfig.loadReactDevTools}
|
|
||||||
onValueChange={(v: boolean) => {
|
onValueChange={(v: boolean) => {
|
||||||
loaderConfig.loadReactDevTools = v;
|
settings.autoDevTools = v;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
<TableRow
|
||||||
</TableRowGroup>
|
label={Strings.CONNECT_TO_REACT_DEVTOOLS}
|
||||||
|
subLabel="Connect React DevTools for component debugging"
|
||||||
|
icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />}
|
||||||
|
onPress={handleReactDevToolsConnect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoaderConfigSupported() && isVendettaLoader() && (
|
||||||
|
<TableSwitchRow
|
||||||
|
label={Strings.LOAD_REACT_DEVTOOLS}
|
||||||
|
subLabel={`${Strings.VERSION}: ${getReactDevToolsVersion()}`}
|
||||||
|
icon={
|
||||||
|
<TableRow.Icon source={findAssetId("ic_badge_staff")} />
|
||||||
|
}
|
||||||
|
value={loaderConfig.loadReactDevTools}
|
||||||
|
onValueChange={(v: boolean) => {
|
||||||
|
loaderConfig.loadReactDevTools = v;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableRowGroup>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoaderConfigSupported() && (
|
{isLoaderConfigSupported() && (
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@ import {
|
||||||
Text,
|
Text,
|
||||||
} from "@metro/common/components";
|
} from "@metro/common/components";
|
||||||
import { findByProps } from "@metro";
|
import { findByProps } from "@metro";
|
||||||
import { useMemo, useState, useEffect } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { PixelRatio, View } from "react-native";
|
import { PixelRatio, View } from "react-native";
|
||||||
|
import { WebView } from "react-native-webview";
|
||||||
import previewHtml from "./preview.html";
|
import previewHtml from "./preview.html";
|
||||||
|
|
||||||
import FontEditor from "./FontEditor";
|
import FontEditor from "./FontEditor";
|
||||||
|
|
@ -35,7 +36,6 @@ const useStyles = createStyles({
|
||||||
|
|
||||||
function FontPreview({ font }: { font: FontDefinition }) {
|
function FontPreview({ font }: { font: FontDefinition }) {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const [WebViewComponent, setWebViewComponent] = useState<any>(null);
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const TEXT_DEFAULT = useToken(tokens.colors.TEXT_DEFAULT);
|
const TEXT_DEFAULT = useToken(tokens.colors.TEXT_DEFAULT);
|
||||||
|
|
@ -53,61 +53,25 @@ function FontPreview({ font }: { font: FontDefinition }) {
|
||||||
[font.main, fontFamily, fontSize, TEXT_DEFAULT],
|
[font.main, fontFamily, fontSize, TEXT_DEFAULT],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize generated HTML to avoid string replacement on every render
|
|
||||||
const html = useMemo(
|
|
||||||
() => previewHtml.replace("$$props", JSON.stringify(props)),
|
|
||||||
[props],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Lazy-load the WebView implementation so it's not included in the main bundle
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
import("react-native-webview")
|
|
||||||
.then((m) => {
|
|
||||||
if (cancelled) return;
|
|
||||||
// support both default export and named WebView export
|
|
||||||
setWebViewComponent(m.WebView || m.default || m);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// ignore dynamic import failures; component will show placeholder
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ width: "100%", height: 64 }}>
|
<View style={{ width: "100%", height: 64 }}>
|
||||||
{WebViewComponent ? (
|
<WebView
|
||||||
<>
|
onMessage={() => setLoaded(true)}
|
||||||
<WebViewComponent
|
source={{
|
||||||
onMessage={() => setLoaded(true)}
|
html: previewHtml.replace("$$props", JSON.stringify(props)),
|
||||||
source={{ html }}
|
}}
|
||||||
javaScriptEnabled
|
javaScriptEnabled
|
||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
style={[
|
style={[
|
||||||
styles.full,
|
styles.full,
|
||||||
{ backgroundColor: "transparent", opacity: Number(loaded) },
|
{ backgroundColor: "transparent", opacity: Number(loaded) },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!loaded && (
|
{!loaded && (
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.full,
|
|
||||||
{ justifyContent: "center", alignItems: "center" },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text color="text-muted" variant="heading-lg/semibold">
|
|
||||||
Loading...
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.full,
|
styles.full,
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
>
|
>
|
||||||
Jolly your discord client!
|
💦Goon💦 your discord client!
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,19 @@ export default function General() {
|
||||||
const navigation = NavigationNative.useNavigation();
|
const navigation = NavigationNative.useNavigation();
|
||||||
|
|
||||||
// Custom Community Card Button
|
// Custom Community Card Button
|
||||||
const CommunityCardButton = ({ icon, label, subLabel, color, onPress }) => (
|
const CommunityCardButton = ({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
subLabel,
|
||||||
|
color,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
icon?: number | { uri: string };
|
||||||
|
label: string;
|
||||||
|
subLabel?: string | null;
|
||||||
|
color?: string;
|
||||||
|
onPress?: () => void;
|
||||||
|
}) => (
|
||||||
<TouchableOpacity style={{ flex: 1 }} onPress={onPress} activeOpacity={0.7}>
|
<TouchableOpacity style={{ flex: 1 }} onPress={onPress} activeOpacity={0.7}>
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -60,7 +72,7 @@ export default function General() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableRow.Icon
|
<TableRow.Icon
|
||||||
source={icon}
|
source={icon as any}
|
||||||
style={{
|
style={{
|
||||||
tintColor: "#FFFFFF",
|
tintColor: "#FFFFFF",
|
||||||
width: 24,
|
width: 24,
|
||||||
|
|
@ -113,8 +125,8 @@ export default function General() {
|
||||||
>
|
>
|
||||||
<TableRowGroup title="App Information">
|
<TableRowGroup title="App Information">
|
||||||
<TableRow
|
<TableRow
|
||||||
label="ShiggyCold"
|
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} />}
|
||||||
/>
|
/>
|
||||||
<TableRow
|
<TableRow
|
||||||
|
|
@ -132,7 +144,7 @@ export default function General() {
|
||||||
subLabel={`${debugInfo.bunny.loader.name} loader`}
|
subLabel={`${debugInfo.bunny.loader.name} loader`}
|
||||||
icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />}
|
icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />}
|
||||||
trailing={
|
trailing={
|
||||||
<TableRow.TrailingText text={debugInfo.bunny.loader.version} />
|
<TableRow.TrailingText text={debugInfo.bunny.loader.version ?? ""} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</TableRowGroup>
|
</TableRowGroup>
|
||||||
|
|
@ -188,7 +200,7 @@ export default function General() {
|
||||||
<TableSwitchRow
|
<TableSwitchRow
|
||||||
label={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS}
|
label={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS}
|
||||||
subLabel={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS_DESC}
|
subLabel={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS_DESC}
|
||||||
icon={<TableRow.Icon source={findAssetId("FlaskIcon")} />}
|
icon={<TableRow.Icon source={findAssetId("StaffBadgeIcon")} />}
|
||||||
value={settings.enableDiscordDeveloperSettings}
|
value={settings.enableDiscordDeveloperSettings}
|
||||||
onValueChange={(v: boolean) => {
|
onValueChange={(v: boolean) => {
|
||||||
settings.enableDiscordDeveloperSettings = v;
|
settings.enableDiscordDeveloperSettings = v;
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import { React } from "@metro/common";
|
|
||||||
import AddonPage from "@core/ui/components/AddonPage";
|
|
||||||
import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
|
|
||||||
import { corePluginInstances, registeredPlugins } from "@lib/addons/plugins";
|
|
||||||
import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models";
|
|
||||||
import unifyBunnyPlugin from "@core/ui/settings/pages/Plugins/models/bunny";
|
|
||||||
import { Strings } from "@core/i18n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core plugins settings page
|
|
||||||
*
|
|
||||||
* This page shows only the built-in core plugins. Core plugins are always
|
|
||||||
* installed (can't be uninstalled) and are registered in `corePluginInstances`.
|
|
||||||
*
|
|
||||||
* We reuse the existing `AddonPage` + `PluginCard` components and feed them
|
|
||||||
* the normalized `UnifiedPluginModel` objects produced by `unifyBunnyPlugin`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function CorePlugins() {
|
|
||||||
return (
|
|
||||||
<AddonPage<UnifiedPluginModel>
|
|
||||||
CardComponent={PluginCard}
|
|
||||||
title={Strings.PLUGINS}
|
|
||||||
searchKeywords={[
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
(p) =>
|
|
||||||
p.authors
|
|
||||||
?.map((a: any) => (typeof a === "string" ? a : a.name))
|
|
||||||
.join() || "",
|
|
||||||
]}
|
|
||||||
// Provide simple sort options so the Search input renders round and a sort menu appears
|
|
||||||
sortOptions={{
|
|
||||||
"Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
|
|
||||||
"Name (Z-A)": (a, b) => b.name.localeCompare(a.name),
|
|
||||||
}}
|
|
||||||
// Only core plugins: map corePluginInstances -> registeredPlugins -> unify
|
|
||||||
items={[...corePluginInstances.keys()]
|
|
||||||
.map((id) => registeredPlugins.get(id))
|
|
||||||
.filter((m): m is any => m !== undefined)
|
|
||||||
.map(unifyBunnyPlugin)}
|
|
||||||
ListHeaderComponent={() => null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -11,10 +11,11 @@ import {
|
||||||
Text,
|
Text,
|
||||||
} from "@metro/common/components";
|
} from "@metro/common/components";
|
||||||
import { showSheet } from "@ui/sheets";
|
import { showSheet } from "@ui/sheets";
|
||||||
import chroma from "chroma-js";
|
|
||||||
import { createContext, useContext, useMemo } from "react";
|
import { createContext, useContext, useMemo } from "react";
|
||||||
import { Image, View } from "react-native";
|
import { Image, View } from "react-native";
|
||||||
|
|
||||||
|
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
|
||||||
|
|
||||||
const CardContext = createContext<{
|
const CardContext = createContext<{
|
||||||
plugin: UnifiedPluginModel;
|
plugin: UnifiedPluginModel;
|
||||||
result: Fuzzysort.KeysResult<UnifiedPluginModel>;
|
result: Fuzzysort.KeysResult<UnifiedPluginModel>;
|
||||||
|
|
@ -22,7 +23,7 @@ const CardContext = createContext<{
|
||||||
const useCardContext = () => useContext(CardContext);
|
const useCardContext = () => useContext(CardContext);
|
||||||
|
|
||||||
function getHighlightColor(): import("react-native").ColorValue {
|
function getHighlightColor(): import("react-native").ColorValue {
|
||||||
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
|
return getChroma()(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
|
||||||
}
|
}
|
||||||
|
|
||||||
function Title() {
|
function Title() {
|
||||||
|
|
|
||||||
|
|
@ -702,7 +702,7 @@ export default function Plugins() {
|
||||||
// Use the insertion order of pluginSettings so newly-installed plugins
|
// Use the insertion order of pluginSettings so newly-installed plugins
|
||||||
// (which add entries to pluginSettings) appear first.
|
// (which add entries to pluginSettings) appear first.
|
||||||
const installedBnIds = Object.keys(pluginSettings || {})
|
const installedBnIds = Object.keys(pluginSettings || {})
|
||||||
.filter((id) => registeredPlugins.has(id) && !isCorePlugin(id))
|
.filter((id) => registeredPlugins.has(id))
|
||||||
.slice()
|
.slice()
|
||||||
.reverse();
|
.reverse();
|
||||||
|
|
||||||
|
|
@ -710,12 +710,12 @@ export default function Plugins() {
|
||||||
.map((id) => registeredPlugins.get(id)!)
|
.map((id) => registeredPlugins.get(id)!)
|
||||||
.map(unifyBunnyPlugin);
|
.map(unifyBunnyPlugin);
|
||||||
|
|
||||||
// Merge lists: show Vendetta-managed plugins first (recent first),
|
// Merge lists: show Vendetta-managed plugins first,
|
||||||
// then Bunny-installed externals (recent first).
|
// then Bunny-installed externals (recent first).
|
||||||
const allPlugins = [...vdPlugins, ...bnPlugins];
|
const allPlugins = [...vdPlugins, ...bnPlugins];
|
||||||
|
|
||||||
// Filter out core plugins from the list
|
// Include core plugins in the list
|
||||||
return allPlugins.filter((plugin) => !isCorePlugin(plugin.id));
|
return allPlugins;
|
||||||
}}
|
}}
|
||||||
ListHeaderComponent={() => null}
|
ListHeaderComponent={() => null}
|
||||||
installAction={{
|
installAction={{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { Strings } from "@core/i18n";
|
|
||||||
import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard";
|
import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard";
|
||||||
import { VdThemeInfo, themes, selectTheme } from "@lib/addons/themes";
|
import { VdThemeInfo, themes, selectTheme } from "@lib/addons/themes";
|
||||||
import { findAssetId } from "@lib/api/assets";
|
import { findAssetId } from "@lib/api/assets";
|
||||||
|
|
@ -36,8 +35,6 @@ export default function ThemeCard({ item: theme }: CardWrapper<VdThemeInfo>) {
|
||||||
{
|
{
|
||||||
icon: "CircleInformationIcon-primary",
|
icon: "CircleInformationIcon-primary",
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
// Using dynamic import directly (without wrapping in function)
|
|
||||||
// This returns a Promise that resolves to the module
|
|
||||||
const importPromise = import("./sheets/ThemeInfoActionSheet");
|
const importPromise = import("./sheets/ThemeInfoActionSheet");
|
||||||
showSheet("ThemeInfoActionSheet", importPromise, {
|
showSheet("ThemeInfoActionSheet", importPromise, {
|
||||||
theme,
|
theme,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import { settings } from "@lib/api/settings";
|
||||||
import { useObservable } from "@lib/api/storage";
|
import { useObservable } from "@lib/api/storage";
|
||||||
import { lazyDestructure } from "@lib/utils/lazy";
|
import { lazyDestructure } from "@lib/utils/lazy";
|
||||||
import safeFetch from "@lib/utils/safeFetch";
|
import safeFetch from "@lib/utils/safeFetch";
|
||||||
import Search from "@ui/components/Search";
|
|
||||||
import { findByProps } from "@metro";
|
import { findByProps } from "@metro";
|
||||||
import { NavigationNative, React, clipboard } from "@metro/common";
|
import { NavigationNative, React, clipboard } from "@metro/common";
|
||||||
import { showSheet } from "@lib/ui/sheets";
|
import { showSheet } from "@lib/ui/sheets";
|
||||||
|
|
@ -26,16 +25,12 @@ import {
|
||||||
BottomSheetTitleHeader,
|
BottomSheetTitleHeader,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
FlashList,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Stack,
|
Stack,
|
||||||
TableRowGroup,
|
TableRowGroup,
|
||||||
TableSwitchRow,
|
TableCheckboxRow,
|
||||||
TableRowIcon,
|
TableRowIcon,
|
||||||
Text,
|
Text,
|
||||||
AlertModal,
|
|
||||||
AlertActions,
|
|
||||||
AlertActionButton,
|
|
||||||
} from "@metro/common/components";
|
} from "@metro/common/components";
|
||||||
import { View, Image } from "react-native";
|
import { View, Image } from "react-native";
|
||||||
|
|
||||||
|
|
@ -296,9 +291,7 @@ function BrowserThemeCard({
|
||||||
loading={isInstalling}
|
loading={isInstalling}
|
||||||
icon={
|
icon={
|
||||||
isInstalling ? (
|
isInstalling ? (
|
||||||
<View
|
<View style={{ transform: [{ rotate: `${rotation}deg` }] }}>
|
||||||
style={{ transform: [{ rotate: `${rotation}deg` }] }}
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
source={findAssetId("RetryIcon")}
|
source={findAssetId("RetryIcon")}
|
||||||
style={{ width: 16, height: 16 }}
|
style={{ width: 16, height: 16 }}
|
||||||
|
|
@ -357,11 +350,6 @@ function BrowseThemePage(props: BrowseThemePageProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Theme options have been changed from radio groups to individual switches
|
|
||||||
* for better UX. Each option can now be toggled independently.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function Themes() {
|
export default function Themes() {
|
||||||
useProxy(settings);
|
useProxy(settings);
|
||||||
useProxy(themes);
|
useProxy(themes);
|
||||||
|
|
@ -410,15 +398,15 @@ export default function Themes() {
|
||||||
<ActionSheet>
|
<ActionSheet>
|
||||||
<BottomSheetTitleHeader title="Options" />
|
<BottomSheetTitleHeader title="Options" />
|
||||||
<View style={{ paddingVertical: 20, gap: 12 }}>
|
<View style={{ paddingVertical: 20, gap: 12 }}>
|
||||||
|
{/* Changed from TableRadioGroup to individual TableSwitchRow components
|
||||||
|
for better UX - users can now toggle options individually */}
|
||||||
<TableRowGroup title="Override Theme Type">
|
<TableRowGroup title="Override Theme Type">
|
||||||
<TableSwitchRow
|
<TableCheckboxRow
|
||||||
label="Auto"
|
label="Auto"
|
||||||
icon={<TableRowIcon source={findAssetId("RobotIcon")} />}
|
icon={<TableRowIcon source={findAssetId("RobotIcon")} />}
|
||||||
value={!colorsPref.type}
|
checked={!colorsPref.type}
|
||||||
onValueChange={(enabled: boolean) => {
|
onPress={() => {
|
||||||
if (enabled) {
|
if (!colorsPref.type) {
|
||||||
colorsPref.type = undefined;
|
|
||||||
} else {
|
|
||||||
colorsPref.type = "dark";
|
colorsPref.type = "dark";
|
||||||
}
|
}
|
||||||
getCurrentTheme()?.data &&
|
getCurrentTheme()?.data &&
|
||||||
|
|
@ -427,28 +415,24 @@ export default function Themes() {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TableSwitchRow
|
<TableCheckboxRow
|
||||||
label="Dark"
|
label="Dark"
|
||||||
icon={
|
icon={<TableRowIcon source={findAssetId("ThemeDarkIcon")} />}
|
||||||
<TableRowIcon source={findAssetId("ThemeDarkIcon")} />
|
checked={colorsPref.type === "dark"}
|
||||||
}
|
onPress={() => {
|
||||||
value={colorsPref.type === "dark"}
|
colorsPref.type = colorsPref.type === "dark" ? undefined : "dark";
|
||||||
onValueChange={(enabled: boolean) => {
|
|
||||||
colorsPref.type = enabled ? "dark" : undefined;
|
|
||||||
getCurrentTheme()?.data &&
|
getCurrentTheme()?.data &&
|
||||||
updateBunnyColor(getCurrentTheme()!.data!, {
|
updateBunnyColor(getCurrentTheme()!.data!, {
|
||||||
update: true,
|
update: true,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TableSwitchRow
|
<TableCheckboxRow
|
||||||
label="Light"
|
label="Light"
|
||||||
icon={
|
icon={<TableRowIcon source={findAssetId("ThemeLightIcon")} />}
|
||||||
<TableRowIcon source={findAssetId("ThemeLightIcon")} />
|
checked={colorsPref.type === "light"}
|
||||||
}
|
onPress={() => {
|
||||||
value={colorsPref.type === "light"}
|
colorsPref.type = colorsPref.type === "light" ? undefined : "light";
|
||||||
onValueChange={(enabled: boolean) => {
|
|
||||||
colorsPref.type = enabled ? "light" : undefined;
|
|
||||||
getCurrentTheme()?.data &&
|
getCurrentTheme()?.data &&
|
||||||
updateBunnyColor(getCurrentTheme()!.data!, {
|
updateBunnyColor(getCurrentTheme()!.data!, {
|
||||||
update: true,
|
update: true,
|
||||||
|
|
@ -457,20 +441,13 @@ export default function Themes() {
|
||||||
/>
|
/>
|
||||||
</TableRowGroup>
|
</TableRowGroup>
|
||||||
<TableRowGroup title="Chat Background">
|
<TableRowGroup title="Chat Background">
|
||||||
<TableSwitchRow
|
<TableCheckboxRow
|
||||||
label="Show Background"
|
label="Show Background"
|
||||||
|
subLabel="Enable or disable themes background on chat"
|
||||||
icon={<TableRowIcon source={findAssetId("ImageIcon")} />}
|
icon={<TableRowIcon source={findAssetId("ImageIcon")} />}
|
||||||
value={!colorsPref.customBackground}
|
checked={!colorsPref.customBackground}
|
||||||
onValueChange={(enabled: boolean) => {
|
onPress={() => {
|
||||||
colorsPref.customBackground = enabled ? null : "hidden";
|
colorsPref.customBackground = !colorsPref.customBackground ? "hidden" : null;
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TableSwitchRow
|
|
||||||
label="Hide Background"
|
|
||||||
icon={<TableRowIcon source={findAssetId("DenyIcon")} />}
|
|
||||||
value={colorsPref.customBackground === "hidden"}
|
|
||||||
onValueChange={(enabled: boolean) => {
|
|
||||||
colorsPref.customBackground = enabled ? "hidden" : null;
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableRowGroup>
|
</TableRowGroup>
|
||||||
|
|
@ -479,7 +456,7 @@ export default function Themes() {
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
|
|
@ -677,6 +654,7 @@ export default function Themes() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normal browse listing
|
||||||
return (
|
return (
|
||||||
<BrowseThemePage
|
<BrowseThemePage
|
||||||
themes={themesList}
|
themes={themesList}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,7 @@ import React, { ComponentProps, useEffect, useState } from "react";
|
||||||
import { ScrollView, View, TouchableOpacity } from "react-native";
|
import { ScrollView, View, TouchableOpacity } from "react-native";
|
||||||
import { showToast } from "@ui/toasts";
|
import { showToast } from "@ui/toasts";
|
||||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||||
import {
|
import { fetchTheme, removeTheme, selectTheme, VdThemeInfo, } from "@lib/addons/themes";
|
||||||
fetchTheme,
|
|
||||||
removeTheme,
|
|
||||||
selectTheme,
|
|
||||||
VdThemeInfo,
|
|
||||||
} from "@lib/addons/themes";
|
|
||||||
import { formatString, Strings } from "@core/i18n";
|
import { formatString, Strings } from "@core/i18n";
|
||||||
import { semanticColors } from "@ui/color";
|
import { semanticColors } from "@ui/color";
|
||||||
|
|
||||||
|
|
@ -31,7 +26,7 @@ function ThemeInfoIconButton(props: ComponentProps<typeof IconButton>) {
|
||||||
return <IconButton {...props} label={props.label} />;
|
return <IconButton {...props} label={props.label} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme title component for header
|
// theme title component for header
|
||||||
function TitleComponent({ theme }: { theme: VdThemeInfo }) {
|
function TitleComponent({ theme }: { theme: VdThemeInfo }) {
|
||||||
const { authors } = theme.data;
|
const { authors } = theme.data;
|
||||||
|
|
||||||
|
|
@ -68,11 +63,11 @@ export default function ThemeInfoActionSheet({
|
||||||
theme,
|
theme,
|
||||||
navigation,
|
navigation,
|
||||||
}: ThemeInfoActionSheetProps) {
|
}: ThemeInfoActionSheetProps) {
|
||||||
// Use component state to track theme data
|
// use component state to track theme data
|
||||||
const [themeState, setThemeState] = useState({ ...theme });
|
const [themeState, setThemeState] = useState({ ...theme });
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// Periodic refresh to detect external changes to theme state
|
// periodic refresh to detect external changes to theme state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setThemeState({ ...theme });
|
setThemeState({ ...theme });
|
||||||
|
|
@ -159,31 +154,25 @@ export default function ThemeInfoActionSheet({
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: 16,
|
gap: 65,
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThemeInfoIconButton
|
<ThemeInfoIconButton
|
||||||
label="Apply"
|
label={Strings.REFETCH}
|
||||||
variant="secondary"
|
|
||||||
icon={findAssetId("CheckmarkLargeIcon")}
|
|
||||||
onPress={applyTheme}
|
|
||||||
/>
|
|
||||||
<ThemeInfoIconButton
|
|
||||||
label="Refetch"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={findAssetId("RetryIcon")}
|
icon={findAssetId("RetryIcon")}
|
||||||
onPress={refetchTheme}
|
onPress={refetchTheme}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<ThemeInfoIconButton
|
<ThemeInfoIconButton
|
||||||
label="Copy URL"
|
label={Strings.COPY_URL}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={findAssetId("LinkIcon")}
|
icon={findAssetId("LinkIcon")}
|
||||||
onPress={copyThemeUrl}
|
onPress={copyThemeUrl}
|
||||||
/>
|
/>
|
||||||
<ThemeInfoIconButton
|
<ThemeInfoIconButton
|
||||||
label="Uninstall"
|
label={Strings.UNINSTALL}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={findAssetId("TrashIcon")}
|
icon={findAssetId("TrashIcon")}
|
||||||
onPress={removeThemeHandler}
|
onPress={removeThemeHandler}
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,12 @@ export const initVendettaObject = (): any => {
|
||||||
return require("moment");
|
return require("moment");
|
||||||
},
|
},
|
||||||
get chroma() {
|
get chroma() {
|
||||||
return require("chroma-js");
|
// Lazy-load chroma-js to reduce startup cost
|
||||||
|
const cached = (this as any)._chroma;
|
||||||
|
if (cached) return cached;
|
||||||
|
const loaded = require("chroma-js");
|
||||||
|
(this as any)._chroma = loaded;
|
||||||
|
return loaded;
|
||||||
},
|
},
|
||||||
get lodash() {
|
get lodash() {
|
||||||
return require("lodash");
|
return require("lodash");
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,8 @@ export interface StorageBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
|
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
|
||||||
|
// Debouncer map for per-store writes to disk
|
||||||
|
const _writeDebouncers = new Map<string, any>();
|
||||||
|
|
||||||
const filePathFixer = (file: string): string => Platform.select({
|
const filePathFixer = (file: string): string => Platform.select({
|
||||||
default: file,
|
default: file,
|
||||||
|
|
@ -215,6 +217,71 @@ export const createMMKVBackend = (store: string, defaultData = {}) => {
|
||||||
})());
|
})());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Lightweight in-memory backend to accelerate startup and avoid blocking IO
|
||||||
|
// while still providing eventual persistence to the native storage.
|
||||||
|
export const createMemoryBackend = (
|
||||||
|
store: string,
|
||||||
|
defaultData: any = {},
|
||||||
|
) => {
|
||||||
|
const mmkvPath = getMMKVPath(store);
|
||||||
|
const defaultStr = JSON.stringify(defaultData);
|
||||||
|
|
||||||
|
// In-memory backing object used by the proxy immediately
|
||||||
|
const memory: any = { ...defaultData };
|
||||||
|
|
||||||
|
// Load persisted data in background without blocking startup
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const cached = await NativeCacheModule.getItem(store);
|
||||||
|
if (cached) {
|
||||||
|
const parsed = JSON.parse(cached);
|
||||||
|
Object.assign(memory, parsed);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const path = `${NativeFileModule.getConstants().DocumentsDirPath}/${mmkvPath}`;
|
||||||
|
if (await NativeFileModule.fileExists(path)) {
|
||||||
|
const content = await NativeFileModule.readFile(path, "utf8");
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
Object.assign(memory, data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Hydration is intentionally omitted for stability in this rewrite
|
||||||
|
|
||||||
|
// Local path helper (inlined to avoid hoisting concerns)
|
||||||
|
const fixPath = (file: string) =>
|
||||||
|
Platform.select({ default: file, ios: NativeFileModule.saveFileToGallery ? file : `Documents/${file}` });
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: async () => memory,
|
||||||
|
set: async (data: any) => {
|
||||||
|
Object.assign(memory, data);
|
||||||
|
// Debounced persistence to disk to avoid IO storms
|
||||||
|
try {
|
||||||
|
const existing = _writeDebouncers.get(store);
|
||||||
|
if (existing) clearTimeout(existing);
|
||||||
|
const timeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await NativeFileModule.writeFile("documents", fixPath(mmkvPath), JSON.stringify(memory), "utf8");
|
||||||
|
} catch {
|
||||||
|
// ignore persistence errors to keep startup fast
|
||||||
|
}
|
||||||
|
}, 120);
|
||||||
|
_writeDebouncers.set(store, timeout);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as unknown as StorageBackend;
|
||||||
|
};
|
||||||
|
|
||||||
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
|
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
|
||||||
return {
|
return {
|
||||||
get: async () => {
|
get: async () => {
|
||||||
|
|
@ -239,4 +306,3 @@ export const createFileBackend = (file: string, defaultData = {}, migratePromise
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ async function initializeShiggyCord() {
|
||||||
Object.freeze = Object.seal = Object;
|
Object.freeze = Object.seal = Object;
|
||||||
|
|
||||||
await require("@metro/internals/caches").initMetroCache();
|
await require("@metro/internals/caches").initMetroCache();
|
||||||
await require(".").default();
|
require(".").default();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { ClientInfoManager } = require("@lib/api/native/modules");
|
const { ClientInfoManager } = require("@lib/api/native/modules");
|
||||||
const stack = e instanceof Error ? e.stack : undefined;
|
const stack = e instanceof Error ? e.stack : undefined;
|
||||||
|
|
|
||||||
56
src/index.ts
56
src/index.ts
|
|
@ -11,13 +11,8 @@ import { injectFluxInterceptor } from "@lib/api/flux";
|
||||||
import { patchJsx } from "@lib/api/react/jsx";
|
import { patchJsx } from "@lib/api/react/jsx";
|
||||||
import { logger } from "@lib/utils/logger";
|
import { logger } from "@lib/utils/logger";
|
||||||
import { patchSettings } from "@ui/settings";
|
import { patchSettings } from "@ui/settings";
|
||||||
import { updaterSettings } from "@lib/api/settings";
|
|
||||||
import { InteractionManager } from "react-native";
|
import { InteractionManager } from "react-native";
|
||||||
import { getDebugInfo, initDebugger } from "@lib/api/debug";
|
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 * as lib from "./lib";
|
||||||
import { timings } from "@lib/utils/timings";
|
import { timings } from "@lib/utils/timings";
|
||||||
|
|
||||||
|
|
@ -66,52 +61,19 @@ export default async () => {
|
||||||
const { VdPluginManager } = await import("@core/vendetta/plugins");
|
const { VdPluginManager } = await import("@core/vendetta/plugins");
|
||||||
const { initPlugins, updatePlugins } = await import("@lib/addons/plugins");
|
const { initPlugins, updatePlugins } = await import("@lib/addons/plugins");
|
||||||
|
|
||||||
// Initialize Vendetta plugins (may start many plugins) — do not block UI.
|
await Promise.all([
|
||||||
VdPluginManager.initPlugins()
|
VdPluginManager.initPlugins()
|
||||||
.then((u) => lib.unload.push(u))
|
.then((u) => u && lib.unload.push(u))
|
||||||
.catch((e) => logger.log("Vendetta init failed:", e));
|
.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.
|
updatePlugins()
|
||||||
// Plugin repository fetching is deferred so the app can finish launching first.
|
.catch((e) => logger.log("updatePlugins failed:", e));
|
||||||
// Stagger plugin startup to reduce CPU/memory spikes: use smaller batches and a small interval.
|
|
||||||
// This keeps the UI responsive while plugins initialize in the background.
|
|
||||||
initPlugins({ staggerInterval: 500, batchSize: 2 });
|
|
||||||
|
|
||||||
// Attempt a lightweight recovery toggle if some core plugins failed to start.
|
|
||||||
try {
|
|
||||||
// Dynamically import the helper (if present) but suppress verbose errors.
|
|
||||||
const mod = await import("@core/debug/toggleCorePlugins").catch(
|
|
||||||
() => null,
|
|
||||||
);
|
|
||||||
if (mod?.default) {
|
|
||||||
// Run helper with minimal noise; ignore failures.
|
|
||||||
mod.default({ offDuration: 1500 }).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to remove the helper source file (best-effort, ignore failures).
|
|
||||||
await import("@lib/api/native/fs")
|
|
||||||
.then((fs) => fs.removeFile("src/core/debug/toggleCorePlugins.ts"))
|
|
||||||
.catch(() => {});
|
|
||||||
} catch {
|
|
||||||
// suppressed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update fonts in background
|
// Update fonts in background
|
||||||
updateFonts().catch((e) => logger.log("updateFonts failed:", e));
|
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).
|
// Preferred: wait until interactions finish (animations / navigation).
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
import { awaitStorage, createMemoryBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||||
import { clearFolder, downloadFile, fileExists, removeFile, writeFile } from "@lib/api/native/fs";
|
import { clearFolder, downloadFile, fileExists, removeFile, writeFile } from "@lib/api/native/fs";
|
||||||
import { safeFetch } from "@lib/utils";
|
import { safeFetch } from "@lib/utils";
|
||||||
|
|
||||||
|
|
@ -13,7 +13,8 @@ export type FontDefinition = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type FontStorage = Record<string, FontDefinition> & { __selected?: string; };
|
type FontStorage = Record<string, FontDefinition> & { __selected?: string; };
|
||||||
export const fonts = wrapSync(createStorage<FontStorage>(createMMKVBackend("BUNNY_FONTS")));
|
// Use a memory-backed storage for faster startup; persistence is still written back to disk
|
||||||
|
export const fonts = wrapSync(createStorage<FontStorage>(createMemoryBackend("BUNNY_FONTS", {})));
|
||||||
|
|
||||||
async function writeFont(font: FontDefinition | null) {
|
async function writeFont(font: FontDefinition | null) {
|
||||||
if (!font && font !== null) throw new Error("Arg font must be a valid object or null");
|
if (!font && font !== null) throw new Error("Arg font must be a valid object or null");
|
||||||
|
|
|
||||||
|
|
@ -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
|
* Safe to call multiple times — skips already-registered plugins.
|
||||||
* with a configurable staggered startup: plugins are started in small batches and
|
|
||||||
* we await a small interval between batches. This reduces short-lived CPU and I/O contention
|
|
||||||
* that causes jank on low-end devices.
|
|
||||||
*
|
|
||||||
* Backwards-compatible: callers can still call initPlugins() with no arguments.
|
|
||||||
*
|
|
||||||
* Options:
|
|
||||||
* - staggerInterval (ms) - wait time between batches (default: 100)
|
|
||||||
* - batchSize - how many plugins to start concurrently in a single batch (default: 3)
|
|
||||||
*/
|
*/
|
||||||
export async function initPlugins(
|
export async function registerInstalledPlugins() {
|
||||||
options: { staggerInterval?: number; batchSize?: number } = {},
|
await awaitStorage(pluginRepositories, pluginSettings);
|
||||||
) {
|
|
||||||
const { staggerInterval = 100, batchSize = 3 } = options;
|
|
||||||
|
|
||||||
|
const ids = Object.keys(pluginSettings ?? {});
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
|
||||||
|
// Load all manifest files from local storage in parallel.
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
ids.map(async (id) => {
|
||||||
|
if (registeredPlugins.has(id) || isCorePlugin(id)) return;
|
||||||
|
const loaded = await preloadStorageIfExists(`plugins/manifests/${id}.json`);
|
||||||
|
if (loaded) {
|
||||||
|
const manifest = getPreloadedStorage<t.BunnyPluginManifest>(
|
||||||
|
`plugins/manifests/${id}.json`,
|
||||||
|
);
|
||||||
|
if (manifest) {
|
||||||
|
registeredPlugins.set(id, manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initPlugins() {
|
||||||
// Ensure core plugins are registered prior to any plugin startup logic so
|
// Ensure core plugins are registered prior to any plugin startup logic so
|
||||||
// settings pages and UI that read registered/core plugins show them.
|
// settings pages and UI that read registered/core plugins show them.
|
||||||
// registerCorePlugins is synchronous and idempotent, but call it here as a
|
// registerCorePlugins is synchronous and idempotent, but call it here as a
|
||||||
|
|
@ -784,6 +811,7 @@ export async function initPlugins(
|
||||||
}
|
}
|
||||||
|
|
||||||
await awaitStorage(pluginRepositories, pluginSettings);
|
await awaitStorage(pluginRepositories, pluginSettings);
|
||||||
|
await registerInstalledPlugins();
|
||||||
|
|
||||||
// Collect enabled plugin ids
|
// Collect enabled plugin ids
|
||||||
const enabledIds = [...registeredPlugins.keys()].filter((id) =>
|
const enabledIds = [...registeredPlugins.keys()].filter((id) =>
|
||||||
|
|
@ -792,32 +820,18 @@ export async function initPlugins(
|
||||||
|
|
||||||
if (enabledIds.length === 0) return;
|
if (enabledIds.length === 0) return;
|
||||||
|
|
||||||
// Helper sleep
|
// Start all enabled plugins in parallel
|
||||||
const sleep = (ms: number) => new Promise<void>((res) => setTimeout(res, ms));
|
await Promise.allSettled(
|
||||||
|
enabledIds.map(async (id) => {
|
||||||
// Start plugins in batches to avoid starting all at once.
|
try {
|
||||||
for (let i = 0; i < enabledIds.length; i += batchSize) {
|
// Only attempt to start if still enabled and not already started
|
||||||
const batch = enabledIds.slice(i, i + batchSize);
|
if (!isPluginEnabled(id) || pluginInstances.has(id)) return;
|
||||||
|
await startPlugin(id).catch((e) => {
|
||||||
// Start the batch in parallel but don't let exceptions bubble to halt everything.
|
console.error(`Failed to start plugin ${id}:`, e);
|
||||||
await Promise.allSettled(
|
});
|
||||||
batch.map(async (id) => {
|
} catch (error) {
|
||||||
try {
|
console.error(`Unexpected error while starting plugin ${id}:`, error);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import patchDefinitionAndResolver from "./patches/resolver";
|
||||||
import patchStorage from "./patches/storage";
|
import patchStorage from "./patches/storage";
|
||||||
import { ColorManifest } from "./types";
|
import { ColorManifest } from "./types";
|
||||||
import { updateBunnyColor } from "./updater";
|
import { updateBunnyColor } from "./updater";
|
||||||
|
import fixStatusBar from "./statusbar";
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export default function initColors(manifest: ColorManifest | null) {
|
export default function initColors(manifest: ColorManifest | null) {
|
||||||
|
|
@ -11,8 +12,9 @@ export default function initColors(manifest: ColorManifest | null) {
|
||||||
const patches = [
|
const patches = [
|
||||||
patchStorage(),
|
patchStorage(),
|
||||||
patchDefinitionAndResolver(),
|
patchDefinitionAndResolver(),
|
||||||
patchChatBackground()
|
patchChatBackground(),
|
||||||
|
fixStatusBar()
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => patches.forEach(p => p());
|
return () => patches.forEach(p => p && (typeof p === "function" ? p() : null));
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { findByProps } from "@metro";
|
import { findByProps } from "@metro";
|
||||||
import chroma from "chroma-js";
|
|
||||||
import { omit } from "es-toolkit";
|
import { omit } from "es-toolkit";
|
||||||
import { Platform, processColor } from "react-native";
|
import { Platform, processColor } from "react-native";
|
||||||
|
|
||||||
|
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
|
||||||
|
|
||||||
import { colorsPref } from "./preferences";
|
import { colorsPref } from "./preferences";
|
||||||
import { ColorManifest, InternalColorDefinition } from "./types";
|
import { ColorManifest, InternalColorDefinition } from "./types";
|
||||||
|
|
||||||
|
|
@ -11,147 +12,224 @@ const tokenRef = findByProps("SemanticColor");
|
||||||
export function parseColorManifest(manifest: ColorManifest): InternalColorDefinition {
|
export function parseColorManifest(manifest: ColorManifest): InternalColorDefinition {
|
||||||
const resolveType = (type = "dark") => (colorsPref.type ?? type) === "dark" ? "darker" : "light";
|
const resolveType = (type = "dark") => (colorsPref.type ?? type) === "dark" ? "darker" : "light";
|
||||||
|
|
||||||
if (manifest.spec === 3) {
|
if (manifest.spec === 3) {
|
||||||
const semanticColorDefinitions: InternalColorDefinition["semantic"] = {};
|
const semanticColorDefinitions: InternalColorDefinition["semantic"] = {};
|
||||||
|
|
||||||
for (const [semanticColorKey, semanticColorValue] of Object.entries(
|
for (const [semanticColorKey, semanticColorValue] of Object.entries(manifest.main.semantic ?? {})) {
|
||||||
manifest.main.semantic ?? {},
|
if (typeof semanticColorValue === "object") {
|
||||||
)) {
|
const { type, value, opacity: semanticColorOpacity } = semanticColorValue;
|
||||||
if (typeof semanticColorValue === "object") {
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
value,
|
|
||||||
opacity: semanticColorOpacity,
|
|
||||||
} = semanticColorValue;
|
|
||||||
|
|
||||||
if (type === "raw") {
|
if (type === "raw") {
|
||||||
semanticColorDefinitions[semanticColorKey] = {
|
semanticColorDefinitions[semanticColorKey] = {
|
||||||
value,
|
value,
|
||||||
opacity: semanticColorOpacity ?? 1,
|
opacity: semanticColorOpacity ?? 1,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const rawColorValue = tokenRef.RawColor[value];
|
const rawColorValue = tokenRef.RawColor[value];
|
||||||
semanticColorDefinitions[semanticColorKey] = {
|
semanticColorDefinitions[semanticColorKey] = {
|
||||||
value: rawColorValue,
|
value: rawColorValue,
|
||||||
opacity: semanticColorOpacity ?? 1,
|
opacity: semanticColorOpacity ?? 1,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
} else if (typeof semanticColorValue === "string") {
|
||||||
|
if (semanticColorValue.startsWith("#")) {
|
||||||
|
semanticColorDefinitions[semanticColorKey] = {
|
||||||
|
value: getChroma()(semanticColorValue).hex(),
|
||||||
|
opacity: 1,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
semanticColorDefinitions[semanticColorKey] = {
|
||||||
|
value: tokenRef.RawColor[semanticColorValue],
|
||||||
|
opacity: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid semantic definitions: ${semanticColorValue}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof semanticColorValue === "string") {
|
|
||||||
if (semanticColorValue.startsWith("#")) {
|
|
||||||
semanticColorDefinitions[semanticColorKey] = {
|
|
||||||
value: chroma.hex(semanticColorValue).hex(),
|
|
||||||
opacity: 1,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
semanticColorDefinitions[semanticColorKey] = {
|
|
||||||
value: tokenRef.RawColor[semanticColorValue],
|
|
||||||
opacity: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`Invalid semantic definitions: ${semanticColorValue}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Platform.OS === "android") applyAndroidAlphaKeys(manifest.main.raw);
|
if (Platform.OS === "android") applyAndroidAlphaKeys(manifest.main.raw);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
spec: 3,
|
spec: 3,
|
||||||
reference: resolveType(manifest.type),
|
reference: resolveType(manifest.type),
|
||||||
semantic: semanticColorDefinitions,
|
semantic: semanticColorDefinitions,
|
||||||
raw: manifest.main.raw ?? {},
|
raw: manifest.main.raw ?? {},
|
||||||
background: manifest.main.background,
|
background: manifest.main.background,
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manifest.spec === 2) {
|
|
||||||
// is Vendetta theme
|
|
||||||
const semanticDefinitions: InternalColorDefinition["semantic"] = {};
|
|
||||||
const background: InternalColorDefinition["background"] | undefined =
|
|
||||||
manifest.background
|
|
||||||
? {
|
|
||||||
...omit(manifest.background, ["alpha"]),
|
|
||||||
opacity: manifest.background.alpha,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (manifest.semanticColors) {
|
|
||||||
for (const key in manifest.semanticColors) {
|
|
||||||
const values = manifest.semanticColors[key]
|
|
||||||
.map((c) => c || undefined)
|
|
||||||
.slice(0, 2);
|
|
||||||
if (!values[0]) continue;
|
|
||||||
|
|
||||||
semanticDefinitions[key] = {
|
|
||||||
value: normalizeToHex(values[resolveType() === "light" ? 1 : 0])!,
|
|
||||||
opacity: 1,
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manifest.rawColors) {
|
if (manifest.spec === 2) { // is Vendetta theme
|
||||||
const draft: typeof manifest.rawColors = {};
|
const semanticDefinitions: InternalColorDefinition["semantic"] = {};
|
||||||
|
const background: InternalColorDefinition["background"] | undefined = manifest.background ? {
|
||||||
|
...omit(manifest.background, ["alpha"]),
|
||||||
|
opacity: manifest.background.alpha
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
for (const key in manifest.rawColors) {
|
if (manifest.semanticColors) {
|
||||||
const value = manifest.rawColors[key];
|
for (const key in manifest.semanticColors) {
|
||||||
if (!value) continue;
|
const values = manifest.semanticColors[key].map(c => c || undefined).slice(0, 2);
|
||||||
draft[key] = normalizeToHex(value)!;
|
if (!values[0]) continue;
|
||||||
}
|
|
||||||
|
|
||||||
if (Platform.OS === "android") applyAndroidAlphaKeys(draft);
|
semanticDefinitions[key] = {
|
||||||
manifest.rawColors = draft;
|
value: normalizeToHex(values[resolveType() === "light" ? 1 : 0])!,
|
||||||
|
opacity: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.rawColors) {
|
||||||
|
const draft: typeof manifest.rawColors = {};
|
||||||
|
|
||||||
|
for (const key in manifest.rawColors) {
|
||||||
|
const value = manifest.rawColors[key];
|
||||||
|
if (!value) continue;
|
||||||
|
draft[key] = normalizeToHex(value)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.OS === "android") applyAndroidAlphaKeys(draft);
|
||||||
|
manifest.rawColors = draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
spec: 2,
|
||||||
|
reference: resolveType(),
|
||||||
|
semantic: semanticDefinitions,
|
||||||
|
raw: manifest.rawColors ?? {},
|
||||||
|
background
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (manifest.spec === 1 || (manifest.theme_color_map && !manifest.spec)) {
|
||||||
spec: 2,
|
|
||||||
reference: resolveType(),
|
|
||||||
semantic: semanticDefinitions,
|
|
||||||
raw: manifest.rawColors ?? {},
|
|
||||||
background,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Invalid theme spec");
|
const semanticDefinitions: InternalColorDefinition["semantic"] = {};
|
||||||
|
const rawDefinitions: Record<string, string> = {};
|
||||||
|
|
||||||
|
const themeIndex = resolveType() === "light" ? 1 : 0;
|
||||||
|
|
||||||
|
if (manifest.theme_color_map) {
|
||||||
|
for (const [key, colorArray] of Object.entries(manifest.theme_color_map)) {
|
||||||
|
if (!Array.isArray(colorArray) || colorArray.length < 2) {
|
||||||
|
console.warn(`[Parser] Invalid color array for ${key}:`, colorArray);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorValue = colorArray[themeIndex];
|
||||||
|
|
||||||
|
if (!colorValue) {
|
||||||
|
console.warn(`[Parser] Missing color value for ${key} at index ${themeIndex}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizeToHex(colorValue);
|
||||||
|
if (normalized) {
|
||||||
|
semanticDefinitions[key] = {
|
||||||
|
value: normalized,
|
||||||
|
opacity: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawColors = manifest.colours || manifest.colors;
|
||||||
|
if (rawColors) {
|
||||||
|
for (const [key, colorValue] of Object.entries(rawColors)) {
|
||||||
|
if (typeof colorValue !== "string") continue;
|
||||||
|
|
||||||
|
const normalized = normalizeToHex(colorValue);
|
||||||
|
if (normalized) {
|
||||||
|
const discordKey = convertEnmityToDiscordRawKey(key);
|
||||||
|
rawDefinitions[discordKey] = normalized;
|
||||||
|
|
||||||
|
if (discordKey !== key) {
|
||||||
|
rawDefinitions[key] = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.unsafe_colors) {
|
||||||
|
for (const [key, colorValue] of Object.entries(manifest.unsafe_colors)) {
|
||||||
|
if (typeof colorValue !== "string") continue;
|
||||||
|
|
||||||
|
const normalized = normalizeToHex(colorValue);
|
||||||
|
if (normalized) {
|
||||||
|
rawDefinitions[key] = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.OS === "android") applyAndroidAlphaKeys(rawDefinitions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
spec: 2,
|
||||||
|
reference: resolveType(),
|
||||||
|
semantic: semanticDefinitions,
|
||||||
|
raw: rawDefinitions,
|
||||||
|
background: manifest.background
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error("Invalid theme spec");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertEnmityToDiscordRawKey(enmityKey: string): string {
|
||||||
|
const conversions: Record<string, string> = {
|
||||||
|
"PRIMARY_DARK": "PRIMARY_100",
|
||||||
|
"PRIMARY_DARK_100": "PRIMARY_100",
|
||||||
|
"PRIMARY_DARK_200": "PRIMARY_200",
|
||||||
|
"PRIMARY_DARK_300": "PRIMARY_300",
|
||||||
|
"PRIMARY_DARK_360": "PRIMARY_360",
|
||||||
|
"PRIMARY_DARK_400": "PRIMARY_400",
|
||||||
|
"PRIMARY_DARK_500": "PRIMARY_500",
|
||||||
|
"PRIMARY_DARK_600": "PRIMARY_600",
|
||||||
|
"PRIMARY_DARK_630": "PRIMARY_630",
|
||||||
|
"PRIMARY_DARK_700": "PRIMARY_700",
|
||||||
|
"PRIMARY_DARK_800": "PRIMARY_800",
|
||||||
|
"PRIMARY_DARK_900": "PRIMARY_900",
|
||||||
|
};
|
||||||
|
|
||||||
|
return conversions[enmityKey] || enmityKey;
|
||||||
|
}
|
||||||
|
|
||||||
export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) {
|
export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) {
|
||||||
if (!rawColors) return;
|
if (!rawColors) return;
|
||||||
|
|
||||||
// these are native Discord Android keys
|
// these are native Discord Android keys
|
||||||
const alphaMap: Record<string, [string, number]> = {
|
const alphaMap: Record<string, [string, number]> = {
|
||||||
BLACK_ALPHA_60: ["BLACK", 0.6],
|
"BLACK_ALPHA_60": ["BLACK", 0.6],
|
||||||
BRAND_NEW_360_ALPHA_20: ["BRAND_360", 0.2],
|
"BRAND_NEW_360_ALPHA_20": ["BRAND_360", 0.2],
|
||||||
BRAND_NEW_360_ALPHA_25: ["BRAND_360", 0.25],
|
"BRAND_NEW_360_ALPHA_25": ["BRAND_360", 0.25],
|
||||||
BRAND_NEW_500_ALPHA_20: ["BRAND_500", 0.2],
|
"BRAND_NEW_500_ALPHA_20": ["BRAND_500", 0.2],
|
||||||
PRIMARY_DARK_500_ALPHA_20: ["PRIMARY_500", 0.2],
|
"PRIMARY_DARK_500_ALPHA_20": ["PRIMARY_500", 0.2],
|
||||||
PRIMARY_DARK_700_ALPHA_60: ["PRIMARY_700", 0.6],
|
"PRIMARY_DARK_700_ALPHA_60": ["PRIMARY_700", 0.6],
|
||||||
STATUS_GREEN_500_ALPHA_20: ["GREEN_500", 0.2],
|
"STATUS_GREEN_500_ALPHA_20": ["GREEN_500", 0.2],
|
||||||
STATUS_RED_500_ALPHA_20: ["RED_500", 0.2],
|
"STATUS_RED_500_ALPHA_20": ["RED_500", 0.2],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const key in alphaMap) {
|
for (const key in alphaMap) {
|
||||||
const [colorKey, alpha] = alphaMap[key];
|
const [colorKey, alpha] = alphaMap[key];
|
||||||
if (!rawColors[colorKey]) continue;
|
if (!rawColors[colorKey]) continue;
|
||||||
rawColors[key] = chroma(rawColors[colorKey]).alpha(alpha).hex();
|
rawColors[key] = getChroma()(rawColors[colorKey]).alpha(alpha).hex();
|
||||||
}
|
}
|
||||||
|
|
||||||
return rawColors;
|
return rawColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeToHex(
|
export function normalizeToHex(colorString: string | undefined): string | undefined {
|
||||||
colorString: string | undefined,
|
if (colorString === undefined) return undefined;
|
||||||
): string | undefined {
|
|
||||||
if (colorString === undefined) return undefined;
|
|
||||||
if (chroma.valid(colorString)) return chroma(colorString).hex();
|
|
||||||
|
|
||||||
const color = Number(processColor(colorString));
|
if (colorString.toLowerCase() === "transparent") {
|
||||||
|
return "#00000000";
|
||||||
|
}
|
||||||
|
|
||||||
return chroma
|
if (getChroma().valid(colorString)) return getChroma()(colorString).hex();
|
||||||
.rgb(
|
|
||||||
(color >> 16) & 0xff, // red
|
const color = Number(processColor(colorString));
|
||||||
(color >> 8) & 0xff, // green
|
|
||||||
color & 0xff, // blue
|
return getChroma().rgb(
|
||||||
(color >> 24) & 0xff, // alpha
|
color >> 16 & 0xff, // red
|
||||||
)
|
color >> 8 & 0xff, // green
|
||||||
.hex();
|
color & 0xff, // blue
|
||||||
|
color >> 24 & 0xff // alpha
|
||||||
|
).hex();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,56 +4,86 @@ import { after } from "@lib/api/patcher";
|
||||||
import { useObservable } from "@lib/api/storage";
|
import { useObservable } from "@lib/api/storage";
|
||||||
import { findInReactTree } from "@lib/utils";
|
import { findInReactTree } from "@lib/utils";
|
||||||
import { findByFilePathLazy } from "@metro";
|
import { findByFilePathLazy } from "@metro";
|
||||||
import chroma from "chroma-js";
|
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
|
||||||
import { ImageBackground, StyleSheet } from "react-native";
|
import { ImageBackground, StyleSheet } from "react-native";
|
||||||
|
import { logger } from "@lib/utils/logger";
|
||||||
|
|
||||||
const Messages = findByFilePathLazy("components_native/chat/Messages.tsx", true);
|
const Messages = findByFilePathLazy("modules/messages/native/Messages.tsx", true);
|
||||||
|
|
||||||
function ThemeBackground({ children }: { children: React.ReactNode }) {
|
function ThemeBackground({ children }: { children: React.ReactNode; }) {
|
||||||
useObservable([colorsPref]);
|
useObservable([colorsPref]);
|
||||||
|
|
||||||
if (!_colorRef.current
|
const background = _colorRef.current?.background;
|
||||||
|| colorsPref.customBackground === "hidden"
|
const url = background?.url;
|
||||||
|| !_colorRef.current.background?.url
|
const blur = background?.blur;
|
||||||
|| _colorRef.current.background?.blur && (typeof _colorRef.current.background?.blur !== "number")
|
|
||||||
){
|
if (
|
||||||
|
!_colorRef.current ||
|
||||||
|
colorsPref.customBackground === "hidden" ||
|
||||||
|
!url
|
||||||
|
) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ImageBackground
|
if (blur !== undefined && typeof blur !== "number") {
|
||||||
style={{ flex: 1, height: "100%" }}
|
return children;
|
||||||
source={{ uri: _colorRef.current.background?.url }}
|
}
|
||||||
blurRadius={_colorRef.current.background?.blur}
|
|
||||||
>
|
return (
|
||||||
{children}
|
<ImageBackground
|
||||||
</ImageBackground>;
|
style={{ flex: 1, height: "100%" }}
|
||||||
|
source={{ uri: url }}
|
||||||
|
blurRadius={typeof blur === "number" ? blur : 0}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ImageBackground>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function patchChatBackground() {
|
export default function patchChatBackground() {
|
||||||
|
try {
|
||||||
|
if (!Messages) return;
|
||||||
|
|
||||||
const patches = [
|
const patches = [
|
||||||
after("render", Messages, (_, ret) => {
|
after("render", Messages, (_, ret) => {
|
||||||
if (!_colorRef.current || !_colorRef.current.background?.url) return;
|
if (!ret) return ret;
|
||||||
const messagesComponent = findInReactTree(
|
|
||||||
ret,
|
|
||||||
x => x && "HACK_fixModalInteraction" in x.props && x?.props?.style
|
|
||||||
);
|
|
||||||
|
|
||||||
if (messagesComponent) {
|
try {
|
||||||
const flattened = StyleSheet.flatten(messagesComponent.props.style);
|
if (!_colorRef.current || !_colorRef.current.background?.url) return ret;
|
||||||
const backgroundColor = chroma(
|
|
||||||
flattened.backgroundColor || "black"
|
|
||||||
).alpha(
|
|
||||||
1 - (_colorRef.current.background?.opacity ?? 1)
|
|
||||||
).hex();
|
|
||||||
|
|
||||||
messagesComponent.props.style = StyleSheet.flatten([
|
const messagesComponent = findInReactTree(
|
||||||
messagesComponent.props.style,
|
ret,
|
||||||
{ backgroundColor }
|
x => x && "HACK_fixModalInteraction" in (x.props ?? {}) && x?.props?.style
|
||||||
]);
|
);
|
||||||
|
|
||||||
|
if (messagesComponent) {
|
||||||
|
try {
|
||||||
|
const flattened = StyleSheet.flatten(messagesComponent.props.style);
|
||||||
|
const backgroundColor = getChroma()(
|
||||||
|
flattened.backgroundColor || "black"
|
||||||
|
).alpha(
|
||||||
|
1 - (_colorRef.current.background?.opacity ?? 1)
|
||||||
|
).hex();
|
||||||
|
|
||||||
|
messagesComponent.props.style = StyleSheet.flatten([
|
||||||
|
messagesComponent.props.style,
|
||||||
|
{ backgroundColor }
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Background patch: failed to modify style", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Background patch: render error", e);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
return <ThemeBackground>{ret}</ThemeBackground>;
|
return <ThemeBackground>{ret}</ThemeBackground>;
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
return () => patches.forEach(x => x());
|
|
||||||
|
return () => patches.forEach(x => x?.());
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Background patch: failed to initialize", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ import { _colorRef } from "@lib/addons/themes/colors/updater";
|
||||||
import { NativeThemeModule } from "@lib/api/native/modules";
|
import { NativeThemeModule } from "@lib/api/native/modules";
|
||||||
import { before, instead } from "@lib/api/patcher";
|
import { before, instead } from "@lib/api/patcher";
|
||||||
import { findByProps } from "@metro";
|
import { findByProps } from "@metro";
|
||||||
import { byMutableProp } from "@metro/filters";
|
|
||||||
import { createLazyModule } from "@metro/lazy";
|
|
||||||
import chroma from "chroma-js";
|
import chroma from "chroma-js";
|
||||||
|
|
||||||
const tokenReference = findByProps("SemanticColor");
|
const tokenReference = findByProps("SemanticColor");
|
||||||
const isThemeModule = createLazyModule(byMutableProp("isThemeDark"));
|
const themeTypes = findByProps("ThemeTypes")?.ThemeTypes;
|
||||||
|
|
||||||
|
const origRawColor = { ...tokenReference.RawColor };
|
||||||
|
const origDarker = themeTypes.DARKER as string;
|
||||||
|
const origLight = themeTypes.LIGHT as string;
|
||||||
|
|
||||||
const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
||||||
"BG_BACKDROP": "BACKGROUND_FLOATING",
|
"BG_BACKDROP": "BACKGROUND_FLOATING",
|
||||||
|
|
@ -22,11 +24,20 @@ const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
||||||
"BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY"
|
"BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY"
|
||||||
};
|
};
|
||||||
|
|
||||||
const origRawColor = { ...tokenReference.RawColor };
|
|
||||||
|
|
||||||
export default function patchDefinitionAndResolver() {
|
export default function patchDefinitionAndResolver() {
|
||||||
const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0;
|
const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0;
|
||||||
|
|
||||||
|
Object.defineProperty(themeTypes, "DARKER", {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
get: () => _colorRef.current?.reference === "darker" ? _colorRef.key : origDarker,
|
||||||
|
});
|
||||||
|
Object.defineProperty(themeTypes, "LIGHT", {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
get: () => _colorRef.current?.reference === "light" ? _colorRef.key : origLight,
|
||||||
|
});
|
||||||
|
|
||||||
Object.keys(tokenReference.RawColor).forEach(key => {
|
Object.keys(tokenReference.RawColor).forEach(key => {
|
||||||
Object.defineProperty(tokenReference.RawColor, key, {
|
Object.defineProperty(tokenReference.RawColor, key, {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|
@ -34,15 +45,12 @@ export default function patchDefinitionAndResolver() {
|
||||||
get: () => {
|
get: () => {
|
||||||
const ret = _colorRef.current?.raw[key];
|
const ret = _colorRef.current?.raw[key];
|
||||||
if (ret) return ret;
|
if (ret) return ret;
|
||||||
|
|
||||||
return origRawColor[key];
|
return origRawColor[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const unpatches = [
|
const unpatches = [
|
||||||
before("isThemeDark", isThemeModule, callback),
|
|
||||||
before("isThemeLight", isThemeModule, callback),
|
|
||||||
before("updateTheme", NativeThemeModule, callback),
|
before("updateTheme", NativeThemeModule, callback),
|
||||||
instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => {
|
instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => {
|
||||||
if (!_colorRef.current) return orig(...args);
|
if (!_colorRef.current) return orig(...args);
|
||||||
|
|
@ -58,8 +66,9 @@ export default function patchDefinitionAndResolver() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (semanticDef?.value) {
|
if (semanticDef?.value) {
|
||||||
if (semanticDef.opacity === 1) return semanticDef.value;
|
return semanticDef.opacity === 1
|
||||||
return chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
|
? semanticDef.value
|
||||||
|
: chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawValue = _colorRef.current.raw[colorDef.raw];
|
const rawValue = _colorRef.current.raw[colorDef.raw];
|
||||||
|
|
@ -72,6 +81,12 @@ export default function patchDefinitionAndResolver() {
|
||||||
return orig(...args);
|
return orig(...args);
|
||||||
}),
|
}),
|
||||||
() => {
|
() => {
|
||||||
|
Object.defineProperty(themeTypes, "DARKER", {
|
||||||
|
configurable: true, writable: true, value: origDarker
|
||||||
|
});
|
||||||
|
Object.defineProperty(themeTypes, "LIGHT", {
|
||||||
|
configurable: true, writable: true, value: origLight
|
||||||
|
});
|
||||||
Object.defineProperty(tokenReference, "RawColor", {
|
Object.defineProperty(tokenReference, "RawColor", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
writable: true,
|
writable: true,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { StatusBar, TextInput, Platform } from "react-native";
|
import { StatusBar, Platform } from "react-native";
|
||||||
import { findByStoreNameLazy } from "@metro/wrappers";
|
import { findByStoreNameLazy } from "@metro/wrappers";
|
||||||
import { _colorRef } from "./updater";
|
import { _colorRef } from "./updater";
|
||||||
|
|
||||||
|
|
@ -7,15 +7,8 @@ const ThemeStore = findByStoreNameLazy("ThemeStore");
|
||||||
function getBarStyle() {
|
function getBarStyle() {
|
||||||
// Check for custom theme
|
// Check for custom theme
|
||||||
const custom = _colorRef.current;
|
const custom = _colorRef.current;
|
||||||
if (custom) {
|
if (custom?.reference) {
|
||||||
// Bunny theme (spec 3)
|
return custom.reference === "light" ? "dark-content" : "light-content";
|
||||||
if (custom.spec === 3 && custom.reference) {
|
|
||||||
return custom.reference === "light" ? "dark-content" : "light-content";
|
|
||||||
}
|
|
||||||
// Vendetta theme (spec 2)
|
|
||||||
if (custom.spec === 2 && custom.reference) {
|
|
||||||
return custom.reference === "light" ? "dark-content" : "light-content";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to Discord theme string
|
// Fallback to Discord theme string
|
||||||
|
|
@ -25,33 +18,35 @@ function getBarStyle() {
|
||||||
return (lowerTheme.includes("dark") || lowerTheme.includes("midnight")) ? "light-content" : "dark-content";
|
return (lowerTheme.includes("dark") || lowerTheme.includes("midnight")) ? "light-content" : "dark-content";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "dark-content";
|
return "light-content";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function fixStatusBar() {
|
export default function fixStatusBar() {
|
||||||
|
function applyStatusBar() {
|
||||||
|
const style = getBarStyle();
|
||||||
|
StatusBar.setBarStyle(style, true);
|
||||||
|
|
||||||
if (Platform.OS == "android") {
|
if (Platform.OS === "android") {
|
||||||
|
StatusBar.setTranslucent?.(true);
|
||||||
|
StatusBar.setBackgroundColor?.("#00000000", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.OS === "android") {
|
||||||
const origSetBarStyle = StatusBar.setBarStyle;
|
const origSetBarStyle = StatusBar.setBarStyle;
|
||||||
StatusBar.setBarStyle = function (_style: any, ...args: any[]) {
|
StatusBar.setBarStyle = function (_style: any, ...args: any[]) {
|
||||||
return origSetBarStyle.call(this, getBarStyle(), ...args);
|
return origSetBarStyle.call(this, getBarStyle(), ...args);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS == "ios") {
|
const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar);
|
||||||
function applyStatusBar() {
|
|
||||||
const style = getBarStyle();
|
|
||||||
StatusBar.setBarStyle(style, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar);
|
applyStatusBar();
|
||||||
|
|
||||||
StatusBar.setBarStyle(getBarStyle());
|
let delay = 200;
|
||||||
|
for (let i = 0; i < 5; i++, delay *= 2) {
|
||||||
let delay = 200;
|
setTimeout(applyStatusBar, delay);
|
||||||
for (let i = 0; i < 5; i++, delay *= 2) {
|
|
||||||
setTimeout(applyStatusBar, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,19 @@ export interface VendettaThemeManifest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enmity themes
|
||||||
|
export interface EnmityColorManifest {
|
||||||
|
spec?: 1;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
authors?: Author[];
|
||||||
|
theme_color_map?: Record<string, string[]>;
|
||||||
|
colours?: Record<string, string>;
|
||||||
|
colors?: Record<string, string>;
|
||||||
|
unsafe_colors?: Record<string, string>;
|
||||||
|
background?: BackgroundDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export interface InternalColorDefinition {
|
export interface InternalColorDefinition {
|
||||||
spec: 1 | 2 | 3;
|
spec: 1 | 2 | 3;
|
||||||
|
|
@ -49,4 +62,4 @@ export interface InternalColorDefinition {
|
||||||
background?: BackgroundDefinition;
|
background?: BackgroundDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ColorManifest = BunnyColorManifest | VendettaThemeManifest;
|
export type ColorManifest = BunnyColorManifest | VendettaThemeManifest | EnmityColorManifest;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ const origRawColor = { ...tokenRef.RawColor };
|
||||||
const AppearanceManager = findByPropsLazy("updateTheme");
|
const AppearanceManager = findByPropsLazy("updateTheme");
|
||||||
const ThemeStore = findByStoreNameLazy("ThemeStore");
|
const ThemeStore = findByStoreNameLazy("ThemeStore");
|
||||||
const FormDivider = findByPropsLazy("DIVIDER_COLORS");
|
const FormDivider = findByPropsLazy("DIVIDER_COLORS");
|
||||||
import { colorsPref } from "./preferences";
|
|
||||||
|
|
||||||
let _inc = 1;
|
let _inc = 1;
|
||||||
|
|
||||||
|
|
@ -28,45 +27,68 @@ export const _colorRef: InternalColorRef = {
|
||||||
lastSetDiscordTheme: "darker"
|
lastSetDiscordTheme: "darker"
|
||||||
};
|
};
|
||||||
|
|
||||||
export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true } ) {
|
export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) {
|
||||||
if (settings.safeMode?.enabled) return;
|
if (settings.safeMode?.enabled) return;
|
||||||
|
|
||||||
const resolveType = (type = "dark") => (colorsPref.type ?? type) === "dark" ? "darker" : "light";
|
|
||||||
const internalDef = colorManifest ? parseColorManifest(colorManifest) : null;
|
const internalDef = colorManifest ? parseColorManifest(colorManifest) : null;
|
||||||
|
const ref = Object.assign(_colorRef, {
|
||||||
|
current: internalDef,
|
||||||
|
key: `bn-theme-${++_inc}`,
|
||||||
|
lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-")
|
||||||
|
? ThemeStore.theme
|
||||||
|
: _colorRef.lastSetDiscordTheme
|
||||||
|
});
|
||||||
|
|
||||||
if (resolveType() == "light" || update) {
|
if (internalDef != null) {
|
||||||
var ref = Object.assign(_colorRef, {
|
// Register the synthetic key BEFORE calling native, and verify it actually
|
||||||
current: internalDef,
|
// landed in every table native reads from. If any registration step fails,
|
||||||
key: `bn-theme-${++_inc}`,
|
// bail out to a known-good theme instead of handing native an unknown key.
|
||||||
lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-") || !ThemeStore.theme.startsWith("darker")
|
try {
|
||||||
? ThemeStore.theme
|
|
||||||
: _colorRef.lastSetDiscordTheme
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var ref = Object.assign(_colorRef, {
|
|
||||||
current: internalDef,
|
|
||||||
key: `dark`,
|
|
||||||
lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-") || !ThemeStore.theme.startsWith("darker")
|
|
||||||
? ThemeStore.theme
|
|
||||||
: _colorRef.lastSetDiscordTheme
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (internalDef != null) {
|
|
||||||
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
|
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
|
||||||
|
|
||||||
|
if (!FormDivider.DIVIDER_COLORS[ref.current!.reference]) {
|
||||||
|
throw new Error(`Missing divider color reference: ${ref.current!.reference}`);
|
||||||
|
}
|
||||||
FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference];
|
FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference];
|
||||||
|
|
||||||
Object.keys(tokenRef.Shadow).forEach(k => tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][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 => {
|
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.key] = {
|
||||||
...tokenRef.SemanticColor[k][ref.current!.reference]
|
...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) {
|
if (update) {
|
||||||
AppearanceManager.setShouldSyncAppearanceSettings(false);
|
AppearanceManager.setShouldSyncAppearanceSettings(false);
|
||||||
AppearanceManager.updateTheme(internalDef != null ? ref.key : "darker");
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ export function getCurrentTheme() {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function getThemeFromLoader(): VdThemeInfo | null {
|
export function getThemeFromLoader(): VdThemeInfo | null {
|
||||||
return getStoredTheme();
|
return getStoredTheme() ?? getCurrentTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -157,6 +157,7 @@ export async function initThemes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
await newAwaitStorage(colorsPref);
|
await newAwaitStorage(colorsPref);
|
||||||
|
await awaitStorage(themes);
|
||||||
|
|
||||||
const currentTheme = getThemeFromLoader();
|
const currentTheme = getThemeFromLoader();
|
||||||
initColors(currentTheme?.data ?? null);
|
initColors(currentTheme?.data ?? null);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ export async function toggleSafeMode() {
|
||||||
setTimeout(BundleUpdaterManager.reload, 400);
|
setTimeout(BundleUpdaterManager.reload, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Debugger State & Logic ---
|
|
||||||
let socket: WebSocket | undefined;
|
let socket: WebSocket | undefined;
|
||||||
let originalConsoleLog: any;
|
let originalConsoleLog: any;
|
||||||
let originalConsoleError: any;
|
let originalConsoleError: any;
|
||||||
|
|
@ -225,13 +224,14 @@ export function connectToDebugger(url: string) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener("close", () => {
|
socket.addEventListener("close", (e: any) => {
|
||||||
showToast("Disconnected from debugger.", findAssetId("Small"));
|
logger.error(`Debugger disconnected. (${e?.code}, ${e?.reason || "no reason specified"})`);
|
||||||
unpatchConsoleAndLogger();
|
unpatchConsoleAndLogger();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener("error", (err: any) => {
|
socket.addEventListener("error", (e: any) => {
|
||||||
console.log(`Debugger error: ${err.message}`);
|
const err = e?.message || e?.error?.message || "WebSocket Connection Errored";
|
||||||
|
logger.error('Debugger error:', err);
|
||||||
showToast("An error occurred with the debugger connection!", findAssetId("Small"));
|
showToast("An error occurred with the debugger connection!", findAssetId("Small"));
|
||||||
unpatchConsoleAndLogger();
|
unpatchConsoleAndLogger();
|
||||||
});
|
});
|
||||||
|
|
@ -254,7 +254,6 @@ export function isConnectedToDebugger(): boolean {
|
||||||
return socket?.readyState === WebSocket.OPEN;
|
return socket?.readyState === WebSocket.OPEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- React DevTools (RDT) Logic ---
|
|
||||||
const rdtPort = 8097;
|
const rdtPort = 8097;
|
||||||
export let rdtClient: WebSocket | null = null;
|
export let rdtClient: WebSocket | null = null;
|
||||||
export let rdtConnected = false;
|
export let rdtConnected = false;
|
||||||
|
|
@ -283,13 +282,14 @@ export function connectRdt(url: string, quiet?: boolean) {
|
||||||
bump();
|
bump();
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener('close', () => {
|
ws.addEventListener('close', (e: any) => {
|
||||||
|
logger.error(`React DevTools disconnected. (${e?.code}, ${e?.reason || "no reason specified"})`);
|
||||||
cleanupRdt();
|
cleanupRdt();
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener('error', (e: any) => {
|
ws.addEventListener('error', (e: any) => {
|
||||||
cleanupRdt();
|
cleanupRdt();
|
||||||
const err = e?.message ?? e?.stack ?? String(e);
|
const err = e?.message || e?.error?.message || "WebSocket Connection Errored";
|
||||||
logger.error('React DevTools error:', err);
|
logger.error('React DevTools error:', err);
|
||||||
if (!quiet) showToast(err, findAssetId("CircleXIcon-primary"));
|
if (!quiet) showToast(err, findAssetId("CircleXIcon-primary"));
|
||||||
});
|
});
|
||||||
|
|
@ -318,8 +318,6 @@ export function useIsRdtConnected() {
|
||||||
return connected;
|
return connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Core Utils ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ export const dispatcher = FluxDispatcher;
|
||||||
|
|
||||||
type Intercept = (payload: Record<string, any> & { type: string; }) => any;
|
type Intercept = (payload: Record<string, any> & { type: string; }) => any;
|
||||||
let intercepts: Intercept[] = [];
|
let intercepts: Intercept[] = [];
|
||||||
|
const interceptIndex = new Map<Intercept, number>();
|
||||||
|
let nextInterceptId = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -33,7 +35,14 @@ export function injectFluxInterceptor() {
|
||||||
|
|
||||||
(dispatcher._interceptors ??= []).unshift(cb);
|
(dispatcher._interceptors ??= []).unshift(cb);
|
||||||
|
|
||||||
return () => dispatcher._interceptors &&= dispatcher._interceptors.filter(v => v !== cb);
|
return () => {
|
||||||
|
const idx = interceptIndex.get(cb);
|
||||||
|
if (idx !== undefined) {
|
||||||
|
intercepts.splice(idx, 1);
|
||||||
|
interceptIndex.delete(cb);
|
||||||
|
intercepts.forEach((intercept, i) => interceptIndex.set(intercept, i));
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,9 +50,16 @@ export function injectFluxInterceptor() {
|
||||||
* nullish -> nothing, falsy -> block, object -> modify
|
* nullish -> nothing, falsy -> block, object -> modify
|
||||||
*/
|
*/
|
||||||
export function intercept(cb: Intercept) {
|
export function intercept(cb: Intercept) {
|
||||||
|
const id = nextInterceptId++;
|
||||||
|
interceptIndex.set(cb, id);
|
||||||
intercepts.push(cb);
|
intercepts.push(cb);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
intercepts = intercepts.filter(i => i !== cb);
|
const idx = interceptIndex.get(cb);
|
||||||
|
if (idx !== undefined) {
|
||||||
|
intercepts.splice(idx, 1);
|
||||||
|
interceptIndex.delete(cb);
|
||||||
|
intercepts.forEach((intercept, i) => interceptIndex.set(intercept, i));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ const rainLoaderIdentity = globalThis.__RAIN_LOADER__;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const vendettaLoaderIdentity = globalThis.__vendetta_loader;
|
const vendettaLoaderIdentity = globalThis.__vendetta_loader;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const shiggyLoaderIdentity = globalThis.__SHIGGY_LOADER__;
|
||||||
|
|
||||||
export interface VendettaLoaderIdentity {
|
export interface VendettaLoaderIdentity {
|
||||||
name: string;
|
name: string;
|
||||||
features: {
|
features: {
|
||||||
|
|
@ -36,6 +39,10 @@ export function isRa1nLoader() {
|
||||||
return rainLoaderIdentity != null;
|
return rainLoaderIdentity != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isShiggyLoader() {
|
||||||
|
return shiggyLoaderIdentity != null;
|
||||||
|
}
|
||||||
|
|
||||||
function polyfillVendettaLoaderIdentity() {
|
function polyfillVendettaLoaderIdentity() {
|
||||||
if (!isPyonLoader() || isVendettaLoader() || !isRa1nLoader()) return null;
|
if (!isPyonLoader() || isVendettaLoader() || !isRa1nLoader()) return null;
|
||||||
|
|
||||||
|
|
@ -98,6 +105,8 @@ export function getLoaderIdentity() {
|
||||||
return getVendettaLoaderIdentity();
|
return getVendettaLoaderIdentity();
|
||||||
} else if (isRa1nLoader()) {
|
} else if (isRa1nLoader()) {
|
||||||
return rainLoaderIdentity();
|
return rainLoaderIdentity();
|
||||||
|
} else if (isShiggyLoader()) {
|
||||||
|
return shiggyLoaderIdentity;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -116,6 +125,7 @@ export function getLoaderName() {
|
||||||
if (isPyonLoader()) return pyonLoaderIdentity.loaderName;
|
if (isPyonLoader()) return pyonLoaderIdentity.loaderName;
|
||||||
else if (isRa1nLoader()) return rainLoaderIdentity.loadername;
|
else if (isRa1nLoader()) return rainLoaderIdentity.loadername;
|
||||||
else if (isVendettaLoader()) return vendettaLoaderIdentity.name;
|
else if (isVendettaLoader()) return vendettaLoaderIdentity.name;
|
||||||
|
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderName;
|
||||||
|
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
@ -123,6 +133,7 @@ export function getLoaderName() {
|
||||||
export function getLoaderVersion(): string | null {
|
export function getLoaderVersion(): string | null {
|
||||||
if (isPyonLoader()) return pyonLoaderIdentity.loaderVersion;
|
if (isPyonLoader()) return pyonLoaderIdentity.loaderVersion;
|
||||||
else if (isRa1nLoader()) return rainLoaderIdentity.loaderVersion;
|
else if (isRa1nLoader()) return rainLoaderIdentity.loaderVersion;
|
||||||
|
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderVersion;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +155,9 @@ export function isThemeSupported() {
|
||||||
} else if (isVendettaLoader()) {
|
} else if (isVendettaLoader()) {
|
||||||
return vendettaLoaderIdentity!!.features.themes != null;
|
return vendettaLoaderIdentity!!.features.themes != null;
|
||||||
} else if (isRa1nLoader()) {
|
} 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;
|
return false;
|
||||||
|
|
@ -158,6 +171,8 @@ export function getStoredTheme(): VdThemeInfo | null {
|
||||||
if (!themeProp) return null;
|
if (!themeProp) return null;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return globalThis[themeProp] || null;
|
return globalThis[themeProp] || null;
|
||||||
|
} else if (isShiggyLoader()) {
|
||||||
|
return shiggyLoaderIdentity.storedTheme || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -168,6 +183,8 @@ export function getThemeFilePath() {
|
||||||
return "pyoncord/current-theme.json";
|
return "pyoncord/current-theme.json";
|
||||||
} else if (isVendettaLoader()) {
|
} else if (isVendettaLoader()) {
|
||||||
return "vendetta_theme.json";
|
return "vendetta_theme.json";
|
||||||
|
} else if (isShiggyLoader()) {
|
||||||
|
return "ShiggyCord/current-theme.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -241,6 +258,7 @@ export function getLoaderConfigPath() {
|
||||||
|
|
||||||
export function isFontSupported() {
|
export function isFontSupported() {
|
||||||
if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2;
|
if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2;
|
||||||
|
else if (isShiggyLoader()) return shiggyLoaderIdentity.fontPatch === 2;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,14 @@ import type { ImageURISource } from "react-native";
|
||||||
import { patchTabsUI } from "./patches/tabs";
|
import { patchTabsUI } from "./patches/tabs";
|
||||||
|
|
||||||
export interface RowConfig {
|
export interface RowConfig {
|
||||||
key: string;
|
key: string;
|
||||||
title: () => string;
|
title: () => string;
|
||||||
onPress?: () => any;
|
onPress?: () => any;
|
||||||
render?: Parameters<typeof lazy>[0];
|
render?: Parameters<typeof lazy>[0];
|
||||||
icon?: ImageURISource | number;
|
icon?: ImageURISource | number;
|
||||||
IconComponent?: React.ReactNode;
|
IconComponent?: React.ReactNode,
|
||||||
usePredicate?: () => boolean;
|
usePredicate?: () => boolean,
|
||||||
useTrailing?: () => string | JSX.Element;
|
useTrailing?: () => string | JSX.Element
|
||||||
rawTabsConfig?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const registeredSections = {} as {
|
export const registeredSections = {} as {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { after } from "@lib/api/patcher";
|
||||||
import { TableRow } from "@metro/common/components";
|
import { TableRow } from "@metro/common/components";
|
||||||
import { findByNameLazy, findByPropsLazy } from "@metro/wrappers";
|
import { findByNameLazy, findByPropsLazy } from "@metro/wrappers";
|
||||||
import { registeredSections } from "@ui/settings";
|
import { registeredSections } from "@ui/settings";
|
||||||
import { Strings } from "@core/i18n";
|
|
||||||
|
|
||||||
import { CustomPageRenderer, wrapOnPress } from "./shared";
|
import { CustomPageRenderer, wrapOnPress } from "./shared";
|
||||||
import { findInReactTree } from "@lib/utils";
|
import { findInReactTree } from "@lib/utils";
|
||||||
|
|
@ -29,9 +28,9 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
||||||
IconComponent: () => <TableRow.Icon source={row.icon} />,
|
IconComponent: () => <TableRow.Icon source={row.icon} />,
|
||||||
usePredicate: row.usePredicate,
|
usePredicate: row.usePredicate,
|
||||||
useTrailing: row.useTrailing,
|
useTrailing: row.useTrailing,
|
||||||
|
useTitle: row.title,
|
||||||
onPress: wrapOnPress(row.onPress, null, row.render, row.title()),
|
onPress: wrapOnPress(row.onPress, null, row.render, row.title()),
|
||||||
withArrow: true,
|
withArrow: true,
|
||||||
...row.rawTabsConfig,
|
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
@ -48,6 +47,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
||||||
VendettaCustomPage: {
|
VendettaCustomPage: {
|
||||||
type: "route",
|
type: "route",
|
||||||
title: () => "ShiggyCord",
|
title: () => "ShiggyCord",
|
||||||
|
useTitle: () => "ShiggyCord",
|
||||||
screen: {
|
screen: {
|
||||||
route: "VendettaCustomPage",
|
route: "VendettaCustomPage",
|
||||||
getComponent: () => CustomPageRenderer,
|
getComponent: () => CustomPageRenderer,
|
||||||
|
|
@ -56,6 +56,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
||||||
SHIGGYCORD_CUSTOM_PAGE: {
|
SHIGGYCORD_CUSTOM_PAGE: {
|
||||||
type: "route",
|
type: "route",
|
||||||
title: () => "ShiggyCord",
|
title: () => "ShiggyCord",
|
||||||
|
useTitle: () => "ShiggyCord",
|
||||||
screen: {
|
screen: {
|
||||||
route: "SHIGGYCORD_CUSTOM_PAGE",
|
route: "SHIGGYCORD_CUSTOM_PAGE",
|
||||||
getComponent: () => CustomPageRenderer,
|
getComponent: () => CustomPageRenderer,
|
||||||
|
|
@ -64,6 +65,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
||||||
BUNNY_CUSTOM_PAGE: {
|
BUNNY_CUSTOM_PAGE: {
|
||||||
type: "route",
|
type: "route",
|
||||||
title: () => "ShiggyCord",
|
title: () => "ShiggyCord",
|
||||||
|
useTitle: () => "ShiggyCord",
|
||||||
screen: {
|
screen: {
|
||||||
route: "BUNNY_CUSTOM_PAGE",
|
route: "BUNNY_CUSTOM_PAGE",
|
||||||
getComponent: () => CustomPageRenderer,
|
getComponent: () => CustomPageRenderer,
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,8 @@ export const OFFICIAL_PLUGINS_REPO_URL =
|
||||||
|
|
||||||
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
|
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
|
||||||
export const VD_DISCORD_SERVER_ID = "1427396621905432699";
|
export const VD_DISCORD_SERVER_ID = "1427396621905432699";
|
||||||
export const VD_PLUGINS_CHANNEL_ID = "1091880384561684561";
|
export const VD_PLUGINS_CHANNEL_ID = "1432796541210333284";
|
||||||
export const VD_THEMES_CHANNEL_ID = "1091880434939482202";
|
export const VD_THEMES_CHANNEL_ID = "1408405946434195540";
|
||||||
|
export const KETTU_DISCORD_SERVER_ID = "1368145952266911755";
|
||||||
|
export const KETTU_PLUGINS_CHANNEL_ID = "1432796541210333284";
|
||||||
|
export const KETTU_THEMES_CHANNEL_ID = "1408405946434195540";
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
export type SearchTree = Record<string, any>;
|
export type SearchTree = Record<string, any>;
|
||||||
export type SearchFilter = (tree: SearchTree) => boolean;
|
export type SearchFilter = (tree: SearchTree) => boolean;
|
||||||
export interface FindInTreeOptions {
|
export interface FindInTreeOptions {
|
||||||
walkable?: string[];
|
walkable?: string[] | Set<string>;
|
||||||
ignore?: string[];
|
ignore?: string[] | Set<string>;
|
||||||
maxDepth?: number;
|
maxDepth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindInTreeOptions>, depth: number): any {
|
function treeSearch(tree: SearchTree, filter: SearchFilter, walkable: Set<string>, ignore: Set<string>, maxDepth: number, depth: number): any {
|
||||||
if (depth > opts.maxDepth) return;
|
if (depth > maxDepth) return;
|
||||||
if (!tree) return;
|
if (!tree) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -22,18 +22,19 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
|
||||||
if (typeof item !== "object" || item === null) continue;
|
if (typeof item !== "object" || item === null) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const found = treeSearch(item, filter, opts, depth + 1);
|
const found = treeSearch(item, filter, walkable, ignore, maxDepth, depth + 1);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
} else if (typeof tree === "object") {
|
} else if (typeof tree === "object") {
|
||||||
for (const key of Object.keys(tree)) {
|
for (const key in tree) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(tree, key) === false) continue;
|
||||||
if (typeof tree[key] !== "object" || tree[key] === null) continue;
|
if (typeof tree[key] !== "object" || tree[key] === null) continue;
|
||||||
if (opts.walkable.length && !opts.walkable.includes(key)) continue;
|
if (walkable.size && !walkable.has(key)) continue;
|
||||||
if (opts.ignore.includes(key)) continue;
|
if (ignore.size && ignore.has(key)) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const found = treeSearch(tree[key], filter, opts, depth + 1);
|
const found = treeSearch(tree[key], filter, walkable, ignore, maxDepth, depth + 1);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
@ -49,5 +50,5 @@ export default function findInTree(
|
||||||
maxDepth = 100
|
maxDepth = 100
|
||||||
}: FindInTreeOptions = {},
|
}: FindInTreeOptions = {},
|
||||||
): any | undefined {
|
): any | undefined {
|
||||||
return treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0);
|
return treeSearch(tree, filter, new Set(walkable), new Set(ignore), maxDepth, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Nullish } from "@lib/utils/types";
|
import { Nullish } from "@lib/utils/types";
|
||||||
import { TextStyles, ThemeColors } from "@ui/types";
|
import { TextStyles, ThemeColors } from "@ui/types";
|
||||||
import { Falsey } from "lodash";
|
import type { Falsey } from "lodash";
|
||||||
import { FC, MutableRefObject, PropsWithoutRef, ReactNode, RefObject } from "react";
|
import { FC, MutableRefObject, PropsWithoutRef, ReactNode, RefObject } from "react";
|
||||||
import type * as RN from "react-native";
|
import type * as RN from "react-native";
|
||||||
import { ImageSourcePropType, PressableProps } from "react-native";
|
import { ImageSourcePropType, PressableProps } from "react-native";
|
||||||
|
|
@ -211,7 +211,7 @@ interface TableRowIconProps {
|
||||||
export type TableRowIcon = React.FC<TableRowIconProps>;
|
export type TableRowIcon = React.FC<TableRowIconProps>;
|
||||||
|
|
||||||
interface TableRowGroupProps {
|
interface TableRowGroupProps {
|
||||||
title: string;
|
title?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,23 +16,17 @@ let _metroCache = null as unknown as ReturnType<typeof buildInitCache>;
|
||||||
export const getMetroCache = () => _metroCache;
|
export const getMetroCache = () => _metroCache;
|
||||||
|
|
||||||
function buildInitCache() {
|
function buildInitCache() {
|
||||||
|
const modulesCount = Object.keys(window.modules).length;
|
||||||
|
|
||||||
const cache = {
|
const cache = {
|
||||||
_v: CACHE_VERSION,
|
_v: CACHE_VERSION,
|
||||||
_buildNumber: NativeClientInfoModule.getConstants().Build,
|
_buildNumber: NativeClientInfoModule.getConstants().Build,
|
||||||
_modulesCount: Object.keys(window.modules).length,
|
_modulesCount: modulesCount,
|
||||||
flagsIndex: {} as Record<string, number>,
|
flagsIndex: {} as Record<string, number>,
|
||||||
findIndex: {} as Record<string, ModulesMap | undefined>,
|
findIndex: {} as Record<string, ModulesMap | undefined>,
|
||||||
polyfillIndex: {} as Record<string, ModulesMap | undefined>
|
polyfillIndex: {} as Record<string, ModulesMap | undefined>
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Force load all modules so useful modules are pre-cached. Add a minor
|
|
||||||
// delay so the cache is initialized before the modules are loaded.
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const id in window.modules) {
|
|
||||||
require("./modules").requireModule(id);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
_metroCache = cache;
|
_metroCache = cache;
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +46,8 @@ export async function initMetroCache() {
|
||||||
_metroCache = null!;
|
_metroCache = null!;
|
||||||
throw "cache invalidated; version mismatch";
|
throw "cache invalidated; version mismatch";
|
||||||
}
|
}
|
||||||
if (_metroCache._modulesCount !== Object.keys(window.modules).length) {
|
let currentCount = Object.keys(window.modules).length;
|
||||||
|
if (_metroCache._modulesCount !== currentCount) {
|
||||||
_metroCache = null!;
|
_metroCache = null!;
|
||||||
throw "cache invalidated; modules count mismatch";
|
throw "cache invalidated; modules count mismatch";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,17 +117,13 @@ for (const [id, moduleExports] of cacher.getModules()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportsKeysLength = Reflect.ownKeys(actualExports).length;
|
const exportsKeysLength = Reflect.ownKeys(actualExports).length;
|
||||||
if (_source[prop] && exportsKeysLength >= _source[prop]) {
|
if (_source[prop] && exportsKeysLength <= _source[prop]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_module[prop] = actualExports[prop];
|
_module[prop] = actualExports[prop];
|
||||||
_source[prop] = Reflect.ownKeys(actualExports).length;
|
_source[prop] = Reflect.ownKeys(actualExports).length;
|
||||||
cacher.cacheId(id);
|
cacher.cacheId(id);
|
||||||
|
|
||||||
if (exportsKeysLength === 1) {
|
|
||||||
redesignProps.delete(prop);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue