Merge branch 'kmmiio99o-main'
This commit is contained in:
commit
a86888e2c0
13 changed files with 880 additions and 495 deletions
|
|
@ -1,22 +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"),
|
||||
"vendetta.notrack": require("./notrack")
|
||||
"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");
|
||||
},
|
||||
});
|
||||
|
|
@ -1,36 +1,230 @@
|
|||
import network from "./lib/network";
|
||||
import console from "./lib/console";
|
||||
import miscellanous from "./lib/miscellanous";
|
||||
import sentry from "./lib/sentry";
|
||||
import { defineCorePlugin } from "..";
|
||||
import { findByProps } from "@metro";
|
||||
import { instead } from "@lib/api/patcher";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
|
||||
const patches = [
|
||||
network(),
|
||||
console(),
|
||||
miscellanous(),
|
||||
// Sentry causes issues
|
||||
// sentry(),
|
||||
];
|
||||
// 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: "vendetta.notrack",
|
||||
version: "1.0.0",
|
||||
type: "plugin",
|
||||
spec: 3,
|
||||
main: "",
|
||||
display: {
|
||||
name: "NoTrack",
|
||||
description: "Disables Discord's telemetry",
|
||||
authors: [{ name: "maisymoe" }]
|
||||
}
|
||||
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() {
|
||||
network(),
|
||||
console(),
|
||||
miscellanous(),
|
||||
sentry()
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
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");
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import { logger } from "@lib/utils/logger";
|
||||
|
||||
export default () => {
|
||||
const sentrified = {};
|
||||
|
||||
try {
|
||||
Object.keys(console).forEach((x) => {
|
||||
sentrified[x] = console[x];
|
||||
console[x] = console[x].__sentry_original__ ?? console[x];
|
||||
});
|
||||
} catch(e) {
|
||||
logger.log("Failed to de-sentrify console functions!", e);
|
||||
};
|
||||
|
||||
return () => Object.keys(console).forEach((x) => console[x] = sentrified[x]);
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { findByProps } from "@metro";
|
||||
import { noop } from "./utils";
|
||||
|
||||
// Stuff in here is non-specific tracking scattered around the app
|
||||
|
||||
const AnalyticsUtils = findByProps("AnalyticsActionHandlers");
|
||||
const SuperPropUtils = findByProps("encodeProperties", "track");
|
||||
const VoiceStateUtils = findByProps("getVoiceStateMetadata");
|
||||
const CrashReportUtils = findByProps("submitLiveCrashReport");
|
||||
const MetricsUtils = findByProps("_metrics");
|
||||
|
||||
export default () => {
|
||||
const patches = [
|
||||
// Global analytics utilities
|
||||
...(["handleTrack", "handleFingerprint"].map((i) => noop(i, AnalyticsUtils.AnalyticsActionHandlers))),
|
||||
// TODO: This is a getter - look into patching it
|
||||
// noop("analyticsTrackingStoreMaker", AnalyticsUtils),
|
||||
|
||||
// Super properties tracking
|
||||
noop("track", SuperPropUtils),
|
||||
|
||||
// Voice state metadata tracking
|
||||
noop("trackWithMetadata", VoiceStateUtils),
|
||||
|
||||
// Crash reporter
|
||||
noop("submitLiveCrashReport", CrashReportUtils),
|
||||
|
||||
// Whatever "metrics" implies
|
||||
noop("push", MetricsUtils._metrics),
|
||||
];
|
||||
|
||||
return () => patches.forEach((p) => p());
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { instead } from "@lib/api/patcher";
|
||||
|
||||
export default () => {
|
||||
// https://github.com/yellowsink/shelter-plugins/blob/3a62d1557b704f4617f72882edba826218eb8c6e/plugins/antitrack/index.js#L10-L25
|
||||
const analyticsTest = /client-analytics\.braintreegateway\.com|discord\.com\/api\/v9\/(science|track)|app\.adjust\..*|.*\.ingest\.sentry\.io/;
|
||||
|
||||
return instead(
|
||||
"send",
|
||||
XMLHttpRequest.prototype,
|
||||
function (args, orig) { if (!analyticsTest.test(this.__sentry_xhr__?.url)) return orig.apply(this, args) },
|
||||
);
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { findByProps } from "@metro";
|
||||
import { noop } from "./utils";
|
||||
|
||||
const Sentry = {
|
||||
initializer: findByProps("initSentry"),
|
||||
main: globalThis.__SENTRY__?.hub,
|
||||
client: globalThis.__SENTRY__?.hub?.getClient(),
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const patches = [
|
||||
noop("initSentry", Sentry.initializer),
|
||||
noop("addBreadcrumb", Sentry.main),
|
||||
];
|
||||
|
||||
Sentry.client.getOptions().enabled = false;
|
||||
Sentry.client.close();
|
||||
Sentry.main.getStackTop().scope.clear();
|
||||
Sentry.main.getScope().clear();
|
||||
|
||||
return () => {
|
||||
patches.forEach((p) => p());
|
||||
Sentry.client.getOptions().enabled = true;
|
||||
Sentry.client.open();
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import { instead } from "@lib/api/patcher";
|
||||
|
||||
export const noop = (prop: string, parent: any) => instead(prop, parent, () => {});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,13 @@ 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";
|
||||
|
|
@ -22,44 +28,53 @@ 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))
|
||||
|
|
@ -74,68 +89,127 @@ export default function Plugins() {
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue