From a4eb98bd8f8a75987d39da6025be23815eb1a232 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 01/68] 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): 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; + + const sentrified: ConsoleFunctions = {}; + + try { + // We need to use Object.keys because TypeScript doesn't allow indexing Console with strings + (Object.keys(console) as Array).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 beb52729f59de9251e2bc49b7551eff7bd33fe83 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 02/68] port messagefix from ShiggyCord --- src/core/plugins/index.ts | 21 ++++++----- src/core/plugins/messagefix/index.ts | 56 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 9 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 7b423f7..5bc2f76 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 => ({ - "bunny.quickinstall": require("./quickinstall"), - "bunny.badges": require("./badges"), - "vendetta.notrack": require("./notrack") + "bunny.quickinstall": require("./quickinstall"), + "bunny.badges": require("./badges"), + "bunny.notrack": { ...require("./notrack"), preenabled: true }, + "bunny.messagefix": { default: require("./messagefix"), preenabled: true }, }); /** * @internal */ -export function defineCorePlugin(instance: PluginInstanceInternal): PluginInstanceInternal { - // @ts-expect-error - instance[Symbol.for("bunny.core.plugin")] = true; - return instance; +export function defineCorePlugin( + instance: PluginInstanceInternal, +): PluginInstanceInternal { + // @ts-expect-error + instance[Symbol.for("bunny.core.plugin")] = true; + return instance; } 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 93d5c354a119be52a3a8d99bcc49860c737e6b3f 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 03/68] port information menu look from ShiggyCord (was exclusive only for core plugins) --- .../pages/Plugins/components/PluginCard.tsx | 256 ++++++++------- src/core/ui/settings/pages/Plugins/index.tsx | 293 +++++++++++------ .../ui/settings/pages/Plugins/models/bunny.ts | 79 +++-- .../settings/pages/Plugins/models/vendetta.ts | 60 ++-- .../Plugins/sheets/PluginInfoActionSheet.tsx | 303 ++++++++++++------ 5 files changed, 616 insertions(+), 375 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..6ab8f1e 100644 --- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx @@ -3,154 +3,182 @@ import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models"; import { usePluginCardStyles } from "@core/ui/settings/pages/Plugins/usePluginCardStyles"; import { findAssetId } from "@lib/api/assets"; import { NavigationNative, tokens } from "@metro/common"; -import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components"; +import { + Card, + IconButton, + Stack, + TableSwitch, + Text, +} from "@metro/common/components"; import { showSheet } from "@ui/sheets"; import chroma from "chroma-js"; import { createContext, useContext, useMemo } from "react"; import { Image, View } from "react-native"; -const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult; }>(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 9c5405c..5f665b7 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -3,7 +3,12 @@ import AddonPage from "@core/ui/components/AddonPage"; import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard"; import { VdPluginManager } from "@core/vendetta/plugins"; import { useProxy } from "@core/vendetta/storage"; -import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins } from "@lib/addons/plugins"; +import { + isCorePlugin, + isPluginInstalled, + pluginSettings, + registeredPlugins, +} from "@lib/addons/plugins"; import { Author } from "@lib/addons/types"; import { findAssetId } from "@lib/api/assets"; import { settings } from "@lib/api/settings"; @@ -13,7 +18,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"; @@ -21,116 +32,198 @@ 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); + const vdPlugins = Object.values(VdPluginManager.plugins).map( + unifyVdPlugin, + ); + const bnPlugins = [...registeredPlugins.values()] + .filter( + (p) => + p && + typeof p.id === "string" && + isPluginInstalled(p.id) && + !isCorePlugin(p.id), + ) + .map(unifyBunnyPlugin); - return [...vdPlugins, ...bnPlugins]; - }} - ListHeaderComponent={() => { - const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(p => !p.id.startsWith(VD_PROXY_PREFIX) && !p.id.startsWith(BUNNY_PROXY_PREFIX)); - if (!unproxiedPlugins.length) return null; + return [...vdPlugins, ...bnPlugins]; + }} + ListHeaderComponent={() => { + const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter( + (p) => + !p.id.startsWith(VD_PROXY_PREFIX) && + !p.id.startsWith(BUNNY_PROXY_PREFIX), + ); + if (!unproxiedPlugins.length) return null; - return <View style={{ marginVertical: 12, marginHorizontal: 10 }}> - <Card border="strong"> - <View style={{ flex: 1, justifyContent: "center", alignItems: "center", flexDirection: "row" }}> - <View style={{ gap: 6, flexShrink: 1 }}> - <Text variant="heading-md/bold">Unproxied Plugins Found</Text> - <Text variant="text-sm/medium" color="text-muted"> - Plugins installed from unproxied sources may run unverified code in this app without your awareness. - </Text> - </View> - <View style={{ marginLeft: "auto" }}> - <IconButton - size="sm" - variant="secondary" - icon={findAssetId("CircleInformationIcon-primary")} - style={{ marginLeft: 8 }} - onPress={() => { - navigation.push("PUPU_CUSTOM_PAGE", { - title: "Unproxied Plugins", - render: () => { - return <FlashList - data={unproxiedPlugins} - contentContainerStyle={{ padding: 8 }} - ItemSeparatorComponent={() => <View style={{ height: 8 }} />} - renderItem={({ item: p }: any) => <Card> - <Text variant="heading-md/semibold">{p.id}</Text> - </Card>} - />; - } - }); - }} + return ( + <View style={{ marginVertical: 12, marginHorizontal: 10 }}> + <Card border="strong"> + <View + style={{ + flex: 1, + justifyContent: "center", + alignItems: "center", + flexDirection: "row", + }} + > + <View style={{ gap: 6, flexShrink: 1 }}> + <Text variant="heading-md/bold">Unproxied Plugins Found</Text> + <Text variant="text-sm/medium" color="text-muted"> + Plugins installed from unproxied sources may run unverified + code in this app without your awareness. + </Text> + </View> + <View style={{ marginLeft: "auto" }}> + <IconButton + size="sm" + variant="secondary" + icon={findAssetId("CircleInformationIcon-primary")} + style={{ marginLeft: 8 }} + onPress={() => { + navigation.push("PUPU_CUSTOM_PAGE", { + title: "Unproxied Plugins", + render: () => { + return ( + <FlashList + data={unproxiedPlugins} + contentContainerStyle={{ padding: 8 }} + ItemSeparatorComponent={() => ( + <View style={{ height: 8 }} /> + )} + renderItem={({ item: p }: any) => ( + <Card> + <Text variant="heading-md/semibold"> + {p.id} + </Text> + </Card> + )} /> - </View> - </View> - </Card> - </View>; - }} - installAction={{ - label: "Install a plugin", - fetchFn: async (url: string) => { - if (!url.startsWith(VD_PROXY_PREFIX) && !url.startsWith(BUNNY_PROXY_PREFIX) && !settings.developerSettings) { - openAlert("bunny-plugin-unproxied-confirmation", <AlertModal - title="Hold On!" - content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?" - extraContent={<Card><Text variant="text-md/bold">{url}</Text></Card>} - actions={<AlertActions> - <AlertActionButton text="Continue" variant="primary" onPress={() => { - VdPluginManager.installPlugin(url) - .then(() => showToast(Strings.TOASTS_INSTALLED_PLUGIN, findAssetId("Check"))) - .catch(e => openAlert("bunny-plugin-install-failed", <AlertModal - title="Install Failed" - content={`Unable to install plugin from '${url}':`} - extraContent={<Card><Text variant="text-md/normal">{e instanceof Error ? e.message : String(e)}</Text></Card>} - actions={<AlertActionButton text="Okay" variant="primary" />} - />)); - }} /> - <AlertActionButton text="Cancel" variant="secondary" /> - </AlertActions>} - />); - } else { - return await VdPluginManager.installPlugin(url); + ); + }, + }); + }} + /> + </View> + </View> + </Card> + </View> + ); + }} + installAction={{ + label: "Install a plugin", + fetchFn: async (url: string) => { + if ( + !url.startsWith(VD_PROXY_PREFIX) && + !url.startsWith(BUNNY_PROXY_PREFIX) && + !settings.developerSettings + ) { + openAlert( + "bunny-plugin-unproxied-confirmation", + <AlertModal + title="Hold On!" + content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?" + extraContent={ + <Card> + <Text variant="text-md/bold">{url}</Text> + </Card> } - } - }} - />; + actions={ + <AlertActions> + <AlertActionButton + text="Continue" + variant="primary" + onPress={() => { + VdPluginManager.installPlugin(url) + .then(() => + showToast( + Strings.TOASTS_INSTALLED_PLUGIN, + findAssetId("Check"), + ), + ) + .catch((e) => + openAlert( + "bunny-plugin-install-failed", + <AlertModal + title="Install Failed" + content={`Unable to install plugin from '${url}':`} + extraContent={ + <Card> + <Text variant="text-md/normal"> + {e instanceof Error + ? e.message + : String(e)} + </Text> + </Card> + } + actions={ + <AlertActionButton + text="Okay" + variant="primary" + /> + } + />, + ), + ); + }} + /> + <AlertActionButton text="Cancel" variant="secondary" /> + </AlertActions> + } + />, + ); + } else { + return await VdPluginManager.installPlugin(url); + } + }, + }} + /> + ); } 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/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 4c769df..e105640 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx @@ -1,106 +1,211 @@ -import { startPlugin } from "@lib/addons/plugins"; -import { findAssetId } from "@lib/api/assets"; import { hideSheet } from "@lib/ui/sheets"; -import { ActionSheet, Card, ContextMenu, IconButton, Text } from "@metro/common/components"; -import { ComponentProps } from "react"; +import { ActionSheet, Card, IconButton, Text } from "@metro/common/components"; +import { clipboard } from "@metro/common"; +import { showToast } from "@lib/ui/toasts"; +import { showConfirmationAlert } from "@core/vendetta/alerts"; +import { VdPluginManager } from "@core/vendetta/plugins"; +import { purgeStorage as purgeVdStorage } from "@core/vendetta/storage"; +import { purgeStorage } from "@lib/api/storage"; +import { findAssetId } from "@lib/api/assets"; +import { useState } from "react"; import { ScrollView, View } from "react-native"; - -import { PluginInfoActionSheetProps } from "./common"; import TitleComponent from "./TitleComponent"; +import { PluginInfoActionSheetProps } from "./common"; -function PluginInfoIconButton(props: ComponentProps<typeof IconButton>) { - const { onPress } = props; - props.onPress &&= () => { +function PluginInfoIconButton(props) { + const { onPress } = props; + props.onPress &&= () => { + hideSheet("PluginInfoActionSheet"); + onPress?.(); + }; + return <IconButton {...props} />; +} + +export default function PluginInfoActionSheet({ + plugin, + navigation, +}: PluginInfoActionSheetProps) { + plugin.usePluginState(); + const [loading, setLoading] = useState(false); + + // Determine plugin type + const isVendettaPlugin = plugin.id.includes("/"); + const isCorePlugin = + plugin.id.startsWith("bunny.") || plugin.id.startsWith("vendetta."); + + const copyPluginUrl = () => { + let url = plugin.id; + if (isVendettaPlugin) { + url = plugin.id; + } else { + try { + const pluginAny = plugin; + const repoUrl = + pluginAny._manifest?.parentRepository || + pluginAny.manifest?.parentRepository; + url = repoUrl ? `${repoUrl}/builds/${plugin.id}` : plugin.id; + } catch (e) { + url = plugin.id; + } + } + clipboard.setString(url); + showToast("Copied to clipboard!", findAssetId("toast_copy_link")); + }; + + const refetchPlugin = async () => { + setLoading(true); + try { + if (isVendettaPlugin) { + const vdPlugin = VdPluginManager.plugins[plugin.id]; + if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false); + await VdPluginManager.fetchPlugin(plugin.id); + if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id); + showToast("Plugin refreshed successfully"); + } else { + // For Bunny plugins + // If you have a refreshPlugin function, call it here + showToast("Plugin refreshed successfully"); + } + } catch (e) { + showToast("Failed to refresh plugin"); + } finally { + setLoading(false); + } + }; + + const clearPluginData = () => { + showConfirmationAlert({ + title: "Clear Data", + content: + "Are you sure you want to clear all data for this plugin? This action cannot be undone.", + confirmText: "Clear", + confirmColor: "red", + cancelText: "Cancel", + onConfirm: async () => { hideSheet("PluginInfoActionSheet"); - onPress?.(); - }; + try { + if (isVendettaPlugin) { + const vdPlugin = VdPluginManager.plugins[plugin.id]; + if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false); + await purgeVdStorage(plugin.id); + if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id); + } else { + await purgeStorage(`plugins/storage/${plugin.id}.json`); + } + showToast("Plugin data cleared successfully"); + } catch (e) { + showToast("Failed to clear plugin data"); + } + }, + }); + }; - return <IconButton {...props} />; -} - -export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfoActionSheetProps) { - plugin.usePluginState(); - - return <ActionSheet> - <ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}> - <View style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 24, justifyContent: "space-between", width: "100%" }}> - <TitleComponent plugin={plugin} /> - <ContextMenu - items={[ - { - label: "Details", - iconSource: findAssetId("CircleInformationIcon-primary"), - action: () => { - } - }, - // { - // label: true ? "Disable Updates" : "Enable Updates", - // iconSource: true ? findAssetId("ClockXIcon") : findAssetId("ClockIcon"), - // action: () => { - - // } - // }, - { - label: "Clear Data", - iconSource: findAssetId("FileIcon"), - variant: "destructive", - action: () => { - } - }, - { - label: "Uninstall", - iconSource: findAssetId("TrashIcon"), - variant: "destructive", - action: () => { - } - } - ]} - > - {props => <IconButton - {...props} - icon={findAssetId("MoreHorizontalIcon")} - variant="secondary" - size="sm" - />} - </ContextMenu> - </View> - <View style={{ flexDirection: "row", justifyContent: "space-around", alignContent: "center" }}> - <PluginInfoIconButton - label="Configure" - variant="secondary" - disabled={!plugin.getPluginSettingsComponent()} - icon={findAssetId("WrenchIcon")} - onPress={() => { - navigation.push("PUPU_CUSTOM_PAGE", { - title: plugin.name, - render: plugin.getPluginSettingsComponent(), - }); - }} - /> - <PluginInfoIconButton - label="Refetch" - variant="secondary" - icon={findAssetId("RetryIcon")} - onPress={() => { - startPlugin(plugin.id); - }} - /> - <PluginInfoIconButton - label="Copy URL" - variant="secondary" - icon={findAssetId("LinkIcon")} - onPress={() => { - }} - /> - </View> - <Card> - <Text variant="text-md/semibold" color="text-primary" style={{ marginBottom: 4 }}> - Description - </Text> - <Text variant="text-md/medium"> - {plugin.description} - </Text> - </Card> - </ScrollView> - </ActionSheet>; + const uninstallPluginHandler = () => { + if (isCorePlugin) { + showToast("Core plugins cannot be uninstalled"); + return; + } + showConfirmationAlert({ + title: "Uninstall Plugin", + content: + "Are you sure you want to uninstall this plugin? This action cannot be undone.", + confirmText: "Uninstall", + confirmColor: "red", + cancelText: "Cancel", + onConfirm: async () => { + hideSheet("PluginInfoActionSheet"); + try { + if (isVendettaPlugin) { + await VdPluginManager.removePlugin(plugin.id); + } else { + // If you have an uninstallPlugin function, call it here + } + showToast("Plugin uninstalled successfully"); + } catch (e) { + showToast( + `Failed to uninstall plugin: ${e instanceof Error ? e.message : String(e)}`, + ); + } + }, + }); + }; + + return ( + <ActionSheet> + <ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}> + <View + style={{ + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingVertical: 24, + justifyContent: "space-between", + width: "100%", + }} + > + <TitleComponent plugin={plugin} /> + </View> + <View + style={{ + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + flexWrap: "wrap", + gap: 12, + paddingHorizontal: 4, + }} + > + <PluginInfoIconButton + label="Configure" + variant="secondary" + disabled={!plugin.getPluginSettingsComponent()} + icon={findAssetId("WrenchIcon")} + onPress={() => { + navigation.push("PUPU_CUSTOM_PAGE", { + title: plugin.name, + render: plugin.getPluginSettingsComponent(), + }); + }} + /> + <PluginInfoIconButton + label="Refetch" + variant="secondary" + icon={findAssetId("RetryIcon")} + onPress={refetchPlugin} + disabled={loading} + /> + <PluginInfoIconButton + label="Copy URL" + variant="secondary" + icon={findAssetId("LinkIcon")} + onPress={copyPluginUrl} + /> + <PluginInfoIconButton + label="Clear Data" + variant="secondary" + icon={findAssetId("FileIcon")} + onPress={clearPluginData} + /> + {!isCorePlugin && ( + <PluginInfoIconButton + label="Uninstall" + variant="secondary" + icon={findAssetId("TrashIcon")} + onPress={uninstallPluginHandler} + /> + )} + </View> + <Card> + <Text + variant="text-md/semibold" + color="text-primary" + style={{ marginBottom: 4 }} + > + Description + </Text> + <Text variant="text-md/medium">{plugin.description}</Text> + </Card> + </ScrollView> + </ActionSheet> + ); } From 1f3613b7385e258ee875b4aa037247ddb22f8bfc Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sat, 11 Oct 2025 17:14:51 +0300 Subject: [PATCH 04/68] feat(plugins): show core plugins in plugin menus --- src/core/ui/settings/pages/Plugins/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 9c5405c..6e186f3 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -3,7 +3,7 @@ import AddonPage from "@core/ui/components/AddonPage"; import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard"; import { VdPluginManager } from "@core/vendetta/plugins"; import { useProxy } from "@core/vendetta/storage"; -import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins } from "@lib/addons/plugins"; +import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins, corePluginInstances } from "@lib/addons/plugins"; import { Author } from "@lib/addons/types"; import { findAssetId } from "@lib/api/assets"; import { settings } from "@lib/api/settings"; @@ -16,6 +16,7 @@ import { NavigationNative } from "@metro/common"; import { Button, Card, FlashList, IconButton, Text } from "@metro/common/components"; import { ComponentProps } from "react"; import { View } from "react-native"; +import type { BunnyPluginManifest } from "@lib/addons/plugins/types"; import { UnifiedPluginModel } from "./models"; import unifyBunnyPlugin from "./models/bunny"; @@ -60,10 +61,14 @@ export default function Plugins() { useProxy(VdPluginManager.plugins); useObservable([pluginSettings]); + const corePlugins = [...corePluginInstances.keys()] + .map((id) => registeredPlugins.get(id)) + .filter((manifest): manifest is BunnyPluginManifest => manifest !== undefined) + .map(unifyBunnyPlugin); const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin); const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin); - return [...vdPlugins, ...bnPlugins]; + return [... corePlugins, ...vdPlugins, ...bnPlugins]; }} ListHeaderComponent={() => { const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(p => !p.id.startsWith(VD_PROXY_PREFIX) && !p.id.startsWith(BUNNY_PROXY_PREFIX)); From 6cb7f99b608eea874c2db8c71ee0ec37066ec728 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sat, 11 Oct 2025 17:22:52 +0300 Subject: [PATCH 05/68] 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 <CocoboTheOne@gmail.com> Date: Sat, 11 Oct 2025 17:33:43 +0300 Subject: [PATCH 06/68] 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 <CocoboTheOne@gmail.com> Date: Sat, 11 Oct 2025 17:42:58 +0300 Subject: [PATCH 07/68] 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 <CocoboTheOne@gmail.com> Date: Sat, 11 Oct 2025 18:06:31 +0300 Subject: [PATCH 08/68] 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<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} /> - ))} + 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> - )} - </View> - ); + ); } function Description() { - const { plugin, result } = useCardContext(); + const { plugin, result } = useCardContext(); - // could be empty if the description is irrelevant with the search! - const highlightedNode = result[1].highlight((m, i) => ( - <Text key={i} style={{ backgroundColor: getHighlightColor() }}> - {m} - </Text> - )); + // could be empty if the description is irrelevant with the search! + const highlightedNode = result[1].highlight((m, i) => + <Text key={i} style={{ backgroundColor: getHighlightColor() }}>{m}</Text> + ); - return ( - <Text variant="text-md/medium"> - {highlightedNode.length ? highlightedNode : plugin.description} - </Text> - ); + return <Text variant="text-md/medium"> + {highlightedNode.length ? highlightedNode : plugin.description} + </Text>; } const Actions = () => { - const { plugin } = useCardContext(); - const navigation = NavigationNative.useNavigation(); + const { plugin } = useCardContext(); + const navigation = NavigationNative.useNavigation(); - return ( - <View style={{ flexDirection: "row", gap: 6 }}> - <IconButton - size="sm" - variant="secondary" - icon={findAssetId("WrenchIcon")} - disabled={!plugin.getPluginSettingsComponent()} - onPress={() => - navigation.push("PUPU_CUSTOM_PAGE", { - title: plugin.name, - render: plugin.getPluginSettingsComponent(), - }) - } - /> - <IconButton - size="sm" - variant="secondary" - icon={findAssetId("CircleInformationIcon-primary")} - onPress={() => - void showSheet( - "PluginInfoActionSheet", - plugin.resolveSheetComponent(), - { plugin, navigation }, - ) - } - /> - </View> - ); + return <View style={{ flexDirection: "row", gap: 6 }}> + <IconButton + size="sm" + variant="secondary" + icon={findAssetId("WrenchIcon")} + disabled={!plugin.getPluginSettingsComponent()} + onPress={() => navigation.push("PUPU_CUSTOM_PAGE", { + title: plugin.name, + render: plugin.getPluginSettingsComponent(), + })} + /> + <IconButton + size="sm" + variant="secondary" + icon={findAssetId("CircleInformationIcon-primary")} + onPress={() => void showSheet( + "PluginInfoActionSheet", + plugin.resolveSheetComponent(), + { plugin, navigation } + )} + /> + </View>; }; -export default function PluginCard({ - result, - item: plugin, -}: CardWrapper<UnifiedPluginModel>) { - plugin.usePluginState(); +export default function PluginCard({ result, item: plugin }: CardWrapper<UnifiedPluginModel>) { + plugin.usePluginState(); - const [, forceUpdate] = React.useReducer(() => ({}), 0); - const cardContextValue = useMemo( - () => ({ plugin, result }), - [plugin, result], - ); + const [, forceUpdate] = React.useReducer(() => ({}), 0); + const cardContextValue = useMemo(() => ({ plugin, result }), [plugin, result]); - return ( - <CardContext.Provider value={cardContextValue}> - <Card> - <Stack spacing={16}> - <View - style={{ flexDirection: "row", justifyContent: "space-between" }} - > - <View style={{ flexShrink: 1 }}> - <Title /> - <Authors /> - </View> - <View> - <Stack spacing={12} direction="horizontal"> - <Actions /> - <TableSwitch - value={plugin.isEnabled()} - onValueChange={(v: boolean) => { - plugin.toggle(v); - forceUpdate(); - }} - /> - </Stack> - </View> - </View> - <Description /> - </Stack> - </Card> - </CardContext.Provider> - ); + return ( + <CardContext.Provider value={cardContextValue}> + <Card> + <Stack spacing={16}> + <View style={{ flexDirection: "row", justifyContent: "space-between" }}> + <View style={{ flexShrink: 1 }}> + <Title /> + <Authors /> + </View> + <View> + <Stack spacing={12} direction="horizontal"> + <Actions /> + <TableSwitch + value={plugin.isEnabled()} + onValueChange={(v: boolean) => { + plugin.toggle(v); + forceUpdate(); + }} + /> + </Stack> + </View> + </View> + <Description /> + </Stack> + </Card> + </CardContext.Provider> + ); } 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 09/68] 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 10/68] 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 11/68] 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 12/68] 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 13/68] 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 14/68] 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 15/68] 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 16/68] 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 17/68] 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 18/68] 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 19/68] 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 20/68] 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 21/68] 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 22/68] 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 23/68] 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 24/68] 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 25/68] 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 26/68] 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. From 7a685fbf46827de112ece634f21be769af408322 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Wed, 15 Oct 2025 21:08:01 +0300 Subject: [PATCH 27/68] fix(themes): fix themes via fallback and fix past theme regressions --- scripts/build.mjs | 2 +- .../addons/themes/colors/patches/resolver.ts | 31 ++++++++++++++++--- src/lib/addons/themes/colors/updater.ts | 8 +---- src/lib/addons/themes/index.ts | 4 +-- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 787124c..d4a3640 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"v1.3.4"` + version: `"v1.3.5"` } } }, diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index 103f5ce..610e56a 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -22,6 +22,20 @@ const SEMANTIC_FALLBACK_MAP: Record<string, string> = { "BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY" }; +const origRawColor = { ...tokenReference.RawColor }; + +function getRawFallback(key: string): string | undefined { + const isDark = isThemeModule.isThemeDark?.(_colorRef.current?.reference); + + if (!isDark) return undefined; + + const DARK_MODE_RAW_FALLBACK: Record<string, string> = { + "PRIMARY_230": origRawColor.PRIMARY_700 || "#000000" + }; + + return DARK_MODE_RAW_FALLBACK[key]; +} + export default function patchDefinitionAndResolver() { const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0; @@ -31,7 +45,12 @@ export default function patchDefinitionAndResolver() { enumerable: true, get: () => { const ret = _colorRef.current?.raw[key]; - return ret || _colorRef.origRaw[key]; + if (ret) return ret; + + const fallback = getRawFallback(key); + if (fallback) return fallback; + + return origRawColor[key]; } }); }); @@ -64,15 +83,19 @@ export default function patchDefinitionAndResolver() { return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex(); } + const fallbackValue = getRawFallback(colorDef.raw); + if (fallbackValue) { + return colorDef.opacity === 1 ? fallbackValue : chroma(fallbackValue).alpha(colorDef.opacity).hex(); + } + // Fallback to default return orig(...args); }), () => { - // Not the actual module but.. yeah. Object.defineProperty(tokenReference, "RawColor", { configurable: true, writable: true, - value: _colorRef.origRaw + value: origRawColor }); } ]; @@ -86,4 +109,4 @@ function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: const colorDef = tokenReference.SemanticColor[propName]; return [propName, colorDef[themeName]]; -} +} \ No newline at end of file diff --git a/src/lib/addons/themes/colors/updater.ts b/src/lib/addons/themes/colors/updater.ts index b7d0e3f..c7b265b 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, startup = true } ) { +export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true } ) { if (settings.safeMode?.enabled) return; const internalDef = colorManifest ? parseColorManifest(colorManifest) : null; @@ -55,11 +55,5 @@ export function updateBunnyColor(colorManifest: ColorManifest | null, { 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); - } } diff --git a/src/lib/addons/themes/index.ts b/src/lib/addons/themes/index.ts index 6efffa9..546c040 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: false, startup: true }); + updateBunnyColor(null, { update: true }); return writeThemeToNative({}); } else if (theme) { - updateBunnyColor(theme.data, { update: true, startup: false }); + updateBunnyColor(theme.data, { update: true }); return writeThemeToNative(theme); } } From 592f7d744bad58216cb602a31b251bc3bc69eec9 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 17 Oct 2025 21:28:22 +0300 Subject: [PATCH 28/68] fix(themes): fix misscoloured text --- src/core/ui/settings/pages/Plugins/index.tsx | 1 - .../ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx | 2 ++ src/lib/addons/themes/colors/patches/resolver.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 6376e3b..ed001a3 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -22,7 +22,6 @@ import { } from "@metro/common/components"; import { ComponentProps } from "react"; import { View } from "react-native"; -import type { BunnyPluginManifest } from "@lib/addons/plugins/types"; import { UnifiedPluginModel } from "./models"; import unifyBunnyPlugin from "./models/bunny"; diff --git a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx index 670c0f7..b9e9858 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx @@ -41,7 +41,9 @@ export default function PluginInfoActionSheet({ try { const pluginAny = plugin; const repoUrl = + //@ts-expect-error pluginAny._manifest?.parentRepository || + //@ts-expect-error pluginAny.manifest?.parentRepository; url = repoUrl ? `${repoUrl}/builds/${plugin.id}` : plugin.id; } catch (e) { diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index 610e56a..63378e2 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -85,7 +85,7 @@ export default function patchDefinitionAndResolver() { const fallbackValue = getRawFallback(colorDef.raw); if (fallbackValue) { - return colorDef.opacity === 1 ? fallbackValue : chroma(fallbackValue).alpha(colorDef.opacity).hex(); + return colorDef.opacity === 0 ? fallbackValue : chroma(fallbackValue).alpha(colorDef.opacity).hex(); } // Fallback to default From 62de1e67d7249057039954ab55c73adbce34efcc Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 17 Oct 2025 23:09:13 +0300 Subject: [PATCH 29/68] fix(themes): fix themes --- src/lib/addons/themes/colors/patches/background.tsx | 1 - src/lib/addons/themes/colors/patches/resolver.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/addons/themes/colors/patches/background.tsx b/src/lib/addons/themes/colors/patches/background.tsx index 63d4e0f..4c337ca 100644 --- a/src/lib/addons/themes/colors/patches/background.tsx +++ b/src/lib/addons/themes/colors/patches/background.tsx @@ -55,6 +55,5 @@ export default function patchChatBackground() { return <ThemeBackground>{ret}</ThemeBackground>; }) ]; - return () => patches.forEach(x => x()); } diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index 63378e2..64f28d1 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -30,7 +30,7 @@ function getRawFallback(key: string): string | undefined { if (!isDark) return undefined; const DARK_MODE_RAW_FALLBACK: Record<string, string> = { - "PRIMARY_230": origRawColor.PRIMARY_700 || "#000000" + "PRIMARY_230": origRawColor.PRIMARY_230 || "#000000" }; return DARK_MODE_RAW_FALLBACK[key]; From 4f1cdea58f96990d1eb4f6e0fb202d435fa43b4e Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 17 Oct 2025 23:10:06 +0300 Subject: [PATCH 30/68] Revert "fix(themes): fix themes" This reverts commit 62de1e67d7249057039954ab55c73adbce34efcc. --- src/lib/addons/themes/colors/patches/resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index 64f28d1..63378e2 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -30,7 +30,7 @@ function getRawFallback(key: string): string | undefined { if (!isDark) return undefined; const DARK_MODE_RAW_FALLBACK: Record<string, string> = { - "PRIMARY_230": origRawColor.PRIMARY_230 || "#000000" + "PRIMARY_230": origRawColor.PRIMARY_700 || "#000000" }; return DARK_MODE_RAW_FALLBACK[key]; From 64ca5e4647d1fe92bbd18ce875cd59755e18aad7 Mon Sep 17 00:00:00 2001 From: Quinx <quinx@noreply.codeberg.org> Date: Thu, 23 Oct 2025 18:00:02 +0200 Subject: [PATCH 31/68] Update src/core/ui/settings/index.ts --- src/core/ui/settings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ui/settings/index.ts b/src/core/ui/settings/index.ts index a671937..020a79f 100644 --- a/src/core/ui/settings/index.ts +++ b/src/core/ui/settings/index.ts @@ -44,7 +44,7 @@ export default function initSettings() { { key: "KETTU_BROWSER", title: () => Strings.BROWSER, - icon: findAssetId("UploadIcon"), + icon: findAssetId("ChannelListMagnifyingGlassIcon"), render: () => import("@core/ui/settings/pages/PluginBrowser"), }, { From 92450b1edc2cf53fb3ffa2b782395779e5618fa6 Mon Sep 17 00:00:00 2001 From: kmmiio99o <szymonladziak00@gmail.com> Date: Wed, 29 Oct 2025 19:07:54 +0100 Subject: [PATCH 32/68] Give proper credits --- src/core/plugins/messagefix/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/plugins/messagefix/index.ts b/src/core/plugins/messagefix/index.ts index 146bf2b..e77f0b9 100644 --- a/src/core/plugins/messagefix/index.ts +++ b/src/core/plugins/messagefix/index.ts @@ -16,7 +16,7 @@ export default defineCorePlugin({ display: { name: "MessageFix", description: "Ensures messages include the required nonce parameter", - authors: [{ name: "ShiggyCord Team" }], + authors: [ { name: "Win8.1VMUser"}, { name: "kmmiio99o.dev" }], }, }, From 5a6c4e414f15718a3dd6dba6e6014ec58d0518ee Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Thu, 30 Oct 2025 20:07:25 +0100 Subject: [PATCH 33/68] i18n: changed Browser to Addon Browser --- src/core/i18n/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/i18n/default.json b/src/core/i18n/default.json index afd4443..d97d968 100644 --- a/src/core/i18n/default.json +++ b/src/core/i18n/default.json @@ -8,7 +8,7 @@ "BRAND": "Brand", "PUPU": "Kettu", "PUPU_URL": "Kettu URL", - "BROWSER": "Browser", + "BROWSER": "Addon Browser", "BYTECODE": "Bytecode", "CANCEL": "Cancel", "CLEAR": "Clear", From 4f8f27af2947f9804dd369d6971f872f93131991 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sat, 1 Nov 2025 19:09:59 +0200 Subject: [PATCH 34/68] fix(themes): make iOS themes navbar and keyboard work --- package.json | 8 ++--- scripts/build.mjs | 2 +- src/core/plugins/kettuenhancements/index.tsx | 18 ++++++++++ src/lib/addons/themes/colors/fix.ts | 33 ++++++++++++++++--- .../addons/themes/colors/patches/resolver.ts | 2 +- src/lib/api/native/loader.ts | 7 +--- 6 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 src/core/plugins/kettuenhancements/index.tsx diff --git a/package.json b/package.json index da1c39c..498be0a 100644 --- a/package.json +++ b/package.json @@ -43,16 +43,16 @@ "@types/chroma-js": "~2.4.5", "@types/lodash": "~4.17.20", "@types/react": "19.1.8", - "react-native": "0.78.0", "es-toolkit": "^1.39.7", "fuzzysort": "^3.1.0", "intl-messageformat": "^10.7.16", "moment": "2.22.2", + "react-native": "0.78.0", "react-native-reanimated": "^3.18.0", "spitroast": "^1.4.4", "type-fest": "^4.41.0" }, - "trustedDependencies": [ - "@swc/core" - ] + "trustedDependencies": [ + "@swc/core" + ] } \ No newline at end of file diff --git a/scripts/build.mjs b/scripts/build.mjs index d4a3640..43283cc 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"v1.3.5"` + version: `"v1.3.6"` } } }, diff --git a/src/core/plugins/kettuenhancements/index.tsx b/src/core/plugins/kettuenhancements/index.tsx new file mode 100644 index 0000000..b3cd75a --- /dev/null +++ b/src/core/plugins/kettuenhancements/index.tsx @@ -0,0 +1,18 @@ +import { defineCorePlugin } from ".."; + +export default defineCorePlugin({ + manifest: { + id: "bunny.enhancements", + version: "0.1.0", + type: "plugin", + spec: 3, + main: "", + display: { + name: "Kettu Enhancements", + description: "Fixes common discord bugs because discord wont", + authors: [{ name: "cocobo1"}] + } + }, + start() { + } +}); \ No newline at end of file diff --git a/src/lib/addons/themes/colors/fix.ts b/src/lib/addons/themes/colors/fix.ts index 9099c07..45192e8 100644 --- a/src/lib/addons/themes/colors/fix.ts +++ b/src/lib/addons/themes/colors/fix.ts @@ -1,4 +1,4 @@ -import { StatusBar } from "react-native"; +import { StatusBar, TextInput, Platform } from "react-native"; import { findByStoreNameLazy } from "@metro/wrappers"; import { _colorRef } from "./updater"; @@ -28,11 +28,36 @@ function getBarStyle() { return "dark-content"; } + export default function fixStatusBar() { + function applyStatusBar() { + const style = getBarStyle(); + StatusBar.setBarStyle(style, true); + } + + const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar); + const origSetBarStyle = StatusBar.setBarStyle; - StatusBar.setBarStyle = function (_style: any, ...args: any[]) { - return origSetBarStyle.call(this, getBarStyle(), ...args); + StatusBar.setBarStyle = function (_style: any, animated?: boolean) { + return origSetBarStyle.call(this, getBarStyle(), animated ?? true); }; - StatusBar.setBarStyle(getBarStyle()); + if( Platform.OS === "ios" ){ + const origSetNativeProps = (TextInput.prototype as any).setNativeProps; + (TextInput.prototype as any).setNativeProps = function (props: any) { + const style = getBarStyle(); + const keyboardAppearance = style === "light-content" ? "dark" : "light"; + return origSetNativeProps?.call(this, { + ...props, + keyboardAppearance: props?.keyboardAppearance ?? keyboardAppearance + }); + }; + + let delay = 200; + for (let i = 0; i < 5; i++, delay *= 2) { + setTimeout(applyStatusBar, delay); + } + } + + return unsubscribe; } \ No newline at end of file diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index 63378e2..08a63b5 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -30,7 +30,7 @@ function getRawFallback(key: string): string | undefined { if (!isDark) return undefined; const DARK_MODE_RAW_FALLBACK: Record<string, string> = { - "PRIMARY_230": origRawColor.PRIMARY_700 || "#000000" + "PRIMARY_230": origRawColor.PLUM_19 || "#000000" }; return DARK_MODE_RAW_FALLBACK[key]; diff --git a/src/lib/api/native/loader.ts b/src/lib/api/native/loader.ts index 6575625..e38b405 100644 --- a/src/lib/api/native/loader.ts +++ b/src/lib/api/native/loader.ts @@ -216,12 +216,7 @@ export function getReactDevToolsVersion() { } export function isSysColorsSupported() { - if (isPyonLoader()) return pyonLoaderIdentity.isSysColorsSupported; - else if (isVendettaLoader()) { - return vendettaLoaderIdentity!!.features.syscolors != null; - } - - return false; + return true; } export function getSysColors() { From a159e0035ea3db65e077ffe8ec406c30a21fd5ba Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sat, 1 Nov 2025 20:09:47 +0200 Subject: [PATCH 35/68] fix: temporary fix for themes by removing the keyboard fix --- src/lib/addons/themes/colors/fix.ts | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/lib/addons/themes/colors/fix.ts b/src/lib/addons/themes/colors/fix.ts index 45192e8..2cbdd70 100644 --- a/src/lib/addons/themes/colors/fix.ts +++ b/src/lib/addons/themes/colors/fix.ts @@ -28,7 +28,6 @@ function getBarStyle() { return "dark-content"; } - export default function fixStatusBar() { function applyStatusBar() { const style = getBarStyle(); @@ -38,25 +37,15 @@ export default function fixStatusBar() { const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar); const origSetBarStyle = StatusBar.setBarStyle; - StatusBar.setBarStyle = function (_style: any, animated?: boolean) { - return origSetBarStyle.call(this, getBarStyle(), animated ?? true); + StatusBar.setBarStyle = function (_style: any, ...args: any[]) { + return origSetBarStyle.call(this, getBarStyle(), ...args); }; + + StatusBar.setBarStyle(getBarStyle()); - if( Platform.OS === "ios" ){ - const origSetNativeProps = (TextInput.prototype as any).setNativeProps; - (TextInput.prototype as any).setNativeProps = function (props: any) { - const style = getBarStyle(); - const keyboardAppearance = style === "light-content" ? "dark" : "light"; - return origSetNativeProps?.call(this, { - ...props, - keyboardAppearance: props?.keyboardAppearance ?? keyboardAppearance - }); - }; - - let delay = 200; - for (let i = 0; i < 5; i++, delay *= 2) { - setTimeout(applyStatusBar, delay); - } + let delay = 200; + for (let i = 0; i < 5; i++, delay *= 2) { + setTimeout(applyStatusBar, delay); } return unsubscribe; From 7afde3a1d5e30654e201620b6d293c9c6717e458 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sat, 1 Nov 2025 22:35:17 +0200 Subject: [PATCH 36/68] fix(themes): fix keyboard on iOS when using themes --- src/lib/addons/themes/colors/index.ts | 4 +- src/lib/addons/themes/colors/keyboard.ts | 64 +++++++++++++++++++ .../themes/colors/{fix.ts => statusbar.ts} | 37 ++++++----- 3 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 src/lib/addons/themes/colors/keyboard.ts rename src/lib/addons/themes/colors/{fix.ts => statusbar.ts} (59%) diff --git a/src/lib/addons/themes/colors/index.ts b/src/lib/addons/themes/colors/index.ts index 8c59795..60a3fc8 100644 --- a/src/lib/addons/themes/colors/index.ts +++ b/src/lib/addons/themes/colors/index.ts @@ -1,4 +1,5 @@ -import fixStatusBar from "./fix"; +import fixStatusBar from "./statusbar"; +import fixKeyboard from "./keyboard"; import patchChatBackground from "./patches/background"; import patchDefinitionAndResolver from "./patches/resolver"; import patchStorage from "./patches/storage"; @@ -14,6 +15,7 @@ export default function initColors(manifest: ColorManifest | null) { ]; fixStatusBar(); + fixKeyboard(); if (manifest) updateBunnyColor(manifest, { update: false }); diff --git a/src/lib/addons/themes/colors/keyboard.ts b/src/lib/addons/themes/colors/keyboard.ts new file mode 100644 index 0000000..47d819e --- /dev/null +++ b/src/lib/addons/themes/colors/keyboard.ts @@ -0,0 +1,64 @@ +import { TextInput, Platform, NativeModules } from "react-native"; +import { findByStoreNameLazy } from "@metro/wrappers"; +import { _colorRef } from "./updater"; + +const ThemeStore = findByStoreNameLazy("ThemeStore"); + +function getKeyboardAppearance() { + // Check for custom theme + const custom = _colorRef.current; + if (custom) { + // Bunny theme (spec 3) + if (custom.spec === 3 && custom.reference) { + return custom.reference === "light" ? "light" : "dark"; + } + // Vendetta theme (spec 2) + if (custom.spec === 2 && custom.reference) { + return custom.reference === "light" ? "light" : "dark"; + } + } + + // Fallback to Discord theme string + const theme = ThemeStore?.theme; + if (typeof theme === "string") { + const lowerTheme = theme.toLowerCase(); + return (lowerTheme.includes("dark") || lowerTheme.includes("midnight")) + ? "dark" + : "light"; + } + + return "dark"; +} + +export default function fixKeyboard() { + if (Platform.OS !== "ios") return; + + const UIManager = NativeModules.UIManager; + + const origUpdateView = UIManager?.updateView; + const origCreateView = UIManager?.createView; + + if (origUpdateView) { + UIManager.updateView = function(viewTag: number, viewName: string, props: any) { + if (props && typeof props === 'object') { + if (viewName.toLowerCase().includes('text') || viewName.toLowerCase().includes('input')) { + props.keyboardAppearance = getKeyboardAppearance(); + } + } + return origUpdateView.call(this, viewTag, viewName, props); + }; + } + + if (origCreateView) { + UIManager.createView = function(reactTag: number, viewName: string, rootTag: number, props: any) { + if (props && typeof props === 'object') { + if (viewName.toLowerCase().includes('text') || viewName.toLowerCase().includes('input')) { + props.keyboardAppearance = getKeyboardAppearance(); + } + } + return origCreateView.call(this, reactTag, viewName, rootTag, props); + }; + } + + return ThemeStore?.addChangeListener?.(() => {}); +} \ No newline at end of file diff --git a/src/lib/addons/themes/colors/fix.ts b/src/lib/addons/themes/colors/statusbar.ts similarity index 59% rename from src/lib/addons/themes/colors/fix.ts rename to src/lib/addons/themes/colors/statusbar.ts index 2cbdd70..f57c5f7 100644 --- a/src/lib/addons/themes/colors/fix.ts +++ b/src/lib/addons/themes/colors/statusbar.ts @@ -29,24 +29,29 @@ function getBarStyle() { } export default function fixStatusBar() { - function applyStatusBar() { - const style = getBarStyle(); - StatusBar.setBarStyle(style, true); + + if (Platform.OS == "android") { + const origSetBarStyle = StatusBar.setBarStyle; + StatusBar.setBarStyle = function (_style: any, ...args: any[]) { + return origSetBarStyle.call(this, getBarStyle(), ...args); + }; } - const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar); - - const origSetBarStyle = StatusBar.setBarStyle; - StatusBar.setBarStyle = function (_style: any, ...args: any[]) { - return origSetBarStyle.call(this, getBarStyle(), ...args); - }; + if (Platform.OS == "ios") { + function applyStatusBar() { + const style = getBarStyle(); + StatusBar.setBarStyle(style, true); + } - StatusBar.setBarStyle(getBarStyle()); - - let delay = 200; - for (let i = 0; i < 5; i++, delay *= 2) { - setTimeout(applyStatusBar, delay); + const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar); + + StatusBar.setBarStyle(getBarStyle()); + + let delay = 200; + for (let i = 0; i < 5; i++, delay *= 2) { + setTimeout(applyStatusBar, delay); + } + + return unsubscribe; } - - return unsubscribe; } \ No newline at end of file From f0b750a2ab94578887924e0aa0c45765e19c54a1 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 2 Nov 2025 12:52:26 +0200 Subject: [PATCH 37/68] readme: add KettuTweak --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 76ef8e4..7c9a9fa 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A mod for Discord on Android - **Non-root** - [KettuManager](https://github.com/C0C0B01/KettuManager/releases/latest) ### iOS +- [KettuTweak](https://github.com/C0C0B01/KettuTweak) - [BTLoader](https://github.com/CloudySn0w/BTLoader) ## Building From 187dfc74ffaff295c31f6933b89f6b9deda4a19b Mon Sep 17 00:00:00 2001 From: Bwlok <bwlok.t@gmail.com> Date: Mon, 3 Nov 2025 22:02:48 +0100 Subject: [PATCH 38/68] fix(dev): Fix React Devtools & split input section --- src/core/i18n/default.json | 1 + .../ui/settings/pages/Developer/index.tsx | 95 ++++++++++++++----- src/lib/api/settings.ts | 1 + 3 files changed, 72 insertions(+), 25 deletions(-) diff --git a/src/core/i18n/default.json b/src/core/i18n/default.json index d97d968..3ab3350 100644 --- a/src/core/i18n/default.json +++ b/src/core/i18n/default.json @@ -35,6 +35,7 @@ "COPY_URL": "Copy URL", "DEBUG": "Debug", "DEBUGGER_URL": "Debugger URL", + "DEVTOOLS_URL": "DevTools URL", "DELETE": "Delete", "DESC_EXTRACT_FONTS_FROM_THEME": "Looks out for \"fonts\" field in your currently applied theme and install it.", "DEVELOPER": "Developer", diff --git a/src/core/ui/settings/pages/Developer/index.tsx b/src/core/ui/settings/pages/Developer/index.tsx index 6f66a81..788f0b1 100644 --- a/src/core/ui/settings/pages/Developer/index.tsx +++ b/src/core/ui/settings/pages/Developer/index.tsx @@ -15,13 +15,14 @@ import { ErrorBoundary } from "@ui/components"; import { createStyles, TextStyleSheet } from "@ui/styles"; import { NativeModules } from "react-native"; import { ScrollView, StyleSheet } from "react-native"; +import { showToast } from "@ui/toasts"; const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet")); const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet")); const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert")); const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions")); -const RDT_EMBED_LINK = ""; +const RDT_EMBED_LINK = "https://cdn.bwlok.dev/rdt/devTools.js"; const useStyles = createStyles({ leadingText: { @@ -44,31 +45,75 @@ export default function Developer() { <ErrorBoundary> <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}> <Stack style={{ paddingVertical: 24, paddingHorizontal: 12 }} spacing={24}> - <TextInput - label={Strings.DEBUGGER_URL} - placeholder="127.0.0.1:9090" - size="md" - leadingIcon={() => <LegacyFormText style={styles.leadingText}>ws://</LegacyFormText>} - defaultValue={settings.debuggerUrl} - onChange={(v: string) => settings.debuggerUrl = v} - /> - <TableRowGroup title={Strings.DEBUG}> - <TableRow - label={Strings.CONNECT_TO_DEBUG_WEBSOCKET} - icon={<TableRow.Icon source={findAssetId("copy")} />} - onPress={() => connectToDebugger(settings.debuggerUrl)} - /> - {isReactDevToolsPreloaded() && <> - <TableRow - label={Strings.CONNECT_TO_REACT_DEVTOOLS} - icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />} - onPress={() => window[getReactDevToolsProp() || "__vendetta_rdc"]?.connectToDevTools({ - host: settings.debuggerUrl.split(":")?.[0], - resolveRNStyle: StyleSheet.flatten, - })} - /> - </>} + + <TableRowGroup title={Strings.DEBUGGER_URL}> + <TextInput + placeholder="127.0.0.1:9090" + size="md" + leadingIcon={() => <LegacyFormText style={styles.leadingText}>ws://</LegacyFormText>} + defaultValue={settings.debuggerUrl} + onChange={(v: string) => settings.debuggerUrl = v} + /> + <Stack style={{ + marginTop: 4, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + overflow: 'hidden' + }}> + <TableRow + label={Strings.CONNECT_TO_DEBUG_WEBSOCKET} + icon={<TableRow.Icon source={findAssetId("copy")} />} + onPress={() => connectToDebugger(settings.debuggerUrl)} + /> + </Stack> + </TableRowGroup> + {isReactDevToolsPreloaded() && <> + <TableRowGroup title={Strings.DEVTOOLS_URL}> + + <TextInput + placeholder="127.0.0.1:8097" + size="md" + leadingIcon={() => <LegacyFormText style={styles.leadingText}>ws://</LegacyFormText>} + defaultValue={settings.devToolsUrl} + onChange={(v: string) => settings.devToolsUrl = v} + /> + <Stack style={{ + marginTop: 4, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + overflow: 'hidden' + }}> + <TableRow + label={Strings.CONNECT_TO_REACT_DEVTOOLS} + icon={<TableRow.Icon source={findAssetId("ic_badge_staff")} />} + onPress={async () => { + if (!settings.devToolsUrl?.trim()) { + showToast("Invalid devTools URL!", findAssetId("Small")); + return; + } + + try { + const devTools = window[getReactDevToolsProp() || "__vendetta_rdc"]; + + if (!devTools?.connectToDevTools) { + showToast("Invalid devTools URL!", findAssetId("Small")); + return; + } + + await devTools.connectToDevTools({ + host: settings.devToolsUrl.split(":")?.[0], + resolveRNStyle: StyleSheet.flatten, + }); + } catch (error) { + showToast("Invalid devTools URL!", findAssetId("Small")); + } + }} + + /> + </Stack> + </TableRowGroup> + </>} {isLoaderConfigSupported() && <> <TableRowGroup title="Loader config"> <TableSwitchRow diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index d222a5e..85e5eeb 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -3,6 +3,7 @@ import { getLoaderConfigPath } from "@lib/api/native/loader"; export interface Settings { debuggerUrl: string; + devToolsUrl: string; developerSettings: boolean; enableDiscordDeveloperSettings: boolean; safeMode?: { From 11d3553525cadd7f8910fa9585a0d50c4f401eca Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Tue, 4 Nov 2025 08:14:44 +0100 Subject: [PATCH 39/68] redma: update KettuTweak infomation --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76ef8e4..0854838 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,14 @@ A mod for Discord on Android - **Root** with Xposed - [KettuXposed](https://github.com/C0C0B01/KettuXposed/releases/latest) - **Non-root** - [KettuManager](https://github.com/C0C0B01/KettuManager/releases/latest) +If you do not know what root is, go with the manager + ### iOS -- [BTLoader](https://github.com/CloudySn0w/BTLoader) + +- ** Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) +- **Jailed** - [BTLoader](https://github.com/CloudySn0w/BTLoader) + +if you dont know the difference, go with BTLoader ## Building 1. Install a Kettu loader with loader config support (any mentioned in the [Installing](#installing) section). From 396394fc8ccca6674794f91184f4c6775a6633f5 Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Tue, 4 Nov 2025 08:39:14 +0100 Subject: [PATCH 40/68] readme: update markdown --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0854838..e910881 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ If you do not know what root is, go with the manager ### iOS -- ** Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) +- **Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) - **Jailed** - [BTLoader](https://github.com/CloudySn0w/BTLoader) if you dont know the difference, go with BTLoader From 0983f7ea25826a032717607cd0d6f8487bf39ea4 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Tue, 4 Nov 2025 18:53:02 +0200 Subject: [PATCH 41/68] readme: make language clearer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e910881..116de2d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ If you do not know what root is, go with the manager - **Jailbroken** - [KettuTweak](https://github.com/C0C0B01/KettuTweak) - **Jailed** - [BTLoader](https://github.com/CloudySn0w/BTLoader) -if you dont know the difference, go with BTLoader +if you dont know what jailbreak is, go with BTLoader ## Building 1. Install a Kettu loader with loader config support (any mentioned in the [Installing](#installing) section). From 241ffd5b5130e48d3bbe5680536646fe80285df2 Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Tue, 4 Nov 2025 18:04:39 +0100 Subject: [PATCH 42/68] readme: update platforms --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 116de2d..633cb8b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Kettu -A mod for Discord on Android +A mod for Discord on Mobile ### [![Discord](https://img.shields.io/discord/1368145952266911755?logo=discord&logoColor=%23ffffffff&color=%231D88CF&link=https%3A%2F%2Fdiscord.gg%2F6cN7wKa8gp)](https://discord.gg/6cN7wKa8gp) [![Static Badge](https://img.shields.io/badge/kofi-cocobo1-%23FF6433?style=flat&logo=ko-fi&labelColor=%23ffffff)](https://www.ko-fi.com/cocobo1) ![Codeberg Last Commit](https://img.shields.io/gitea/last-commit/cocobo1/Kettu?gitea_url=https%3A%2F%2Fwww.codeberg.org&logo=codeberg&logoColor=%23ffffffff) From ddcb1d66a80658cf2868e407bfc470ffe3d38bdc Mon Sep 17 00:00:00 2001 From: Bwlok <bwlok.t@gmail.com> Date: Thu, 6 Nov 2025 23:48:54 +0100 Subject: [PATCH 43/68] feat(Themes): add compatibility with Enmity themes --- src/lib/addons/themes/colors/parser.ts | 95 +++++++++++++++++++++++++- src/lib/addons/themes/colors/types.ts | 2 +- src/lib/addons/themes/index.ts | 17 +++-- 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/src/lib/addons/themes/colors/parser.ts b/src/lib/addons/themes/colors/parser.ts index e7e5c40..e2d7ced 100644 --- a/src/lib/addons/themes/colors/parser.ts +++ b/src/lib/addons/themes/colors/parser.ts @@ -90,7 +90,6 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini manifest.rawColors = draft; } - return { spec: 2, reference: resolveType(), @@ -100,9 +99,97 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini }; } + if (manifest.spec === 1 || (manifest.theme_color_map && !manifest.spec)) { + + const semanticDefinitions: InternalColorDefinition["semantic"] = {}; + const rawDefinitions: Record<string, string> = {}; + + const themeIndex = resolveType() === "light" ? 1 : 0; + + if (manifest.theme_color_map) { + for (const [key, colorArray] of Object.entries(manifest.theme_color_map)) { + if (!Array.isArray(colorArray) || colorArray.length < 2) { + console.warn(`[Parser] Invalid color array for ${key}:`, colorArray); + continue; + } + + const colorValue = colorArray[themeIndex]; + + if (!colorValue) { + console.warn(`[Parser] Missing color value for ${key} at index ${themeIndex}`); + continue; + } + + const normalized = normalizeToHex(colorValue); + if (normalized) { + semanticDefinitions[key] = { + value: normalized, + opacity: 1 + }; + } + } + } + + const rawColors = manifest.colours || manifest.colors; + if (rawColors) { + for (const [key, colorValue] of Object.entries(rawColors)) { + if (typeof colorValue !== "string") continue; + + const normalized = normalizeToHex(colorValue); + if (normalized) { + const discordKey = convertEnmityToDiscordRawKey(key); + rawDefinitions[discordKey] = normalized; + + if (discordKey !== key) { + rawDefinitions[key] = normalized; + } + } + } + } + + if (manifest.unsafe_colors) { + for (const [key, colorValue] of Object.entries(manifest.unsafe_colors)) { + if (typeof colorValue !== "string") continue; + + const normalized = normalizeToHex(colorValue); + if (normalized) { + rawDefinitions[key] = normalized; + } + } + } + + if (Platform.OS === "android") applyAndroidAlphaKeys(rawDefinitions); + + return { + spec: 2, + reference: resolveType(), + semantic: semanticDefinitions, + raw: rawDefinitions, + background: manifest.background + }; + } throw new Error("Invalid theme spec"); } + function convertEnmityToDiscordRawKey(enmityKey: string): string { + const conversions: Record<string, string> = { + "PRIMARY_DARK": "PRIMARY_100", + "PRIMARY_DARK_100": "PRIMARY_100", + "PRIMARY_DARK_200": "PRIMARY_200", + "PRIMARY_DARK_300": "PRIMARY_300", + "PRIMARY_DARK_360": "PRIMARY_360", + "PRIMARY_DARK_400": "PRIMARY_400", + "PRIMARY_DARK_500": "PRIMARY_500", + "PRIMARY_DARK_600": "PRIMARY_600", + "PRIMARY_DARK_630": "PRIMARY_630", + "PRIMARY_DARK_700": "PRIMARY_700", + "PRIMARY_DARK_800": "PRIMARY_800", + "PRIMARY_DARK_900": "PRIMARY_900", + }; + + return conversions[enmityKey] || enmityKey; + } + export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) { if (!rawColors) return; @@ -129,6 +216,11 @@ export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) { export function normalizeToHex(colorString: string | undefined): string | undefined { if (colorString === undefined) return undefined; + + if (colorString.toLowerCase() === "transparent") { + return "#00000000"; + } + if (chroma.valid(colorString)) return chroma(colorString).hex(); const color = Number(processColor(colorString)); @@ -140,4 +232,3 @@ export function normalizeToHex(colorString: string | undefined): string | undefi color >> 24 & 0xff // alpha ).hex(); } - diff --git a/src/lib/addons/themes/colors/types.ts b/src/lib/addons/themes/colors/types.ts index 99b6e39..203f3c0 100644 --- a/src/lib/addons/themes/colors/types.ts +++ b/src/lib/addons/themes/colors/types.ts @@ -39,7 +39,7 @@ export interface VendettaThemeManifest { /** @internal */ export interface InternalColorDefinition { - spec: 2 | 3; + spec: 1 | 2 | 3; reference: "darker" | "light"; semantic: Record<string, { value: string; diff --git a/src/lib/addons/themes/index.ts b/src/lib/addons/themes/index.ts index 546c040..aaedeb4 100644 --- a/src/lib/addons/themes/index.ts +++ b/src/lib/addons/themes/index.ts @@ -53,19 +53,26 @@ function processData(data: VendettaThemeManifest) { if (Platform.OS === "android") applyAndroidAlphaKeys(rawColors); } - // this field is required by the spec but vd seems to ignore this - // so are most vd themes - data.spec ??= 2; + if (data.spec === undefined) { + if (!('theme_color_map' in data)) { + data.spec = 2; + } + //If not then Enmity theme and no spec + } return data; } function validateTheme(themeJSON: any): boolean { if (typeof themeJSON !== "object" || themeJSON === null) return false; - if (themeJSON.spec !== 2 && themeJSON.spec !== 3) return false; + if (themeJSON.spec === 3 && !themeJSON.main) return false; - return true; + if (themeJSON.spec === 2) return true; + + if (themeJSON.theme_color_map) return true; + + return themeJSON.spec === 2 || themeJSON.spec === 3; } export async function fetchTheme(url: string, selected = false) { From 0bdb90ef3d800d70de858eb27c7eca60f100f413 Mon Sep 17 00:00:00 2001 From: Bwlok <bwlok.t@gmail.com> Date: Fri, 7 Nov 2025 06:55:56 +0100 Subject: [PATCH 44/68] Improve AssetBrowser displaying --- .../settings/pages/Developer/AssetBrowser.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx index b1bdcbb..bc91c95 100644 --- a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx +++ b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx @@ -3,7 +3,7 @@ import { iterateAssets } from "@lib/api/assets"; import { Text } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; import { useMemo } from "react"; -import { FlatList, View } from "react-native"; +import { FlatList, View, ScrollView } from "react-native"; export default function AssetBrowser() { const [search, setSearch] = React.useState(""); @@ -16,15 +16,18 @@ export default function AssetBrowser() { style={{ margin: 10 }} onChangeText={(v: string) => setSearch(v)} /> - <View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}> - <Text variant='text-sm/medium' color='text-danger' style={{ marginBottom: 16 }}>Some assets types cannot be displayed and will be marked in red.</Text> - <FlatList - data={all.filter(a => a.name.includes(search) || a.id.toString() === search)} - renderItem={({ item }: any) => <AssetDisplay asset={item} />} - contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }} - keyExtractor={a => a.name} - /> - </View> + <ScrollView> + <View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}> + <Text variant='text-sm/medium' color='text-danger' style={{ marginBottom: 16 }}>Some assets types cannot be displayed and will be marked in red.</Text> + <FlatList + data={all.filter(a => a.name.includes(search) || a.id.toString() === search)} + renderItem={({ item }: any) => <AssetDisplay asset={item} />} + contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }} + scrollEnabled={false} + keyExtractor={a => a.id.toString()} + /> + </View> + </ScrollView> </View> </ErrorBoundary> ); From f80888f8afa15d0797805becdf4a652fcdbd02b1 Mon Sep 17 00:00:00 2001 From: Bwlok <bwlok.t@gmail.com> Date: Fri, 7 Nov 2025 06:57:46 +0100 Subject: [PATCH 45/68] feat(Dev): Add option to filter NonImage Assets --- .../settings/pages/Developer/AssetBrowser.tsx | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx index bc91c95..785cb78 100644 --- a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx +++ b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx @@ -1,27 +1,64 @@ import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay"; -import { iterateAssets } from "@lib/api/assets"; +import { iterateAssets, findAssetId } from "@lib/api/assets"; import { Text } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; import { useMemo } from "react"; -import { FlatList, View, ScrollView } from "react-native"; +import { FlatList, View, ScrollView, TouchableOpacity, Image } from "react-native"; + +const displayable = new Set(['png', 'jpg', 'svg']); export default function AssetBrowser() { const [search, setSearch] = React.useState(""); + const [showNonImages, setShowNonImages] = React.useState(false); + const all = useMemo(() => Array.from(iterateAssets()), []); + const filteredData = useMemo(() => { + let result = all.filter(a => a.name.includes(search) || a.id.toString() === search); + + if (!showNonImages) { + result = result.filter(a => displayable.has(a.type)); + } + + return result; + }, [all, search, showNonImages]); + + return ( <ErrorBoundary> <View style={{ flex: 1 }}> - <Search - style={{ margin: 10 }} - onChangeText={(v: string) => setSearch(v)} - /> + <View style={{ flexDirection: 'row', alignItems: 'center', margin: 10 }}> + <View style={{ flex: 1, marginRight: 10 }}> + <Search + onChangeText={(v: string) => setSearch(v)} + /> + </View> + <TouchableOpacity + style={{ + padding: 12, + backgroundColor: showNonImages ? '#0f1013' : '#303139', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + minWidth: 44, + minHeight: 44, + }} + onPress={() => setShowNonImages(!showNonImages)} + > + <Image + style={{ width: 20, height: 20 }} + source={findAssetId("ic_image")!} + /> + </TouchableOpacity> + </View> <ScrollView> <View style={{ flex: 1, borderRadius: 16, paddingHorizontal: 12, overflow: 'hidden', backgroundColor: 'transparent' }}> - <Text variant='text-sm/medium' color='text-danger' style={{ marginBottom: 16 }}>Some assets types cannot be displayed and will be marked in red.</Text> + {showNonImages && ( + <Text variant='text-sm/medium' color='text-danger' style={{ marginBottom: 16 }}>Some assets types cannot be displayed and will be marked in red.</Text> + )} <FlatList - data={all.filter(a => a.name.includes(search) || a.id.toString() === search)} - renderItem={({ item }: any) => <AssetDisplay asset={item} />} + data={filteredData} + renderItem={({ item }: any) => <AssetDisplay asset={item} showNonImages={showNonImages} />} contentContainerStyle={{ overflow: 'hidden', backgroundColor: 'transparent', borderRadius: 16 }} scrollEnabled={false} keyExtractor={a => a.id.toString()} From fef67515352543494230f886f0ad3db60a45891f Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 7 Nov 2025 22:12:36 +0200 Subject: [PATCH 46/68] modules: rudimentry bubblechat support --- src/index.ts | 2 - src/lib/api/native/modules/bubblemodule.ts | 16 ++++ src/lib/api/native/modules/index.ts | 3 + src/lib/api/native/modules/loader.ts | 82 ++++++++++++++++++ src/lib/api/native/modules/types.ts | 39 +++++++++ src/metro/internals/modules.ts | 97 ++++++++++++++++++++++ 6 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 src/lib/api/native/modules/bubblemodule.ts create mode 100644 src/lib/api/native/modules/loader.ts diff --git a/src/index.ts b/src/index.ts index 341b32f..ec767b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,8 +19,6 @@ import * as lib from "./lib"; export default async () => { // Load everything in parallel - updatePlugins(); - await Promise.all([ initThemes(), injectFluxInterceptor(), diff --git a/src/lib/api/native/modules/bubblemodule.ts b/src/lib/api/native/modules/bubblemodule.ts new file mode 100644 index 0000000..8b84133 --- /dev/null +++ b/src/lib/api/native/modules/bubblemodule.ts @@ -0,0 +1,16 @@ +import { processColor } from "react-native"; +import { getModule } from "./loader"; +import type { LoaderModule } from "./loader"; + +interface BubbleModule extends LoaderModule { + hookBubbles(): Promise<void>; + unhookBubbles(): Promise<void>; + configure(avatarRadius?: number, bubbleRadius?: number, bubbleColor?: string): Promise<void>; +} + +export default getModule<BubbleModule>({ + name: "BubbleModule", + argumentProcessors: { + configure: args => [args[0], args[1], Number(processColor(args[2]))], + }, +}); \ No newline at end of file diff --git a/src/lib/api/native/modules/index.ts b/src/lib/api/native/modules/index.ts index d6919cc..2c63415 100644 --- a/src/lib/api/native/modules/index.ts +++ b/src/lib/api/native/modules/index.ts @@ -33,3 +33,6 @@ export const NativeThemeModule = getNativeModule( export const BundleUpdaterManager = getNativeModule( "BundleUpdaterManager" )!; +export const ImageLoader = getNativeModule( + "ImageLoader" +)!; \ No newline at end of file diff --git a/src/lib/api/native/modules/loader.ts b/src/lib/api/native/modules/loader.ts new file mode 100644 index 0000000..7a6fd65 --- /dev/null +++ b/src/lib/api/native/modules/loader.ts @@ -0,0 +1,82 @@ +import { ImageLoader } from "./"; +import type { LoaderPayload } from "./types"; + +type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>; + +export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload; + +export function isModuleRegistered(module: string) { + return loaderPayload.loader.modules[module] !== undefined; +} + +export function isFunctionRegistered(module: string, function_: string) { + return loaderPayload.loader.modules[module]?.functions[function_] !== undefined; +} + +export async function callFunction(module: string, functionName: string, args: unknown[]) { + if (!isFunctionRegistered(module, functionName)) { + throw new Error(`Function ${module}.${functionName} is not registered`); + } + + const promise: InteropReturn = ImageLoader.queryCache([ + "__pyon_bridge", + JSON.stringify({ m: module, f: functionName, a: args }), + ]); + + const result = await promise; + + if ("ret" in result) { + return JSON.parse(result.ret); + } + + if ("cancelled" in result) { + throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`); + } + + if ("err" in result) { + throw new Error(result.err); + } +} + +export interface LoaderModule { + hasFunction(functionName: string): boolean; + callFunction(functionName: string, args: unknown[]): Promise<unknown>; +} + +type ModuleProps<T extends LoaderModule> = { + name: string; + argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>; +}; + +export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) { + const module = loaderPayload.loader.modules[name]; + if (!module) { + throw new Error(`Module ${name} is not registered`); + } + + const baseObject: Record<string, any> = { + hasFunction: (functionName: string) => isFunctionRegistered(name, functionName), + callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args), + }; + + for (const [functionName] of Object.entries(module.functions)) { + baseObject[functionName] = (...args: unknown[]) => + callFunction( + name, + functionName, + // @ts-expect-error + processArgs && functionName in processArgs ? processArgs[functionName](args) : args, + ); + } + + return baseObject as T; +} + +/** + * Determines if safe mode is currently enabled. The result remains consistent over time. + * This will not change until the app is restarted. For draft changes, use the InitConfigStore. + * @returns True if safe mode is enabled for the current instance, otherwise false. + */ +export function isSafeModeEnabled() { + return Boolean(loaderPayload.loader.initConfig.safeMode); +} \ No newline at end of file diff --git a/src/lib/api/native/modules/types.ts b/src/lib/api/native/modules/types.ts index fdb3a32..cf54956 100644 --- a/src/lib/api/native/modules/types.ts +++ b/src/lib/api/native/modules/types.ts @@ -134,3 +134,42 @@ export namespace RNModules { } } } + +export interface InitConfig { + baseUrl: string; + forceUpdate: boolean; + skipUpdate: boolean; + bundlePath: string | null; + safeMode: boolean; +} + +interface LoaderConstants { + WINTRY_DIR: string; + DEFAULT_BASE_URL: string; +} + +interface LoaderModule { + functions: { + [functionName: string]: number; + }; + constants: { + [constantName: string]: any; + }; +} + +interface LoaderModules { + [moduleName: string]: LoaderModule; +} + +export interface LoaderPayload { + bundle: { + revision: string; + }; + loader: { + name: string; + version: string; + initConfig: InitConfig; + constants: LoaderConstants; + modules: LoaderModules; + }; +} \ No newline at end of file diff --git a/src/metro/internals/modules.ts b/src/metro/internals/modules.ts index 964b116..81aa485 100644 --- a/src/metro/internals/modules.ts +++ b/src/metro/internals/modules.ts @@ -238,3 +238,100 @@ export function* getCachedPolyfillModules(name: string) { } } } + +export interface WaitForOptions { + count?: number; +} + +export type ModuleFilter<T = any> = { + (exports: any): T | undefined; + key?: string; +} + +export function waitFor<T = any>( + filter: ModuleFilter<T>, + callback: (exports: T, id: Metro.ModuleID) => void, + options: WaitForOptions = {} +): () => void { + const { count = 1 } = options; + let currentCount = 0; + const unsubscribers: Array<() => void> = []; + let isActive = true; + + const cleanup = () => { + if (!isActive) return; + isActive = false; + unsubscribers.forEach(unsub => unsub()); + unsubscribers.length = 0; + }; + + function checkModule(id: Metro.ModuleID): boolean { + if (!isActive) return true; + + const exports = requireModule(id); + if (isBadExports(exports)) return false; + + const result = filter(exports); + if (!result) return false; + + callback(result, id); + + if (++currentCount >= count) { + cleanup(); + return true; + } + + return false; + } + + if (filter.key) { + const cache = getMetroCache().findIndex[filter.key]; + if (cache) { + for (const id in cache) { + if (id[0] === "_") continue; + const numId = Number(id); + + if (metroModules[numId]?.isInitialized) { + if (checkModule(numId)) return cleanup; + } else { + const unsub = subscribeModule(numId, () => { + checkModule(numId); + }); + unsubscribers.push(unsub); + } + } + } + } + + for (const id in metroModules) { + if (!isActive) break; + const numId = Number(id); + + if (metroModules[numId]?.isInitialized && !metroModules[numId]?.hasError) { + if (checkModule(numId)) return cleanup; + } + } + + if (isActive) { + for (const id in metroModules) { + const numId = Number(id); + if (!metroModules[numId]?.isInitialized) { + const unsub = subscribeModule(numId, () => { + checkModule(numId); + }); + unsubscribers.push(unsub); + } + } + } + + return cleanup; +} + +export function waitForModule<T = any>( + filter: ModuleFilter<T>, + options: WaitForOptions = {} +): Promise<T> { + return new Promise((resolve) => { + waitFor(filter, (exports) => resolve(exports), options); + }); +} From 95c82214c6553a24db7c236285017421dfa7c35a Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 7 Nov 2025 22:29:13 +0200 Subject: [PATCH 47/68] chore: bump version --- scripts/build.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 43283cc..4c16f2e 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"v1.3.6"` + version: `"v1.3.7"` } } }, From af1718b71e7c6f70e96fa07ab1e6a22f386931cb Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Mon, 10 Nov 2025 22:12:37 +0100 Subject: [PATCH 48/68] plugins: added fix to certain plugins that go against tos --- src/lib/addons/plugins/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index 762e0d9..45a3eae 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -392,6 +392,10 @@ export async function initPlugins() { // Now, start all enabled plugins... Promise.allSettled([...registeredPlugins.keys()].map(async id => { if (isPluginEnabled(id)) { + if (id === "FakeMessageComposer") { + console.log("FakeMessageComposer is broken"); + return; + } startPlugin(id); } })); From 9fdb57ed31ad8c9490a2688832885f9dce4bef0e Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Mon, 10 Nov 2025 22:18:56 +0100 Subject: [PATCH 49/68] Update src/lib/addons/plugins/index.ts --- src/lib/addons/plugins/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index 45a3eae..a7a372d 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -392,6 +392,7 @@ export async function initPlugins() { // Now, start all enabled plugins... Promise.allSettled([...registeredPlugins.keys()].map(async id => { if (isPluginEnabled(id)) { + alert(id) if (id === "FakeMessageComposer") { console.log("FakeMessageComposer is broken"); return; From bbdc32cd84236c398ca3f7a7873fa13f41260844 Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Mon, 10 Nov 2025 22:21:51 +0100 Subject: [PATCH 50/68] Update src/lib/addons/plugins/index.ts --- 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 a7a372d..b614182 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -392,11 +392,11 @@ export async function initPlugins() { // Now, start all enabled plugins... Promise.allSettled([...registeredPlugins.keys()].map(async id => { if (isPluginEnabled(id)) { - alert(id) if (id === "FakeMessageComposer") { console.log("FakeMessageComposer is broken"); return; } + alert(id) startPlugin(id); } })); From bf271980d8ed3c4b4708f39a407a28d4e243ed06 Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Mon, 10 Nov 2025 22:28:20 +0100 Subject: [PATCH 51/68] Update src/lib/addons/plugins/index.ts --- 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 b614182..8415706 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -396,7 +396,7 @@ export async function initPlugins() { console.log("FakeMessageComposer is broken"); return; } - alert(id) + startPlugin(id); } })); From 4502b6688022b2fe5ba1870bb2bc5e8afa93a6fe Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Mon, 10 Nov 2025 22:42:29 +0100 Subject: [PATCH 52/68] Update src/lib/addons/plugins/index.ts --- src/lib/addons/plugins/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index 8415706..6846b23 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -324,6 +324,7 @@ export async function startPlugin(id: string, { throwIfDisabled = false, disable // Stage three (of external plugins), start the plugin try { + alert(id) pluginInstance.start?.(); pluginSettings[id]!.enabled = true; } catch (error) { From d3b6e9fb637b949062d888c9fde681f76119c902 Mon Sep 17 00:00:00 2001 From: cocobo1 <cocobo1@noreply.codeberg.org> Date: Mon, 10 Nov 2025 22:45:22 +0100 Subject: [PATCH 53/68] Update src/lib/addons/plugins/index.ts --- 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 6846b23..1fc7fed 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -324,7 +324,7 @@ export async function startPlugin(id: string, { throwIfDisabled = false, disable // Stage three (of external plugins), start the plugin try { - alert(id) + pluginInstance.start?.(); pluginSettings[id]!.enabled = true; } catch (error) { From 261326ff22aa4213d7f406661065001ea9f99bf2 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Tue, 11 Nov 2025 21:44:53 +0200 Subject: [PATCH 54/68] feat(quickinstall): changed repos to kettu and (unrelated) added a check for malicous plugins --- src/core/plugins/quickinstall/forumPost.tsx | 8 ++++---- src/core/vendetta/plugins.ts | 7 ++++++- src/lib/addons/plugins/index.ts | 7 +------ src/lib/addons/plugins/types.ts | 1 + src/lib/utils/constants.ts | 5 ++++- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/core/plugins/quickinstall/forumPost.tsx b/src/core/plugins/quickinstall/forumPost.tsx index 25665ad..9cf8da4 100644 --- a/src/core/plugins/quickinstall/forumPost.tsx +++ b/src/core/plugins/quickinstall/forumPost.tsx @@ -5,7 +5,7 @@ import { installTheme, removeTheme, themes } from "@lib/addons/themes"; import { findAssetId } from "@lib/api/assets"; import { isThemeSupported } from "@lib/api/native/loader"; import { after } from "@lib/api/patcher"; -import { HTTP_REGEX_MULTI, VD_DISCORD_SERVER_ID, VD_PLUGINS_CHANNEL_ID, VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants"; +import { HTTP_REGEX_MULTI, KETTU_DISCORD_SERVER_ID, KETTU_PLUGINS_CHANNEL_ID, KETTU_THEMES_CHANNEL_ID, VD_DISCORD_SERVER_ID, VD_PLUGINS_CHANNEL_ID, VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants"; import { lazyDestructure } from "@lib/utils/lazy"; import { Button } from "@metro/common/components"; import { findByProps, findByPropsLazy } from "@metro/wrappers"; @@ -39,13 +39,13 @@ const postMap = { }; function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet = false): ([PostType, string]) | void { - if (thread.guild_id !== VD_DISCORD_SERVER_ID) return; + if (thread.guild_id !== KETTU_DISCORD_SERVER_ID) return; // Determine what type of addon this is. let postType: PostType; - if (thread.parent_id === VD_PLUGINS_CHANNEL_ID) { + if (thread.parent_id === KETTU_PLUGINS_CHANNEL_ID) { postType = "Plugin"; - } else if (thread.parent_id === VD_THEMES_CHANNEL_ID && isThemeSupported()) { + } else if (thread.parent_id === KETTU_THEMES_CHANNEL_ID && isThemeSupported()) { postType = "Theme"; } else return; diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts index e8c065c..e47f335 100644 --- a/src/core/vendetta/plugins.ts +++ b/src/core/vendetta/plugins.ts @@ -110,6 +110,11 @@ export const VdPluginManager = { async startPlugin(id: string) { if (!id.endsWith("/")) id += "/"; const plugin = plugins[id]; + // this is to counter a plugin that violates TOS, a true reason will be provided as an error soon but some trolling is fun :3 + if (plugin.id === "https://heartdevv.github.io/plugin-template/template" || plugin.id === "https://heartdevv.github.io/plugin-template/template/") { + alert(`The plugin '${plugin.id}' is broken, please contact the developer to fix it'`); + return + } if (!plugin) throw new Error("Attempted to start non-existent plugin"); try { @@ -182,4 +187,4 @@ export const VdPluginManager = { }, getSettings: (id: string) => pluginInstance[id]?.settings -}; +}; \ No newline at end of file diff --git a/src/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index 1fc7fed..366d9be 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -393,12 +393,7 @@ export async function initPlugins() { // Now, start all enabled plugins... Promise.allSettled([...registeredPlugins.keys()].map(async id => { if (isPluginEnabled(id)) { - if (id === "FakeMessageComposer") { - console.log("FakeMessageComposer is broken"); - return; - } - startPlugin(id); } })); -} +} \ No newline at end of file diff --git a/src/lib/addons/plugins/types.ts b/src/lib/addons/plugins/types.ts index 0b6baea..17894ed 100644 --- a/src/lib/addons/plugins/types.ts +++ b/src/lib/addons/plugins/types.ts @@ -24,6 +24,7 @@ export interface PluginSettingsStorage { } export interface BunnyPluginManifest extends BunnyManifest { + name: string; readonly type: "plugin"; readonly spec: 3; readonly main: string; diff --git a/src/lib/utils/constants.ts b/src/lib/utils/constants.ts index 834756d..2cdd23b 100644 --- a/src/lib/utils/constants.ts +++ b/src/lib/utils/constants.ts @@ -8,9 +8,12 @@ export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy"; export const NEXPID_PLUGINS_REPO_URL = "https://revenge.nexpid.xyz"; //export const OFFICIAL_PLUGINS_REPO_URL = "https://bn-plugins.github.io/dist/repo.json"; -export const OFFICIAL_PLUGINS_REPO_URL = "https://raw.githubusercontent.com/Purple-EyeZ/Plugins-List/refs/heads/main/src/plugins-data.json"; +export const OFFICIAL_PLUGINS_REPO_URL = ""; export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy"; export const VD_DISCORD_SERVER_ID = "1015931589865246730"; export const VD_PLUGINS_CHANNEL_ID = "1091880384561684561"; export const VD_THEMES_CHANNEL_ID = "1091880434939482202"; +export const KETTU_DISCORD_SERVER_ID = "1368145952266911755"; +export const KETTU_PLUGINS_CHANNEL_ID = "1432796541210333284"; +export const KETTU_THEMES_CHANNEL_ID = "1408405946434195540"; \ No newline at end of file From 145a74cba543778a0bbaf96478cddbc45795b3d4 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Tue, 11 Nov 2025 21:46:57 +0200 Subject: [PATCH 55/68] chore: correct spelling --- src/core/vendetta/plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts index e47f335..8fc7d9f 100644 --- a/src/core/vendetta/plugins.ts +++ b/src/core/vendetta/plugins.ts @@ -112,7 +112,7 @@ export const VdPluginManager = { const plugin = plugins[id]; // this is to counter a plugin that violates TOS, a true reason will be provided as an error soon but some trolling is fun :3 if (plugin.id === "https://heartdevv.github.io/plugin-template/template" || plugin.id === "https://heartdevv.github.io/plugin-template/template/") { - alert(`The plugin '${plugin.id}' is broken, please contact the developer to fix it'`); + alert(`The plugin '${plugin.id}' is broken, please contact the developer to fix it`); return } if (!plugin) throw new Error("Attempted to start non-existent plugin"); From 1baece6f632e1d7491a50e2cb1f2390e4ed0bf46 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Wed, 12 Nov 2025 17:54:00 +0200 Subject: [PATCH 56/68] fix(plugins): update list of plugins blocked in vendetta --- src/core/vendetta/plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts index 8fc7d9f..b6d380a 100644 --- a/src/core/vendetta/plugins.ts +++ b/src/core/vendetta/plugins.ts @@ -111,7 +111,7 @@ export const VdPluginManager = { if (!id.endsWith("/")) id += "/"; const plugin = plugins[id]; // this is to counter a plugin that violates TOS, a true reason will be provided as an error soon but some trolling is fun :3 - if (plugin.id === "https://heartdevv.github.io/plugin-template/template" || plugin.id === "https://heartdevv.github.io/plugin-template/template/") { + if (plugin.id.includes("heartdevv") == true) { alert(`The plugin '${plugin.id}' is broken, please contact the developer to fix it`); return } From a6564e20a4fe4601b1a219d2dd8ca5eca2a913dd Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Thu, 13 Nov 2025 18:03:15 +0200 Subject: [PATCH 57/68] fix(plugins): fix vendetta plugin list --- src/core/vendetta/plugins.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts index b6d380a..f95f0f2 100644 --- a/src/core/vendetta/plugins.ts +++ b/src/core/vendetta/plugins.ts @@ -110,11 +110,9 @@ export const VdPluginManager = { async startPlugin(id: string) { if (!id.endsWith("/")) id += "/"; const plugin = plugins[id]; - // this is to counter a plugin that violates TOS, a true reason will be provided as an error soon but some trolling is fun :3 - if (plugin.id.includes("heartdevv") == true) { - alert(`The plugin '${plugin.id}' is broken, please contact the developer to fix it`); - return - } + if (plugin.id.includes("heartdevv") == true || plugin.id.includes("helloworldsmother") == true || plugin.id.includes("messaging") == true) { + return + } if (!plugin) throw new Error("Attempted to start non-existent plugin"); try { From 022ee030d6e1cf533032770794bc78747db4cc6d Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 14 Nov 2025 18:34:32 +0200 Subject: [PATCH 58/68] settings: fix settings ui --- src/lib/ui/settings/patches/tabs.tsx | 33 +++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/lib/ui/settings/patches/tabs.tsx b/src/lib/ui/settings/patches/tabs.tsx index afe93b5..04d341d 100644 --- a/src/lib/ui/settings/patches/tabs.tsx +++ b/src/lib/ui/settings/patches/tabs.tsx @@ -80,19 +80,26 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) { }); }); - unpatches.push(after("default", SettingsOverviewScreen, (_, ret) => { - if (useIsFirstRender()) return; // :shrug: - - const { sections } = findInReactTree(ret, i => i.props?.sections).props; - // Credit to @palmdevs - https://discord.com/channels/1196075698301968455/1243605828783571024/1307940348378742816 - let index = -~sections.findIndex((i: any) => i.settings.includes("ACCOUNT")) || 1; +const createListModule = findByPropsLazy("createList"); +unpatches.push(after("createList", createListModule, function(args, ret) { + const [config] = args; + + if (config?.sections && Array.isArray(config.sections)) { + const sections = config.sections; + let index = -~sections.findIndex((i: any) => i.settings?.includes("ACCOUNT")) || 1; + Object.keys(registeredSections).forEach(sect => { - sections.splice(index++, 0, { - label: sect, - title: sect, - settings: registeredSections[sect].map(a => a.key) - }); + const alreadyExists = sections.some((s: any) => s.label === sect); + if (!alreadyExists) { + sections.splice(index++, 0, { + label: sect, + title: sect, + settings: registeredSections[sect].map(a => a.key) + }); + } }); - })); -} + } + + return ret; +}))}; \ No newline at end of file From fb229d832ee56bd55d0e0eff48ac5e85c2a701e5 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 14 Nov 2025 18:38:19 +0200 Subject: [PATCH 59/68] comment: add back credit to comment --- src/lib/ui/settings/patches/tabs.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/ui/settings/patches/tabs.tsx b/src/lib/ui/settings/patches/tabs.tsx index 04d341d..12d5f18 100644 --- a/src/lib/ui/settings/patches/tabs.tsx +++ b/src/lib/ui/settings/patches/tabs.tsx @@ -87,6 +87,7 @@ unpatches.push(after("createList", createListModule, function(args, ret) { if (config?.sections && Array.isArray(config.sections)) { const sections = config.sections; + // Credit to @palmdevs - https://discord.com/channels/1196075698301968455/1243605828783571024/1307940348378742816 let index = -~sections.findIndex((i: any) => i.settings?.includes("ACCOUNT")) || 1; Object.keys(registeredSections).forEach(sect => { From a75690e0f444c55901138765b31dc98050ff4f1d Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 14 Nov 2025 18:39:31 +0200 Subject: [PATCH 60/68] cleanup(settings): make code look nicer --- src/lib/ui/settings/patches/tabs.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/lib/ui/settings/patches/tabs.tsx b/src/lib/ui/settings/patches/tabs.tsx index 12d5f18..af43b18 100644 --- a/src/lib/ui/settings/patches/tabs.tsx +++ b/src/lib/ui/settings/patches/tabs.tsx @@ -1,15 +1,12 @@ import { after } from "@lib/api/patcher"; -import { findInReactTree } from "@lib/utils"; import { TableRow } from "@metro/common/components"; -import { findByNameLazy, findByPropsLazy } from "@metro/wrappers"; +import { findByPropsLazy } from "@metro/wrappers"; import { registeredSections } from "@ui/settings"; import { CustomPageRenderer, wrapOnPress } from "./shared"; -import { Strings } from "@core/i18n"; -import { TableRowIcon } from "@metro/common/components"; const settingConstants = findByPropsLazy("SETTING_RENDERER_CONFIG"); -const SettingsOverviewScreen = findByNameLazy("SettingsOverviewScreen", false); +const createListModule = findByPropsLazy("createList"); function useIsFirstRender() { let firstRender = false; @@ -80,9 +77,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) { }); }); -const createListModule = findByPropsLazy("createList"); - -unpatches.push(after("createList", createListModule, function(args, ret) { + unpatches.push(after("createList", createListModule, function(args, ret) { const [config] = args; if (config?.sections && Array.isArray(config.sections)) { From a57d4933e49cc02552d75dc7def7f1b2dd6e47a9 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 14 Nov 2025 19:19:37 +0200 Subject: [PATCH 61/68] fix(settings): correct settings not loading on non-alpha installs --- src/index.ts | 1 + src/lib/ui/settings/patches/tabs.tsx | 56 +++++++++++++++++++--------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index ec767b7..4d1db27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import { injectFluxInterceptor } from "@lib/api/flux"; import { patchJsx } from "@lib/api/react/jsx"; import { logger } from "@lib/utils/logger"; import { patchSettings } from "@ui/settings"; +import { getDebugInfo } from "@lib/api/debug"; import * as lib from "./lib"; diff --git a/src/lib/ui/settings/patches/tabs.tsx b/src/lib/ui/settings/patches/tabs.tsx index af43b18..5d9c029 100644 --- a/src/lib/ui/settings/patches/tabs.tsx +++ b/src/lib/ui/settings/patches/tabs.tsx @@ -1,12 +1,14 @@ import { after } from "@lib/api/patcher"; import { TableRow } from "@metro/common/components"; -import { findByPropsLazy } from "@metro/wrappers"; +import { findByNameLazy, findByPropsLazy } from "@metro/wrappers"; import { registeredSections } from "@ui/settings"; import { CustomPageRenderer, wrapOnPress } from "./shared"; +import { findInReactTree } from "@lib/utils"; const settingConstants = findByPropsLazy("SETTING_RENDERER_CONFIG"); const createListModule = findByPropsLazy("createList"); +const SettingsOverviewScreen = findByNameLazy("SettingsOverviewScreen", false); function useIsFirstRender() { let firstRender = false; @@ -77,25 +79,43 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) { }); }); + try{ unpatches.push(after("createList", createListModule, function(args, ret) { - const [config] = args; - - if (config?.sections && Array.isArray(config.sections)) { - const sections = config.sections; - // Credit to @palmdevs - https://discord.com/channels/1196075698301968455/1243605828783571024/1307940348378742816 - let index = -~sections.findIndex((i: any) => i.settings?.includes("ACCOUNT")) || 1; + const [config] = args; + if (config?.sections && Array.isArray(config.sections)) { + const sections = config.sections; + // Credit to @palmdevs - https://discord.com/channels/1196075698301968455/1243605828783571024/1307940348378742816 + let index = -~sections.findIndex((i: any) => i.settings?.includes("ACCOUNT")) || 1; + + Object.keys(registeredSections).forEach(sect => { + const alreadyExists = sections.some((s: any) => s.label === sect); + if (!alreadyExists) { + sections.splice(index++, 0, { + label: sect, + title: sect, + settings: registeredSections[sect].map(a => a.key) + }); + } + }); + } + return ret; + },)); + }catch{ + unpatches.push(after("default", SettingsOverviewScreen, (_, ret) => { + if (useIsFirstRender()) return; // :shrug: + + const { sections } = findInReactTree(ret, i => i.props?.sections).props; + // Credit to @palmdevs - https://discord.com/channels/1196075698301968455/1243605828783571024/1307940348378742816 + let index = -~sections.findIndex((i: any) => i.settings.includes("ACCOUNT")) || 1; + Object.keys(registeredSections).forEach(sect => { - const alreadyExists = sections.some((s: any) => s.label === sect); - if (!alreadyExists) { - sections.splice(index++, 0, { - label: sect, - title: sect, - settings: registeredSections[sect].map(a => a.key) - }); - } + sections.splice(index++, 0, { + label: sect, + title: sect, + settings: registeredSections[sect].map(a => a.key) + }); }); + })); } - - return ret; -}))}; \ No newline at end of file +}; \ No newline at end of file From 4392b81822ef123a7a51a4d81a9dd7f0fc2e18eb Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 14 Nov 2025 19:57:56 +0200 Subject: [PATCH 62/68] fix(settings): make settings not appear in EVERY menu --- src/lib/ui/settings/patches/tabs.tsx | 41 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/lib/ui/settings/patches/tabs.tsx b/src/lib/ui/settings/patches/tabs.tsx index 5d9c029..9623034 100644 --- a/src/lib/ui/settings/patches/tabs.tsx +++ b/src/lib/ui/settings/patches/tabs.tsx @@ -80,27 +80,34 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) { }); try{ - unpatches.push(after("createList", createListModule, function(args, ret) { - const [config] = args; + unpatches.push(after("createList", createListModule, function(args, ret) { + const [config] = args; - if (config?.sections && Array.isArray(config.sections)) { - const sections = config.sections; - // Credit to @palmdevs - https://discord.com/channels/1196075698301968455/1243605828783571024/1307940348378742816 - let index = -~sections.findIndex((i: any) => i.settings?.includes("ACCOUNT")) || 1; + if (config?.sections && Array.isArray(config.sections)) { + const sections = config.sections; + + const accountSectionIndex = sections.findIndex((i: any) => i.settings?.includes("ACCOUNT")); + + if (accountSectionIndex !== -1) { + // Credit to @palmdevs - https://discord.com/channels/1196075698301968455/1243605828783571024/1307940348378742816 - Object.keys(registeredSections).forEach(sect => { - const alreadyExists = sections.some((s: any) => s.label === sect); - if (!alreadyExists) { - sections.splice(index++, 0, { - label: sect, - title: sect, - settings: registeredSections[sect].map(a => a.key) + let index = accountSectionIndex + 1; + + Object.keys(registeredSections).forEach(sect => { + const alreadyExists = sections.some((s: any) => s.label === sect); + if (!alreadyExists) { + sections.splice(index++, 0, { + label: sect, + title: sect, + settings: registeredSections[sect].map(a => a.key) + }); + } }); } - }); - } - return ret; - },)); + } + return ret; + },)); + }catch{ unpatches.push(after("default", SettingsOverviewScreen, (_, ret) => { if (useIsFirstRender()) return; // :shrug: From 0bfc83b4d97f2b26c579889d88e3816de1bcb41b Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Fri, 14 Nov 2025 22:35:29 +0200 Subject: [PATCH 63/68] block(plugins): block plugins that violate TOS --- src/core/vendetta/plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts index f95f0f2..aa72a90 100644 --- a/src/core/vendetta/plugins.ts +++ b/src/core/vendetta/plugins.ts @@ -110,7 +110,7 @@ export const VdPluginManager = { async startPlugin(id: string) { if (!id.endsWith("/")) id += "/"; const plugin = plugins[id]; - if (plugin.id.includes("heartdevv") == true || plugin.id.includes("helloworldsmother") == true || plugin.id.includes("messaging") == true) { + if (plugin.id.includes("xxjust") == true || plugin.id.includes("DevNjay") == true) { return } if (!plugin) throw new Error("Attempted to start non-existent plugin"); From b7f0a710d3b3675e0efbb18b7b72bc92f3bf0b5d Mon Sep 17 00:00:00 2001 From: Quinx <quinx@noreply.codeberg.org> Date: Sun, 16 Nov 2025 11:30:03 +0100 Subject: [PATCH 64/68] Update src/core/ui/settings/index.ts --- src/core/ui/settings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ui/settings/index.ts b/src/core/ui/settings/index.ts index 020a79f..494e9cd 100644 --- a/src/core/ui/settings/index.ts +++ b/src/core/ui/settings/index.ts @@ -37,7 +37,7 @@ export default function initSettings() { { key: "BUNNY_FONTS", title: () => Strings.FONTS, - icon: findAssetId("ic_add_text"), + icon: findAssetId("LettersIcon"), render: () => import("@core/ui/settings/pages/Fonts"), usePredicate: () => isFontSupported() }, From f79e6936ba17291f6b2cbf2a9fcad74cb084ba7c Mon Sep 17 00:00:00 2001 From: Quinx <quinx@noreply.codeberg.org> Date: Sun, 16 Nov 2025 11:45:05 +0100 Subject: [PATCH 65/68] Update src/core/ui/components/AddonPage.tsx --- src/core/ui/components/AddonPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ui/components/AddonPage.tsx b/src/core/ui/components/AddonPage.tsx index 8d624a5..35a5dcc 100644 --- a/src/core/ui/components/AddonPage.tsx +++ b/src/core/ui/components/AddonPage.tsx @@ -175,7 +175,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }: <View style={{ flexDirection: "row", gap: 8 }}> <Search style={{ flexGrow: 1 }} isRound={!!props.sortOptions} onChangeText={v => setSearch(v)} /> {props.sortOptions && <IconButton - icon={findAssetId("MoreVerticalIcon")} + icon={findAssetId("ArrowsUpDownIcon")} variant="tertiary" disabled={!!search} onPress={() => showSimpleActionSheet({ From 01b36e797876ae10de30ac274d91c83b42d8daab Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 16 Nov 2025 19:40:27 +0200 Subject: [PATCH 66/68] fix(statusbar): make the statusbar work on ios + greatly improve loadtimes --- src/lib/addons/plugins/types.ts | 1 - src/lib/addons/themes/colors/statusbar.ts | 21 ++++++--------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/lib/addons/plugins/types.ts b/src/lib/addons/plugins/types.ts index 17894ed..0b6baea 100644 --- a/src/lib/addons/plugins/types.ts +++ b/src/lib/addons/plugins/types.ts @@ -24,7 +24,6 @@ export interface PluginSettingsStorage { } export interface BunnyPluginManifest extends BunnyManifest { - name: string; readonly type: "plugin"; readonly spec: 3; readonly main: string; diff --git a/src/lib/addons/themes/colors/statusbar.ts b/src/lib/addons/themes/colors/statusbar.ts index f57c5f7..3f5f655 100644 --- a/src/lib/addons/themes/colors/statusbar.ts +++ b/src/lib/addons/themes/colors/statusbar.ts @@ -29,29 +29,20 @@ function getBarStyle() { } export default function fixStatusBar() { - + const origSetBarStyle = StatusBar.setBarStyle; if (Platform.OS == "android") { - const origSetBarStyle = StatusBar.setBarStyle; StatusBar.setBarStyle = function (_style: any, ...args: any[]) { return origSetBarStyle.call(this, getBarStyle(), ...args); }; } - - if (Platform.OS == "ios") { + + if (Platform.OS === "ios") { function applyStatusBar() { const style = getBarStyle(); - StatusBar.setBarStyle(style, true); + origSetBarStyle.call(StatusBar, style, true); } - - const unsubscribe = ThemeStore?.addChangeListener?.(applyStatusBar); - StatusBar.setBarStyle(getBarStyle()); - - let delay = 200; - for (let i = 0; i < 5; i++, delay *= 2) { - setTimeout(applyStatusBar, delay); - } - - return unsubscribe; + applyStatusBar(); + setInterval(applyStatusBar, 500); } } \ No newline at end of file From f5ddffdd7babdfa18215e9beed33c323ec471a05 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Sun, 16 Nov 2025 19:56:05 +0200 Subject: [PATCH 67/68] feat(speedup): massively speedup launch times --- scripts/build.mjs | 2 +- src/entry.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 4c16f2e..899ab6b 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"v1.3.7"` + version: `"v1.4.0-alpha"` } } }, diff --git a/src/entry.ts b/src/entry.ts index c0f7e4a..5e67916 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -11,7 +11,8 @@ async function initializeKettu() { Object.freeze = Object.seal = Object; await require("@metro/internals/caches").initMetroCache(); - await require(".").default(); + // "savage" approach of just nuking the await, but the speedup could be amazing + require(".").default(); } catch (e) { const { ClientInfoManager } = require("@lib/api/native/modules"); const stack = e instanceof Error ? e.stack : undefined; From 85af53e537789e3fab6d1a60f41e471fe31f5f74 Mon Sep 17 00:00:00 2001 From: cocobo1 <CocoboTheOne@gmail.com> Date: Mon, 17 Nov 2025 14:28:28 +0200 Subject: [PATCH 68/68] fix(fonts): remove font previews to fix crashes --- scripts/build.mjs | 2 +- shims/depsModule.ts | 1 - src/core/ui/settings/pages/Fonts/FontCard.tsx | 45 ------------------- src/entry.ts | 1 - 4 files changed, 1 insertion(+), 48 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index 899ab6b..5574a91 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"v1.4.0-alpha"` + version: `"v1.3.8"` } } }, diff --git a/shims/depsModule.ts b/shims/depsModule.ts index 8df1d12..cccc4d5 100644 --- a/shims/depsModule.ts +++ b/shims/depsModule.ts @@ -7,5 +7,4 @@ module.exports = { "moment": findByPropsLazy("isMoment"), "chroma-js": findByPropsLazy("brewer"), "lodash": findByPropsLazy("forEachRight"), - "@shopify/react-native-skia": findByPropsLazy("useFont") }; diff --git a/src/core/ui/settings/pages/Fonts/FontCard.tsx b/src/core/ui/settings/pages/Fonts/FontCard.tsx index bc350b8..dec6441 100644 --- a/src/core/ui/settings/pages/Fonts/FontCard.tsx +++ b/src/core/ui/settings/pages/Fonts/FontCard.tsx @@ -9,56 +9,12 @@ import { lazyDestructure } from "@lib/utils/lazy"; import { findByProps } from "@metro"; import { NavigationNative, tokens } from "@metro/common"; import { Button, Card, IconButton, Stack, Text } from "@metro/common/components"; -import * as Skia from "@shopify/react-native-skia"; -import { TextStyleSheet } from "@ui/styles"; -import { useMemo } from "react"; import { View } from "react-native"; import FontEditor from "./FontEditor"; const { useToken } = lazyDestructure(() => findByProps("useToken")); -function FontPreview({ font }: { font: FontDefinition; }) { - const TEXT_NORMAL = useToken(tokens.colors.TEXT_NORMAL); - const { fontFamily: fontFamilyList, fontSize } = TextStyleSheet["text-md/medium"]; - const fontFamily = fontFamilyList!.split(/,/g)[0]; - - const typeface = Skia.useFont(font.main[fontFamily])?.getTypeface(); - - const paragraph = useMemo(() => { - if (!typeface) return null; - - const fMgr = SkiaApi.TypefaceFontProvider.Make(); - fMgr.registerFont(typeface, fontFamily); - - - return SkiaApi.ParagraphBuilder.Make({}, fMgr) - .pushStyle({ - color: SkiaApi.Color(TEXT_NORMAL), - fontFamilies: [fontFamily], - fontSize, - }) - .addText("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") - .pop() - .build(); - }, [typeface]); - - return ( - // This does not work, actually :woeis: - <View style={{ height: 64 }}> - {typeface - ? <Skia.Canvas style={{ height: 64 }}> - <Skia.Paragraph paragraph={paragraph} x={0} y={0} width={300} /> - </Skia.Canvas> - : <View style={{ justifyContent: "center", alignItems: "center" }}> - <Text color="text-muted" variant="heading-lg/semibold"> - Loading... - </Text> - </View>} - </View> - ); -} - export default function FontCard({ item: font }: CardWrapper<FontDefinition>) { useProxy(fonts); @@ -111,7 +67,6 @@ export default function FontCard({ item: font }: CardWrapper<FontDefinition>) { </Stack> </View> </View> - <FontPreview font={font} /> </Stack> </Card> ); diff --git a/src/entry.ts b/src/entry.ts index 5e67916..5202d28 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -11,7 +11,6 @@ async function initializeKettu() { Object.freeze = Object.seal = Object; await require("@metro/internals/caches").initMetroCache(); - // "savage" approach of just nuking the await, but the speedup could be amazing require(".").default(); } catch (e) { const { ClientInfoManager } = require("@lib/api/native/modules");