From 6cb7f99b608eea874c2db8c71ee0ec37066ec728 Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Sat, 11 Oct 2025 17:22:52 +0300 Subject: [PATCH 01/22] version(bump): bumped to v1,3,1 --- scripts/build.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 75e3ffb..8f559e0 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"1.3.0"` + version: `"1.3.1"` } } }, From 89b866da180e18979b7efc8b2a3f62faba9fb040 Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Sat, 11 Oct 2025 17:33:43 +0300 Subject: [PATCH 02/22] fix(plugins): fixed plugins menu crash --- scripts/build.mjs | 2 +- src/core/ui/settings/pages/Plugins/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 8f559e0..ead167a 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"1.3.1"` + version: `"v1.3.1"` } } }, diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index e03ca3d..2789ca3 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -78,7 +78,7 @@ export default function Plugins() { const corePlugins = [...corePluginInstances.keys()] .map((id) => registeredPlugins.get(id)) - .filter((manifest): manifest is BunnyPluginManifest => manifest !== undefined) + .filter(Boolean) .map(unifyBunnyPlugin); const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin); const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin); From 2cd8b641c6d7b195e122534c9921ec57edb00bb7 Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Sat, 11 Oct 2025 17:42:58 +0300 Subject: [PATCH 03/22] fix(plugins): potentailly revert issue --- src/core/ui/settings/pages/Plugins/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 2789ca3..86b8470 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -76,14 +76,10 @@ export default function Plugins() { useProxy(VdPluginManager.plugins); useObservable([pluginSettings]); - const corePlugins = [...corePluginInstances.keys()] - .map((id) => registeredPlugins.get(id)) - .filter(Boolean) - .map(unifyBunnyPlugin); const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin); - const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin); + const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && isCorePlugin(p.id)).map(unifyBunnyPlugin); - return [... corePlugins, ...vdPlugins, ...bnPlugins]; + return [...vdPlugins, ...bnPlugins]; }} ListHeaderComponent={() => { const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(p => !p.id.startsWith(VD_PROXY_PREFIX) && !p.id.startsWith(BUNNY_PROXY_PREFIX)); From 7a4a8ec23e82b30f6b56685421ed7e24fa5d7b3b Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Sat, 11 Oct 2025 18:06:31 +0300 Subject: [PATCH 04/22] Revert "port information menu look from ShiggyCord (was exclusive only for core" This reverts commit 93d5c354a119be52a3a8d99bcc49860c737e6b3f. --- .../pages/Plugins/components/PluginCard.tsx | 256 +++++++-------- src/core/ui/settings/pages/Plugins/index.tsx | 258 ++++++--------- .../ui/settings/pages/Plugins/models/bunny.ts | 79 ++--- .../settings/pages/Plugins/models/vendetta.ts | 60 ++-- .../Plugins/sheets/PluginInfoActionSheet.tsx | 299 ++++++------------ 5 files changed, 365 insertions(+), 587 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx index 6ab8f1e..b199f55 100644 --- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx @@ -3,182 +3,154 @@ 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; -}>(null!); +const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult; }>(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) => ( - - {m} - - )); + // could be empty if the plugin name is irrelevant! + const highlightedNode = result[0].highlight((m, i) => + + {m} + + ); - const icon = plugin.icon && findAssetId(plugin.icon); + const icon = plugin.icon && findAssetId(plugin.icon); - const textNode = ( - - {highlightedNode.length ? highlightedNode : plugin.name} - - ); + const textNode = ( + + {highlightedNode.length ? highlightedNode : plugin.name} + + ); - return ( - - {icon && } - {textNode} - - ); + return + {icon && } + {textNode} + ; } 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) => ( - - {m} - - )); + // could be empty if the author(s) are irrelevant with the search! + const highlightedNode = result[2].highlight((m, i) => + + {m} + + ); - 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 ( - - - by {authorText} - - {badges.length > 0 && ( - - {badges.map((b, i) => ( - - ))} + return ( + + + by {authorText} + + {badges.length > 0 && + {badges.map((b, i) => )} + } - )} - - ); + ); } 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) => ( - - {m} - - )); + // could be empty if the description is irrelevant with the search! + const highlightedNode = result[1].highlight((m, i) => + {m} + ); - return ( - - {highlightedNode.length ? highlightedNode : plugin.description} - - ); + return + {highlightedNode.length ? highlightedNode : plugin.description} + ; } const Actions = () => { - const { plugin } = useCardContext(); - const navigation = NavigationNative.useNavigation(); + const { plugin } = useCardContext(); + const navigation = NavigationNative.useNavigation(); - return ( - - - navigation.push("PUPU_CUSTOM_PAGE", { - title: plugin.name, - render: plugin.getPluginSettingsComponent(), - }) - } - /> - - void showSheet( - "PluginInfoActionSheet", - plugin.resolveSheetComponent(), - { plugin, navigation }, - ) - } - /> - - ); + return + navigation.push("PUPU_CUSTOM_PAGE", { + title: plugin.name, + render: plugin.getPluginSettingsComponent(), + })} + /> + void showSheet( + "PluginInfoActionSheet", + plugin.resolveSheetComponent(), + { plugin, navigation } + )} + /> + ; }; -export default function PluginCard({ - result, - item: plugin, -}: CardWrapper) { - plugin.usePluginState(); +export default function PluginCard({ result, item: plugin }: CardWrapper) { + 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 ( - - - - - - - <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> + ); } diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 86b8470..afdf9fe 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -13,13 +13,7 @@ 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"; @@ -28,53 +22,44 @@ 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 vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin); const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && isCorePlugin(p.id)).map(unifyBunnyPlugin); @@ -85,127 +70,68 @@ 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> + </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); } - 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); - } - }, - }} - /> - ); + } + }} + />; } diff --git a/src/core/ui/settings/pages/Plugins/models/bunny.ts b/src/core/ui/settings/pages/Plugins/models/bunny.ts index 252e6ff..106be65 100644 --- a/src/core/ui/settings/pages/Plugins/models/bunny.ts +++ b/src/core/ui/settings/pages/Plugins/models/bunny.ts @@ -1,52 +1,43 @@ 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 a Promise resolving to the sheet component for interface consistency - return Promise.resolve({ - default: require("../sheets/PluginInfoActionSheet").default, - }); - }, - 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 import("../sheets/PluginInfoActionSheet"); + }, + getPluginSettingsComponent() { + return getPluginSettingsComponent(manifest.id); + }, + }; } diff --git a/src/core/ui/settings/pages/Plugins/models/vendetta.ts b/src/core/ui/settings/pages/Plugins/models/vendetta.ts index fc36653..ee735f5 100644 --- a/src/core/ui/settings/pages/Plugins/models/vendetta.ts +++ b/src/core/ui/settings/pages/Plugins/models/vendetta.ts @@ -3,38 +3,32 @@ 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 a Promise resolving to the unified PluginInfoActionSheet for consistency - return Promise.resolve({ - default: require("../sheets/PluginInfoActionSheet").default, - }); - }, - 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 import("../sheets/VdPluginInfoActionSheet"); + }, + getPluginSettingsComponent() { + return VdPluginManager.getSettings(vdPlugin.id); + }, + }; } diff --git a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx index e105640..4c769df 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx @@ -1,211 +1,106 @@ -import { hideSheet } from "@lib/ui/sheets"; -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 { startPlugin } from "@lib/addons/plugins"; import { findAssetId } from "@lib/api/assets"; -import { useState } from "react"; +import { hideSheet } from "@lib/ui/sheets"; +import { ActionSheet, Card, ContextMenu, IconButton, Text } from "@metro/common/components"; +import { ComponentProps } from "react"; import { ScrollView, View } from "react-native"; -import TitleComponent from "./TitleComponent"; + import { PluginInfoActionSheetProps } from "./common"; +import TitleComponent from "./TitleComponent"; -function PluginInfoIconButton(props) { - const { onPress } = props; - props.onPress &&= () => { - hideSheet("PluginInfoActionSheet"); - onPress?.(); - }; - return <IconButton {...props} />; +function PluginInfoIconButton(props: ComponentProps<typeof IconButton>) { + 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); +export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfoActionSheetProps) { + plugin.usePluginState(); - // Determine plugin type - const isVendettaPlugin = plugin.id.includes("/"); - const isCorePlugin = - plugin.id.startsWith("bunny.") || plugin.id.startsWith("vendetta."); + 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: () => { - 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"); - 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"); - } - }, - }); - }; - - 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> - ); + // } + // }, + { + 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>; } From 2c2508bcae74dddf3100cd20c52e42dfda73cbaf Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sat, 11 Oct 2025 18:06:51 +0300 Subject: [PATCH 05/22] Revert "port messagefix from ShiggyCord" This reverts commit beb52729f59de9251e2bc49b7551eff7bd33fe83. --- src/core/plugins/index.ts | 21 +++++------ src/core/plugins/messagefix/index.ts | 56 ---------------------------- 2 files changed, 9 insertions(+), 68 deletions(-) delete mode 100644 src/core/plugins/messagefix/index.ts diff --git a/src/core/plugins/index.ts b/src/core/plugins/index.ts index 5bc2f76..7b423f7 100644 --- a/src/core/plugins/index.ts +++ b/src/core/plugins/index.ts @@ -1,25 +1,22 @@ 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.notrack": { ...require("./notrack"), preenabled: true }, - "bunny.messagefix": { default: require("./messagefix"), preenabled: true }, + "bunny.quickinstall": require("./quickinstall"), + "bunny.badges": require("./badges"), + "vendetta.notrack": require("./notrack") }); /** * @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; } diff --git a/src/core/plugins/messagefix/index.ts b/src/core/plugins/messagefix/index.ts deleted file mode 100644 index 146bf2b..0000000 --- a/src/core/plugins/messagefix/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { defineCorePlugin } from ".."; -import { findByProps } from "@metro"; -import { logger } from "@lib/utils/logger"; - -// Find the MessageActions module -const MessageActions = findByProps("sendMessage"); -let originalSendMessage: Function; - -export default defineCorePlugin({ - manifest: { - id: "bunny.messagefix", - version: "1.0.0", - type: "plugin", - spec: 3, - main: "", - display: { - name: "MessageFix", - description: "Ensures messages include the required nonce parameter", - authors: [{ name: "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"); - }, -}); From 67b31cbf25b5045ac4dcfd8bc95180724869e170 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sat, 11 Oct 2025 18:07:18 +0300 Subject: [PATCH 06/22] Revert "Fix notrack" This reverts commit a4eb98bd8f8a75987d39da6025be23815eb1a232. --- src/core/plugins/notrack/index.ts | 252 +++---------------- src/core/plugins/notrack/lib/console.ts | 16 ++ src/core/plugins/notrack/lib/miscellanous.ts | 33 +++ src/core/plugins/notrack/lib/network.ts | 12 + src/core/plugins/notrack/lib/sentry.ts | 26 ++ src/core/plugins/notrack/lib/utils.ts | 3 + 6 files changed, 119 insertions(+), 223 deletions(-) create mode 100644 src/core/plugins/notrack/lib/console.ts create mode 100644 src/core/plugins/notrack/lib/miscellanous.ts create mode 100644 src/core/plugins/notrack/lib/network.ts create mode 100644 src/core/plugins/notrack/lib/sentry.ts create mode 100644 src/core/plugins/notrack/lib/utils.ts diff --git a/src/core/plugins/notrack/index.ts b/src/core/plugins/notrack/index.ts index 8459303..58865de 100644 --- a/src/core/plugins/notrack/index.ts +++ b/src/core/plugins/notrack/index.ts @@ -1,230 +1,36 @@ +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"; -// 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 - } - }; -} +const patches = [ + network(), + console(), + miscellanous(), + // Sentry causes issues + // sentry(), +]; 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" }], + manifest: { + id: "vendetta.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"); - }, + start() { + network(), + console(), + miscellanous(), + sentry() + } }); + diff --git a/src/core/plugins/notrack/lib/console.ts b/src/core/plugins/notrack/lib/console.ts new file mode 100644 index 0000000..7a69471 --- /dev/null +++ b/src/core/plugins/notrack/lib/console.ts @@ -0,0 +1,16 @@ +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]); +} diff --git a/src/core/plugins/notrack/lib/miscellanous.ts b/src/core/plugins/notrack/lib/miscellanous.ts new file mode 100644 index 0000000..b6a6093 --- /dev/null +++ b/src/core/plugins/notrack/lib/miscellanous.ts @@ -0,0 +1,33 @@ +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()); +} diff --git a/src/core/plugins/notrack/lib/network.ts b/src/core/plugins/notrack/lib/network.ts new file mode 100644 index 0000000..68f762e --- /dev/null +++ b/src/core/plugins/notrack/lib/network.ts @@ -0,0 +1,12 @@ +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) }, + ); +} diff --git a/src/core/plugins/notrack/lib/sentry.ts b/src/core/plugins/notrack/lib/sentry.ts new file mode 100644 index 0000000..5b01b7e --- /dev/null +++ b/src/core/plugins/notrack/lib/sentry.ts @@ -0,0 +1,26 @@ +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(); + }; +} diff --git a/src/core/plugins/notrack/lib/utils.ts b/src/core/plugins/notrack/lib/utils.ts new file mode 100644 index 0000000..55f871f --- /dev/null +++ b/src/core/plugins/notrack/lib/utils.ts @@ -0,0 +1,3 @@ +import { instead } from "@lib/api/patcher"; + +export const noop = (prop: string, parent: any) => instead(prop, parent, () => {}); From f32ef6a2b75bd7cc4ea868d6ca1a3a074f045fe0 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Fri, 10 Oct 2025 23:54:06 +0200 Subject: [PATCH 07/22] Fix notrack --- src/core/plugins/notrack/index.ts | 252 ++++++++++++++++--- src/core/plugins/notrack/lib/console.ts | 16 -- src/core/plugins/notrack/lib/miscellanous.ts | 33 --- src/core/plugins/notrack/lib/network.ts | 12 - src/core/plugins/notrack/lib/sentry.ts | 26 -- src/core/plugins/notrack/lib/utils.ts | 3 - 6 files changed, 223 insertions(+), 119 deletions(-) delete mode 100644 src/core/plugins/notrack/lib/console.ts delete mode 100644 src/core/plugins/notrack/lib/miscellanous.ts delete mode 100644 src/core/plugins/notrack/lib/network.ts delete mode 100644 src/core/plugins/notrack/lib/sentry.ts delete mode 100644 src/core/plugins/notrack/lib/utils.ts diff --git a/src/core/plugins/notrack/index.ts b/src/core/plugins/notrack/index.ts index 58865de..8459303 100644 --- a/src/core/plugins/notrack/index.ts +++ b/src/core/plugins/notrack/index.ts @@ -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"); + }, +}); diff --git a/src/core/plugins/notrack/lib/console.ts b/src/core/plugins/notrack/lib/console.ts deleted file mode 100644 index 7a69471..0000000 --- a/src/core/plugins/notrack/lib/console.ts +++ /dev/null @@ -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]); -} diff --git a/src/core/plugins/notrack/lib/miscellanous.ts b/src/core/plugins/notrack/lib/miscellanous.ts deleted file mode 100644 index b6a6093..0000000 --- a/src/core/plugins/notrack/lib/miscellanous.ts +++ /dev/null @@ -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()); -} diff --git a/src/core/plugins/notrack/lib/network.ts b/src/core/plugins/notrack/lib/network.ts deleted file mode 100644 index 68f762e..0000000 --- a/src/core/plugins/notrack/lib/network.ts +++ /dev/null @@ -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) }, - ); -} diff --git a/src/core/plugins/notrack/lib/sentry.ts b/src/core/plugins/notrack/lib/sentry.ts deleted file mode 100644 index 5b01b7e..0000000 --- a/src/core/plugins/notrack/lib/sentry.ts +++ /dev/null @@ -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(); - }; -} diff --git a/src/core/plugins/notrack/lib/utils.ts b/src/core/plugins/notrack/lib/utils.ts deleted file mode 100644 index 55f871f..0000000 --- a/src/core/plugins/notrack/lib/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { instead } from "@lib/api/patcher"; - -export const noop = (prop: string, parent: any) => instead(prop, parent, () => {}); From 41993c6eded34e63a7c3d1eba4f765e6d20d1693 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sat, 11 Oct 2025 18:14:26 +0300 Subject: [PATCH 08/22] fix(Notrack): fixed notrack --- src/core/plugins/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/plugins/index.ts b/src/core/plugins/index.ts index 7b423f7..728fa70 100644 --- a/src/core/plugins/index.ts +++ b/src/core/plugins/index.ts @@ -9,7 +9,7 @@ interface CorePlugin { export const getCorePlugins = (): Record<string, CorePlugin> => ({ "bunny.quickinstall": require("./quickinstall"), "bunny.badges": require("./badges"), - "vendetta.notrack": require("./notrack") + "bunny.notrack": require("./notrack") }); /** From 0e8604ab0e176a6992ff8548c9c03eef75f55b57 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 13:05:35 +0300 Subject: [PATCH 09/22] fix(plugins): make it so coreplugins are force enabled --- .../pages/Plugins/components/PluginCard.tsx | 23 ++++++++++++------- src/core/ui/settings/pages/Plugins/index.tsx | 5 ++-- .../Plugins/sheets/PluginInfoActionSheet.tsx | 15 ------------ 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx index b199f55..1f37a35 100644 --- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx @@ -8,6 +8,7 @@ import { showSheet } from "@ui/sheets"; import chroma from "chroma-js"; import { createContext, useContext, useMemo } from "react"; import { Image, View } from "react-native"; +import { isCorePlugin } from "@lib/addons/plugins"; const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel>; }>(null!); const useCardContext = () => useContext(CardContext); @@ -125,6 +126,7 @@ export default function PluginCard({ result, item: plugin }: CardWrapper<Unified const [, forceUpdate] = React.useReducer(() => ({}), 0); const cardContextValue = useMemo(() => ({ plugin, result }), [plugin, result]); + const core = isCorePlugin(plugin.id); return ( <CardContext.Provider value={cardContextValue}> @@ -138,13 +140,18 @@ export default function PluginCard({ result, item: plugin }: CardWrapper<Unified <View> <Stack spacing={12} direction="horizontal"> <Actions /> - <TableSwitch - value={plugin.isEnabled()} - onValueChange={(v: boolean) => { - plugin.toggle(v); - forceUpdate(); - }} - /> + <View style={core ? { opacity: 0.5 } : undefined}> + <TableSwitch + value={core ? true : plugin.isEnabled()} + disabled={core} + onValueChange={(v: boolean) => { + if (!core) { + plugin.toggle(v); + forceUpdate(); + } + }} + /> + </View> </Stack> </View> </View> @@ -153,4 +160,4 @@ export default function PluginCard({ result, item: plugin }: CardWrapper<Unified </Card> </CardContext.Provider> ); -} +} \ No newline at end of file diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index afdf9fe..b09bf9f 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -61,10 +61,11 @@ export default function Plugins() { useProxy(VdPluginManager.plugins); useObservable([pluginSettings]); + const corePlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && isCorePlugin(p.id)).map(unifyBunnyPlugin); const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin); - const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && isCorePlugin(p.id)).map(unifyBunnyPlugin); + 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)); diff --git a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx index 4c769df..bcf0256 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx @@ -77,21 +77,6 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo }); }} /> - <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 }}> From 715223d72f1203caf94f8320cfdea022b5672146 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 14:44:59 +0300 Subject: [PATCH 10/22] fix(themes): make themes work again + bump version --- scripts/build.mjs | 2 +- src/lib/addons/themes/colors/updater.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index ead167a..6b489ca 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"v1.3.1"` + version: `"v1.3.2"` } } }, diff --git a/src/lib/addons/themes/colors/updater.ts b/src/lib/addons/themes/colors/updater.ts index 7721628..3d1f7a2 100644 --- a/src/lib/addons/themes/colors/updater.ts +++ b/src/lib/addons/themes/colors/updater.ts @@ -52,6 +52,9 @@ export function updateBunnyColor(colorManifest: ColorManifest | null, { update = } if (update) { + // scuffed but it works + AppearanceManager.setShouldSyncAppearanceSettings(true); + AppearanceManager.updateTheme(internalDef != null ? ref.key : ref.lastSetDiscordTheme); AppearanceManager.setShouldSyncAppearanceSettings(false); AppearanceManager.updateTheme(internalDef != null ? ref.key : ref.lastSetDiscordTheme); } From d6a604f69c045f7d05865c0e2a93fcfae272cdcc Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 15:09:48 +0300 Subject: [PATCH 11/22] fix(themes): fix the issue on startup while allowing reloadless themes to work with the old method --- src/lib/addons/themes/colors/updater.ts | 9 ++++++--- src/lib/addons/themes/index.ts | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib/addons/themes/colors/updater.ts b/src/lib/addons/themes/colors/updater.ts index 3d1f7a2..b7d0e3f 100644 --- a/src/lib/addons/themes/colors/updater.ts +++ b/src/lib/addons/themes/colors/updater.ts @@ -27,7 +27,7 @@ export const _colorRef: InternalColorRef = { lastSetDiscordTheme: "darker" }; -export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) { +export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true, startup = true } ) { if (settings.safeMode?.enabled) return; const internalDef = colorManifest ? parseColorManifest(colorManifest) : null; @@ -52,11 +52,14 @@ export function updateBunnyColor(colorManifest: ColorManifest | null, { update = } if (update) { + AppearanceManager.setShouldSyncAppearanceSettings(false); + AppearanceManager.updateTheme(internalDef != null ? ref.key : ref.lastSetDiscordTheme); + } + + if (startup) { // scuffed but it works AppearanceManager.setShouldSyncAppearanceSettings(true); AppearanceManager.updateTheme(internalDef != null ? ref.key : ref.lastSetDiscordTheme); - AppearanceManager.setShouldSyncAppearanceSettings(false); - AppearanceManager.updateTheme(internalDef != null ? ref.key : ref.lastSetDiscordTheme); } } diff --git a/src/lib/addons/themes/index.ts b/src/lib/addons/themes/index.ts index 546c040..af13486 100644 --- a/src/lib/addons/themes/index.ts +++ b/src/lib/addons/themes/index.ts @@ -104,10 +104,10 @@ export function selectTheme(theme: VdThemeInfo | null, write = true) { ); if (theme == null && write) { - updateBunnyColor(null, { update: true }); + updateBunnyColor(null, { update: true, startup: false }); return writeThemeToNative({}); } else if (theme) { - updateBunnyColor(theme.data, { update: true }); + updateBunnyColor(theme.data, { update: true, startup: false }); return writeThemeToNative(theme); } } From 17752879b9789f4f7e458a09df4018fecf2c7049 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 15:12:51 +0300 Subject: [PATCH 12/22] fix(themes): disable requiring reload for disabling themes --- src/lib/addons/themes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/addons/themes/index.ts b/src/lib/addons/themes/index.ts index af13486..6efffa9 100644 --- a/src/lib/addons/themes/index.ts +++ b/src/lib/addons/themes/index.ts @@ -104,7 +104,7 @@ export function selectTheme(theme: VdThemeInfo | null, write = true) { ); if (theme == null && write) { - updateBunnyColor(null, { update: true, startup: false }); + updateBunnyColor(null, { update: false, startup: true }); return writeThemeToNative({}); } else if (theme) { updateBunnyColor(theme.data, { update: true, startup: false }); From c4c3e2eabf44a0da6b92f0d5d675f5b146861f64 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 16:01:14 +0300 Subject: [PATCH 13/22] feat(speed): potential speed increase? --- src/core/vendetta/plugins.ts | 2 +- src/lib/addons/plugins/index.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts index 4026b36..e8c065c 100644 --- a/src/core/vendetta/plugins.ts +++ b/src/core/vendetta/plugins.ts @@ -169,7 +169,7 @@ export const VdPluginManager = { if (!settings.safeMode?.enabled) { // Loop over any plugin that is enabled, update it if allowed, then start it. - await Promise.allSettled(allIds.filter(pl => plugins[pl].enabled).map(async pl => (plugins[pl].update && await this.fetchPlugin(pl).catch((e: Error) => logger.error(e.message)), await this.startPlugin(pl)))); + Promise.allSettled(allIds.filter(pl => plugins[pl].enabled).map(async pl => (plugins[pl].update && await this.fetchPlugin(pl).catch((e: Error) => logger.error(e.message)), await this.startPlugin(pl)))); // Wait for the above to finish, then update all disabled plugins that are allowed to. allIds.filter(pl => !plugins[pl].enabled && plugins[pl].update).forEach(pl => this.fetchPlugin(pl)); } diff --git a/src/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index 2d4e9c1..5b8ee5e 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -380,19 +380,19 @@ export async function updatePlugins() { corePluginInstances.set(id, instance); } - await updateAllRepository(); + updateAllRepository(); } /** * @internal */ export async function initPlugins() { - await awaitStorage(pluginRepositories, pluginSettings); + awaitStorage(pluginRepositories, pluginSettings); // Now, start all enabled plugins... - await Promise.allSettled([...registeredPlugins.keys()].map(async id => { + Promise.allSettled([...registeredPlugins.keys()].map(async id => { if (isPluginEnabled(id)) { - await startPlugin(id); + startPlugin(id); } })); } From 4b3802cce90246c418899f59ddf56dc0c31a9239 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 21:32:35 +0300 Subject: [PATCH 14/22] feat(performance): speedup startup --- .forgejo/workflows/build.yaml | 23 +++++++++-------------- src/index.ts | 19 +++++++------------ src/lib/addons/fonts/index.ts | 2 +- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 907ca99..af6c50c 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -2,54 +2,49 @@ name: Node.js CI / CD (Bun + Forgejo) on: push: - branches: [ "main" ] - + branches: [ "main", "dev" ] jobs: buildAndRelease: runs-on: codeberg-tiny - steps: - uses: actions/checkout@v3 - - name: Install Bun run: | curl -fsSL https://bun.sh/install | bash export BUN_INSTALL="$HOME/.bun" export PATH="$BUN_INSTALL/bin:$PATH" echo "$BUN_INSTALL/bin" >> $GITHUB_PATH - - name: Install dependencies run: | export BUN_INSTALL="$HOME/.bun" export PATH="$BUN_INSTALL/bin:$PATH" bun install - - name: Build project run: | export BUN_INSTALL="$HOME/.bun" export PATH="$BUN_INSTALL/bin:$PATH" bun run build --release-branch=${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME}} --build-minify - - name: Push JS files to dist branch run: | git config --local user.email "actions@forgejo.local" git config --local user.name "Forgejo Actions" - mkdir -p temp_dist find dist -name "*.js" -exec cp {} temp_dist/ \; - git checkout --orphan dist-temp git rm -rf --cached . - cp -r temp_dist/* . rm -rf temp_dist - git add *.js git commit -m "Update build $GITHUB_SHA" || exit 0 - git push -f origin HEAD:dist - + + if [ "${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME}}" = "dev" ]; then + TARGET_BRANCH="testing-dist" + else + TARGET_BRANCH="dist" + fi + + git push -f origin HEAD:$TARGET_BRANCH git checkout -f ${GITHUB_REF_NAME:-${CI_COMMIT_REF_NAME}} - - name: Merge main into github if: github.ref == 'refs/heads/main' run: | diff --git a/src/index.ts b/src/index.ts index 7a690c1..341b32f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,9 @@ import * as lib from "./lib"; export default async () => { // Load everything in parallel + + updatePlugins(); + await Promise.all([ initThemes(), injectFluxInterceptor(), @@ -30,7 +33,10 @@ export default async () => { initSettings(), initFixes(), // patchErrorBoundary(), - updatePlugins() + updatePlugins(), + updateFonts(), + initPlugins(), + VdPluginManager.initPlugins(), ]).then( // Push them all to unloader u => u.forEach(f => f && lib.unload.push(f)) @@ -39,17 +45,6 @@ export default async () => { // Assign window object window.bunny = lib; - // Once done, load Vendetta plugins - VdPluginManager.initPlugins() - .then(u => lib.unload.push(u)) - .catch(() => alert("Failed to initialize Vendetta plugins")); - - // And then, load Kettu plugins - initPlugins(); - - // Update the fonts - updateFonts(); - // We good :) logger.log("Kettu is ready!"); }; \ No newline at end of file diff --git a/src/lib/addons/fonts/index.ts b/src/lib/addons/fonts/index.ts index 730ceb1..47fc4d4 100644 --- a/src/lib/addons/fonts/index.ts +++ b/src/lib/addons/fonts/index.ts @@ -109,7 +109,7 @@ export async function removeFont(name: string) { export async function updateFonts() { await awaitStorage(fonts); - await Promise.allSettled( + Promise.allSettled( Object.keys(fonts).map( name => saveFont(fonts[name], fonts.__selected === name) ) From 7ea4156ad26224c036182fe171af60363ea03d63 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 21:40:32 +0300 Subject: [PATCH 15/22] fix(plugins): fix some plugins having cache issues --- src/lib/addons/plugins/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index 5b8ee5e..762e0d9 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -387,7 +387,7 @@ export async function updatePlugins() { * @internal */ export async function initPlugins() { - awaitStorage(pluginRepositories, pluginSettings); + await awaitStorage(pluginRepositories, pluginSettings); // Now, start all enabled plugins... Promise.allSettled([...registeredPlugins.keys()].map(async id => { From b9b17bd58ba981fcb7dbeaee0721550e65a3c028 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 22:08:05 +0300 Subject: [PATCH 16/22] chore(bump): bump to v1.3.3 --- scripts/build.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 6b489ca..2327899 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"v1.3.2"` + version: `"v1.3.3"` } } }, From 7c69cba7f01c96283eaac526bbc4ce50bb866503 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 11 Oct 2025 10:24:19 +0200 Subject: [PATCH 17/22] port messagefix from ShiggyCord --- src/core/plugins/index.ts | 17 +++++---- src/core/plugins/messagefix/index.ts | 56 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 src/core/plugins/messagefix/index.ts diff --git a/src/core/plugins/index.ts b/src/core/plugins/index.ts index 728fa70..3e1ed75 100644 --- a/src/core/plugins/index.ts +++ b/src/core/plugins/index.ts @@ -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"), - "bunny.notrack": require("./notrack") + "bunny.notrack": require("./notrack"), + "bunny.messagefix": require("./messagefix"), }); /** * @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; } diff --git a/src/core/plugins/messagefix/index.ts b/src/core/plugins/messagefix/index.ts new file mode 100644 index 0000000..146bf2b --- /dev/null +++ b/src/core/plugins/messagefix/index.ts @@ -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"); + }, +}); From 9af91ed1a252997223ac6b4740aabeeb00edb463 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:38:51 +0200 Subject: [PATCH 18/22] port information menu look from ShiggyCord (was exclusive only for core plugins) --- .../pages/Plugins/components/PluginCard.tsx | 262 ++++++++-------- src/core/ui/settings/pages/Plugins/index.tsx | 258 ++++++++++------ .../settings/pages/Plugins/models/vendetta.ts | 60 ++-- .../Plugins/sheets/PluginInfoActionSheet.tsx | 288 +++++++++++++----- 4 files changed, 545 insertions(+), 323 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx index 1f37a35..db48764 100644 --- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx @@ -3,161 +3,183 @@ 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"; import { isCorePlugin } from "@lib/addons/plugins"; -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 core = isCorePlugin(plugin.id); + 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 /> - <View style={core ? { opacity: 0.5 } : undefined}> - <TableSwitch - value={core ? true : plugin.isEnabled()} - disabled={core} - onValueChange={(v: boolean) => { - if (!core) { - plugin.toggle(v); - forceUpdate(); - } - }} - /> - </View> - </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> + ); } \ No newline at end of file diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index b09bf9f..6376e3b 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -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 = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && isCorePlugin(p.id)).map(unifyBunnyPlugin); const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin); @@ -71,68 +86,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); + } + }, + }} + /> + ); } diff --git a/src/core/ui/settings/pages/Plugins/models/vendetta.ts b/src/core/ui/settings/pages/Plugins/models/vendetta.ts index ee735f5..fc36653 100644 --- a/src/core/ui/settings/pages/Plugins/models/vendetta.ts +++ b/src/core/ui/settings/pages/Plugins/models/vendetta.ts @@ -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); + }, + }; } diff --git a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx index bcf0256..e105640 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx @@ -1,91 +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(), - }); - }} - /> - </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> + ); } From 677939c333146111991d6ada5559c659772f95d4 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 12 Oct 2025 23:12:28 +0300 Subject: [PATCH 19/22] chore(bump) --- scripts/build.mjs | 2 +- .../ui/settings/pages/Plugins/models/bunny.ts | 79 +++++++++++-------- .../Plugins/sheets/PluginInfoActionSheet.tsx | 6 +- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 2327899..787124c 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"v1.3.3"` + version: `"v1.3.4"` } } }, diff --git a/src/core/ui/settings/pages/Plugins/models/bunny.ts b/src/core/ui/settings/pages/Plugins/models/bunny.ts index 106be65..252e6ff 100644 --- a/src/core/ui/settings/pages/Plugins/models/bunny.ts +++ b/src/core/ui/settings/pages/Plugins/models/bunny.ts @@ -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); + }, + }; } diff --git a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx index e105640..670c0f7 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx @@ -151,7 +151,7 @@ export default function PluginInfoActionSheet({ justifyContent: "center", alignItems: "center", flexWrap: "wrap", - gap: 12, + gap: 22, paddingHorizontal: 4, }} > @@ -167,6 +167,7 @@ export default function PluginInfoActionSheet({ }); }} /> + {!isCorePlugin && ( <PluginInfoIconButton label="Refetch" variant="secondary" @@ -174,12 +175,15 @@ export default function PluginInfoActionSheet({ onPress={refetchPlugin} disabled={loading} /> + )} + {!isCorePlugin && ( <PluginInfoIconButton label="Copy URL" variant="secondary" icon={findAssetId("LinkIcon")} onPress={copyPluginUrl} /> + )} <PluginInfoIconButton label="Clear Data" variant="secondary" From ce9968ee73ecf29d6991a865e1f3951a634c8179 Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Sun, 12 Oct 2025 22:33:15 +0200 Subject: [PATCH 20/22] fix(plugins): fix broken ui --- .../pages/Plugins/components/PluginCard.tsx | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx index db48764..e98fba6 100644 --- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx @@ -153,33 +153,36 @@ export default function PluginCard({ [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 /> + <View style={core ? { opacity: 0.5 } : undefined}> + <TableSwitch + value={core ? true : plugin.isEnabled()} + disabled={core} + onValueChange={(v: boolean) => { + if (!core) { + plugin.toggle(v); + forceUpdate(); + } + }} + /> + </View> + </Stack> + </View> + </View> + <Description /> + </Stack> + </Card> + </CardContext.Provider> + ); } \ No newline at end of file From 081990fb242240329687964a855fc89f9822f5d7 Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Sun, 12 Oct 2025 22:40:00 +0200 Subject: [PATCH 21/22] fixx(ui): fix bug --- .../ui/settings/pages/Plugins/components/PluginCard.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx index e98fba6..daf3bb1 100644 --- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx @@ -147,11 +147,10 @@ export default function PluginCard({ }: 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]); + const core = isCorePlugin(plugin.id); return ( <CardContext.Provider value={cardContextValue}> From b0107786e3166a705738b11ebbe105e210bf31f1 Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Mon, 13 Oct 2025 08:38:36 +0200 Subject: [PATCH 22/22] docs(readme): update information on BTLoader now loading kettu --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 6cc416e..76ef8e4 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@ A mod for Discord on Android ### iOS - [BTLoader](https://github.com/CloudySn0w/BTLoader) -note: btloader is not developed by me and loads revenge by default - ## Building 1. Install a Kettu loader with loader config support (any mentioned in the [Installing](#installing) section). 1. Go to Settings > General and enable Developer Settings.