Merge branch 'main' into github
This commit is contained in:
commit
d15443b50a
37 changed files with 1587 additions and 826 deletions
|
|
@ -1,46 +0,0 @@
|
|||
name: Feature request
|
||||
description: Suggest an idea for Kettu
|
||||
title: "[feature]: <insert title here>"
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Revenge feature request
|
||||
|
||||
Before creating a new feature request, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/revenge-mod/revenge-bundle/issues?q=label%3A%22Feature+request%22).
|
||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/revenge-mod/revenge-bundle/blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, join us on [Discord](https://discord.gg/ddcQf3s2Uq).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: What feature you want Kettu to have? Provide detailed description about what it should look like or where it should be added.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
A strong motivation is necessary for a feature request to be considered.
|
||||
|
||||
- Why should this feature be implemented?
|
||||
- What is the explicit use case?
|
||||
- What are the benefits?
|
||||
- What makes this feature important?
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.26979 99H294.723L312.234 215.273C312.723 222.597 310.021 229.993 304.839 235.184L265.514 274.935C260.741 279.717 254.27 283.273 247.514 283.273H182.785C168.723 283.273 152.981 270.126 152.981 256.073V255.61C152.981 250.056 156.895 244.851 160.088 240.605L160.088 240.605L160.288 240.339L75.9944 236.224C73.9944 236.135 69.0077 236.055 62.9899 235.984C43.5233 235.762 27.1144 221.37 24.4033 202.09L8.74994 103.006C8.84706 101.648 9.02178 100.311 9.26979 99ZM84.5458 137.592L139.983 152.446C139.994 152.788 140 153.132 140 153.477C140 170.045 126.568 183.477 110 183.477C93.4314 183.477 79.9999 170.045 79.9999 153.477C79.9999 147.643 81.665 142.198 84.5458 137.592ZM180.019 152.4L235.429 137.553C238.325 142.168 240 147.627 240 153.477C240 170.045 226.568 183.477 210 183.477C193.431 183.477 180 170.045 180 153.477C180 153.116 180.006 152.757 180.019 152.4Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M288.328 82.0258C288.569 83.4188 289.198 84.7073 290.018 85.8593C292.71 89.6425 294.372 94.0776 294.688 98.766L295.899 106.809C296.628 111.647 292.881 116 287.988 116H17.6382C13.7018 116 10.3504 113.136 9.73617 109.248L8.89299 103.911C8.79798 103.31 8.76815 102.697 8.82742 102.091C10.4271 85.7347 23.3039 72.6711 39.7278 70.9082L87.1856 63.328C89.9772 62.8821 92.7967 63.9426 94.6025 66.1175L105.915 79.7431C107.719 81.9152 110.533 82.9759 113.322 82.5343L203.656 68.2267C205.851 67.8792 207.802 66.6347 209.042 64.7913L222.878 44.2296C224.116 42.3892 226.063 41.1457 228.253 40.7958L247.639 37.6994C265.683 35.7705 282.181 48.0371 285.523 65.8683L288.328 82.0258Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.26979 99H294.723L312.234 215.273C312.723 222.597 310.021 229.993 304.839 235.184L265.514 274.935C260.741 279.717 254.27 283.273 247.514 283.273H182.785C168.723 283.273 152.981 270.126 152.981 256.073V255.61C152.981 250.056 156.895 244.851 160.088 240.605L160.088 240.605L160.288 240.339L75.9944 236.224C73.9944 236.135 69.0077 236.055 62.9899 235.984C43.5233 235.762 27.1144 221.37 24.4033 202.09L8.74994 103.006C8.84706 101.648 9.02178 100.311 9.26979 99ZM84.5458 137.592L139.983 152.446C139.994 152.788 140 153.132 140 153.477C140 170.045 126.568 183.477 110 183.477C93.4314 183.477 79.9999 170.045 79.9999 153.477C79.9999 147.643 81.665 142.198 84.5458 137.592ZM180.019 152.4L235.429 137.553C238.325 142.168 240 147.627 240 153.477C240 170.045 226.568 183.477 210 183.477C193.431 183.477 180 170.045 180 153.477C180 153.116 180.006 152.757 180.019 152.4Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M288.328 82.0258C288.569 83.4188 289.198 84.7073 290.018 85.8593C292.71 89.6425 294.372 94.0776 294.688 98.766L295.899 106.809C296.628 111.647 292.881 116 287.988 116H17.6382C13.7018 116 10.3504 113.136 9.73617 109.248L8.89299 103.911C8.79798 103.31 8.76815 102.697 8.82742 102.091C10.4271 85.7347 23.3039 72.6711 39.7278 70.9082L87.1856 63.328C89.9772 62.8821 92.7967 63.9426 94.6025 66.1175L105.915 79.7431C107.719 81.9152 110.533 82.9759 113.322 82.5343L203.656 68.2267C205.851 67.8792 207.802 66.6347 209.042 64.7913L222.878 44.2296C224.116 42.3892 226.063 41.1457 228.253 40.7958L247.639 37.6994C265.683 35.7705 282.181 48.0371 285.523 65.8683L288.328 82.0258Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 19 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 19 KiB |
|
|
@ -1,29 +0,0 @@
|
|||
## Building
|
||||
1. Install a Revenge loader with config support (any mentioned in the [Installing](#installing) section).
|
||||
2. Go to **Settings** > **General** and enable **Developer Settings**.
|
||||
3. Clone the repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/revenge-mod/revenge-bundle.git
|
||||
```
|
||||
|
||||
4. Install dependencies
|
||||
|
||||
```
|
||||
bun i
|
||||
```
|
||||
|
||||
5. Build Revenge's code
|
||||
|
||||
```
|
||||
bun run build
|
||||
```
|
||||
|
||||
6. In the newly created `dist` directory, run a HTTP server. I recommend [http-server](https://www.npmjs.com/package/http-server).
|
||||
7. 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/revenge.js`) in the new input box labeled `Revenge URL`.
|
||||
8. Restart Discord. Upon reload, you should notice that your device will download Revenge's bundled code from your server, rather than GitHub.
|
||||
9. Make your changes, rebuild, reload, go wild!
|
||||
|
||||
Alternatively, you can directly *serve* the bundled code by running `bun serve`. `revenge.js` will be served on your local address under the port 4040. You will then insert `http://<local ip address>:4040/revenge.js` as a custom URL and reload. Whenever you restart your mobile client, the script will rebuild the bundle as your client fetches it.
|
||||
|
||||
If the bundle keeps being cached and not updated, you can instead tap the **Settings** > **Developer** > **Clear JS bundle** option which will prompt you to reload.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# 👊 Revenge
|
||||
|
||||
This documentation explains how to use [Revenge](https://github.com/revenge-mod/revenge-bundle).
|
||||
|
||||
## 📖 Table of contents
|
||||
|
||||
TODO.
|
||||
|
||||
## ⏭️ Start here
|
||||
|
||||
The next page will tell you about the prerequisites for using Revenge.
|
||||
|
||||
Continue: [💼 Prerequisites](0_prerequisites.md)
|
||||
|
|
@ -74,7 +74,7 @@ const config = {
|
|||
constModules: {
|
||||
globals: {
|
||||
"bunny-build-info": {
|
||||
version: `"1.2.0"`
|
||||
version: `"1.3.0"`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
BIN
src/assets/icons/codeberg-logo_icon_white.png
Normal file
BIN
src/assets/icons/codeberg-logo_icon_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
|
|
@ -28,7 +28,8 @@ export default () => <ApplicationCommand>{
|
|||
if (ephemeral?.value) {
|
||||
messageUtil.sendBotMessage(ctx.channel.id, content);
|
||||
} else {
|
||||
messageUtil.sendMessage(ctx.channel.id, { content });
|
||||
const fixNonce = Date.now().toString();
|
||||
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {nonce:fixNonce});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ export default () => <ApplicationCommand>{
|
|||
if (ephemeral?.value) {
|
||||
messageUtil.sendBotMessage(ctx.channel.id, content);
|
||||
} else {
|
||||
messageUtil.sendMessage(ctx.channel.id, { content });
|
||||
const fixNonce = Date.now().toString();
|
||||
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {nonce:fixNonce});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"BRAND": "Brand",
|
||||
"PUPU": "Kettu",
|
||||
"PUPU_URL": "Kettu URL",
|
||||
"BROWSER": "Browser",
|
||||
"BYTECODE": "Bytecode",
|
||||
"CANCEL": "Cancel",
|
||||
"CLEAR": "Clear",
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
"CLEAR_DATA": "Clear data",
|
||||
"CLEAR_DATA_FAILED": "Failed to clear data for {name}",
|
||||
"CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}",
|
||||
"CODEBERG": "Codeberg",
|
||||
"CODENAME": "Codename",
|
||||
"COMMAND_DEBUG_DESC": "Send Kettu debug info.",
|
||||
"COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.",
|
||||
|
|
@ -113,7 +115,7 @@
|
|||
"UH_OH": "Uh Oh",
|
||||
"UNINSTALL": "Uninstall",
|
||||
"UNINSTALL_TITLE": "Uninstall {title}",
|
||||
"URL_PLACEHOLDER": "https://github.com/C0C0B01/Kettu",
|
||||
"URL_PLACEHOLDER": "https://codeberg.org/cocobo1/Kettu",
|
||||
"VERSION": "Version",
|
||||
"VERSIONS": "Versions"
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { after } from "@lib/api/patcher";
|
|||
import { onJsxCreate } from "@lib/api/react/jsx";
|
||||
import { findByName } from "@metro";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { defineCorePlugin } from "..";
|
||||
|
||||
interface BunnyBadge {
|
||||
|
|
@ -15,47 +14,79 @@ const useBadgesModule = findByName("useBadges", false);
|
|||
export default defineCorePlugin({
|
||||
manifest: {
|
||||
id: "bunny.badges",
|
||||
name: "Badges",
|
||||
version: "1.0.0",
|
||||
description: "Adds badges to user's profile",
|
||||
authors: [{ name: "pylixonly" }]
|
||||
version: "1.1.0",
|
||||
type: "plugin",
|
||||
spec: 3,
|
||||
main: "",
|
||||
display: {
|
||||
name: "Badges",
|
||||
description: "Adds badges to user's profile",
|
||||
authors: [{ name: "cocobo1"}, { name: "pylixonly" }]
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
const propHolder = {} as Record<string, any>;
|
||||
const badgeCache = {} as Record<string, BunnyBadge[]>;
|
||||
|
||||
onJsxCreate("RenderedBadge", (_, ret) => {
|
||||
if (ret.props.id.match(/bunny-\d+-\d+/)) {
|
||||
Object.assign(ret.props, propHolder[ret.props.id]);
|
||||
let allBadges: { [x: string]: any; } | null = null;
|
||||
const badgeProps = {} as Record<string, any>;
|
||||
|
||||
onJsxCreate("ProfileBadge", (component, ret) => {
|
||||
if (ret.props.id?.startsWith("bunny-")) {
|
||||
const cachedProps = badgeProps[ret.props.id];
|
||||
if (cachedProps) {
|
||||
ret.props.source = cachedProps.source;
|
||||
ret.props.label = cachedProps.label;
|
||||
ret.props.id = cachedProps.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
after("default", useBadgesModule, ([user], r) => {
|
||||
const [badges, setBadges] = useState<BunnyBadge[]>(user ? badgeCache[user.userId] ??= [] : []);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetch(`https://raw.githubusercontent.com/pyoncord/badges/refs/heads/main/${user.userId}.json`)
|
||||
.then(r => r.json())
|
||||
.then(badges => setBadges(badgeCache[user.userId] = badges));
|
||||
|
||||
onJsxCreate("RenderedBadge", (component, ret) => {
|
||||
if (ret.props.id?.startsWith("bunny-")) {
|
||||
const cachedProps = badgeProps[ret.props.id];
|
||||
if (cachedProps) {
|
||||
Object.assign(ret.props, cachedProps);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
if (user) {
|
||||
badges.forEach((badges, i) => {
|
||||
propHolder[`bunny-${user.userId}-${i}`] = {
|
||||
source: { uri: badges.url },
|
||||
id: `bunny-${i}`,
|
||||
label: badges.label
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
r.push({
|
||||
id: `bunny-${user.userId}-${i}`,
|
||||
description: badges.label,
|
||||
icon: "_",
|
||||
result.push({
|
||||
id: badgeId,
|
||||
description: badge.label,
|
||||
icon: " _",
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,21 +1,25 @@
|
|||
import { PluginInstanceInternal } from "@lib/addons/plugins/types";
|
||||
|
||||
interface CorePlugin {
|
||||
default: PluginInstanceInternal;
|
||||
preenabled: boolean;
|
||||
default: PluginInstanceInternal;
|
||||
preenabled: boolean;
|
||||
}
|
||||
|
||||
// Called from @lib/plugins
|
||||
export const getCorePlugins = (): Record<string, CorePlugin> => ({
|
||||
"bunny.quickinstall": require("./quickinstall"),
|
||||
"bunny.badges": require("./badges")
|
||||
"bunny.quickinstall": require("./quickinstall"),
|
||||
"bunny.badges": require("./badges"),
|
||||
"bunny.notrack": { ...require("./notrack"), preenabled: true },
|
||||
"bunny.messagefix": { default: require("./messagefix"), preenabled: true },
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function defineCorePlugin(instance: PluginInstanceInternal): PluginInstanceInternal {
|
||||
// @ts-expect-error
|
||||
instance[Symbol.for("bunny.core.plugin")] = true;
|
||||
return instance;
|
||||
export function defineCorePlugin(
|
||||
instance: PluginInstanceInternal,
|
||||
): PluginInstanceInternal {
|
||||
// @ts-expect-error
|
||||
instance[Symbol.for("bunny.core.plugin")] = true;
|
||||
return instance;
|
||||
}
|
||||
|
|
|
|||
56
src/core/plugins/messagefix/index.ts
Normal file
56
src/core/plugins/messagefix/index.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
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: "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");
|
||||
},
|
||||
});
|
||||
230
src/core/plugins/notrack/index.ts
Normal file
230
src/core/plugins/notrack/index.ts
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
import { defineCorePlugin } from "..";
|
||||
import { findByProps } from "@metro";
|
||||
import { instead } from "@lib/api/patcher";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
|
||||
// Type for functions that need to be cleaned up when plugin is disabled
|
||||
type PatchCleanupFn = () => void;
|
||||
|
||||
// Store all active patches for cleanup
|
||||
let patches: PatchCleanupFn[] = [];
|
||||
|
||||
// AnalyticsUtils and other tracking utilities
|
||||
const AnalyticsUtils = findByProps("AnalyticsActionHandlers");
|
||||
const SuperPropUtils = findByProps("encodeProperties", "track");
|
||||
const VoiceStateUtils = findByProps("getVoiceStateMetadata");
|
||||
const CrashReportUtils = findByProps("submitLiveCrashReport");
|
||||
const MetricsUtils = findByProps("_metrics");
|
||||
|
||||
// Sentry interface definition to avoid TypeScript errors
|
||||
interface SentryClient {
|
||||
getOptions: () => { enabled: boolean };
|
||||
close: () => void;
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
interface SentryScope {
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
interface SentryHub {
|
||||
getClient: () => SentryClient | undefined;
|
||||
getStackTop: () => { scope: SentryScope };
|
||||
getScope: () => SentryScope;
|
||||
addBreadcrumb?: (breadcrumb: unknown) => void;
|
||||
}
|
||||
|
||||
interface GlobalSentry {
|
||||
hub?: SentryHub;
|
||||
}
|
||||
|
||||
// Get Sentry objects with proper type checking
|
||||
const sentryGlobal = (globalThis as unknown as { __SENTRY__?: GlobalSentry })
|
||||
.__SENTRY__;
|
||||
const sentryHub = sentryGlobal?.hub;
|
||||
const sentryClient = sentryHub?.getClient();
|
||||
|
||||
// Sentry related objects with type safety
|
||||
const Sentry = {
|
||||
initializer: findByProps("initSentry"),
|
||||
main: sentryHub,
|
||||
client: sentryClient,
|
||||
};
|
||||
|
||||
// Utility function to create no-operation patches
|
||||
const noop = (prop: string, parent: Record<string, any>): PatchCleanupFn => {
|
||||
try {
|
||||
return instead(prop, parent, () => undefined);
|
||||
} catch (e) {
|
||||
return () => false;
|
||||
}
|
||||
};
|
||||
|
||||
// Network tracking prevention
|
||||
function patchNetwork(): PatchCleanupFn {
|
||||
// Analytics and tracking endpoints to block
|
||||
const analyticsTest =
|
||||
/client-analytics\.braintreegateway\.com|discord\.com\/api\/v9\/(science|track)|app\.adjust\..*|.*\.ingest\.sentry\.io/;
|
||||
|
||||
try {
|
||||
const unpatch = instead(
|
||||
"send",
|
||||
XMLHttpRequest.prototype,
|
||||
function (
|
||||
this: XMLHttpRequest & { __sentry_xhr__?: { url: string } },
|
||||
args: unknown[],
|
||||
orig: Function,
|
||||
) {
|
||||
if (
|
||||
this.__sentry_xhr__?.url &&
|
||||
analyticsTest.test(this.__sentry_xhr__.url)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return orig.apply(this, args);
|
||||
},
|
||||
);
|
||||
return unpatch;
|
||||
} catch (e) {
|
||||
return () => false;
|
||||
}
|
||||
}
|
||||
|
||||
// Console patching to remove Sentry wrappers
|
||||
function patchConsole(): PatchCleanupFn {
|
||||
type ConsoleFunction = Function & { __sentry_original__?: Function };
|
||||
type ConsoleFunctions = Record<string, ConsoleFunction>;
|
||||
|
||||
const sentrified: ConsoleFunctions = {};
|
||||
|
||||
try {
|
||||
// We need to use Object.keys because TypeScript doesn't allow indexing Console with strings
|
||||
(Object.keys(console) as Array<keyof Console>).forEach((key) => {
|
||||
const consoleFunc = (console as unknown as ConsoleFunctions)[key];
|
||||
if (consoleFunc) {
|
||||
sentrified[key] = consoleFunc;
|
||||
const originalFunc = consoleFunc.__sentry_original__;
|
||||
(console as unknown as ConsoleFunctions)[key] =
|
||||
originalFunc ?? consoleFunc;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger.log("Failed to de-sentrify console functions!", e);
|
||||
}
|
||||
|
||||
return () => {
|
||||
Object.keys(sentrified).forEach((key) => {
|
||||
if (sentrified[key]) {
|
||||
(console as unknown as ConsoleFunctions)[key] = sentrified[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Miscellaneous tracking functions
|
||||
function patchMiscellaneous(): PatchCleanupFn {
|
||||
const miscPatches = [
|
||||
// Global analytics utilities
|
||||
AnalyticsUtils?.AnalyticsActionHandlers &&
|
||||
noop("handleTrack", AnalyticsUtils.AnalyticsActionHandlers),
|
||||
AnalyticsUtils?.AnalyticsActionHandlers &&
|
||||
noop("handleFingerprint", AnalyticsUtils.AnalyticsActionHandlers),
|
||||
|
||||
// Super properties tracking
|
||||
SuperPropUtils && noop("track", SuperPropUtils),
|
||||
|
||||
// Voice state metadata tracking
|
||||
VoiceStateUtils && noop("trackWithMetadata", VoiceStateUtils),
|
||||
|
||||
// Crash reporter
|
||||
CrashReportUtils && noop("submitLiveCrashReport", CrashReportUtils),
|
||||
|
||||
// Metrics
|
||||
MetricsUtils?._metrics && noop("push", MetricsUtils._metrics),
|
||||
].filter(Boolean) as PatchCleanupFn[];
|
||||
|
||||
return () => miscPatches.forEach((p) => p());
|
||||
}
|
||||
|
||||
// Sentry error reporting prevention
|
||||
function patchSentry(): PatchCleanupFn {
|
||||
const sentryPatches: PatchCleanupFn[] = [];
|
||||
|
||||
// Add patches if Sentry components exist
|
||||
if (Sentry.initializer) {
|
||||
sentryPatches.push(noop("initSentry", Sentry.initializer));
|
||||
}
|
||||
|
||||
if (Sentry.main && Sentry.main.addBreadcrumb) {
|
||||
sentryPatches.push(noop("addBreadcrumb", Sentry.main));
|
||||
}
|
||||
|
||||
// Disable Sentry client
|
||||
if (Sentry.client) {
|
||||
try {
|
||||
Sentry.client.getOptions().enabled = false;
|
||||
Sentry.client.close();
|
||||
|
||||
if (Sentry.main) {
|
||||
if (Sentry.main.getStackTop) {
|
||||
Sentry.main.getStackTop().scope.clear();
|
||||
}
|
||||
|
||||
if (Sentry.main.getScope) {
|
||||
Sentry.main.getScope().clear();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
try {
|
||||
sentryPatches.forEach((p) => p());
|
||||
|
||||
// Re-enable Sentry client on cleanup
|
||||
if (Sentry.client) {
|
||||
Sentry.client.getOptions().enabled = true;
|
||||
Sentry.client.open();
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineCorePlugin({
|
||||
manifest: {
|
||||
id: "bunny.notrack",
|
||||
version: "1.0.0",
|
||||
type: "plugin",
|
||||
spec: 3,
|
||||
main: "",
|
||||
display: {
|
||||
name: "NoTrack",
|
||||
description: "Disables Discord's telemetry",
|
||||
authors: [{ name: "maisymoe" }],
|
||||
},
|
||||
},
|
||||
|
||||
start() {
|
||||
// Apply all patches
|
||||
patches = [
|
||||
patchNetwork(),
|
||||
patchConsole(),
|
||||
patchMiscellaneous(),
|
||||
patchSentry(),
|
||||
].filter(Boolean);
|
||||
|
||||
logger.log("NoTrack: Enabled - all telemetry tracking disabled");
|
||||
},
|
||||
|
||||
stop() {
|
||||
// Call all cleanup functions
|
||||
patches.forEach((p) => p?.());
|
||||
patches = [];
|
||||
|
||||
logger.log("NoTrack: Disabled - telemetry tracking restored");
|
||||
},
|
||||
});
|
||||
|
|
@ -7,10 +7,15 @@ let patches = [] as (() => unknown)[];
|
|||
export default defineCorePlugin({
|
||||
manifest: {
|
||||
id: "bunny.quickinstall",
|
||||
name: "QuickInstall",
|
||||
version: "1.0.0",
|
||||
description: "Quickly install Vendetta plugins and themes",
|
||||
authors: [{ name: "Vendetta Team" }]
|
||||
type: "plugin",
|
||||
spec: 3,
|
||||
main: "",
|
||||
display: {
|
||||
name: "QuickInstall",
|
||||
description: "Quickly install Vendetta plugins and themes",
|
||||
authors: [{ name: "Vendetta Team" }]
|
||||
}
|
||||
},
|
||||
start() {
|
||||
patches = [patchForumPost(), patchUrl()];
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
<View style={{ flexDirection: "row", gap: 8 }}>
|
||||
<Search style={{ flexGrow: 1 }} isRound={!!props.sortOptions} onChangeText={v => setSearch(v)} />
|
||||
{props.sortOptions && <IconButton
|
||||
icon={findAssetId("ic_forum_channel_sort_order_24px")}
|
||||
icon={findAssetId("MoreVerticalIcon")}
|
||||
variant="tertiary"
|
||||
disabled={!!search}
|
||||
onPress={() => showSimpleActionSheet({
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function ErrorBoundaryScreen(props: {
|
|||
<SafeAreaView style={styles.container}>
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text variant="display-lg">Uh oh.</Text>
|
||||
<Text variant="text-md/normal">A crash occurred while rendering a component. This could be caused by a plugin, Revenge, or Discord itself.</Text>
|
||||
<Text variant="text-md/normal">A crash occurred while rendering a component. This could be caused by a plugin, Kettu, or Discord itself.</Text>
|
||||
<Text variant="text-sm/normal" color="text-muted">{debugInfo.os.name}; {debugInfo.discord.build} ({debugInfo.discord.version}); {debugInfo.bunny.version}</Text>
|
||||
</View>
|
||||
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ export default function initSettings() {
|
|||
render: () => import("@core/ui/settings/pages/Fonts"),
|
||||
usePredicate: () => isFontSupported()
|
||||
},
|
||||
{
|
||||
key: "KETTU_BROWSER",
|
||||
title: () => Strings.BROWSER,
|
||||
icon: findAssetId("UploadIcon"),
|
||||
render: () => import("@core/ui/settings/pages/PluginBrowser"),
|
||||
},
|
||||
{
|
||||
key: "BUNNY_DEVELOPER",
|
||||
title: () => Strings.DEVELOPER,
|
||||
|
|
@ -51,12 +57,6 @@ export default function initSettings() {
|
|||
]
|
||||
});
|
||||
|
||||
// Compat with Vendetta Plugins that use configs in settings
|
||||
registerSection({
|
||||
name: "Vendetta",
|
||||
items: []
|
||||
});
|
||||
|
||||
// Compat with Bunny Plugins that use configs in settings
|
||||
registerSection({
|
||||
name: "Bunny",
|
||||
|
|
@ -68,4 +68,10 @@ export default function initSettings() {
|
|||
name: "Revenge",
|
||||
items: []
|
||||
});
|
||||
|
||||
// Compat with Vendetta Plugins that use configs in settings
|
||||
registerSection({
|
||||
name: "Vendetta",
|
||||
items: []
|
||||
});
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleA
|
|||
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
|
||||
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
|
||||
|
||||
const RDT_EMBED_LINK = "https://github.com/revenge-mod/react-devtools-core/releases/latest/download/rdtc.js";
|
||||
const RDT_EMBED_LINK = "";
|
||||
|
||||
const useStyles = createStyles({
|
||||
leadingText: {
|
||||
|
|
@ -103,7 +103,7 @@ export default function Developer() {
|
|||
// Thanks for this Revenge team!
|
||||
label={Strings.CLEAR_BUNDLE}
|
||||
|
||||
icon={<TableRow.Icon source={findAssetId("trash")} />}
|
||||
icon={<TableRow.Icon source={findAssetId("TrashIcon")} />}
|
||||
onPress={() => {
|
||||
openAlert("pupu-clear-bundle-reload-confirmation", <AlertModal
|
||||
title={Strings.MODAL_RELOAD_REQUIRED}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default function About() {
|
|||
{
|
||||
label: Strings.BRAND,
|
||||
version: debugInfo.device.brand,
|
||||
icon: "SparklesIcon"
|
||||
icon: "MagicWandIcon"
|
||||
},
|
||||
{
|
||||
label: Strings.MODEL,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ import { getDebugInfo } from "@lib/api/debug";
|
|||
import { BundleUpdaterManager } from "@lib/api/native/modules";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { openAlert } from "@lib/ui/alerts";
|
||||
import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants";
|
||||
import { DISCORD_SERVER, GITHUB, CODEBERG } from "@lib/utils/constants";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { AlertActionButton, AlertActions, AlertModal, Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components";
|
||||
import { Linking, ScrollView } from "react-native";
|
||||
import CodebergIcon from "@assets/icons/codeberg-logo_icon_white.png";
|
||||
|
||||
export default function General() {
|
||||
useProxy(settings);
|
||||
|
|
@ -50,6 +51,12 @@ export default function General() {
|
|||
icon={<TableRow.Icon source={findAssetId("Discord")!} />}
|
||||
onPress={() => Linking.openURL(DISCORD_SERVER)}
|
||||
/>
|
||||
<TableRow
|
||||
arrow={true}
|
||||
label={Strings.CODEBERG}
|
||||
icon={<TableRow.Icon source={{ uri: CodebergIcon}} />}
|
||||
onPress={() => Linking.openURL(CODEBERG)}
|
||||
/>
|
||||
<TableRow
|
||||
arrow={true}
|
||||
label={Strings.GITHUB}
|
||||
|
|
|
|||
|
|
@ -1,73 +1,223 @@
|
|||
import { deleteRepository, installPlugin, isCorePlugin, isPluginInstalled, pluginRepositories, registeredPlugins, uninstallPlugin, updateAllRepository, updateRepository } from "@lib/addons/plugins";
|
||||
import { BunnyPluginManifestInternal } from "@lib/addons/plugins/types";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { dismissAlert, openAlert } from "@lib/ui/alerts";
|
||||
import { AlertActionButton } from "@lib/ui/components/wrappers";
|
||||
import { hideSheet, showSheet } from "@lib/ui/sheets";
|
||||
import { showToast } from "@lib/ui/toasts";
|
||||
import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants";
|
||||
import { NEXPID_PLUGINS_REPO_URL } from "@lib/utils/constants";
|
||||
import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
|
||||
import { clipboard, NavigationNative } from "@metro/common";
|
||||
import { ActionSheet, AlertActions, AlertModal, Button, Card, FlashList, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput } from "@metro/common/components";
|
||||
import { QueryClient, QueryClientProvider, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import { React, NavigationNative } from "@metro/common";
|
||||
import { View } from "react-native";
|
||||
import { Stack, Button, IconButton, Text, Card, FlashList } from "@metro/common/components";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import safeFetch from "@lib/utils/safeFetch";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import Search from "@ui/components/Search";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { installTheme, themes, removeTheme } from "@lib/addons/themes";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { hideSheet, showSheet } from "@lib/ui/sheets";
|
||||
import { AlertActionButton } from "@lib/ui/components/wrappers";
|
||||
import { dismissAlert, openAlert } from "@lib/ui/alerts";
|
||||
import { ActionSheet, AlertModal, AlertActions, TableRow, TableRowGroup } from "@metro/common/components";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
|
||||
|
||||
async function getManifests() {
|
||||
await updateAllRepository();
|
||||
const plugins = [...registeredPlugins.values()];
|
||||
return plugins.filter(p => !isCorePlugin(p.id));
|
||||
interface BaseAddonData {
|
||||
name: string;
|
||||
description: string;
|
||||
authors: string[];
|
||||
installUrl: string;
|
||||
}
|
||||
|
||||
function InstallButton(props: { id: string; }) {
|
||||
const [installed, setInstalled] = useState(isPluginInstalled(props.id));
|
||||
const installationState = useMutation<void, Error, { install: boolean; }>({
|
||||
mutationFn: async ({ install }) => {
|
||||
await (install ? installPlugin : uninstallPlugin)(props.id, true);
|
||||
},
|
||||
onSettled() {
|
||||
setInstalled(isPluginInstalled(props.id));
|
||||
},
|
||||
onError(error) {
|
||||
showToast(error instanceof Error ? error.message : String(error));
|
||||
interface PluginData extends BaseAddonData {
|
||||
status: "working" | "broken" | "warning" | string;
|
||||
sourceUrl: string;
|
||||
warningMessage?: string;
|
||||
}
|
||||
|
||||
interface ThemeData extends BaseAddonData {}
|
||||
|
||||
type AddonData = PluginData | ThemeData;
|
||||
|
||||
const PLUGIN_URL = "https://raw.githubusercontent.com/Purple-EyeZ/Plugins-List/refs/heads/main/src/plugins-data.json";
|
||||
const THEME_URL = "https://raw.githubusercontent.com/kmmiio99o/theme-marketplace/refs/heads/main/themes.json";
|
||||
|
||||
function normalizeIdFromInstallUrl(url: string) {
|
||||
return url.endsWith("/") ? url : url + "/";
|
||||
}
|
||||
|
||||
// @ts-ignore (i cant be bothered to type these)
|
||||
function InstallButton({ addon, isPluginMode, installing, setInstalling, setRefreshTick }) {
|
||||
const normId = normalizeIdFromInstallUrl(addon.installUrl);
|
||||
const [installed, setInstalled] = React.useState(() =>
|
||||
isPluginMode ? Boolean(VdPluginManager.plugins[normId]) : Boolean(themes[addon.installUrl])
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setInstalled(isPluginMode ? Boolean(VdPluginManager.plugins[normId]) : Boolean(themes[addon.installUrl]));
|
||||
}, [addon.installUrl, setRefreshTick, isPluginMode]);
|
||||
|
||||
const installAddon = async () => {
|
||||
if (installing.has(normId)) return;
|
||||
setInstalling((prev: Iterable<unknown> | null | undefined) => new Set(prev).add(normId));
|
||||
try {
|
||||
if (isPluginMode) {
|
||||
await VdPluginManager.installPlugin(normId, true);
|
||||
} else {
|
||||
await installTheme(addon.installUrl);
|
||||
}
|
||||
showToast(`Installed ${addon.name}`, findAssetId("CheckIcon"));
|
||||
setInstalled(true);
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : String(e), findAssetId("CircleXIcon-primary"));
|
||||
} finally {
|
||||
setInstalling((prev: Iterable<unknown> | null | undefined) => { const s = new Set(prev); s.delete(normId); return s; });
|
||||
setRefreshTick((t: number) => t + 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return <Button
|
||||
size="sm"
|
||||
loading={installationState.isPending}
|
||||
text={!installed ? "Install" : "Uninstall"}
|
||||
onPress={() => installationState.mutate({ install: !installed })}
|
||||
variant={!installed ? "primary" : "destructive"}
|
||||
icon={findAssetId(!installed ? "DownloadIcon" : "TrashIcon")}
|
||||
/>;
|
||||
}
|
||||
const uninstallAddon = async () => {
|
||||
try {
|
||||
if (isPluginMode) {
|
||||
await VdPluginManager.removePlugin(normId);
|
||||
} else {
|
||||
await removeTheme(addon.installUrl);
|
||||
}
|
||||
showToast(`Uninstalled ${addon.name}`, findAssetId("TrashIcon"));
|
||||
setInstalled(false);
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : String(e), findAssetId("CircleXIcon-primary"));
|
||||
} finally {
|
||||
setRefreshTick((t: number) => t + 1);
|
||||
}
|
||||
};
|
||||
|
||||
function TrailingButtons(props: { id: string; }) {
|
||||
return <Stack spacing={8} direction="horizontal">
|
||||
<IconButton
|
||||
const promptInstall = () => {
|
||||
if (!isPluginMode) return installAddon();
|
||||
|
||||
const plugin = addon as PluginData;
|
||||
const needsWarn = (plugin.status && plugin.status !== "working") || (plugin.warningMessage && plugin.warningMessage.trim().length > 0);
|
||||
if (!needsWarn) return installAddon();
|
||||
|
||||
const lines: string[] = [];
|
||||
if (plugin.status && plugin.status !== "working") {
|
||||
if (plugin.status === "broken") lines.push("This plugin is marked as broken, please be aware you may encounter issues");
|
||||
else if (plugin.status === "warning") lines.push("This plugin may have issues");
|
||||
else lines.push(`Status: ${plugin.status}`);
|
||||
}
|
||||
if (plugin.warningMessage) lines.push(plugin.warningMessage);
|
||||
|
||||
openAlert("plugins-list-install-warning", (
|
||||
<AlertModal
|
||||
title="Warning!"
|
||||
content="This plugin may not work as expected."
|
||||
extraContent={<Text variant="text-sm/normal" color="text-muted">{lines.join("\n\n")}</Text>}
|
||||
actions={<AlertActions>
|
||||
<AlertActionButton
|
||||
text="Install Anyway"
|
||||
variant="primary"
|
||||
onPress={() => { dismissAlert("plugins-list-install-warning"); installAddon(); }}
|
||||
/>
|
||||
<AlertActionButton
|
||||
text="Cancel"
|
||||
variant="secondary"
|
||||
onPress={() => dismissAlert("plugins-list-install-warning")}
|
||||
/>
|
||||
</AlertActions>}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
showSheet("plugin-info", () => {
|
||||
return <ActionSheet>
|
||||
<TableRowGroup title="Plugin Info">
|
||||
<TableRow label="ID" subLabel={props.id} />
|
||||
</TableRowGroup>
|
||||
</ActionSheet>;
|
||||
});
|
||||
}}
|
||||
variant="secondary"
|
||||
icon={findAssetId("CircleInformationIcon")}
|
||||
loading={installing.has(normId)}
|
||||
text={!installed ? (installing.has(normId) ? "Installing..." : "Install") : "Uninstall"}
|
||||
disabled={installing.has(normId)}
|
||||
onPress={!installed ? promptInstall : uninstallAddon}
|
||||
variant={!installed ? "primary" : "destructive"}
|
||||
icon={findAssetId(!installed ? "DownloadIcon" : "TrashIcon")}
|
||||
/>
|
||||
<InstallButton id={props.id} />
|
||||
</Stack>;
|
||||
);
|
||||
}
|
||||
|
||||
function PluginCard(props: { manifest: BunnyPluginManifestInternal; }) {
|
||||
const { display, version } = props.manifest;
|
||||
// @ts-ignore (i cant be bothered to type these)
|
||||
function TrailingButtons({ addon, isPluginMode, installing, setInstalling, setRefreshTick }) {
|
||||
const copyAddonLink = () => {
|
||||
clipboard.setString(addon.installUrl);
|
||||
// @ts-ignore
|
||||
showToast.showCopyToClipboard?.();
|
||||
};
|
||||
|
||||
const copySourceUrl = () => {
|
||||
const plugin = addon as PluginData;
|
||||
clipboard.setString(plugin.sourceUrl);
|
||||
// @ts-ignore
|
||||
showToast.showCopyToClipboard?.();
|
||||
};
|
||||
|
||||
const openAddonMenu = () => {
|
||||
const actions = [
|
||||
{
|
||||
label: `Copy ${isPluginMode ? 'Plugin' : 'Theme'} Link`,
|
||||
icon: findAssetId("CopyIcon"),
|
||||
onPress: copyAddonLink
|
||||
}
|
||||
];
|
||||
|
||||
if (isPluginMode && (addon as PluginData).sourceUrl) {
|
||||
actions.push({
|
||||
label: "Copy Source URL",
|
||||
icon: findAssetId("CopyIcon"),
|
||||
onPress: copySourceUrl
|
||||
});
|
||||
}
|
||||
|
||||
const sheetKey = `${isPluginMode ? 'plugin' : 'theme'}-menu`;
|
||||
showSheet(sheetKey, () => (
|
||||
<ActionSheet>
|
||||
<TableRowGroup title={`${isPluginMode ? 'Plugin' : 'Theme'} Info`}>
|
||||
{actions.map((action, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
label={action.label}
|
||||
icon={<TableRow.Icon source={action.icon} />}
|
||||
onPress={() => {
|
||||
action.onPress();
|
||||
hideSheet(sheetKey);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</TableRowGroup>
|
||||
</ActionSheet>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={8} direction="horizontal">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onPress={openAddonMenu}
|
||||
variant="secondary"
|
||||
icon={findAssetId("MoreHorizontalIcon")}
|
||||
/>
|
||||
<InstallButton
|
||||
addon={addon}
|
||||
isPluginMode={isPluginMode}
|
||||
installing={installing}
|
||||
setInstalling={setInstalling}
|
||||
setRefreshTick={setRefreshTick}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore (i cant be bothered to type these)
|
||||
function AddonCard({ addon, isPluginMode, installing, setInstalling, setRefreshTick }) {
|
||||
const { name, description, authors } = addon;
|
||||
const plugin = addon as PluginData;
|
||||
|
||||
let statusColor = "text-normal";
|
||||
if (isPluginMode) {
|
||||
if (plugin.status === "working") statusColor = "#4ADE80";
|
||||
if (plugin.status === "broken") statusColor = "#EF4444";
|
||||
if (plugin.status === "warning") statusColor = "#F59E0B";
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -75,184 +225,275 @@ function PluginCard(props: { manifest: BunnyPluginManifestInternal; }) {
|
|||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<Text numberOfLines={1} variant="heading-lg/semibold">
|
||||
{display.name}
|
||||
{name}
|
||||
</Text>
|
||||
<Text variant="text-md/semibold" color="text-muted">
|
||||
by {display.authors?.map(a => a.name).join(", ") || "Unknown"} ({version})
|
||||
by {authors?.join(", ") || "Unknown"}
|
||||
</Text>
|
||||
{isPluginMode && (
|
||||
<Text variant="text-md/semibold" style={{ color: statusColor }}>
|
||||
Status: {plugin.status}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View>
|
||||
<TrailingButtons id={props.manifest.id} />
|
||||
<TrailingButtons
|
||||
addon={addon}
|
||||
isPluginMode={isPluginMode}
|
||||
installing={installing}
|
||||
setInstalling={setInstalling}
|
||||
setRefreshTick={setRefreshTick}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text variant="text-md/medium">
|
||||
{display.description}
|
||||
{description}
|
||||
</Text>
|
||||
{isPluginMode && plugin.warningMessage && (
|
||||
<Text variant="text-sm/medium" color="text-muted">
|
||||
Warning: {plugin.warningMessage}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function BrowserPage() {
|
||||
enum Sort {
|
||||
DateNewest = "Newest",
|
||||
DateOldest = "Oldest",
|
||||
NameAZ = "Name (A–Z)",
|
||||
NameZA = "Name (Z–A)",
|
||||
WorkingFirst = "Working First",
|
||||
BrokenFirst = "Broken First",
|
||||
}
|
||||
|
||||
export default function BrowserPage() {
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
useEffect(() => {
|
||||
|
||||
const [mode, setMode] = React.useState<"plugins" | "themes">("plugins");
|
||||
const [plugins, setPlugins] = React.useState<PluginData[]>([]);
|
||||
const [themesList, setThemesList] = React.useState<ThemeData[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [installing, setInstalling] = React.useState<Set<string>>(new Set());
|
||||
const [refreshTick, setRefreshTick] = React.useState(0);
|
||||
const [sort, setSort] = React.useState<Sort>(Sort.DateNewest);
|
||||
|
||||
React.useEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: "Plugin Browser",
|
||||
headerRight: () => <IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("PlusSmallIcon")}
|
||||
onPress={() => {
|
||||
showSheet("plugin-browser-options", PluginBrowserOptions);
|
||||
}}
|
||||
/>
|
||||
title: "Browser"
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const { data, error, isPending, refetch } = useQuery({
|
||||
queryKey: ["plugins-repo-fetch"],
|
||||
queryFn: () => getManifests()
|
||||
});
|
||||
const fetchData = React.useCallback(async (isPluginMode: boolean) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = isPluginMode ? PLUGIN_URL : THEME_URL;
|
||||
const response = await safeFetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
const data = await response.json();
|
||||
|
||||
let addonList: AddonData[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
addonList = data;
|
||||
} else if (isPluginMode && data.OFFICIAL_PLUGINS) {
|
||||
addonList = data.OFFICIAL_PLUGINS;
|
||||
} else if (!isPluginMode) {
|
||||
// Handle any other structure for themes - try common property names
|
||||
addonList = data.OFFICIAL_THEMES || data.themes || data.THEMES || data.items || [];
|
||||
}
|
||||
|
||||
if (isPluginMode) {
|
||||
setPlugins(addonList as PluginData[]);
|
||||
} else {
|
||||
setThemesList(addonList as ThemeData[]);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
if (isPluginMode) {
|
||||
setPlugins([]);
|
||||
} else {
|
||||
setThemesList([]);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchPlugins = React.useCallback(() => fetchData(true), [fetchData]);
|
||||
const fetchThemes = React.useCallback(() => fetchData(false), [fetchData]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchPlugins();
|
||||
fetchThemes();
|
||||
}, [fetchPlugins, fetchThemes]);
|
||||
|
||||
const filterList = (list: AddonData[]) => {
|
||||
if (!list) return [] as AddonData[];
|
||||
const q = searchQuery.toLowerCase();
|
||||
if (!q) return list;
|
||||
return list.filter(p =>
|
||||
p.name.toLowerCase().includes(q)
|
||||
|| p.description.toLowerCase().includes(q)
|
||||
|| (p.authors || []).some(a => a.toLowerCase().includes(q))
|
||||
);
|
||||
};
|
||||
|
||||
const sortedAndFiltered = React.useMemo(() => {
|
||||
const list = filterList(mode === "plugins" ? plugins : themesList);
|
||||
|
||||
const getStatusPriority = (status: PluginData["status"], sortBy: Sort): number => {
|
||||
if (sortBy === Sort.WorkingFirst) {
|
||||
return status === "working" || status === "warning" ? 0 : 1;
|
||||
}
|
||||
if (sortBy === Sort.BrokenFirst) {
|
||||
return status === "broken" ? 0 : 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
switch (sort) {
|
||||
case Sort.DateNewest:
|
||||
return [...list].reverse();
|
||||
case Sort.DateOldest:
|
||||
return [...list];
|
||||
case Sort.NameAZ:
|
||||
return [...list].sort((a, b) => a.name.localeCompare(b.name));
|
||||
case Sort.NameZA:
|
||||
return [...list].sort((a, b) => b.name.localeCompare(a.name));
|
||||
case Sort.WorkingFirst:
|
||||
if (mode === "plugins") {
|
||||
return [...list].sort((a, b) => {
|
||||
const pa = getStatusPriority((a as PluginData).status, Sort.WorkingFirst);
|
||||
const pb = getStatusPriority((b as PluginData).status, Sort.WorkingFirst);
|
||||
return pa !== pb ? pa - pb : a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
return list;
|
||||
case Sort.BrokenFirst:
|
||||
if (mode === "plugins") {
|
||||
return [...list].sort((a, b) => {
|
||||
const pa = getStatusPriority((a as PluginData).status, Sort.BrokenFirst);
|
||||
const pb = getStatusPriority((b as PluginData).status, Sort.BrokenFirst);
|
||||
return pa !== pb ? pa - pb : a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
return list;
|
||||
default:
|
||||
return list;
|
||||
}
|
||||
}, [plugins, themesList, mode, searchQuery, sort]);
|
||||
|
||||
if (error) {
|
||||
return <View style={{
|
||||
flex: 1,
|
||||
paddingHorizontal: 8,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<Card style={{ gap: 8 }}>
|
||||
<Text style={{ textAlign: "center" }} variant="heading-lg/bold">
|
||||
An error occured while fetching the repository!
|
||||
</Text>
|
||||
<Text style={{ textAlign: "center" }} variant="text-sm/medium" color="text-muted">
|
||||
{error instanceof Error ? error.message : String(error)}
|
||||
</Text>
|
||||
<Button
|
||||
size="lg"
|
||||
text="Refetch"
|
||||
onPress={refetch}
|
||||
icon={findAssetId("RetryIcon")}
|
||||
/>
|
||||
</Card>
|
||||
</View>;
|
||||
return (
|
||||
<View style={{ flex: 1, paddingHorizontal: 8, justifyContent: "center", alignItems: "center" }}>
|
||||
<Card style={{ gap: 8 }}>
|
||||
<Text style={{ textAlign: "center" }} variant="heading-lg/bold">
|
||||
An error occurred while fetching the repository
|
||||
</Text>
|
||||
<Text style={{ textAlign: "center" }} variant="text-sm/medium" color="text-muted">
|
||||
{error}
|
||||
</Text>
|
||||
<Button
|
||||
size="lg"
|
||||
text="Refetch"
|
||||
onPress={() => fetchData(mode === "plugins")}
|
||||
icon={findAssetId("RetryIcon")}
|
||||
/>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <FlashList
|
||||
data={data}
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
estimatedItemSize={136}
|
||||
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
|
||||
renderItem={({ item: manifest }: any) => (
|
||||
<View style={{ paddingVertical: 6, paddingHorizontal: 8 }}>
|
||||
<PluginCard manifest={manifest} />
|
||||
</View>
|
||||
)}
|
||||
/>;
|
||||
}
|
||||
|
||||
function AddRepositoryAlert() {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return <AlertModal
|
||||
title="Add Repository"
|
||||
content="Enter the URL of the repository you want to add."
|
||||
extraContent={<TextInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
placeholder="https://example.com/repo.json" />}
|
||||
actions={<AlertActions>
|
||||
<AlertActionButton
|
||||
text="Add"
|
||||
variant="primary"
|
||||
disabled={!isValidHttpUrl(value)}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await updateRepository(value);
|
||||
showToast("Added repository!", findAssetId("Check"));
|
||||
} catch (e) {
|
||||
showToast("Failed to add repository!", findAssetId("Small"));
|
||||
} finally {
|
||||
dismissAlert("bunny-add-plugin-repository");
|
||||
showSheet("plugin-browser-options", PluginBrowserOptions);
|
||||
}
|
||||
}} />
|
||||
</AlertActions>} />;
|
||||
}
|
||||
|
||||
function PluginBrowserOptions() {
|
||||
return <ActionSheet>
|
||||
<TableRowGroup title="Repositories">
|
||||
{Object.keys(pluginRepositories).map(url => {
|
||||
return <RepositoryRow key={url} url={url} />;
|
||||
})}
|
||||
<TableRow
|
||||
label="Add Repository..."
|
||||
icon={<TableRow.Icon source={findAssetId("PlusMediumIcon")} />}
|
||||
onPress={() => {
|
||||
openAlert("bunny-add-plugin-repository", <AddRepositoryAlert />);
|
||||
hideSheet("plugin-browser-options");
|
||||
}} />
|
||||
</TableRowGroup>
|
||||
</ActionSheet>;
|
||||
}
|
||||
|
||||
function RepositoryRow(props: { url: string; }) {
|
||||
const repo = pluginRepositories[props.url];
|
||||
const isOfficialBunny = props.url === OFFICIAL_PLUGINS_REPO_URL;
|
||||
const isNexpid = props.url === NEXPID_PLUGINS_REPO_URL;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
label={isOfficialBunny ? "Bunny's Repository" : (repo.$meta?.name ?? "Unknown")}
|
||||
subLabel={props.url}
|
||||
trailing={(
|
||||
<Stack direction="horizontal">
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("LinkIcon")}
|
||||
onPress={() => {
|
||||
clipboard.setString(props.url);
|
||||
showToast.showCopyToClipboard();
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={isOfficialBunny}
|
||||
icon={findAssetId("TrashIcon")}
|
||||
onPress={() => {
|
||||
openAlert("bunny-remove-repository", <AlertModal
|
||||
title="Remove Repository"
|
||||
content="Are you sure you want to remove this repository?"
|
||||
extraContent={<Card>
|
||||
<Text variant="text-md/normal">{props.url}</Text>
|
||||
</Card>}
|
||||
actions={<AlertActions>
|
||||
<AlertActionButton
|
||||
text="Remove"
|
||||
variant="destructive"
|
||||
onPress={async () => {
|
||||
await deleteRepository(props.url);
|
||||
showToast("Removed repository!", findAssetId("Trash"));
|
||||
dismissAlert("bunny-remove-repository");
|
||||
}}
|
||||
/>
|
||||
</AlertActions>}
|
||||
/>);
|
||||
}}
|
||||
/>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ justifyContent: "center", alignItems: "center", paddingHorizontal: 10 }}>
|
||||
<Stack spacing={12}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "center", alignItems: 'center', paddingTop: 10 }}>
|
||||
<Stack spacing={10} style={{ flexDirection: "row", justifyContent: "center", alignItems: 'center' }}>
|
||||
<Button
|
||||
size="sm"
|
||||
text="Plugins"
|
||||
variant={mode === "plugins" ? "primary" : "secondary"}
|
||||
onPress={() => setMode("plugins")}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
text="Themes"
|
||||
variant={mode === "themes" ? "primary" : "secondary"}
|
||||
onPress={() => setMode("themes")}
|
||||
/>
|
||||
</Stack>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingBottom: 6 }}>
|
||||
<Search
|
||||
placeholder={`Search ${mode}...`}
|
||||
onChangeText={setSearchQuery}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
icon={findAssetId("MoreVerticalIcon")}
|
||||
disabled={!!searchQuery}
|
||||
onPress={() => showSimpleActionSheet({
|
||||
key: "AddonListSortOptions",
|
||||
header: {
|
||||
title: "Sort Options",
|
||||
onClose: () => hideActionSheet("AddonListSortOptions"),
|
||||
},
|
||||
options: Object.entries(Sort).map(([key, value]) => ({
|
||||
label: value,
|
||||
onPress: () => {
|
||||
setSort(value as Sort);
|
||||
}
|
||||
}))
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</Stack>
|
||||
)} />
|
||||
</View>
|
||||
|
||||
<FlashList
|
||||
data={sortedAndFiltered}
|
||||
refreshing={loading}
|
||||
onRefresh={mode === "plugins" ? fetchPlugins : fetchThemes}
|
||||
estimatedItemSize={200}
|
||||
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
|
||||
ListHeaderComponent={mode === "plugins" ? (
|
||||
<View style={{ paddingVertical: 6, paddingHorizontal: 8 }}>
|
||||
<Card border="strong">
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", flexDirection: "row" }}>
|
||||
<View style={{ gap: 6, flexShrink: 1 }}>
|
||||
<Text variant="heading-md/bold">Unproxied Plugins</Text>
|
||||
<Text variant="text-sm/medium" color="text-muted">
|
||||
Plugins installed from this source have not been checked for safety, install at your own risk
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
</View>
|
||||
) : null}
|
||||
//@ts-ignore
|
||||
renderItem={({ item: addon }) => (
|
||||
<View style={{ paddingVertical: 6, paddingHorizontal: 8 }}>
|
||||
<AddonCard
|
||||
addon={addon}
|
||||
isPluginMode={mode === "plugins"}
|
||||
installing={installing}
|
||||
setInstalling={setInstalling}
|
||||
setRefreshTick={setRefreshTick}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PluginBrowser() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserPage />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,154 +3,182 @@ import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models";
|
|||
import { usePluginCardStyles } from "@core/ui/settings/pages/Plugins/usePluginCardStyles";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { NavigationNative, tokens } from "@metro/common";
|
||||
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
|
||||
import {
|
||||
Card,
|
||||
IconButton,
|
||||
Stack,
|
||||
TableSwitch,
|
||||
Text,
|
||||
} from "@metro/common/components";
|
||||
import { showSheet } from "@ui/sheets";
|
||||
import chroma from "chroma-js";
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { Image, View } from "react-native";
|
||||
|
||||
const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel>; }>(null!);
|
||||
const CardContext = createContext<{
|
||||
plugin: UnifiedPluginModel;
|
||||
result: Fuzzysort.KeysResult<UnifiedPluginModel>;
|
||||
}>(null!);
|
||||
const useCardContext = () => useContext(CardContext);
|
||||
|
||||
function getHighlightColor(): import("react-native").ColorValue {
|
||||
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
|
||||
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
|
||||
}
|
||||
|
||||
function Title() {
|
||||
const styles = usePluginCardStyles();
|
||||
const { plugin, result } = useCardContext();
|
||||
const styles = usePluginCardStyles();
|
||||
const { plugin, result } = useCardContext();
|
||||
|
||||
// could be empty if the plugin name is irrelevant!
|
||||
const highlightedNode = result[0].highlight((m, i) =>
|
||||
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
|
||||
{m}
|
||||
</Text>
|
||||
);
|
||||
// could be empty if the plugin name is irrelevant!
|
||||
const highlightedNode = result[0].highlight((m, i) => (
|
||||
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
|
||||
{m}
|
||||
</Text>
|
||||
));
|
||||
|
||||
const icon = plugin.icon && findAssetId(plugin.icon);
|
||||
const icon = plugin.icon && findAssetId(plugin.icon);
|
||||
|
||||
const textNode = (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
variant="heading-lg/semibold"
|
||||
>
|
||||
{highlightedNode.length ? highlightedNode : plugin.name}
|
||||
</Text>
|
||||
);
|
||||
const textNode = (
|
||||
<Text numberOfLines={1} variant="heading-lg/semibold">
|
||||
{highlightedNode.length ? highlightedNode : plugin.name}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
{icon && <Image
|
||||
style={styles.smallIcon}
|
||||
source={icon}
|
||||
/>}
|
||||
{textNode}
|
||||
</View>;
|
||||
return (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
{icon && <Image style={styles.smallIcon} source={icon} />}
|
||||
{textNode}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Authors() {
|
||||
const { plugin, result } = useCardContext();
|
||||
const styles = usePluginCardStyles();
|
||||
const { plugin, result } = useCardContext();
|
||||
const styles = usePluginCardStyles();
|
||||
|
||||
if (!plugin.authors) return null;
|
||||
if (!plugin.authors) return null;
|
||||
|
||||
// could be empty if the author(s) are irrelevant with the search!
|
||||
const highlightedNode = result[2].highlight((m, i) =>
|
||||
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
|
||||
{m}
|
||||
</Text>
|
||||
);
|
||||
// could be empty if the author(s) are irrelevant with the search!
|
||||
const highlightedNode = result[2].highlight((m, i) => (
|
||||
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
|
||||
{m}
|
||||
</Text>
|
||||
));
|
||||
|
||||
const badges = plugin.getBadges();
|
||||
const authorText = highlightedNode.length > 0 ? highlightedNode : plugin.authors.map(a => a.name).join(", ");
|
||||
const badges = plugin.getBadges();
|
||||
const authorText =
|
||||
highlightedNode.length > 0
|
||||
? highlightedNode
|
||||
: plugin.authors.map((a) => a.name).join(", ");
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", flexShrink: 1, gap: 4 }}>
|
||||
<Text variant="text-sm/semibold" color="text-muted">
|
||||
by {authorText}
|
||||
</Text>
|
||||
{badges.length > 0 && <View style={styles.badgesContainer}>
|
||||
{badges.map((b, i) => <Image
|
||||
key={i}
|
||||
source={b.source}
|
||||
style={styles.badgeIcon}
|
||||
/>)}
|
||||
</View>}
|
||||
return (
|
||||
<View
|
||||
style={{ flexDirection: "row", flexWrap: "wrap", flexShrink: 1, gap: 4 }}
|
||||
>
|
||||
<Text variant="text-sm/semibold" color="text-muted">
|
||||
by {authorText}
|
||||
</Text>
|
||||
{badges.length > 0 && (
|
||||
<View style={styles.badgesContainer}>
|
||||
{badges.map((b, i) => (
|
||||
<Image key={i} source={b.source} style={styles.badgeIcon} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Description() {
|
||||
const { plugin, result } = useCardContext();
|
||||
const { plugin, result } = useCardContext();
|
||||
|
||||
// could be empty if the description is irrelevant with the search!
|
||||
const highlightedNode = result[1].highlight((m, i) =>
|
||||
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>{m}</Text>
|
||||
);
|
||||
// could be empty if the description is irrelevant with the search!
|
||||
const highlightedNode = result[1].highlight((m, i) => (
|
||||
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
|
||||
{m}
|
||||
</Text>
|
||||
));
|
||||
|
||||
return <Text variant="text-md/medium">
|
||||
{highlightedNode.length ? highlightedNode : plugin.description}
|
||||
</Text>;
|
||||
return (
|
||||
<Text variant="text-md/medium">
|
||||
{highlightedNode.length ? highlightedNode : plugin.description}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const Actions = () => {
|
||||
const { plugin } = useCardContext();
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
const { plugin } = useCardContext();
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
return <View style={{ flexDirection: "row", gap: 6 }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
disabled={!plugin.getPluginSettingsComponent()}
|
||||
onPress={() => navigation.push("PUPU_CUSTOM_PAGE", {
|
||||
title: plugin.name,
|
||||
render: plugin.getPluginSettingsComponent(),
|
||||
})}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("CircleInformationIcon-primary")}
|
||||
onPress={() => void showSheet(
|
||||
"PluginInfoActionSheet",
|
||||
plugin.resolveSheetComponent(),
|
||||
{ plugin, navigation }
|
||||
)}
|
||||
/>
|
||||
</View>;
|
||||
return (
|
||||
<View style={{ flexDirection: "row", gap: 6 }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
disabled={!plugin.getPluginSettingsComponent()}
|
||||
onPress={() =>
|
||||
navigation.push("PUPU_CUSTOM_PAGE", {
|
||||
title: plugin.name,
|
||||
render: plugin.getPluginSettingsComponent(),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("CircleInformationIcon-primary")}
|
||||
onPress={() =>
|
||||
void showSheet(
|
||||
"PluginInfoActionSheet",
|
||||
plugin.resolveSheetComponent(),
|
||||
{ plugin, navigation },
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function PluginCard({ result, item: plugin }: CardWrapper<UnifiedPluginModel>) {
|
||||
plugin.usePluginState();
|
||||
export default function PluginCard({
|
||||
result,
|
||||
item: plugin,
|
||||
}: CardWrapper<UnifiedPluginModel>) {
|
||||
plugin.usePluginState();
|
||||
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), 0);
|
||||
const cardContextValue = useMemo(() => ({ plugin, result }), [plugin, result]);
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), 0);
|
||||
const cardContextValue = useMemo(
|
||||
() => ({ plugin, result }),
|
||||
[plugin, result],
|
||||
);
|
||||
|
||||
return (
|
||||
<CardContext.Provider value={cardContextValue}>
|
||||
<Card>
|
||||
<Stack spacing={16}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<Title />
|
||||
<Authors />
|
||||
</View>
|
||||
<View>
|
||||
<Stack spacing={12} direction="horizontal">
|
||||
<Actions />
|
||||
<TableSwitch
|
||||
value={plugin.isEnabled()}
|
||||
onValueChange={(v: boolean) => {
|
||||
plugin.toggle(v);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</View>
|
||||
</View>
|
||||
<Description />
|
||||
</Stack>
|
||||
</Card>
|
||||
</CardContext.Provider>
|
||||
);
|
||||
return (
|
||||
<CardContext.Provider value={cardContextValue}>
|
||||
<Card>
|
||||
<Stack spacing={16}>
|
||||
<View
|
||||
style={{ flexDirection: "row", justifyContent: "space-between" }}
|
||||
>
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<Title />
|
||||
<Authors />
|
||||
</View>
|
||||
<View>
|
||||
<Stack spacing={12} direction="horizontal">
|
||||
<Actions />
|
||||
<TableSwitch
|
||||
value={plugin.isEnabled()}
|
||||
onValueChange={(v: boolean) => {
|
||||
plugin.toggle(v);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</View>
|
||||
</View>
|
||||
<Description />
|
||||
</Stack>
|
||||
</Card>
|
||||
</CardContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import AddonPage from "@core/ui/components/AddonPage";
|
|||
import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins } from "@lib/addons/plugins";
|
||||
import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins, corePluginInstances } from "@lib/addons/plugins";
|
||||
import { Author } from "@lib/addons/types";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
|
|
@ -13,124 +13,203 @@ import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
|||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { Button, Card, FlashList, IconButton, Text } from "@metro/common/components";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
FlashList,
|
||||
IconButton,
|
||||
Text,
|
||||
} from "@metro/common/components";
|
||||
import { ComponentProps } from "react";
|
||||
import { View } from "react-native";
|
||||
import type { BunnyPluginManifest } from "@lib/addons/plugins/types";
|
||||
|
||||
import { UnifiedPluginModel } from "./models";
|
||||
import unifyBunnyPlugin from "./models/bunny";
|
||||
import unifyVdPlugin from "./models/vendetta";
|
||||
|
||||
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
|
||||
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
|
||||
const { openAlert } = lazyDestructure(() =>
|
||||
findByProps("openAlert", "dismissAlert"),
|
||||
);
|
||||
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() =>
|
||||
findByProps("AlertModal", "AlertActions"),
|
||||
);
|
||||
|
||||
interface PluginPageProps extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
|
||||
useItems: () => unknown[];
|
||||
interface PluginPageProps
|
||||
extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
|
||||
useItems: () => unknown[];
|
||||
}
|
||||
|
||||
function PluginPage(props: PluginPageProps) {
|
||||
const items = props.useItems();
|
||||
const items = props.useItems();
|
||||
|
||||
return <AddonPage<UnifiedPluginModel>
|
||||
CardComponent={PluginCard}
|
||||
title={Strings.PLUGINS}
|
||||
searchKeywords={[
|
||||
"name",
|
||||
"description",
|
||||
p => p.authors?.map(
|
||||
(a: Author | string) => typeof a === "string" ? a : a.name
|
||||
).join() || ""
|
||||
]}
|
||||
sortOptions={{
|
||||
"Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
|
||||
"Name (Z-A)": (a, b) => b.name.localeCompare(a.name)
|
||||
}}
|
||||
safeModeHint={{ message: Strings.SAFE_MODE_NOTICE_PLUGINS }}
|
||||
items={items}
|
||||
{...props}
|
||||
/>;
|
||||
return (
|
||||
<AddonPage<UnifiedPluginModel>
|
||||
CardComponent={PluginCard}
|
||||
title={Strings.PLUGINS}
|
||||
searchKeywords={[
|
||||
"name",
|
||||
"description",
|
||||
(p) =>
|
||||
p.authors
|
||||
?.map((a: Author | string) => (typeof a === "string" ? a : a.name))
|
||||
.join() || "",
|
||||
]}
|
||||
sortOptions={{
|
||||
"Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
|
||||
"Name (Z-A)": (a, b) => b.name.localeCompare(a.name),
|
||||
}}
|
||||
safeModeHint={{ message: Strings.SAFE_MODE_NOTICE_PLUGINS }}
|
||||
items={items}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Plugins() {
|
||||
useProxy(settings);
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
useProxy(settings);
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
return <PluginPage
|
||||
useItems={() => {
|
||||
useProxy(VdPluginManager.plugins);
|
||||
useObservable([pluginSettings]);
|
||||
return (
|
||||
<PluginPage
|
||||
useItems={() => {
|
||||
useProxy(VdPluginManager.plugins);
|
||||
useObservable([pluginSettings]);
|
||||
|
||||
const corePlugins = [...corePluginInstances.keys()]
|
||||
.map((id) => registeredPlugins.get(id))
|
||||
.filter((manifest): manifest is BunnyPluginManifest => manifest !== undefined)
|
||||
.map(unifyBunnyPlugin);
|
||||
const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin);
|
||||
const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin);
|
||||
|
||||
return [...vdPlugins, ...bnPlugins];
|
||||
return [... corePlugins, ...vdPlugins, ...bnPlugins];
|
||||
}}
|
||||
ListHeaderComponent={() => {
|
||||
const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(p => !p.id.startsWith(VD_PROXY_PREFIX) && !p.id.startsWith(BUNNY_PROXY_PREFIX));
|
||||
if (!unproxiedPlugins.length) return null;
|
||||
|
||||
return <View style={{ marginVertical: 12, marginHorizontal: 10 }}>
|
||||
<Card border="strong">
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", flexDirection: "row" }}>
|
||||
<View style={{ gap: 6, flexShrink: 1 }}>
|
||||
<Text variant="heading-md/bold">Unproxied Plugins Found</Text>
|
||||
<Text variant="text-sm/medium" color="text-muted">
|
||||
Plugins installed from unproxied sources may run unverified code in this app without your awareness.
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ marginLeft: "auto" }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("CircleInformationIcon-primary")}
|
||||
style={{ marginLeft: 8 }}
|
||||
onPress={() => {
|
||||
navigation.push("PUPU_CUSTOM_PAGE", {
|
||||
title: "Unproxied Plugins",
|
||||
render: () => {
|
||||
return <FlashList
|
||||
data={unproxiedPlugins}
|
||||
contentContainerStyle={{ padding: 8 }}
|
||||
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
|
||||
renderItem={({ item: p }: any) => <Card>
|
||||
<Text variant="heading-md/semibold">{p.id}</Text>
|
||||
</Card>}
|
||||
/>;
|
||||
}
|
||||
});
|
||||
}}
|
||||
return (
|
||||
<View style={{ marginVertical: 12, marginHorizontal: 10 }}>
|
||||
<Card border="strong">
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: 6, flexShrink: 1 }}>
|
||||
<Text variant="heading-md/bold">Unproxied Plugins Found</Text>
|
||||
<Text variant="text-sm/medium" color="text-muted">
|
||||
Plugins installed from unproxied sources may run unverified
|
||||
code in this app without your awareness.
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ marginLeft: "auto" }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("CircleInformationIcon-primary")}
|
||||
style={{ marginLeft: 8 }}
|
||||
onPress={() => {
|
||||
navigation.push("PUPU_CUSTOM_PAGE", {
|
||||
title: "Unproxied Plugins",
|
||||
render: () => {
|
||||
return (
|
||||
<FlashList
|
||||
data={unproxiedPlugins}
|
||||
contentContainerStyle={{ padding: 8 }}
|
||||
ItemSeparatorComponent={() => (
|
||||
<View style={{ height: 8 }} />
|
||||
)}
|
||||
renderItem={({ item: p }: any) => (
|
||||
<Card>
|
||||
<Text variant="heading-md/semibold">
|
||||
{p.id}
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
</View>;
|
||||
}}
|
||||
installAction={{
|
||||
label: "Install a plugin",
|
||||
fetchFn: async (url: string) => {
|
||||
if (!url.startsWith(VD_PROXY_PREFIX) && !url.startsWith(BUNNY_PROXY_PREFIX) && !settings.developerSettings) {
|
||||
openAlert("bunny-plugin-unproxied-confirmation", <AlertModal
|
||||
title="Hold On!"
|
||||
content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?"
|
||||
extraContent={<Card><Text variant="text-md/bold">{url}</Text></Card>}
|
||||
actions={<AlertActions>
|
||||
<AlertActionButton text="Continue" variant="primary" onPress={() => {
|
||||
VdPluginManager.installPlugin(url)
|
||||
.then(() => showToast(Strings.TOASTS_INSTALLED_PLUGIN, findAssetId("Check")))
|
||||
.catch(e => openAlert("bunny-plugin-install-failed", <AlertModal
|
||||
title="Install Failed"
|
||||
content={`Unable to install plugin from '${url}':`}
|
||||
extraContent={<Card><Text variant="text-md/normal">{e instanceof Error ? e.message : String(e)}</Text></Card>}
|
||||
actions={<AlertActionButton text="Okay" variant="primary" />}
|
||||
/>));
|
||||
}} />
|
||||
<AlertActionButton text="Cancel" variant="secondary" />
|
||||
</AlertActions>}
|
||||
/>);
|
||||
} else {
|
||||
return await VdPluginManager.installPlugin(url);
|
||||
);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
installAction={{
|
||||
label: "Install a plugin",
|
||||
fetchFn: async (url: string) => {
|
||||
if (
|
||||
!url.startsWith(VD_PROXY_PREFIX) &&
|
||||
!url.startsWith(BUNNY_PROXY_PREFIX) &&
|
||||
!settings.developerSettings
|
||||
) {
|
||||
openAlert(
|
||||
"bunny-plugin-unproxied-confirmation",
|
||||
<AlertModal
|
||||
title="Hold On!"
|
||||
content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?"
|
||||
extraContent={
|
||||
<Card>
|
||||
<Text variant="text-md/bold">{url}</Text>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>;
|
||||
actions={
|
||||
<AlertActions>
|
||||
<AlertActionButton
|
||||
text="Continue"
|
||||
variant="primary"
|
||||
onPress={() => {
|
||||
VdPluginManager.installPlugin(url)
|
||||
.then(() =>
|
||||
showToast(
|
||||
Strings.TOASTS_INSTALLED_PLUGIN,
|
||||
findAssetId("Check"),
|
||||
),
|
||||
)
|
||||
.catch((e) =>
|
||||
openAlert(
|
||||
"bunny-plugin-install-failed",
|
||||
<AlertModal
|
||||
title="Install Failed"
|
||||
content={`Unable to install plugin from '${url}':`}
|
||||
extraContent={
|
||||
<Card>
|
||||
<Text variant="text-md/normal">
|
||||
{e instanceof Error
|
||||
? e.message
|
||||
: String(e)}
|
||||
</Text>
|
||||
</Card>
|
||||
}
|
||||
actions={
|
||||
<AlertActionButton
|
||||
text="Okay"
|
||||
variant="primary"
|
||||
/>
|
||||
}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AlertActionButton text="Cancel" variant="secondary" />
|
||||
</AlertActions>
|
||||
}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
return await VdPluginManager.installPlugin(url);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,52 @@
|
|||
import { PupuIcon } from "@core/ui/settings";
|
||||
import { disablePlugin, enablePlugin, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins";
|
||||
import {
|
||||
disablePlugin,
|
||||
enablePlugin,
|
||||
getPluginSettingsComponent,
|
||||
isPluginEnabled,
|
||||
pluginSettings,
|
||||
} from "@lib/addons/plugins";
|
||||
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
|
||||
import { useObservable } from "@lib/api/storage";
|
||||
|
||||
import { UnifiedPluginModel } from ".";
|
||||
|
||||
export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): UnifiedPluginModel {
|
||||
return {
|
||||
id: manifest.id,
|
||||
name: manifest.display.name,
|
||||
description: manifest.display.description,
|
||||
authors: manifest.display.authors,
|
||||
export default function unifyBunnyPlugin(
|
||||
manifest: BunnyPluginManifest,
|
||||
): UnifiedPluginModel {
|
||||
return {
|
||||
id: manifest.id,
|
||||
name: manifest.display.name,
|
||||
description: manifest.display.description,
|
||||
authors: manifest.display.authors,
|
||||
|
||||
getBadges() {
|
||||
return [
|
||||
{ source: { uri: PupuIcon } },
|
||||
// { source: findAssetId("CheckmarkLargeBoldIcon")! }
|
||||
];
|
||||
},
|
||||
isEnabled: () => isPluginEnabled(manifest.id),
|
||||
isInstalled: () => manifest.id in pluginSettings,
|
||||
usePluginState() {
|
||||
useObservable([pluginSettings]);
|
||||
},
|
||||
toggle(start: boolean) {
|
||||
try {
|
||||
start
|
||||
? enablePlugin(manifest.id, true)
|
||||
: disablePlugin(manifest.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// showToast("Failed to toggle plugin " + e, findAssetId("Small"));
|
||||
}
|
||||
},
|
||||
resolveSheetComponent() {
|
||||
return import("../sheets/PluginInfoActionSheet");
|
||||
},
|
||||
getPluginSettingsComponent() {
|
||||
return getPluginSettingsComponent(manifest.id);
|
||||
},
|
||||
};
|
||||
getBadges() {
|
||||
return [
|
||||
{ source: { uri: PupuIcon } },
|
||||
// { source: findAssetId("CheckmarkLargeBoldIcon")! }
|
||||
];
|
||||
},
|
||||
isEnabled: () => isPluginEnabled(manifest.id),
|
||||
isInstalled: () => manifest.id in pluginSettings,
|
||||
usePluginState() {
|
||||
useObservable([pluginSettings]);
|
||||
},
|
||||
toggle(start: boolean) {
|
||||
try {
|
||||
start ? enablePlugin(manifest.id, true) : disablePlugin(manifest.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// showToast("Failed to toggle plugin " + e, findAssetId("Small"));
|
||||
}
|
||||
},
|
||||
resolveSheetComponent() {
|
||||
// Return a Promise resolving to the sheet component for interface consistency
|
||||
return Promise.resolve({
|
||||
default: require("../sheets/PluginInfoActionSheet").default,
|
||||
});
|
||||
},
|
||||
getPluginSettingsComponent() {
|
||||
return getPluginSettingsComponent(manifest.id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,32 +3,38 @@ import { useProxy } from "@core/vendetta/storage";
|
|||
|
||||
import { UnifiedPluginModel } from ".";
|
||||
|
||||
export default function unifyVdPlugin(vdPlugin: VendettaPlugin): UnifiedPluginModel {
|
||||
return {
|
||||
id: vdPlugin.id,
|
||||
name: vdPlugin.manifest.name,
|
||||
description: vdPlugin.manifest.description,
|
||||
authors: vdPlugin.manifest.authors,
|
||||
icon: vdPlugin.manifest.vendetta?.icon,
|
||||
export default function unifyVdPlugin(
|
||||
vdPlugin: VendettaPlugin,
|
||||
): UnifiedPluginModel {
|
||||
return {
|
||||
id: vdPlugin.id,
|
||||
name: vdPlugin.manifest.name,
|
||||
description: vdPlugin.manifest.description,
|
||||
authors: vdPlugin.manifest.authors,
|
||||
icon: vdPlugin.manifest.vendetta?.icon,
|
||||
|
||||
getBadges() {
|
||||
return [];
|
||||
},
|
||||
isEnabled: () => vdPlugin.enabled,
|
||||
isInstalled: () => Boolean(vdPlugin && VdPluginManager.plugins[vdPlugin.id]),
|
||||
usePluginState() {
|
||||
useProxy(VdPluginManager.plugins[vdPlugin.id]);
|
||||
},
|
||||
toggle(start: boolean) {
|
||||
start
|
||||
? VdPluginManager.startPlugin(vdPlugin.id)
|
||||
: VdPluginManager.stopPlugin(vdPlugin.id);
|
||||
},
|
||||
resolveSheetComponent() {
|
||||
return import("../sheets/VdPluginInfoActionSheet");
|
||||
},
|
||||
getPluginSettingsComponent() {
|
||||
return VdPluginManager.getSettings(vdPlugin.id);
|
||||
},
|
||||
};
|
||||
getBadges() {
|
||||
return [];
|
||||
},
|
||||
isEnabled: () => vdPlugin.enabled,
|
||||
isInstalled: () =>
|
||||
Boolean(vdPlugin && VdPluginManager.plugins[vdPlugin.id]),
|
||||
usePluginState() {
|
||||
useProxy(VdPluginManager.plugins[vdPlugin.id]);
|
||||
},
|
||||
toggle(start: boolean) {
|
||||
start
|
||||
? VdPluginManager.startPlugin(vdPlugin.id)
|
||||
: VdPluginManager.stopPlugin(vdPlugin.id);
|
||||
},
|
||||
resolveSheetComponent() {
|
||||
// Return a Promise resolving to the unified PluginInfoActionSheet for consistency
|
||||
return Promise.resolve({
|
||||
default: require("../sheets/PluginInfoActionSheet").default,
|
||||
});
|
||||
},
|
||||
getPluginSettingsComponent() {
|
||||
return VdPluginManager.getSettings(vdPlugin.id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,106 +1,211 @@
|
|||
import { startPlugin } from "@lib/addons/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { hideSheet } from "@lib/ui/sheets";
|
||||
import { ActionSheet, Card, ContextMenu, IconButton, Text } from "@metro/common/components";
|
||||
import { ComponentProps } from "react";
|
||||
import { ActionSheet, Card, IconButton, Text } from "@metro/common/components";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { showToast } from "@lib/ui/toasts";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { purgeStorage as purgeVdStorage } from "@core/vendetta/storage";
|
||||
import { purgeStorage } from "@lib/api/storage";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { useState } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
|
||||
import { PluginInfoActionSheetProps } from "./common";
|
||||
import TitleComponent from "./TitleComponent";
|
||||
import { PluginInfoActionSheetProps } from "./common";
|
||||
|
||||
function PluginInfoIconButton(props: ComponentProps<typeof IconButton>) {
|
||||
const { onPress } = props;
|
||||
props.onPress &&= () => {
|
||||
function PluginInfoIconButton(props) {
|
||||
const { onPress } = props;
|
||||
props.onPress &&= () => {
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
onPress?.();
|
||||
};
|
||||
return <IconButton {...props} />;
|
||||
}
|
||||
|
||||
export default function PluginInfoActionSheet({
|
||||
plugin,
|
||||
navigation,
|
||||
}: PluginInfoActionSheetProps) {
|
||||
plugin.usePluginState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Determine plugin type
|
||||
const isVendettaPlugin = plugin.id.includes("/");
|
||||
const isCorePlugin =
|
||||
plugin.id.startsWith("bunny.") || plugin.id.startsWith("vendetta.");
|
||||
|
||||
const copyPluginUrl = () => {
|
||||
let url = plugin.id;
|
||||
if (isVendettaPlugin) {
|
||||
url = plugin.id;
|
||||
} else {
|
||||
try {
|
||||
const pluginAny = plugin;
|
||||
const repoUrl =
|
||||
pluginAny._manifest?.parentRepository ||
|
||||
pluginAny.manifest?.parentRepository;
|
||||
url = repoUrl ? `${repoUrl}/builds/${plugin.id}` : plugin.id;
|
||||
} catch (e) {
|
||||
url = plugin.id;
|
||||
}
|
||||
}
|
||||
clipboard.setString(url);
|
||||
showToast("Copied to clipboard!", findAssetId("toast_copy_link"));
|
||||
};
|
||||
|
||||
const refetchPlugin = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isVendettaPlugin) {
|
||||
const vdPlugin = VdPluginManager.plugins[plugin.id];
|
||||
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
await VdPluginManager.fetchPlugin(plugin.id);
|
||||
if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id);
|
||||
showToast("Plugin refreshed successfully");
|
||||
} else {
|
||||
// For Bunny plugins
|
||||
// If you have a refreshPlugin function, call it here
|
||||
showToast("Plugin refreshed successfully");
|
||||
}
|
||||
} catch (e) {
|
||||
showToast("Failed to refresh plugin");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearPluginData = () => {
|
||||
showConfirmationAlert({
|
||||
title: "Clear Data",
|
||||
content:
|
||||
"Are you sure you want to clear all data for this plugin? This action cannot be undone.",
|
||||
confirmText: "Clear",
|
||||
confirmColor: "red",
|
||||
cancelText: "Cancel",
|
||||
onConfirm: async () => {
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
onPress?.();
|
||||
};
|
||||
try {
|
||||
if (isVendettaPlugin) {
|
||||
const vdPlugin = VdPluginManager.plugins[plugin.id];
|
||||
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
await purgeVdStorage(plugin.id);
|
||||
if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id);
|
||||
} else {
|
||||
await purgeStorage(`plugins/storage/${plugin.id}.json`);
|
||||
}
|
||||
showToast("Plugin data cleared successfully");
|
||||
} catch (e) {
|
||||
showToast("Failed to clear plugin data");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <IconButton {...props} />;
|
||||
}
|
||||
|
||||
export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfoActionSheetProps) {
|
||||
plugin.usePluginState();
|
||||
|
||||
return <ActionSheet>
|
||||
<ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 24, justifyContent: "space-between", width: "100%" }}>
|
||||
<TitleComponent plugin={plugin} />
|
||||
<ContextMenu
|
||||
items={[
|
||||
{
|
||||
label: "Details",
|
||||
iconSource: findAssetId("CircleInformationIcon-primary"),
|
||||
action: () => {
|
||||
}
|
||||
},
|
||||
// {
|
||||
// label: true ? "Disable Updates" : "Enable Updates",
|
||||
// iconSource: true ? findAssetId("ClockXIcon") : findAssetId("ClockIcon"),
|
||||
// action: () => {
|
||||
|
||||
// }
|
||||
// },
|
||||
{
|
||||
label: "Clear Data",
|
||||
iconSource: findAssetId("FileIcon"),
|
||||
variant: "destructive",
|
||||
action: () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Uninstall",
|
||||
iconSource: findAssetId("TrashIcon"),
|
||||
variant: "destructive",
|
||||
action: () => {
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
{props => <IconButton
|
||||
{...props}
|
||||
icon={findAssetId("MoreHorizontalIcon")}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
/>}
|
||||
</ContextMenu>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-around", alignContent: "center" }}>
|
||||
<PluginInfoIconButton
|
||||
label="Configure"
|
||||
variant="secondary"
|
||||
disabled={!plugin.getPluginSettingsComponent()}
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
onPress={() => {
|
||||
navigation.push("PUPU_CUSTOM_PAGE", {
|
||||
title: plugin.name,
|
||||
render: plugin.getPluginSettingsComponent(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PluginInfoIconButton
|
||||
label="Refetch"
|
||||
variant="secondary"
|
||||
icon={findAssetId("RetryIcon")}
|
||||
onPress={() => {
|
||||
startPlugin(plugin.id);
|
||||
}}
|
||||
/>
|
||||
<PluginInfoIconButton
|
||||
label="Copy URL"
|
||||
variant="secondary"
|
||||
icon={findAssetId("LinkIcon")}
|
||||
onPress={() => {
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Card>
|
||||
<Text variant="text-md/semibold" color="text-primary" style={{ marginBottom: 4 }}>
|
||||
Description
|
||||
</Text>
|
||||
<Text variant="text-md/medium">
|
||||
{plugin.description}
|
||||
</Text>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
</ActionSheet>;
|
||||
const uninstallPluginHandler = () => {
|
||||
if (isCorePlugin) {
|
||||
showToast("Core plugins cannot be uninstalled");
|
||||
return;
|
||||
}
|
||||
showConfirmationAlert({
|
||||
title: "Uninstall Plugin",
|
||||
content:
|
||||
"Are you sure you want to uninstall this plugin? This action cannot be undone.",
|
||||
confirmText: "Uninstall",
|
||||
confirmColor: "red",
|
||||
cancelText: "Cancel",
|
||||
onConfirm: async () => {
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
try {
|
||||
if (isVendettaPlugin) {
|
||||
await VdPluginManager.removePlugin(plugin.id);
|
||||
} else {
|
||||
// If you have an uninstallPlugin function, call it here
|
||||
}
|
||||
showToast("Plugin uninstalled successfully");
|
||||
} catch (e) {
|
||||
showToast(
|
||||
`Failed to uninstall plugin: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionSheet>
|
||||
<ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
paddingVertical: 24,
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<TitleComponent plugin={plugin} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 12,
|
||||
paddingHorizontal: 4,
|
||||
}}
|
||||
>
|
||||
<PluginInfoIconButton
|
||||
label="Configure"
|
||||
variant="secondary"
|
||||
disabled={!plugin.getPluginSettingsComponent()}
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
onPress={() => {
|
||||
navigation.push("PUPU_CUSTOM_PAGE", {
|
||||
title: plugin.name,
|
||||
render: plugin.getPluginSettingsComponent(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PluginInfoIconButton
|
||||
label="Refetch"
|
||||
variant="secondary"
|
||||
icon={findAssetId("RetryIcon")}
|
||||
onPress={refetchPlugin}
|
||||
disabled={loading}
|
||||
/>
|
||||
<PluginInfoIconButton
|
||||
label="Copy URL"
|
||||
variant="secondary"
|
||||
icon={findAssetId("LinkIcon")}
|
||||
onPress={copyPluginUrl}
|
||||
/>
|
||||
<PluginInfoIconButton
|
||||
label="Clear Data"
|
||||
variant="secondary"
|
||||
icon={findAssetId("FileIcon")}
|
||||
onPress={clearPluginData}
|
||||
/>
|
||||
{!isCorePlugin && (
|
||||
<PluginInfoIconButton
|
||||
label="Uninstall"
|
||||
variant="secondary"
|
||||
icon={findAssetId("TrashIcon")}
|
||||
onPress={uninstallPluginHandler}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Card>
|
||||
<Text
|
||||
variant="text-md/semibold"
|
||||
color="text-primary"
|
||||
style={{ marginBottom: 4 }}
|
||||
>
|
||||
Description
|
||||
</Text>
|
||||
<Text variant="text-md/medium">{plugin.description}</Text>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
</ActionSheet>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,80 +2,141 @@ import { formatString, Strings } from "@core/i18n";
|
|||
import AddonPage from "@core/ui/components/AddonPage";
|
||||
import ThemeCard from "@core/ui/settings/pages/Themes/ThemeCard";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { getCurrentTheme, installTheme, themes, VdThemeInfo } from "@lib/addons/themes";
|
||||
import {
|
||||
getCurrentTheme,
|
||||
installTheme,
|
||||
themes,
|
||||
VdThemeInfo,
|
||||
} from "@lib/addons/themes";
|
||||
import { colorsPref } from "@lib/addons/themes/colors/preferences";
|
||||
import { updateBunnyColor } from "@lib/addons/themes/colors/updater";
|
||||
import { Author } from "@lib/addons/types";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useObservable } from "@lib/api/storage";
|
||||
import { ActionSheet, BottomSheetTitleHeader, Button, TableRadioGroup, TableRadioRow, TableRowIcon } from "@metro/common/components";
|
||||
import {
|
||||
ActionSheet,
|
||||
BottomSheetTitleHeader,
|
||||
Button,
|
||||
TableRowGroup,
|
||||
TableSwitchRow,
|
||||
TableRowIcon,
|
||||
} from "@metro/common/components";
|
||||
import { View } from "react-native";
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
useProxy(settings);
|
||||
useProxy(themes);
|
||||
useProxy(settings);
|
||||
useProxy(themes);
|
||||
|
||||
return (
|
||||
<AddonPage<VdThemeInfo>
|
||||
title={Strings.THEMES}
|
||||
searchKeywords={[
|
||||
"data.name",
|
||||
"data.description",
|
||||
p => p.data.authors?.map((a: Author) => a.name).join(", ") ?? ""
|
||||
]}
|
||||
sortOptions={{
|
||||
"Name (A-Z)": (a, b) => a.data.name.localeCompare(b.data.name),
|
||||
"Name (Z-A)": (a, b) => b.data.name.localeCompare(a.data.name)
|
||||
}}
|
||||
installAction={{
|
||||
label: "Install a theme",
|
||||
fetchFn: installTheme
|
||||
}}
|
||||
items={Object.values(themes)}
|
||||
safeModeHint={{
|
||||
message: formatString("SAFE_MODE_NOTICE_THEMES", { enabled: Boolean(settings.safeMode?.currentThemeId) }),
|
||||
footer: settings.safeMode?.currentThemeId && <Button
|
||||
size="small"
|
||||
text={Strings.DISABLE_THEME}
|
||||
onPress={() => delete settings.safeMode?.currentThemeId}
|
||||
style={{ marginTop: 8 }}
|
||||
return (
|
||||
<AddonPage<VdThemeInfo>
|
||||
title={Strings.THEMES}
|
||||
searchKeywords={[
|
||||
"data.name",
|
||||
"data.description",
|
||||
(p) => p.data.authors?.map((a: Author) => a.name).join(", ") ?? "",
|
||||
]}
|
||||
sortOptions={{
|
||||
"Name (A-Z)": (a, b) => a.data.name.localeCompare(b.data.name),
|
||||
"Name (Z-A)": (a, b) => b.data.name.localeCompare(a.data.name),
|
||||
}}
|
||||
installAction={{
|
||||
label: "Install a theme",
|
||||
fetchFn: installTheme,
|
||||
}}
|
||||
items={Object.values(themes)}
|
||||
safeModeHint={{
|
||||
message: formatString("SAFE_MODE_NOTICE_THEMES", {
|
||||
enabled: Boolean(settings.safeMode?.currentThemeId),
|
||||
}),
|
||||
footer: settings.safeMode?.currentThemeId && (
|
||||
<Button
|
||||
size="small"
|
||||
text={Strings.DISABLE_THEME}
|
||||
onPress={() => delete settings.safeMode?.currentThemeId}
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
CardComponent={ThemeCard}
|
||||
OptionsActionSheetComponent={() => {
|
||||
useObservable([colorsPref]);
|
||||
|
||||
return (
|
||||
<ActionSheet>
|
||||
<BottomSheetTitleHeader title="Options" />
|
||||
<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">
|
||||
<TableSwitchRow
|
||||
label="Auto"
|
||||
icon={<TableRowIcon source={findAssetId("RobotIcon")} />}
|
||||
value={!colorsPref.type}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
if (enabled) {
|
||||
colorsPref.type = undefined;
|
||||
} else {
|
||||
colorsPref.type = "dark";
|
||||
}
|
||||
getCurrentTheme()?.data &&
|
||||
updateBunnyColor(getCurrentTheme()!.data!, {
|
||||
update: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
CardComponent={ThemeCard}
|
||||
OptionsActionSheetComponent={() => {
|
||||
useObservable([colorsPref]);
|
||||
|
||||
return <ActionSheet>
|
||||
<BottomSheetTitleHeader title="Options" />
|
||||
<View style={{ paddingVertical: 20, gap: 12 }}>
|
||||
<TableRadioGroup
|
||||
title="Override Theme Type"
|
||||
value={colorsPref.type ?? "auto"}
|
||||
hasIcons={true}
|
||||
onChange={type => {
|
||||
colorsPref.type = type !== "auto" ? type as "dark" | "light" : undefined;
|
||||
getCurrentTheme()?.data && updateBunnyColor(getCurrentTheme()!.data!, { update: true });
|
||||
}}
|
||||
>
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("RobotIcon")} />} label="Auto" value="auto" />
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("ThemeDarkIcon")} />} label="Dark" value="dark" />
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("ThemeLightIcon")} />} label="Light" value="light" />
|
||||
</TableRadioGroup>
|
||||
<TableRadioGroup
|
||||
title="Chat Background"
|
||||
value={colorsPref.customBackground ?? "shown"}
|
||||
hasIcons={true}
|
||||
onChange={type => {
|
||||
colorsPref.customBackground = type !== "shown" ? type as "hidden" : null;
|
||||
}}
|
||||
>
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("ImageIcon")} />} label="Show" value={"shown"} />
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("DenyIcon")} />} label="Hide" value={"hidden"} />
|
||||
</TableRadioGroup>
|
||||
</View>
|
||||
</ActionSheet>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
<TableSwitchRow
|
||||
label="Dark"
|
||||
icon={<TableRowIcon source={findAssetId("ThemeDarkIcon")} />}
|
||||
value={colorsPref.type === "dark"}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
colorsPref.type = enabled ? "dark" : undefined;
|
||||
getCurrentTheme()?.data &&
|
||||
updateBunnyColor(getCurrentTheme()!.data!, {
|
||||
update: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Light"
|
||||
icon={<TableRowIcon source={findAssetId("ThemeLightIcon")} />}
|
||||
value={colorsPref.type === "light"}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
colorsPref.type = enabled ? "light" : undefined;
|
||||
getCurrentTheme()?.data &&
|
||||
updateBunnyColor(getCurrentTheme()!.data!, {
|
||||
update: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
<TableRowGroup title="Chat Background">
|
||||
<TableSwitchRow
|
||||
label="Show Background"
|
||||
icon={<TableRowIcon source={findAssetId("ImageIcon")} />}
|
||||
value={!colorsPref.customBackground}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
colorsPref.customBackground = enabled ? null : "hidden";
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Hide Background"
|
||||
icon={<TableRowIcon source={findAssetId("DenyIcon")} />}
|
||||
value={colorsPref.customBackground === "hidden"}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
colorsPref.customBackground = enabled ? "hidden" : null;
|
||||
}}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
</View>
|
||||
</ActionSheet>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const { instead } = require("spitroast");
|
|||
// @ts-ignore - window is defined later in the bundle, so we assign it early
|
||||
globalThis.window = globalThis;
|
||||
|
||||
async function initializeRevenge() {
|
||||
async function initializeKettu() {
|
||||
try {
|
||||
// Make 'freeze' and 'seal' do nothing
|
||||
Object.freeze = Object.seal = Object;
|
||||
|
|
@ -31,7 +31,6 @@ if (typeof window.__r === "undefined") {
|
|||
var _requireFunc: any;
|
||||
|
||||
// Calls from the native side are deferred until the index.ts(x) is loaded
|
||||
// Revenge delays the execution of index.ts(x) because Revenge's initialization is asynchronous
|
||||
interface DeferredQueue {
|
||||
object: any;
|
||||
method: string;
|
||||
|
|
@ -102,7 +101,7 @@ if (typeof window.__r === "undefined") {
|
|||
}
|
||||
|
||||
const startDiscord = async () => {
|
||||
await initializeRevenge();
|
||||
await initializeKettu();
|
||||
|
||||
for (const unpatch of unpatches) unpatch();
|
||||
unpatches.length = 0;
|
||||
|
|
@ -143,7 +142,5 @@ if (typeof window.__r === "undefined") {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
// It is too late to late to hook __r, so we just initialize Revenge here
|
||||
// Likely because of using the legacy loader (from Vendetta)
|
||||
initializeRevenge();
|
||||
initializeKettu();
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { StatusBar } from "react-native";
|
||||
import { findByStoreNameLazy } from "@metro/wrappers";
|
||||
import { _colorRef } from "./updater"; // <-- import your colorRef
|
||||
import { _colorRef } from "./updater";
|
||||
|
||||
const ThemeStore = findByStoreNameLazy("ThemeStore");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export interface SearchProps {
|
|||
}
|
||||
|
||||
function SearchIcon() {
|
||||
return <Image style={{ width: 16, height: 16 }} source={findAssetId("search")!} />;
|
||||
return <Image style={{ width: 16, height: 16 }} source={findAssetId("icon-search")!} />;
|
||||
}
|
||||
|
||||
export default ({ onChangeText, placeholder, style, isRound }: SearchProps) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const DISCORD_SERVER = "https://discord.gg/6cN7wKa8gp";
|
||||
export const CODEBERG = "https://codeberg.org/cocobo1/Kettu";
|
||||
export const GITHUB = "https://github.com/C0C0B01/Kettu";
|
||||
export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
|
||||
export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
|
||||
|
|
@ -6,7 +7,8 @@ export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,25
|
|||
export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
|
||||
|
||||
export const NEXPID_PLUGINS_REPO_URL = "https://revenge.nexpid.xyz";
|
||||
export const OFFICIAL_PLUGINS_REPO_URL = "https://bn-plugins.github.io/dist/repo.json";
|
||||
//export const OFFICIAL_PLUGINS_REPO_URL = "https://bn-plugins.github.io/dist/repo.json";
|
||||
export const OFFICIAL_PLUGINS_REPO_URL = "https://raw.githubusercontent.com/Purple-EyeZ/Plugins-List/refs/heads/main/src/plugins-data.json";
|
||||
|
||||
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
|
||||
export const VD_DISCORD_SERVER_ID = "1015931589865246730";
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export const CompatButton = findByPropsLazy("Looks", "Colors", "Sizes");
|
|||
export const HelpMessage = findByNameLazy("HelpMessage");
|
||||
|
||||
// React Native's included SafeAreaView only adds padding on iOS.
|
||||
// The project doesnt build without it so it kinda has to sit here
|
||||
export const { SafeAreaView, SafeAreaProvider, useSafeAreaInsets } = lazyDestructure(() => findByProps("useSafeAreaInsets"));
|
||||
|
||||
// ActionSheet
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { FilterCheckDef, FilterDefinition, ModuleExports } from "./types";
|
||||
|
||||
export function createFilterDefinition<A extends unknown[]>(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue