chore: merge upstream/main to dev
This commit is contained in:
commit
6205c54bb8
78 changed files with 7201 additions and 2839 deletions
|
|
@ -32,6 +32,7 @@
|
|||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gullerya/object-observer": "^6.1.3",
|
||||
"@shopify/react-native-skia": "^1.3.8",
|
||||
"@tanstack/react-query": "^5.51.16",
|
||||
"@types/chroma-js": "~2.4.4",
|
||||
|
|
|
|||
8067
pnpm-lock.yaml
generated
8067
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,10 @@ import { findByPropsLazy } from "@metro/wrappers";
|
|||
const jsxRuntime = findByPropsLazy("jsx", "jsxs", "Fragment");
|
||||
|
||||
function unproxyFirstArg<T>(args: T[]) {
|
||||
if (!args[0]) {
|
||||
throw new Error("The first argument (Component) is falsy. Ensure that you are passing a valid component.");
|
||||
}
|
||||
|
||||
const factory = getProxyFactory(args[0]);
|
||||
if (factory) args[0] = factory();
|
||||
return args;
|
||||
|
|
|
|||
61
src/core/plugins/badges/index.tsx
Normal file
61
src/core/plugins/badges/index.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { after } from "@lib/api/patcher";
|
||||
import { onJsxCreate } from "@lib/api/react/jsx";
|
||||
import { findByName } from "@metro";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { defineCorePlugin } from "..";
|
||||
|
||||
interface BunnyBadge {
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const useBadgesModule = findByName("useBadges", false);
|
||||
|
||||
export default defineCorePlugin({
|
||||
manifest: {
|
||||
id: "bunny.badges",
|
||||
name: "Badges",
|
||||
version: "1.0.0",
|
||||
description: "Adds badges to user's profile",
|
||||
authors: [{ name: "pylixonly" }]
|
||||
},
|
||||
start() {
|
||||
const propHolder = {} as Record<string, any>;
|
||||
const badgeCache = {} as Record<string, BunnyBadge[]>;
|
||||
|
||||
onJsxCreate("RenderedBadge", (_, ret) => {
|
||||
if (ret.props.id.match(/bunny-\d+-\d+/)) {
|
||||
Object.assign(ret.props, propHolder[ret.props.id]);
|
||||
}
|
||||
});
|
||||
|
||||
after("default", useBadgesModule, ([user], r) => {
|
||||
const [badges, setBadges] = useState<BunnyBadge[]>(user ? badgeCache[user.userId] ??= [] : []);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetch(`https://raw.githubusercontent.com/pyoncord/badges/refs/heads/main/${user.userId}.json`)
|
||||
.then(r => r.json())
|
||||
.then(badges => setBadges(badgeCache[user.userId] = badges));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
if (user) {
|
||||
badges.forEach((badges, i) => {
|
||||
propHolder[`bunny-${user.userId}-${i}`] = {
|
||||
source: { uri: badges.url },
|
||||
id: `bunny-${i}`,
|
||||
label: badges.label
|
||||
};
|
||||
|
||||
r.push({
|
||||
id: `bunny-${user.userId}-${i}`,
|
||||
description: badges.label,
|
||||
icon: "_",
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { PluginInstanceInternal } from "@lib/plugins/types";
|
||||
import { PluginInstanceInternal } from "@lib/addons/plugins/types";
|
||||
|
||||
interface CorePlugin {
|
||||
default: PluginInstanceInternal;
|
||||
|
|
@ -7,7 +7,8 @@ interface CorePlugin {
|
|||
|
||||
// Called from @lib/plugins
|
||||
export const getCorePlugins = (): Record<string, CorePlugin> => ({
|
||||
"bunny.quickinstall": require("./quickinstall")
|
||||
"bunny.quickinstall": require("./quickinstall"),
|
||||
"bunny.badges": require("./badges")
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
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 { useProxy } from "@lib/api/storage";
|
||||
import { installTheme, removeTheme, themes } from "@lib/themes";
|
||||
import { DISCORD_SERVER_ID, HTTP_REGEX_MULTI, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { HTTP_REGEX_MULTI, VD_DISCORD_SERVER_ID, VD_PLUGINS_CHANNEL_ID, VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { 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 !== DISCORD_SERVER_ID) return;
|
||||
if (thread.guild_id !== VD_DISCORD_SERVER_ID) return;
|
||||
|
||||
// Determine what type of addon this is.
|
||||
let postType: PostType;
|
||||
if (thread.parent_id === PLUGINS_CHANNEL_ID) {
|
||||
if (thread.parent_id === VD_PLUGINS_CHANNEL_ID) {
|
||||
postType = "Plugin";
|
||||
} else if (thread.parent_id === THEMES_CHANNEL_ID && isThemeSupported()) {
|
||||
} else if (thread.parent_id === VD_THEMES_CHANNEL_ID && isThemeSupported()) {
|
||||
postType = "Theme";
|
||||
} else return;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default defineCorePlugin({
|
|||
name: "QuickInstall",
|
||||
version: "1.0.0",
|
||||
description: "Quickly install Vendetta plugins and themes",
|
||||
authors: ["pyoncord"]
|
||||
authors: [{ name: "Vendetta Team" }]
|
||||
},
|
||||
start() {
|
||||
patches = [patchForumPost(), patchUrl()];
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { installTheme } from "@lib/addons/themes";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { isThemeSupported } from "@lib/api/native/loader";
|
||||
import { after, instead } from "@lib/api/patcher";
|
||||
import { installTheme } from "@lib/themes";
|
||||
import { THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { VD_PROXY_PREFIX,VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { channels, url } from "@metro/common";
|
||||
import { byMutableProp } from "@metro/filters";
|
||||
import { findExports } from "@metro/finders";
|
||||
import { findByProps, findByPropsLazy } from "@metro/wrappers";
|
||||
import { showConfirmationAlert } from "@ui/alerts";
|
||||
import { showToast } from "@ui/toasts";
|
||||
|
||||
const showSimpleActionSheet = findExports(byMutableProp("showSimpleActionSheet"));
|
||||
|
|
@ -66,7 +66,7 @@ export default () => {
|
|||
if (!urlType) return orig.apply(this, args);
|
||||
|
||||
// Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed.
|
||||
if (urlType === "theme" && getChannel(getChannelId())?.parent_id !== THEMES_CHANNEL_ID) return orig.apply(this, args);
|
||||
if (urlType === "theme" && getChannel(getChannelId())?.parent_id !== VD_THEMES_CHANNEL_ID) return orig.apply(this, args);
|
||||
|
||||
showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import AlertModal, { AlertActionButton } from "@lib/ui/components/wrappers/AlertModal";
|
||||
import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
|
|
@ -14,7 +15,6 @@ import { Image, ScrollView, View } from "react-native";
|
|||
|
||||
const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
|
||||
const { openAlert, dismissAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
|
||||
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
|
||||
|
||||
type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
|
||||
|
||||
|
|
@ -27,19 +27,19 @@ interface AddonPageProps<T extends object, I = any> {
|
|||
safeModeHint?: {
|
||||
message?: string;
|
||||
footer?: ReactNode;
|
||||
}
|
||||
};
|
||||
installAction?: {
|
||||
label?: string;
|
||||
// Ignored when onPress is defined!
|
||||
fetchFn?: (url: string) => Promise<void>;
|
||||
onPress?: () => void;
|
||||
}
|
||||
};
|
||||
CardComponent: ComponentType<CardWrapper<T>>;
|
||||
ListHeaderComponent?: ComponentType<any>;
|
||||
ListFooterComponent?: ComponentType<any>;
|
||||
}
|
||||
|
||||
function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<void> }) {
|
||||
function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<void>; }) {
|
||||
const [value, setValue] = React.useState("");
|
||||
const [error, setError] = React.useState("");
|
||||
const [isFetching, setIsFetching] = React.useState(false);
|
||||
|
|
@ -55,7 +55,7 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<vo
|
|||
|
||||
return <AlertModal
|
||||
title={props.label}
|
||||
content="Type in the source URL you want to install from:"
|
||||
content="Enter the URL of the source you want to install from:"
|
||||
extraContent={
|
||||
<Stack style={{ marginTop: -12 }}>
|
||||
<TextInput
|
||||
|
|
@ -122,7 +122,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
}, [props.items, sortFn, search]);
|
||||
|
||||
const onInstallPress = useCallback(() => {
|
||||
if (!props.installAction) return () => {};
|
||||
if (!props.installAction) return () => { };
|
||||
const { label, onPress, fetchFn } = props.installAction;
|
||||
if (fetchFn) {
|
||||
openAlert("AddonInputAlert", <InputAlert label={label ?? "Install"} fetchFn={fetchFn} />);
|
||||
|
|
@ -175,7 +175,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
})}
|
||||
/>}
|
||||
</View>
|
||||
{props.ListHeaderComponent && <props.ListHeaderComponent />}
|
||||
{props.ListHeaderComponent && !search && <props.ListHeaderComponent />}
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import PyoncordIcon from "@assets/icons/revenge.png";
|
||||
import { Strings } from "@core/i18n";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { isFontSupported, isThemeSupported } from "@lib/api/native/loader";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { registerSection } from "@ui/settings";
|
||||
import { version } from "bunny-build-info";
|
||||
|
||||
|
|
@ -19,9 +19,7 @@ export default function initSettings() {
|
|||
title: () => Strings.BUNNY,
|
||||
icon: { uri: PyoncordIcon },
|
||||
render: () => import("@core/ui/settings/pages/General"),
|
||||
rawTabsConfig: {
|
||||
useTrailing: () => `(${version})`
|
||||
}
|
||||
useTrailing: () => `(${version})`
|
||||
},
|
||||
{
|
||||
key: "BUNNY_PLUGINS",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { CheckState, useFileExists } from "@core/ui/hooks/useFS";
|
||||
import AssetBrowser from "@core/ui/settings/pages/Developer/AssetBrowser";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { connectToDebugger } from "@lib/api/debug";
|
||||
import { getReactDevToolsProp, getReactDevToolsVersion, isLoaderConfigSupported, isReactDevToolsPreloaded, isVendettaLoader } from "@lib/api/native/loader";
|
||||
import { loaderConfig, settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { Button, LegacyFormText, Stack, TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@metro/common/components";
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { FontDefinition, fonts, selectFont } from "@lib/addons/fonts";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { BundleUpdaterManager } from "@lib/api/native/modules";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { FontDefinition, fonts, selectFont } from "@lib/fonts";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { ButtonColors } from "@lib/utils/types";
|
||||
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 { showConfirmationAlert } from "@ui/alerts";
|
||||
import { TextStyleSheet } from "@ui/styles";
|
||||
import { useMemo } from "react";
|
||||
import { View } from "react-native";
|
||||
|
|
@ -104,7 +103,7 @@ export default function FontCard({ item: font }: CardWrapper<FontDefinition>) {
|
|||
content: "Reload Discord to apply changes?",
|
||||
confirmText: Strings.RELOAD,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
confirmColor: "red",
|
||||
onConfirm: BundleUpdaterManager.reload
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { createProxy, useProxy } from "@core/vendetta/storage";
|
||||
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/addons/fonts";
|
||||
import { getCurrentTheme } from "@lib/addons/themes";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { createProxy, useProxy } from "@lib/api/storage";
|
||||
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/fonts";
|
||||
import { getCurrentTheme } from "@lib/themes";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { ActionSheet, BottomSheetTitleHeader, Button, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput } from "@metro/common/components";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import AddonPage from "@core/ui/components/AddonPage";
|
||||
import FontEditor from "@core/ui/settings/pages/Fonts/FontEditor";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { FontDefinition, fonts } from "@lib/addons/fonts";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { FontDefinition, fonts } from "@lib/fonts";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
|
||||
import FontCard from "./FontCard";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import Version from "@core/ui/settings/pages/General/Version";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { getDebugInfo } from "@lib/api/debug";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { Stack, TableRowGroup } from "@metro/common/components";
|
||||
import { Platform, ScrollView } from "react-native";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { PyoncordIcon } from "@core/ui/settings";
|
||||
import About from "@core/ui/settings/pages/General/About";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { getDebugInfo, toggleSafeMode } from "@lib/api/debug";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants";
|
||||
import { NavigationNative, url } from "@metro/common";
|
||||
import { Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { installPlugin, isPluginInstalled, uninstallPlugin } from "@lib/addons/plugins";
|
||||
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { installPlugin, isPluginInstalled, uninstallPlugin } from "@lib/plugins";
|
||||
import { BunnyPluginManifest } from "@lib/plugins/types";
|
||||
import { showToast } from "@lib/ui/toasts";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants";
|
||||
|
|
@ -41,7 +41,7 @@ async function* getManifests(repoUrl: string) {
|
|||
|
||||
function InstallButton(props: { id: string; }) {
|
||||
const [installed, setInstalled] = useState(isPluginInstalled(props.id));
|
||||
const installationState = useMutation<void, Error, { install: boolean }>({
|
||||
const installationState = useMutation<void, Error, { install: boolean; }>({
|
||||
mutationFn: async ({ install }) => {
|
||||
await (install ? installPlugin : uninstallPlugin)(props.id, true);
|
||||
},
|
||||
|
|
@ -63,11 +63,11 @@ function InstallButton(props: { id: string; }) {
|
|||
/>;
|
||||
}
|
||||
|
||||
function TrailingButtons(props: { id: string }) {
|
||||
function TrailingButtons(props: { id: string; }) {
|
||||
return <Stack spacing={8} direction="horizontal">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onPress={() => {}}
|
||||
onPress={() => { }}
|
||||
variant="secondary"
|
||||
icon={findAssetId("CircleInformationIcon")}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import { NavigationNative, tokens } from "@metro/common";
|
|||
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
|
||||
import { showSheet } from "@ui/sheets";
|
||||
import chroma from "chroma-js";
|
||||
import { createContext, memo, useContext, useMemo } from "react";
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { Image, View } from "react-native";
|
||||
|
||||
import { UnifiedPluginModel } from "..";
|
||||
|
||||
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 {
|
||||
|
|
@ -92,7 +92,7 @@ function Description() {
|
|||
</Text>;
|
||||
}
|
||||
|
||||
const Actions = memo(() => {
|
||||
const Actions = () => {
|
||||
const { plugin } = useCardContext();
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ const Actions = memo(() => {
|
|||
)}
|
||||
/>
|
||||
</View>;
|
||||
});
|
||||
};
|
||||
|
||||
export default function PluginCard({ result, item: plugin }: CardWrapper<UnifiedPluginModel>) {
|
||||
plugin.usePluginState();
|
||||
|
|
|
|||
|
|
@ -2,26 +2,29 @@ import { Strings } from "@core/i18n";
|
|||
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 { Author } from "@lib/addons/types";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useObservable } from "@lib/api/storage";
|
||||
import { showToast } from "@lib/ui/toasts";
|
||||
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { Author } from "@lib/utils/types";
|
||||
import { findByProps } from "@metro";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { Button, Card, FlashList, Text } from "@metro/common/components";
|
||||
import { Card, FlashList, IconButton, Text } from "@metro/common/components";
|
||||
import { ComponentProps } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import unifyBunnyPlugin from "./models/bunny";
|
||||
import unifyVdPlugin from "./models/vendetta";
|
||||
|
||||
export interface UnifiedPluginModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
authors?: Array<Author | string>;
|
||||
authors?: Author[];
|
||||
icon?: string;
|
||||
|
||||
isEnabled(): boolean;
|
||||
|
|
@ -35,13 +38,6 @@ export interface UnifiedPluginModel {
|
|||
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
|
||||
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
|
||||
|
||||
function navigateToPluginBrowser(navigation: any) {
|
||||
navigation.push("BUNNY_CUSTOM_PAGE", {
|
||||
title: "Plugin Browser",
|
||||
render: React.lazy(() => import("../PluginBrowser")),
|
||||
});
|
||||
}
|
||||
|
||||
interface PluginPageProps extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
|
||||
useItems: () => unknown[];
|
||||
}
|
||||
|
|
@ -74,40 +70,54 @@ export default function Plugins() {
|
|||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
return <PluginPage
|
||||
useItems={() => useProxy(VdPluginManager.plugins) && Object.values(VdPluginManager.plugins)}
|
||||
resolveItem={unifyVdPlugin}
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
// TODO: Make this dismissable
|
||||
return <Card style={{ marginVertical: 8, gap: 4 }}>
|
||||
<Text variant="heading-lg/bold">Unproxied Plugins Detected</Text>
|
||||
<Text variant="text-md/medium">Installed plugins from unproxied sources may execute unreviewed code in this app without your knowledge.</Text>
|
||||
<View style={{ marginTop: 8, flexDirection: "row" }}>
|
||||
<Button
|
||||
style={{ flexShrink: 1 }}
|
||||
size="sm"
|
||||
text="Review"
|
||||
variant="secondary"
|
||||
onPress={() => {
|
||||
navigation.push("BUNNY_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>
|
||||
</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("BUNNY_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",
|
||||
|
|
@ -137,61 +147,4 @@ export default function Plugins() {
|
|||
}
|
||||
}}
|
||||
/>;
|
||||
|
||||
// const navigation = NavigationNative.useNavigation();
|
||||
// const { width: pageWidth } = useWindowDimensions();
|
||||
|
||||
// const state = useSegmentedControlState({
|
||||
// pageWidth,
|
||||
// items: [
|
||||
// {
|
||||
// label: "Vendetta",
|
||||
// id: "vendetta-plugins",
|
||||
// page: (
|
||||
// <PluginPage
|
||||
// useItems={() => useProxy(VdPluginManager.plugins) && Object.values(VdPluginManager.plugins)}
|
||||
// resolveItem={unifyVdPlugin}
|
||||
// fetchFunction={(url: string) => VdPluginManager.installPlugin(url)}
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
// {
|
||||
// label: "Bunny",
|
||||
// id: "bunny-plugins",
|
||||
// page: (
|
||||
// <PluginPage
|
||||
// useItems={() => (useNewProxy(pluginSettings), [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id)))}
|
||||
// resolveItem={unifyBunnyPlugin}
|
||||
// ListHeaderComponent={() => (
|
||||
// <View style={{ marginBottom: 10 }}>
|
||||
// <HelpMessage messageType={0}>
|
||||
// Bunny plugin system is in no way ready, try not getting yourself burnt ⚠️
|
||||
// </HelpMessage>
|
||||
// </View>
|
||||
// )}
|
||||
// ListFooterComponent={() => (
|
||||
// <View style={{ alignItems: "center", justifyContent: "center", paddingTop: 16, gap: 12 }}>
|
||||
// <Text variant="heading-lg/bold">{"Looking for more?"}</Text>
|
||||
// <Button
|
||||
// size="lg"
|
||||
// text="Browse plugins"
|
||||
// icon={findAssetId("discover")}
|
||||
// onPress={() => navigateToPluginBrowser(navigation)}
|
||||
// />
|
||||
// </View>
|
||||
// )}
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
// ]
|
||||
// });
|
||||
|
||||
// return (
|
||||
// <View style={{ alignItems: "center", justifyContent: "center", height: "100%" }}>
|
||||
// <View style={{ padding: 8, paddingBottom: 0 }}>
|
||||
// <SegmentedControl state={state} />
|
||||
// </View>
|
||||
// <SegmentedControlPages state={state} />
|
||||
// </View>
|
||||
// );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useProxy } from "@lib/api/storage/new";
|
||||
import { disablePlugin, enablePlugin, getId, getPluginSettingsComponent,isPluginEnabled, pluginSettings } from "@lib/plugins";
|
||||
import { BunnyPluginManifest } from "@lib/plugins/types";
|
||||
import { disablePlugin, enablePlugin, getId, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins";
|
||||
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
|
||||
import { useObservable } from "@lib/api/storage";
|
||||
|
||||
import { UnifiedPluginModel } from "..";
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): Unified
|
|||
isEnabled: () => isPluginEnabled(getId(manifest)),
|
||||
isInstalled: () => manifest.id in pluginSettings,
|
||||
usePluginState() {
|
||||
useProxy(pluginSettings);
|
||||
useObservable([pluginSettings]);
|
||||
},
|
||||
toggle(start: boolean) {
|
||||
start
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
|
||||
import { UnifiedPluginModel } from "..";
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,16 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
|
|||
plugin.usePluginState();
|
||||
|
||||
return <ActionSheet>
|
||||
<ScrollView>
|
||||
<ScrollView contentContainerStyle={{ gap: 8, marginBottom: 12 }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", paddingVertical: 24 }}>
|
||||
<Text variant="heading-xl/semibold">
|
||||
{plugin.name}
|
||||
</Text>
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text variant="heading-xl/semibold">
|
||||
{plugin.name}
|
||||
</Text>
|
||||
<Text variant="text-md/medium" color="text-muted">
|
||||
{plugin.description}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ marginLeft: "auto" }}>
|
||||
{plugin.getPluginSettingsComponent() && <Button
|
||||
size="md"
|
||||
|
|
@ -30,6 +35,11 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
|
|||
/>}
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", justifyContent: "center", alignContent: "center" }}>
|
||||
<Text variant="text-lg/medium">
|
||||
Oops, you shouldn't see this!
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ActionSheet>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { purgeStorage } from "@core/vendetta/storage";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { purgeStorage } from "@lib/api/storage";
|
||||
import { ButtonColors } from "@lib/utils/types";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { ActionSheet, ActionSheetRow, Button, TableRow, Text } from "@metro/common/components";
|
||||
import { showConfirmationAlert } from "@ui/alerts";
|
||||
import { hideSheet } from "@ui/sheets";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { ScrollView, View } from "react-native";
|
||||
|
|
@ -85,7 +84,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
|
|||
content: formatString("ARE_YOU_SURE_TO_CLEAR_DATA", { name: plugin.name }),
|
||||
confirmText: Strings.CLEAR,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
confirmColor: "red",
|
||||
onConfirm: async () => {
|
||||
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
|
||||
|
|
@ -122,7 +121,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
|
|||
content: formatString("ARE_YOU_SURE_TO_DELETE_PLUGIN", { name: plugin.name }),
|
||||
confirmText: Strings.DELETE,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
confirmColor: "red",
|
||||
onConfirm: () => {
|
||||
try {
|
||||
VdPluginManager.removePlugin(plugin.id);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/addons/themes";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/themes";
|
||||
import { ButtonColors } from "@lib/utils/types";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { showConfirmationAlert } from "@ui/alerts";
|
||||
import { showToast } from "@ui/toasts";
|
||||
|
||||
function selectAndApply(value: boolean, theme: Theme) {
|
||||
|
|
@ -68,7 +67,7 @@ export default function ThemeCard({ item: theme }: CardWrapper<Theme>) {
|
|||
content: formatString("ARE_YOU_SURE_TO_DELETE_THEME", { name: theme.data.name }),
|
||||
confirmText: Strings.DELETE,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
confirmColor: "red",
|
||||
onConfirm: () => {
|
||||
removeTheme(theme.id).then(wasSelected => {
|
||||
setRemoved(true);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import AddonPage from "@core/ui/components/AddonPage";
|
||||
import ThemeCard from "@core/ui/settings/pages/Themes/ThemeCard";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { installTheme, Theme, themes } from "@lib/addons/themes";
|
||||
import { Author } from "@lib/addons/types";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { installTheme, Theme, themes } from "@lib/themes";
|
||||
import { Author } from "@lib/utils/types";
|
||||
import { Button } from "@metro/common/components";
|
||||
|
||||
export default function Themes() {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ export type EmitterEvent = "SET" | "GET" | "DEL";
|
|||
|
||||
export interface EmitterListenerData {
|
||||
path: string[];
|
||||
value: any;
|
||||
parent: any;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export type EmitterListener = (
|
||||
|
|
@ -17,7 +16,7 @@ export type EmitterListener = (
|
|||
data: EmitterListenerData | any
|
||||
) => any;
|
||||
|
||||
export type EmitterListeners = Record<string, Set<EmitterListener>>
|
||||
export type EmitterListeners = Record<string, Set<EmitterListener>>;
|
||||
|
||||
export class Emitter {
|
||||
listeners = Object.values(Events).reduce<EmitterListeners>(
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { ButtonColors } from "@lib/utils/types";
|
||||
import { findByPropsLazy } from "@metro/wrappers";
|
||||
import InputAlert, { InputAlertProps } from "@ui/components/InputAlert";
|
||||
|
||||
|
|
@ -13,7 +12,7 @@ export interface ConfirmationAlertOptions {
|
|||
title?: string;
|
||||
content: string | JSX.Element | (string | JSX.Element)[];
|
||||
confirmText?: string;
|
||||
confirmColor?: ButtonColors;
|
||||
confirmColor?: string;
|
||||
onConfirm: () => void;
|
||||
secondaryConfirmText?: string;
|
||||
onConfirmSecondary?: () => void;
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
import * as alerts from "@core/vendetta/alerts";
|
||||
import * as storage from "@core/vendetta/storage";
|
||||
import { createStorage } from "@core/vendetta/storage";
|
||||
import * as themes from "@lib/addons/themes";
|
||||
import * as assets from "@lib/api/assets";
|
||||
import * as commands from "@lib/api/commands";
|
||||
import * as debug from "@lib/api/debug";
|
||||
import { getVendettaLoaderIdentity, isPyonLoader } from "@lib/api/native/loader";
|
||||
import patcher from "@lib/api/patcher";
|
||||
import { loaderConfig, settings } from "@lib/api/settings";
|
||||
import * as storage from "@lib/api/storage";
|
||||
import { createStorage } from "@lib/api/storage";
|
||||
import * as themes from "@lib/themes";
|
||||
import * as utils from "@lib/utils";
|
||||
import { cyrb64Hash } from "@lib/utils/cyrb64";
|
||||
import { DiscordLogger } from "@lib/utils/logger";
|
||||
import { LoggerClass } from "@lib/utils/logger";
|
||||
import * as metro from "@metro";
|
||||
import * as common from "@metro/common";
|
||||
import { Forms } from "@metro/common/components";
|
||||
import * as commonComponents from "@metro/common/components";
|
||||
import * as alerts from "@ui/alerts";
|
||||
import * as color from "@ui/color";
|
||||
import * as components from "@ui/components";
|
||||
import { createThemedStyleSheet } from "@ui/styles";
|
||||
|
|
@ -34,7 +34,7 @@ export async function createVdPluginObject(plugin: VendettaPlugin) {
|
|||
// Wrapping this with wrapSync is NOT an option.
|
||||
storage: await createStorage<Record<string, any>>(storage.createMMKVBackend(plugin.id)),
|
||||
},
|
||||
logger: new DiscordLogger(`Bunny » ${plugin.manifest.name}`),
|
||||
logger: new LoggerClass(`Bunny » ${plugin.manifest.name}`),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import { Author } from "@lib/addons/types";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { DiscordLogger, logger } from "@lib/utils/logger";
|
||||
import { Author } from "@lib/utils/types";
|
||||
import { logger,LoggerClass } from "@lib/utils/logger";
|
||||
|
||||
type EvaledPlugin = {
|
||||
onLoad?(): void;
|
||||
|
|
@ -39,7 +39,9 @@ export const VdPluginManager = {
|
|||
plugins,
|
||||
async pluginFetch(url: string) {
|
||||
if (url.startsWith(VD_PROXY_PREFIX)) {
|
||||
url = url.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX);
|
||||
url = url
|
||||
.replace("https://bunny-mod.github.io/plugins-proxy", BUNNY_PROXY_PREFIX)
|
||||
.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX);
|
||||
}
|
||||
|
||||
return await safeFetch(url, { cache: "no-store" });
|
||||
|
|
@ -61,7 +63,7 @@ export const VdPluginManager = {
|
|||
|
||||
if (existingPlugin?.manifest.hash !== pluginManifest.hash) {
|
||||
try {
|
||||
// by polymanifest spec, plugins should always specify their main file, but just in case
|
||||
// by polymanifest spec, plugins should always specify their main file, but just in case
|
||||
pluginJs = await (await this.pluginFetch(id + (pluginManifest.main || "index.js"))).text();
|
||||
} catch { } // Empty catch, checked below
|
||||
}
|
||||
|
|
@ -96,7 +98,7 @@ export const VdPluginManager = {
|
|||
// Wrapping this with wrapSync is NOT an option.
|
||||
storage: await createStorage<Record<string, any>>(createMMKVBackend(plugin.id)),
|
||||
},
|
||||
logger: new DiscordLogger(`Bunny » ${plugin.manifest.name}`),
|
||||
logger: new LoggerClass(`Bunny » ${plugin.manifest.name}`),
|
||||
};
|
||||
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`;
|
||||
|
||||
|
|
@ -166,7 +168,7 @@ export const VdPluginManager = {
|
|||
const allIds = Object.keys(plugins);
|
||||
|
||||
if (!settings.safeMode?.enabled) {
|
||||
// Loop over any plugin that is enabled, update it if allowed, then start it.
|
||||
// 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))));
|
||||
// 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));
|
||||
|
|
|
|||
238
src/core/vendetta/storage.ts
Normal file
238
src/core/vendetta/storage.ts
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData } from "@core/vendetta/Emitter";
|
||||
import { FileManager, MMKVManager } from "@lib/api/native/modules";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
|
||||
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");
|
||||
|
||||
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
|
||||
const emitter = new Emitter();
|
||||
|
||||
const childrens = new WeakMap<any, any>();
|
||||
const proxiedChildrenSet = new WeakSet<any>();
|
||||
|
||||
function createProxy(target: any, path: string[]): any {
|
||||
return new Proxy(target, {
|
||||
get(target, prop: string) {
|
||||
if ((prop as unknown) === emitterSymbol) return emitter;
|
||||
|
||||
const newPath = [...path, prop];
|
||||
const value: any = target[prop];
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
emitter.emit("GET", {
|
||||
path: newPath,
|
||||
value,
|
||||
});
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (proxiedChildrenSet.has(value)) return value;
|
||||
if (childrens.has(value)) return childrens.get(value);
|
||||
|
||||
const childrenProxy = createProxy(value, newPath);
|
||||
childrens.set(value, childrenProxy);
|
||||
return childrenProxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
set(target, prop: string, value) {
|
||||
if (typeof value === "object") {
|
||||
if (childrens.has(value)) {
|
||||
target[prop] = childrens.get(value);
|
||||
} else {
|
||||
const childrenProxy = createProxy(value, [...path, prop]);
|
||||
childrens.set(value, childrenProxy);
|
||||
proxiedChildrenSet.add(value);
|
||||
target[prop] = childrenProxy;
|
||||
}
|
||||
} else {
|
||||
target[prop] = value;
|
||||
}
|
||||
|
||||
emitter.emit("SET", {
|
||||
path: [...path, prop],
|
||||
value: target[prop],
|
||||
});
|
||||
// we do not care about success, if this actually does fail we have other problems
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteProperty(target, prop: string) {
|
||||
const value = typeof target[prop] === "object" ? childrens.get(target[prop])! : target[prop];
|
||||
const success = delete target[prop];
|
||||
if (success)
|
||||
emitter.emit("DEL", {
|
||||
value,
|
||||
path: [...path, prop],
|
||||
});
|
||||
return success;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
proxy: createProxy(target, []),
|
||||
emitter,
|
||||
};
|
||||
}
|
||||
|
||||
export function useProxy<T>(storage: T): T {
|
||||
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
|
||||
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
|
||||
|
||||
const [, forceUpdate] = React.useReducer(n => ~n, 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
|
||||
if (event === "DEL" && data.value === storage) return;
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
emitter.on("SET", listener);
|
||||
emitter.on("DEL", listener);
|
||||
|
||||
return () => {
|
||||
emitter.off("SET", listener);
|
||||
emitter.off("DEL", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
|
||||
const data = await backend.get();
|
||||
const { proxy, emitter } = createProxy(data);
|
||||
|
||||
const handler = () => backend.set(proxy);
|
||||
emitter.on("SET", handler);
|
||||
emitter.on("DEL", handler);
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
|
||||
let awaited: any = undefined;
|
||||
|
||||
const awaitQueue: (() => void)[] = [];
|
||||
const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb));
|
||||
|
||||
store.then(v => {
|
||||
awaited = v;
|
||||
awaitQueue.forEach(cb => cb());
|
||||
});
|
||||
|
||||
return new Proxy({} as Awaited<T>, {
|
||||
...Object.fromEntries(
|
||||
Object.getOwnPropertyNames(Reflect)
|
||||
// @ts-expect-error
|
||||
.map(k => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)])
|
||||
),
|
||||
get(target, prop, recv) {
|
||||
if (prop === syncAwaitSymbol) return awaitInit;
|
||||
return Reflect.get(awaited ?? target, prop, recv);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function awaitStorage(...stores: any[]) {
|
||||
return Promise.all(
|
||||
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
|
||||
);
|
||||
}
|
||||
|
||||
export interface StorageBackend {
|
||||
get: () => unknown | Promise<unknown>;
|
||||
set: (data: unknown) => void | Promise<void>;
|
||||
}
|
||||
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
|
||||
const filePathFixer = (file: string): string => Platform.select({
|
||||
default: file,
|
||||
ios: FileManager.saveFileToGallery ? file : `Documents/${file}`,
|
||||
});
|
||||
const getMMKVPath = (name: string): string => {
|
||||
if (ILLEGAL_CHARS_REGEX.test(name)) {
|
||||
// Replace forbidden characters with hyphens
|
||||
name = name.replace(ILLEGAL_CHARS_REGEX, "-").replace(/-+/g, "-");
|
||||
}
|
||||
|
||||
return `vd_mmkv/${name}`;
|
||||
};
|
||||
|
||||
export const purgeStorage = async (store: string) => {
|
||||
if (await MMKVManager.getItem(store)) {
|
||||
MMKVManager.removeItem(store);
|
||||
}
|
||||
|
||||
const mmkvPath = getMMKVPath(store);
|
||||
if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) {
|
||||
await FileManager.removeFile?.("documents", mmkvPath);
|
||||
}
|
||||
};
|
||||
|
||||
export const createMMKVBackend = (store: string, defaultData = {}) => {
|
||||
const mmkvPath = getMMKVPath(store);
|
||||
const defaultStr = JSON.stringify(defaultData);
|
||||
|
||||
return createFileBackend(mmkvPath, defaultData, (async () => {
|
||||
const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`;
|
||||
if (await FileManager.fileExists(path)) return;
|
||||
|
||||
let oldData = await MMKVManager.getItem(store) ?? defaultStr;
|
||||
|
||||
// From the testing on Android, it seems to return this if the data is too large
|
||||
if (oldData === "!!LARGE_VALUE!!") {
|
||||
const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`;
|
||||
if (await FileManager.fileExists(cachePath)) {
|
||||
oldData = await FileManager.readFile(cachePath, "utf8");
|
||||
} else {
|
||||
console.log(`${store}: Experienced data loss :(`);
|
||||
oldData = defaultStr;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(oldData);
|
||||
} catch {
|
||||
console.error(`${store} had an unparseable data while migrating`);
|
||||
oldData = defaultStr;
|
||||
}
|
||||
|
||||
await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8");
|
||||
if (await MMKVManager.getItem(store) !== null) {
|
||||
MMKVManager.removeItem(store);
|
||||
console.log(`Successfully migrated ${store} store from MMKV storage to fs`);
|
||||
}
|
||||
})());
|
||||
};
|
||||
|
||||
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
|
||||
return {
|
||||
get: async () => {
|
||||
await migratePromise;
|
||||
const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`;
|
||||
|
||||
if (await FileManager.fileExists(path)) {
|
||||
const content = await FileManager.readFile(path, "utf8");
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
// Corrupted content, ignore
|
||||
}
|
||||
}
|
||||
|
||||
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(defaultData), "utf8");
|
||||
return JSON.parse(await FileManager.readFile(path, "utf8"));
|
||||
},
|
||||
set: async data => {
|
||||
await migratePromise;
|
||||
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
42
src/index.ts
42
src/index.ts
|
|
@ -3,37 +3,36 @@ import { initFetchI18nStrings } from "@core/i18n";
|
|||
import initSettings from "@core/ui/settings";
|
||||
import { initVendettaObject } from "@core/vendetta/api";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { updateFonts } from "@lib/addons/fonts";
|
||||
import { initPlugins, updatePlugins } from "@lib/addons/plugins";
|
||||
import { initThemes, patchChatBackground } from "@lib/addons/themes";
|
||||
import { patchCommands } from "@lib/api/commands";
|
||||
import { patchLogHook } from "@lib/api/debug";
|
||||
import { injectFluxInterceptor } from "@lib/api/flux";
|
||||
import { removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { writeFile } from "@lib/api/native/fs";
|
||||
import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
|
||||
import { FileManager } from "@lib/api/native/modules";
|
||||
import { updateFonts } from "@lib/fonts";
|
||||
import { initPlugins } from "@lib/plugins";
|
||||
import { initThemes, patchChatBackground } from "@lib/themes";
|
||||
import { patchJsx } from "@lib/api/react/jsx";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
import initSafeMode from "@ui/safeMode";
|
||||
import { patchSettings } from "@ui/settings";
|
||||
|
||||
import * as lib from "./lib";
|
||||
|
||||
export default async () => {
|
||||
// Themes
|
||||
if (isThemeSupported()) {
|
||||
try {
|
||||
if (isPyonLoader()) {
|
||||
if (FileManager.removeFile != null) {
|
||||
removeFile("vendetta_theme.json", "");
|
||||
} else {
|
||||
writeFile("vendetta_theme.json", "null", "");
|
||||
}
|
||||
}
|
||||
initThemes();
|
||||
} catch (e) {
|
||||
console.error("[Bunny] Failed to initialize themes...", e);
|
||||
function maybeLoadThemes() {
|
||||
if (!isThemeSupported()) return;
|
||||
|
||||
try {
|
||||
if (isPyonLoader()) {
|
||||
writeFile("../vendetta_theme.json", "null");
|
||||
}
|
||||
initThemes();
|
||||
} catch (e) {
|
||||
console.error("Failed to initialize themes", e);
|
||||
}
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
maybeLoadThemes();
|
||||
|
||||
// Load everything in parallel
|
||||
await Promise.all([
|
||||
|
|
@ -42,11 +41,13 @@ export default async () => {
|
|||
patchLogHook(),
|
||||
patchCommands(),
|
||||
patchChatBackground(),
|
||||
patchJsx(),
|
||||
initVendettaObject(),
|
||||
initFetchI18nStrings(),
|
||||
initSettings(),
|
||||
initFixes(),
|
||||
initSafeMode()
|
||||
initSafeMode(),
|
||||
updatePlugins()
|
||||
]).then(
|
||||
// Push them all to unloader
|
||||
u => u.forEach(f => f && lib.unload.push(f))
|
||||
|
|
@ -60,6 +61,7 @@ export default async () => {
|
|||
.then(u => lib.unload.push(u))
|
||||
.catch(() => alert("Failed to initialize Vendetta plugins"));
|
||||
|
||||
// And then, load Bunny plugins
|
||||
initPlugins();
|
||||
|
||||
// Update the fonts
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import { clearFolder, downloadFile, fileExists, removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
|
||||
type FontMap = Record<string, string>;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { patcher } from "@lib/api";
|
||||
import { registerCommand } from "@lib/api/commands";
|
||||
import { createStorage } from "@lib/api/storage/new";
|
||||
import { createStorage } from "@lib/api/storage";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
|
||||
import { registeredPlugins } from ".";
|
||||
|
|
@ -39,7 +39,7 @@ export function createBunnyPluginAPI(id: string) {
|
|||
},
|
||||
// Added something in here? Make sure to also update BunnyPluginProperty in ./types
|
||||
plugin: {
|
||||
createStorage: () => createStorage(`plugins/storage/${id}.json`),
|
||||
createStorage: <T extends object = any>() => createStorage<T>(`plugins/storage/${id}.json`),
|
||||
manifest: registeredPlugins.get(id),
|
||||
logger
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { getCorePlugins } from "@core/plugins";
|
||||
import { readFile, removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorageAsync } from "@lib/api/storage/new";
|
||||
import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorage } from "@lib/api/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants";
|
||||
import { semver } from "@metro/common";
|
||||
|
|
@ -51,6 +51,10 @@ function isExternalPlugin(manifest: t.BunnyPluginManifest): manifest is t.BunnyP
|
|||
return "parentRepository" in manifest;
|
||||
}
|
||||
|
||||
export function isCorePlugin(id: string) {
|
||||
return corePluginInstances.has(id);
|
||||
}
|
||||
|
||||
export function getId<T extends t.BunnyPluginManifest>(manifest: T): string {
|
||||
const id = manifestToId.get(manifest);
|
||||
assert(id, manifest?.name ?? "unknown", "getting ID from an unregistered/invalid manifest");
|
||||
|
|
@ -93,7 +97,7 @@ export async function updateAndWritePlugin(repoUrl: string, id: string, fetchScr
|
|||
await writeFile(manifest.jsPath, js);
|
||||
}
|
||||
|
||||
await updateStorageAsync(`plugins/manifests/${id}.json`, manifest);
|
||||
await updateStorage(`plugins/manifests/${id}.json`, manifest);
|
||||
|
||||
if (registeredPlugins.has(id)) {
|
||||
const existingManifest = registeredPlugins.get(id)!;
|
||||
|
|
@ -347,9 +351,24 @@ export function stopPlugin(id: string) {
|
|||
pluginInstances.delete(id);
|
||||
}
|
||||
|
||||
export async function checkAndRegisterUpdates() {
|
||||
async function updateAllRepository() {
|
||||
try {
|
||||
await updateRepository(OFFICIAL_PLUGINS_REPO_URL);
|
||||
} catch (error) {
|
||||
console.error("Failed to update official plugins repository", error);
|
||||
}
|
||||
|
||||
await Promise.allSettled(Object.keys(pluginRepositories).map(async repo => {
|
||||
if (repo !== OFFICIAL_PLUGINS_REPO_URL) {
|
||||
await updateRepository(repo);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export async function updatePlugins() {
|
||||
await awaitStorage(pluginRepositories, pluginSettings);
|
||||
|
||||
// Register core plugins
|
||||
const corePlugins = getCorePlugins();
|
||||
for (const id in corePlugins) {
|
||||
const {
|
||||
|
|
@ -367,12 +386,7 @@ export async function checkAndRegisterUpdates() {
|
|||
corePluginInstances.set(id, instance);
|
||||
}
|
||||
|
||||
// await updateRepository(OFFICIAL_PLUGINS_REPO_URL);
|
||||
// await Promise.allSettled(Object.keys(pluginRepositories).map(async repo => {
|
||||
// if (repo !== OFFICIAL_PLUGINS_REPO_URL) {
|
||||
// await updateRepository(repo);
|
||||
// }
|
||||
// }));
|
||||
await updateAllRepository();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createStorage } from "@lib/api/storage";
|
||||
import { createStorage } from "@core/vendetta/storage";
|
||||
import { Author } from "@lib/addons/types";
|
||||
import { Logger } from "@lib/utils/logger";
|
||||
import { Author } from "@lib/utils/types";
|
||||
|
||||
export interface PluginRepo {
|
||||
[id: string]: {
|
||||
|
|
@ -26,7 +26,7 @@ export interface BunnyPluginManifest {
|
|||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly version: string;
|
||||
readonly authors: (Author | string)[];
|
||||
readonly authors: Author[];
|
||||
}
|
||||
|
||||
export interface BunnyPluginManifestInternal extends BunnyPluginManifest {
|
||||
|
|
@ -2,13 +2,13 @@
|
|||
* Theming system in Bunny is currently a prototype, expect an unreadable theme implementation below
|
||||
*/
|
||||
|
||||
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import { Author } from "@lib/addons/types";
|
||||
import { getStoredTheme, getThemeFilePath } from "@lib/api/native/loader";
|
||||
import { ThemeManager } from "@lib/api/native/modules";
|
||||
import { after, before, instead } from "@lib/api/patcher";
|
||||
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { findInReactTree, safeFetch } from "@lib/utils";
|
||||
import { lazyDestructure, proxyLazy } from "@lib/utils/lazy";
|
||||
import { Author } from "@lib/utils/types";
|
||||
import { byMutableProp } from "@metro/filters";
|
||||
import { createLazyModule } from "@metro/lazy";
|
||||
import { findByNameLazy, findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro/wrappers";
|
||||
4
src/lib/addons/types.ts
Normal file
4
src/lib/addons/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface Author {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { getImportingModuleId, requireModule } from "@metro/internals/modules";
|
|||
|
||||
// TODO: Deprecate this map, make another that maps to an array of assets (Asset[]) instead
|
||||
/**
|
||||
* @internal
|
||||
* Pitfall: Multiple assets may have the same name, this is fine if we require the asset only for display,\
|
||||
* but not when used to get the registered id/index. In some condition, this would break some plugins like HideGiftButton that gets id by name for patching.\
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { getThemeFromLoader, selectTheme, themes } from "@lib/addons/themes";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { getLoaderName, getLoaderVersion, isThemeSupported } from "@lib/api/native/loader";
|
||||
import { BundleUpdaterManager, ClientInfoManager, DeviceManager } from "@lib/api/native/modules";
|
||||
import { after } from "@lib/api/patcher";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { getThemeFromLoader, selectTheme, themes } from "@lib/themes";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { version } from "bunny-build-info";
|
||||
import { Platform, type PlatformConstants } from "react-native";
|
||||
export let socket: WebSocket;
|
||||
|
||||
export interface RNConstants extends PlatformConstants {
|
||||
// Android
|
||||
|
|
@ -44,6 +43,7 @@ export async function toggleSafeMode() {
|
|||
setTimeout(BundleUpdaterManager.reload, 400);
|
||||
}
|
||||
|
||||
let socket: WebSocket;
|
||||
export function connectToDebugger(url: string) {
|
||||
if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) socket.close();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@ export * as debug from "./debug";
|
|||
export * as flux from "./flux";
|
||||
export * as native from "./native";
|
||||
export * as patcher from "./patcher";
|
||||
export * as react from "./react";
|
||||
export * as settings from "./settings";
|
||||
export * as storage from "./storage";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { FileManager } from "./modules";
|
|||
* Removes all files in a directory from the path given
|
||||
* @param path Path to the targeted directory
|
||||
*/
|
||||
export async function clearFolder(path: string, prefix = "pyoncord/") {
|
||||
export async function clearFolder(path: string, { prefix = "pyoncord/" } = {}) {
|
||||
if (typeof FileManager.clearFolder !== "function") throw new Error("'fs.clearFolder' is not supported");
|
||||
return void await FileManager.clearFolder("documents", `${prefix}${path}`);
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ export async function clearFolder(path: string, prefix = "pyoncord/") {
|
|||
* Remove file from given path, currently no check for any failure
|
||||
* @param path Path to the file
|
||||
*/
|
||||
export async function removeFile(path: string, prefix = "pyoncord/") {
|
||||
export async function removeFile(path: string, { prefix = "pyoncord/" } = {}) {
|
||||
if (typeof FileManager.removeFile !== "function") throw new Error("'fs.removeFile' is not supported");
|
||||
return void await FileManager.removeFile("documents", `${prefix}${path}`);
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ export async function removeCacheFile(path: string, prefix = "pyoncord/") {
|
|||
* Check if the file or directory given by the path exists
|
||||
* @param path Path to the file
|
||||
*/
|
||||
export async function fileExists(path: string, prefix = "pyoncord/") {
|
||||
export async function fileExists(path: string, { prefix = "pyoncord/" } = {}) {
|
||||
return await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${prefix}${path}`);
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ export async function fileExists(path: string, prefix = "pyoncord/") {
|
|||
* @param path Path to the file
|
||||
* @param data String data to write to the file
|
||||
*/
|
||||
export async function writeFile(path: string, data: string, prefix = "pyoncord/"): Promise<void> {
|
||||
export async function writeFile(path: string, data: string, { prefix = "pyoncord/" } = {}): Promise<void> {
|
||||
if (typeof data !== "string") throw new Error("Argument 'data' must be a string");
|
||||
return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "utf8");
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export async function writeFile(path: string, data: string, prefix = "pyoncord/"
|
|||
* @param path Path to the file
|
||||
* @param fallback Fallback data to return if the file doesn't exist, and will be written to the file
|
||||
*/
|
||||
export async function readFile(path: string, prefix = "pyoncord/"): Promise<string> {
|
||||
export async function readFile(path: string, { prefix = "pyoncord/" } = {}): Promise<string> {
|
||||
try {
|
||||
return await FileManager.readFile(`${FileManager.getConstants().DocumentsDirPath}/${prefix}${path}`, "utf8");
|
||||
} catch (err) {
|
||||
|
|
@ -58,30 +58,19 @@ export async function readFile(path: string, prefix = "pyoncord/"): Promise<stri
|
|||
}
|
||||
}
|
||||
|
||||
export async function downloadFile(url: string, path: string, prefix = "pyoncord/") {
|
||||
const blob = await fetch(url).then(r => r.blob());
|
||||
const dataURL: string | null = await new Promise(r => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => r(reader.result as unknown as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
let data: string;
|
||||
if (dataURL == null) {
|
||||
throw new Error("Failed to convert blob to data URL");
|
||||
} else {
|
||||
const index = dataURL.indexOf("base64,");
|
||||
if (index === -1) throw new Error("dataURL does not contain base64");
|
||||
data = dataURL.slice(index + 7);
|
||||
/**
|
||||
* Download a file from the given URL and save it to the path given
|
||||
* @param url URL to download the file from
|
||||
* @param path Path to save the file to
|
||||
*/
|
||||
export async function downloadFile(url: string, path: string, { prefix = "pyoncord/" } = {}) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download file from ${url}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const data = Buffer.from(arrayBuffer).toString("base64");
|
||||
|
||||
return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "base64");
|
||||
}
|
||||
|
||||
// // thanks Rosie, doesnt work though
|
||||
// export async function downloadFile(url: string, path: string, prefix = "pyoncord/") {
|
||||
// const arrayBuffer = await fetch(url).then(r => r.arrayBuffer());
|
||||
// const data = Buffer.from(arrayBuffer).toString("base64");
|
||||
|
||||
// return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "base64");
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export * as fs from "./fs";
|
||||
export * as loader from "./loader";
|
||||
export * as modules from "./modules";
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function polyfillVendettaLoaderIdentity() {
|
|||
const id = getStoredTheme()?.id;
|
||||
if (!id) return null;
|
||||
|
||||
const { themes } = require("@lib/themes");
|
||||
const { themes } = require("@lib/addons/themes");
|
||||
return themes[id] ?? getStoredTheme() ?? null;
|
||||
},
|
||||
configurable: true
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
const nmp = window.nativeModuleProxy;
|
||||
|
||||
/**
|
||||
* A key-value storage based upon `SharedPreferences` on Android.
|
||||
*
|
||||
* These types are based on Android though everything should be the same between
|
||||
* platforms.
|
||||
*/
|
||||
export interface MMKVManager {
|
||||
/**
|
||||
* Get the value for the given `key`, or null
|
||||
* @param key The key to fetch
|
||||
*/
|
||||
getItem: (key: string) => Promise<string | null>;
|
||||
/**
|
||||
* Deletes the value for the given `key`
|
||||
* @param key The key to delete
|
||||
*/
|
||||
removeItem: (key: string) => void;
|
||||
/**
|
||||
* Sets the value of `key` to `value`
|
||||
*/
|
||||
setItem: (key: string, value: string) => void;
|
||||
/**
|
||||
* Goes through every item in storage and returns it, excluding the
|
||||
* keys specified in `exclude`.
|
||||
* @param exclude A list of items to exclude from result
|
||||
*/
|
||||
refresh: (exclude: string[]) => Promise<Record<string, string>>;
|
||||
/**
|
||||
* You will be murdered if you use this function.
|
||||
* Clears ALL of Discord's settings.
|
||||
*/
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export interface FileManager {
|
||||
/**
|
||||
* @param path **Full** path to file
|
||||
*/
|
||||
fileExists: (path: string) => Promise<boolean>;
|
||||
/**
|
||||
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
|
||||
*/
|
||||
getSize: (uri: string) => Promise<boolean>;
|
||||
/**
|
||||
* @param path **Full** path to file
|
||||
* @param encoding Set to `base64` in order to encode response
|
||||
*/
|
||||
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
|
||||
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
|
||||
/**
|
||||
* @param storageDir Either `cache` or `documents`.
|
||||
* @param path Path in `storageDir`, parents are recursively created.
|
||||
* @param data The data to write to the file
|
||||
* @param encoding Set to `base64` if `data` is base64 encoded.
|
||||
* @returns Promise that resolves to path of the file once it got written
|
||||
*/
|
||||
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
|
||||
/**
|
||||
* Removes a file from the path given.
|
||||
* (!) On Android, this always returns false, regardless if it fails or not!
|
||||
* @param storageDir Either `cache` or `documents`
|
||||
* @param path Path to the file to be removed
|
||||
*/
|
||||
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
|
||||
/**
|
||||
* Clear the folder from the path given
|
||||
* (!) On Android, this only clears all *files* and not subdirectories!
|
||||
* @param storageDir Either `cache` or `documents`
|
||||
* @param path Path to the folder to be cleared
|
||||
* @returns Whether the clearance succeeded
|
||||
*/
|
||||
clearFolder(storageDir: "cache" | "documents", path: string): Promise<boolean>;
|
||||
getConstants: () => {
|
||||
/**
|
||||
* The path the `documents` storage dir (see {@link writeFile}) represents.
|
||||
*/
|
||||
DocumentsDirPath: string;
|
||||
CacheDirPath: string;
|
||||
};
|
||||
/**
|
||||
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
DocumentsDirPath: string;
|
||||
}
|
||||
|
||||
export const MMKVManager = nmp.MMKVManager as MMKVManager;
|
||||
//! 173.10 renamed this to RTNFileManager.
|
||||
export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as FileManager;
|
||||
//! 173.13 renamed this to RTNClientInfoManager.
|
||||
export const ClientInfoManager = nmp.InfoDictionaryManager ?? nmp.RTNClientInfoManager;
|
||||
//! 173.14 renamed this to RTNDeviceManager.
|
||||
export const DeviceManager = nmp.DCDDeviceManager ?? nmp.RTNDeviceManager;
|
||||
export const { BundleUpdaterManager } = nmp;
|
||||
export const ThemeManager = nmp.RTNThemeManager ?? nmp.DCDTheme;
|
||||
11
src/lib/api/native/modules/index.ts
Normal file
11
src/lib/api/native/modules/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { RNModules } from "./types";
|
||||
|
||||
const nmp = window.nativeModuleProxy;
|
||||
|
||||
export const MMKVManager = nmp.MMKVManager as RNModules.MMKVManager;
|
||||
export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as RNModules.FileManager;
|
||||
export const ClientInfoManager = nmp.InfoDictionaryManager ?? nmp.RTNClientInfoManager;
|
||||
export const DeviceManager = nmp.DCDDeviceManager ?? nmp.RTNDeviceManager;
|
||||
export const { BundleUpdaterManager } = nmp;
|
||||
export const ThemeManager = nmp.RTNThemeManager ?? nmp.DCDTheme;
|
||||
|
||||
89
src/lib/api/native/modules/types.ts
Normal file
89
src/lib/api/native/modules/types.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
|
||||
|
||||
export namespace RNModules {
|
||||
/**
|
||||
* A key-value storage based upon `SharedPreferences` on Android.
|
||||
*
|
||||
* These types are based on Android though everything should be the same between
|
||||
* platforms.
|
||||
*/
|
||||
export interface MMKVManager {
|
||||
/**
|
||||
* Get the value for the given `key`, or null
|
||||
* @param key The key to fetch
|
||||
*/
|
||||
getItem: (key: string) => Promise<string | null>;
|
||||
/**
|
||||
* Deletes the value for the given `key`
|
||||
* @param key The key to delete
|
||||
*/
|
||||
removeItem: (key: string) => void;
|
||||
/**
|
||||
* Sets the value of `key` to `value`
|
||||
*/
|
||||
setItem: (key: string, value: string) => void;
|
||||
/**
|
||||
* Goes through every item in storage and returns it, excluding the
|
||||
* keys specified in `exclude`.
|
||||
* @param exclude A list of items to exclude from result
|
||||
*/
|
||||
refresh: (exclude: string[]) => Promise<Record<string, string>>;
|
||||
/**
|
||||
* You will be murdered if you use this function.
|
||||
* Clears ALL of Discord's settings.
|
||||
*/
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export interface FileManager {
|
||||
/**
|
||||
* @param path **Full** path to file
|
||||
*/
|
||||
fileExists: (path: string) => Promise<boolean>;
|
||||
/**
|
||||
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
|
||||
*/
|
||||
getSize: (uri: string) => Promise<boolean>;
|
||||
/**
|
||||
* @param path **Full** path to file
|
||||
* @param encoding Set to `base64` in order to encode response
|
||||
*/
|
||||
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
|
||||
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
|
||||
/**
|
||||
* @param storageDir Either `cache` or `documents`.
|
||||
* @param path Path in `storageDir`, parents are recursively created.
|
||||
* @param data The data to write to the file
|
||||
* @param encoding Set to `base64` if `data` is base64 encoded.
|
||||
* @returns Promise that resolves to path of the file once it got written
|
||||
*/
|
||||
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
|
||||
/**
|
||||
* Removes a file from the path given.
|
||||
* (!) On Android, this always returns false, regardless if it fails or not!
|
||||
* @param storageDir Either `cache` or `documents`
|
||||
* @param path Path to the file to be removed
|
||||
*/
|
||||
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
|
||||
/**
|
||||
* Clear the folder from the path given
|
||||
* (!) On Android, this only clears all *files* and not subdirectories!
|
||||
* @param storageDir Either `cache` or `documents`
|
||||
* @param path Path to the folder to be cleared
|
||||
* @returns Whether the clearance succeeded
|
||||
*/
|
||||
clearFolder(storageDir: "cache" | "documents", path: string): Promise<boolean>;
|
||||
getConstants: () => {
|
||||
/**
|
||||
* The path the `documents` storage dir (see {@link writeFile}) represents.
|
||||
*/
|
||||
DocumentsDirPath: string;
|
||||
CacheDirPath: string;
|
||||
};
|
||||
/**
|
||||
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
DocumentsDirPath: string;
|
||||
}
|
||||
}
|
||||
1
src/lib/api/react/index.ts
Normal file
1
src/lib/api/react/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * as jsx from "./jsx";
|
||||
43
src/lib/api/react/jsx.ts
Normal file
43
src/lib/api/react/jsx.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { after } from "@lib/api/patcher";
|
||||
import { findByPropsLazy } from "@metro";
|
||||
|
||||
type Callback = (Component: any, ret: JSX.Element) => JSX.Element;
|
||||
const callbacks = new Map<string, Callback[]>();
|
||||
|
||||
const jsxRuntime = findByPropsLazy("jsx", "jsxs");
|
||||
|
||||
export function onJsxCreate(Component: string, callback: Callback) {
|
||||
if (!callbacks.has(Component)) callbacks.set(Component, []);
|
||||
callbacks.get(Component)!.push(callback);
|
||||
}
|
||||
|
||||
export function deleteJsxCreate(Component: string, callback: Callback) {
|
||||
if (!callbacks.has(Component)) return;
|
||||
const cbs = callbacks.get(Component)!;
|
||||
cbs.splice(cbs.indexOf(callback), 1);
|
||||
if (cbs.length === 0) callbacks.delete(Component);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function patchJsx() {
|
||||
const callback = ([Component]: unknown[], ret: JSX.Element) => {
|
||||
// The check could be more complex, but this is fine for now to avoid overhead
|
||||
if (typeof Component === "function" && callbacks.has(Component.name)) {
|
||||
const cbs = callbacks.get(Component.name)!;
|
||||
for (const cb of cbs) {
|
||||
const _ret = cb(Component, ret);
|
||||
if (_ret !== undefined) ret = _ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
const patches = [
|
||||
after("jsx", jsxRuntime, callback),
|
||||
after("jsxs", jsxRuntime, callback)
|
||||
];
|
||||
|
||||
return () => patches.forEach(unpatch => unpatch());
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import { getLoaderConfigPath } from "@lib/api/native/loader";
|
||||
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
|
||||
export interface Settings {
|
||||
debuggerUrl: string;
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import { FileManager, MMKVManager } from "@lib/api/native/modules";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
export interface StorageBackend {
|
||||
get: () => unknown | Promise<unknown>;
|
||||
set: (data: unknown) => void | Promise<void>;
|
||||
}
|
||||
|
||||
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
|
||||
|
||||
const filePathFixer = (file: string): string => Platform.select({
|
||||
default: file,
|
||||
ios: FileManager.saveFileToGallery ? file : `Documents/${file}`,
|
||||
});
|
||||
|
||||
const getMMKVPath = (name: string): string => {
|
||||
if (ILLEGAL_CHARS_REGEX.test(name)) {
|
||||
// Replace forbidden characters with hyphens
|
||||
name = name.replace(ILLEGAL_CHARS_REGEX, "-").replace(/-+/g, "-");
|
||||
}
|
||||
|
||||
return `vd_mmkv/${name}`;
|
||||
};
|
||||
|
||||
export const purgeStorage = async (store: string) => {
|
||||
if (await MMKVManager.getItem(store)) {
|
||||
MMKVManager.removeItem(store);
|
||||
}
|
||||
|
||||
const mmkvPath = getMMKVPath(store);
|
||||
if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) {
|
||||
await FileManager.removeFile?.("documents", mmkvPath);
|
||||
}
|
||||
};
|
||||
|
||||
export const createMMKVBackend = (store: string, defaultData = {}) => {
|
||||
const mmkvPath = getMMKVPath(store);
|
||||
const defaultStr = JSON.stringify(defaultData);
|
||||
|
||||
return createFileBackend(mmkvPath, defaultData, (async () => {
|
||||
const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`;
|
||||
if (await FileManager.fileExists(path)) return;
|
||||
|
||||
let oldData = await MMKVManager.getItem(store) ?? defaultStr;
|
||||
|
||||
// From the testing on Android, it seems to return this if the data is too large
|
||||
if (oldData === "!!LARGE_VALUE!!") {
|
||||
const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`;
|
||||
if (await FileManager.fileExists(cachePath)) {
|
||||
oldData = await FileManager.readFile(cachePath, "utf8");
|
||||
} else {
|
||||
console.log(`${store}: Experienced data loss :(`);
|
||||
oldData = defaultStr;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(oldData);
|
||||
} catch {
|
||||
console.error(`${store} had an unparseable data while migrating`);
|
||||
oldData = defaultStr;
|
||||
}
|
||||
|
||||
await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8");
|
||||
if (await MMKVManager.getItem(store) !== null) {
|
||||
MMKVManager.removeItem(store);
|
||||
console.log(`Successfully migrated ${store} store from MMKV storage to fs`);
|
||||
}
|
||||
})());
|
||||
};
|
||||
|
||||
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
|
||||
return {
|
||||
get: async () => {
|
||||
await migratePromise;
|
||||
const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`;
|
||||
|
||||
if (await FileManager.fileExists(path)) {
|
||||
const content = await FileManager.readFile(path, "utf8");
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
// Corrupted content, ignore
|
||||
}
|
||||
}
|
||||
|
||||
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(defaultData), "utf8");
|
||||
return JSON.parse(await FileManager.readFile(path, "utf8"));
|
||||
},
|
||||
set: async data => {
|
||||
await migratePromise;
|
||||
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -1,156 +1,190 @@
|
|||
import { StorageBackend } from "@lib/api/storage/backends";
|
||||
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData } from "@lib/utils/Emitter";
|
||||
import { Emitter } from "@core/vendetta/Emitter";
|
||||
import { Observable, ObserverOptions } from "@gullerya/object-observer";
|
||||
import { fileExists, readFile, removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { debounce } from "es-toolkit";
|
||||
|
||||
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
|
||||
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");
|
||||
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
|
||||
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
|
||||
|
||||
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
|
||||
const emitter = new Emitter();
|
||||
const parentTarget = target;
|
||||
const _loadedStorage = {} as Record<string, any>;
|
||||
|
||||
const childrens = new WeakMap<any, any>();
|
||||
const proxiedChildrenSet = new WeakSet<any>();
|
||||
|
||||
function createProxy(target: any, path: string[]): any {
|
||||
return new Proxy(target, {
|
||||
get(target, prop: string) {
|
||||
if ((prop as unknown) === emitterSymbol) return emitter;
|
||||
|
||||
const newPath = [...path, prop];
|
||||
const value: any = target[prop];
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
emitter.emit("GET", {
|
||||
parent: parentTarget,
|
||||
path: newPath,
|
||||
value,
|
||||
});
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (proxiedChildrenSet.has(value)) return value;
|
||||
if (childrens.has(value)) return childrens.get(value);
|
||||
|
||||
const childrenProxy = createProxy(value, newPath);
|
||||
childrens.set(value, childrenProxy);
|
||||
return childrenProxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
set(target, prop: string, value) {
|
||||
if (typeof value === "object") {
|
||||
if (childrens.has(value)) {
|
||||
target[prop] = childrens.get(value);
|
||||
} else {
|
||||
const childrenProxy = createProxy(value, [...path, prop]);
|
||||
childrens.set(value, childrenProxy);
|
||||
proxiedChildrenSet.add(value);
|
||||
target[prop] = childrenProxy;
|
||||
}
|
||||
} else {
|
||||
target[prop] = value;
|
||||
}
|
||||
|
||||
emitter.emit("SET", {
|
||||
parent: parentTarget,
|
||||
path: [...path, prop],
|
||||
value: target[prop],
|
||||
});
|
||||
// we do not care about success, if this actually does fail we have other problems
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteProperty(target, prop: string) {
|
||||
const value = typeof target[prop] === "object" ? childrens.get(target[prop])! : target[prop];
|
||||
const success = delete target[prop];
|
||||
if (success)
|
||||
emitter.emit("DEL", {
|
||||
value,
|
||||
parent: parentTarget,
|
||||
path: [...path, prop],
|
||||
});
|
||||
return success;
|
||||
},
|
||||
});
|
||||
}
|
||||
function createFileBackend<T extends object>(filePath: string) {
|
||||
const write = debounce((data: T) => {
|
||||
writeFile(filePath, JSON.stringify(data));
|
||||
}, 500);
|
||||
|
||||
return {
|
||||
proxy: createProxy(target, []),
|
||||
emitter,
|
||||
get: async () => {
|
||||
try {
|
||||
return JSON.parse(await readFile(filePath)) as T;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
|
||||
}
|
||||
},
|
||||
set: async (data: T) => {
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("data needs to be an object");
|
||||
}
|
||||
|
||||
write(data);
|
||||
},
|
||||
exists: async () => {
|
||||
return await fileExists(filePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useProxy<T>(storage: T): T {
|
||||
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
|
||||
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
|
||||
export function useObservable(observables: Observable[], opts?: ObserverOptions) {
|
||||
if (observables.some((o: any) => o?.[storageInitErrorSymbol])) throw new Error(
|
||||
"An error occured while initializing the storage",
|
||||
);
|
||||
|
||||
if (observables.some(o => !Observable.isObservable(o))) {
|
||||
throw new Error("Argument passed isn't an Observable");
|
||||
}
|
||||
|
||||
const [, forceUpdate] = React.useReducer(n => ~n, 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
|
||||
if (event === "DEL" && data.value === storage) return;
|
||||
forceUpdate();
|
||||
};
|
||||
const listener = () => forceUpdate();
|
||||
|
||||
emitter.on("SET", listener);
|
||||
emitter.on("DEL", listener);
|
||||
observables.forEach(o => Observable.observe(o, listener, opts));
|
||||
|
||||
return () => {
|
||||
emitter.off("SET", listener);
|
||||
emitter.off("DEL", listener);
|
||||
observables.forEach(o => Observable.unobserve(o, listener));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
|
||||
const data = await backend.get();
|
||||
const { proxy, emitter } = createProxy(data);
|
||||
|
||||
const handler = () => backend.set(proxy);
|
||||
emitter.on("SET", handler);
|
||||
emitter.on("DEL", handler);
|
||||
|
||||
return proxy;
|
||||
export async function updateStorage<T extends object = {}>(path: string, value: T): Promise<void> {
|
||||
_loadedStorage[path] = value;
|
||||
createFileBackend<T>(path).set(value);
|
||||
}
|
||||
|
||||
export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
|
||||
let awaited: any = undefined;
|
||||
export function createStorageAndCallback<T extends object = {}>(
|
||||
path: string,
|
||||
cb: (proxy: T) => void,
|
||||
{
|
||||
dflt = {} as T,
|
||||
nullIfEmpty = false
|
||||
} = {}
|
||||
) {
|
||||
let emitter: Emitter;
|
||||
|
||||
const awaitQueue: (() => void)[] = [];
|
||||
const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb));
|
||||
const callback = (data: any) => {
|
||||
const proxy = new Proxy(Observable.from(data), {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === Symbol.for("vendetta.storage.emitter")) {
|
||||
if (emitter) return emitter;
|
||||
emitter = new Emitter();
|
||||
|
||||
store.then(v => {
|
||||
awaited = v;
|
||||
awaitQueue.forEach(cb => cb());
|
||||
});
|
||||
Observable.observe(target, changes => {
|
||||
for (const change of changes) {
|
||||
emitter.emit(change.type !== "delete" ? "SET" : "DEL", {
|
||||
path: change.path,
|
||||
value: change.value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return new Proxy({} as Awaited<T>, {
|
||||
return emitter;
|
||||
}
|
||||
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
|
||||
const handler = () => backend.set(proxy);
|
||||
Observable.observe(proxy, handler);
|
||||
|
||||
cb(proxy);
|
||||
};
|
||||
|
||||
const backend = createFileBackend<T>(path);
|
||||
if (_loadedStorage[path]) {
|
||||
callback(_loadedStorage[path]);
|
||||
} else {
|
||||
backend.exists().then(async exists => {
|
||||
if (!exists) {
|
||||
if (nullIfEmpty) {
|
||||
callback(_loadedStorage[path] = null);
|
||||
} else {
|
||||
_loadedStorage[path] = dflt;
|
||||
await backend.set(dflt);
|
||||
callback(dflt);
|
||||
}
|
||||
} else {
|
||||
callback(_loadedStorage[path] = await backend.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type StorageOptions<T extends object> = Parameters<typeof createStorageAndCallback<T>>[2];
|
||||
|
||||
export async function createStorageAsync<T extends object = {}>(
|
||||
path: string,
|
||||
opts: StorageOptions<T> = {}
|
||||
): Promise<T> {
|
||||
return new Promise(r => createStorageAndCallback(path, r, opts));
|
||||
}
|
||||
|
||||
export const createStorage = <T extends object = {}>(
|
||||
path: string,
|
||||
opts: StorageOptions<T> = {}
|
||||
): T => {
|
||||
const promise = new Promise(r => resolvePromise = r);
|
||||
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
|
||||
|
||||
createStorageAndCallback(path, proxy => {
|
||||
awaited = proxy;
|
||||
resolved = true;
|
||||
resolvePromise();
|
||||
}, opts);
|
||||
|
||||
const check = () => {
|
||||
if (resolved) return true;
|
||||
throw new Error(`Attempted to access storage without initializing: ${path}`);
|
||||
};
|
||||
|
||||
return new Proxy({} as any, {
|
||||
...Object.fromEntries(
|
||||
Object.getOwnPropertyNames(Reflect)
|
||||
// @ts-expect-error
|
||||
.map(k => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)])
|
||||
.map(k => [k, (t: T, ...a: any[]) => {
|
||||
// @ts-expect-error
|
||||
return check() && Reflect[k](awaited, ...a);
|
||||
}])
|
||||
),
|
||||
get(target, prop, recv) {
|
||||
if (prop === syncAwaitSymbol) return awaitInit;
|
||||
return Reflect.get(awaited ?? target, prop, recv);
|
||||
if (prop === storageInitErrorSymbol) return error;
|
||||
if (prop === storagePromiseSymbol) return promise;
|
||||
return check() && Reflect.get(awaited ?? target, prop, recv);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export async function preloadStorageIfExists(path: string): Promise<boolean> {
|
||||
if (_loadedStorage[path]) return true;
|
||||
|
||||
const backend = createFileBackend(path);
|
||||
if (await backend.exists()) {
|
||||
_loadedStorage[path] = await backend.get();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function awaitStorage(...stores: any[]) {
|
||||
return Promise.all(
|
||||
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
|
||||
);
|
||||
export async function purgeStorage(path: string) {
|
||||
await removeFile(path);
|
||||
delete _loadedStorage[path];
|
||||
}
|
||||
|
||||
export {
|
||||
createFileBackend,
|
||||
createMMKVBackend,
|
||||
purgeStorage
|
||||
} from "@lib/api/storage/backends";
|
||||
export function awaitStorage(...proxies: any[]) {
|
||||
return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol]));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function getPreloadedStorage<T extends object = {}>(path: string): T | undefined {
|
||||
return _loadedStorage[path];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,205 +0,0 @@
|
|||
// New iteration of storage API, mostly yoinked from unreleased pyoncord (and sunrise?)
|
||||
import { fileExists, readFile, writeFile } from "@lib/api/native/fs";
|
||||
import { Emitter } from "@lib/utils/Emitter";
|
||||
|
||||
interface StorageBackend<T = unknown> {
|
||||
get: () => Promise<T>;
|
||||
set: (data: T) => Promise<void>;
|
||||
exists: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const emitterSymbol = Symbol.for("bunny.storage.emitter");
|
||||
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
|
||||
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
|
||||
|
||||
const _loadedPath = {} as Record<string, any>;
|
||||
|
||||
function createFileBackend<T = any>(filePath: string): StorageBackend<T> {
|
||||
return {
|
||||
get: async () => {
|
||||
try {
|
||||
return JSON.parse(await readFile(filePath));
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
|
||||
}
|
||||
},
|
||||
set: async data => {
|
||||
if (!data || typeof data !== "object") throw new Error("data needs to be an object");
|
||||
await writeFile(filePath, JSON.stringify(data));
|
||||
},
|
||||
exists: async () => {
|
||||
return await fileExists(filePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _createProxy(target: any, path: string[], emitter: Emitter): any {
|
||||
// cache the children proxies so proxy.child === proxy.child (yields the same value)
|
||||
const objChildrens = new WeakMap<object, object>();
|
||||
|
||||
return new Proxy(target, {
|
||||
get(target, prop: string) {
|
||||
if ((prop as unknown) === emitterSymbol) return emitter;
|
||||
|
||||
const newPath = [...path, prop];
|
||||
let value: any = target[prop];
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
const origValue = value;
|
||||
value = objChildrens.get(origValue);
|
||||
if (!value) {
|
||||
value = _createProxy(origValue, newPath, emitter);
|
||||
objChildrens.set(origValue, value!);
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
emitter.emit("GET", {
|
||||
path: newPath,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
set(target, prop: string, value) {
|
||||
target[prop] = value;
|
||||
emitter.emit("SET", {
|
||||
path: [...path, prop],
|
||||
value,
|
||||
});
|
||||
|
||||
// we do not care about success, if this actually does fail we have other problems
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteProperty(target, prop: string) {
|
||||
const success = delete target[prop];
|
||||
if (success)
|
||||
emitter.emit("DEL", {
|
||||
path: [...path, prop],
|
||||
});
|
||||
return success;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
|
||||
const emitter = new Emitter();
|
||||
|
||||
return {
|
||||
proxy: _createProxy(target, [], emitter),
|
||||
emitter,
|
||||
};
|
||||
}
|
||||
|
||||
export function useProxy<T>(storage: T & { [key: string | symbol]: any; }) {
|
||||
if (storage[storageInitErrorSymbol]) throw new Error(
|
||||
"An error occured while initializing the storage",
|
||||
{ cause: storage[storageInitErrorSymbol] }
|
||||
);
|
||||
|
||||
const emitter = storage[emitterSymbol] as Emitter;
|
||||
|
||||
if (emitter == null) {
|
||||
throw new Error(`InvalidArgumentException - storage[emitterSymbol] is ${typeof emitter}`);
|
||||
}
|
||||
|
||||
const [, forceUpdate] = React.useReducer(n => ~n, 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => forceUpdate();
|
||||
|
||||
emitter.on("SET", listener);
|
||||
emitter.on("DEL", listener);
|
||||
|
||||
return () => {
|
||||
emitter.off("SET", listener);
|
||||
emitter.off("DEL", listener);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
export async function updateStorageAsync<T>(path: string, value: T): Promise<void> {
|
||||
_loadedPath[path] = value;
|
||||
await createFileBackend<T>(path).set(value);
|
||||
}
|
||||
|
||||
export function createStorageAndCallback<T>(path: string, dflt = {} as T, cb: (proxy: T) => void) {
|
||||
const callback = (data: any) => {
|
||||
const { proxy, emitter } = createProxy(data);
|
||||
|
||||
const handler = () => backend.set(proxy);
|
||||
emitter.on("SET", handler);
|
||||
emitter.on("DEL", handler);
|
||||
|
||||
cb(proxy);
|
||||
};
|
||||
|
||||
const backend = createFileBackend<T>(path);
|
||||
if (_loadedPath[path]) callback(_loadedPath[path]);
|
||||
else {
|
||||
backend.exists().then(async exists => {
|
||||
if (!exists) {
|
||||
await backend.set(dflt);
|
||||
callback(dflt);
|
||||
} else {
|
||||
callback(await backend.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function createStorageAsync<T>(path: string, dflt = {} as T): Promise<T> {
|
||||
return new Promise(r => createStorageAndCallback(path, dflt, r));
|
||||
}
|
||||
|
||||
export const createStorage = <T>(path: string, dflt = {} as T): T & { [key: symbol]: any; } => {
|
||||
const promise = new Promise(r => resolvePromise = r);
|
||||
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
|
||||
|
||||
createStorageAndCallback(path, dflt, proxy => {
|
||||
awaited = proxy;
|
||||
resolved = true;
|
||||
resolvePromise();
|
||||
});
|
||||
|
||||
const check = () => {
|
||||
if (resolved) return true;
|
||||
throw new Error("Attempted to access storage without initializing");
|
||||
};
|
||||
|
||||
return new Proxy({} as any, {
|
||||
...Object.fromEntries(
|
||||
Object.getOwnPropertyNames(Reflect)
|
||||
.map(k => [k, (t: T, ...a: any[]) => {
|
||||
// @ts-expect-error
|
||||
return check() && Reflect[k](awaited, ...a);
|
||||
}])
|
||||
),
|
||||
get(target, prop, recv) {
|
||||
if (prop === storageInitErrorSymbol) return error;
|
||||
if (prop === storagePromiseSymbol) return promise;
|
||||
return check() && Reflect.get(awaited ?? target, prop, recv);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export async function preloadStorageIfExists(path: string) {
|
||||
if (_loadedPath[path]) return _loadedPath[path];
|
||||
|
||||
const backend = createFileBackend(path);
|
||||
if (await backend.exists()) {
|
||||
return _loadedPath[path] = await backend.get();
|
||||
}
|
||||
|
||||
console.log("no " + path);
|
||||
}
|
||||
|
||||
export function getPreloadedStorage<T>(path: string): T {
|
||||
return _loadedPath[path];
|
||||
}
|
||||
export function awaitStorage(...proxies: any[]) {
|
||||
return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol]));
|
||||
}
|
||||
|
|
@ -1,19 +1,27 @@
|
|||
import "../global.d.ts"; // eslint-disable-line import-alias/import-alias
|
||||
import "../modules.d.ts"; // eslint-disable-line import-alias/import-alias
|
||||
|
||||
export * as fonts from "./addons/fonts/index.js";
|
||||
export * as plugins from "./addons/plugins/index.js";
|
||||
export * as themes from "./addons/themes/index.js";
|
||||
export * as api from "./api";
|
||||
export * as fonts from "./fonts";
|
||||
export * as plugins from "./plugins";
|
||||
export * as themes from "./themes";
|
||||
export * as ui from "./ui";
|
||||
export * as utils from "./utils";
|
||||
export * as metro from "@metro";
|
||||
|
||||
import * as fonts from "./fonts";
|
||||
import * as plugins from "./plugins";
|
||||
import * as themes from "./themes";
|
||||
import * as fonts from "./addons/fonts/index.js";
|
||||
import * as plugins from "./addons/plugins/index.js";
|
||||
import * as themes from "./addons/themes/index.js";
|
||||
|
||||
/** @internal */
|
||||
export * as _jsx from "react/jsx-runtime";
|
||||
|
||||
import { proxyLazy } from "./utils/lazy";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @deprecated Moved to top level (bunny.*)
|
||||
*/
|
||||
export const managers = proxyLazy(() => {
|
||||
console.warn("bunny.managers.* is deprecated, and moved the top level (bunny.*). bunny.managers will be eventually removed soon");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { color } from "@lib/themes";
|
||||
import { color } from "@lib/addons/themes";
|
||||
import { constants } from "@metro/common";
|
||||
import { findByStoreNameLazy } from "@metro/wrappers";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { Nullish } from "@lib/utils/types";
|
||||
import { React } from "@metro/common";
|
||||
import { Button, LegacyFormText } from "@metro/common/components";
|
||||
import { Codeblock } from "@ui/components";
|
||||
import { createThemedStyleSheet } from "@ui/styles";
|
||||
import { ScrollView } from "react-native";
|
||||
import { Falsy, ScrollView } from "react-native";
|
||||
|
||||
type ErrorBoundaryState = {
|
||||
hasErr: false;
|
||||
|
|
@ -14,7 +13,7 @@ type ErrorBoundaryState = {
|
|||
};
|
||||
|
||||
export interface ErrorBoundaryProps {
|
||||
children: JSX.Element | Nullish | (JSX.Element | Nullish)[];
|
||||
children: JSX.Element | Falsy | (JSX.Element | Falsy)[];
|
||||
}
|
||||
|
||||
const styles = createThemedStyleSheet({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { ButtonColors } from "@lib/utils/types";
|
||||
import { LegacyAlert, LegacyFormInput } from "@metro/common/components";
|
||||
import { findByPropsLazy } from "@metro/wrappers";
|
||||
|
||||
|
|
@ -7,7 +6,7 @@ const Alerts = findByPropsLazy("openLazy", "close");
|
|||
export interface InputAlertProps {
|
||||
title?: string;
|
||||
confirmText?: string;
|
||||
confirmColor?: ButtonColors;
|
||||
confirmColor?: string;
|
||||
onConfirm: (input: string) => (void | Promise<void>);
|
||||
cancelText?: string;
|
||||
placeholder?: string;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * as wrappers from "./wrappers";
|
||||
export { default as Codeblock } from "@ui/components/Codeblock";
|
||||
export { default as ErrorBoundary } from "@ui/components/ErrorBoundary";
|
||||
export { default as Search } from "@ui/components/Search";
|
||||
|
|
|
|||
39
src/lib/ui/components/wrappers/AlertModal.tsx
Normal file
39
src/lib/ui/components/wrappers/AlertModal.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByFilePath, findByProps } from "@metro";
|
||||
import { Text } from "@metro/common/components";
|
||||
import { Button } from "@metro/common/types/components";
|
||||
import { ComponentProps, ComponentType, ReactNode } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
type ActionButtonProps = Omit<ComponentProps<Button>, "onPress"> & {
|
||||
onPress?: () => void | Promise<unknown>;
|
||||
};
|
||||
|
||||
const {
|
||||
AlertModal: _AlertModal,
|
||||
AlertActionButton: _AlertActionButton
|
||||
} = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
|
||||
|
||||
export const AlertActionButton = _AlertActionButton as ComponentType<ActionButtonProps>;
|
||||
|
||||
export default function AlertModal(props: Record<string, unknown>) {
|
||||
const forwardFailedModal = findByFilePath("modules/forwarding/native/ForwardFailedAlertModal.tsx");
|
||||
|
||||
// ponyfill for extraContent
|
||||
if (!forwardFailedModal && "extraContent" in props) {
|
||||
props.content = (
|
||||
<View style={{ gap: 16 }}>
|
||||
<Text variant="text-md/medium" color="text-muted">
|
||||
{props.content as string}
|
||||
</Text>
|
||||
<View>
|
||||
{props.extraContent as ReactNode}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
delete props.extraContent;
|
||||
}
|
||||
|
||||
return <_AlertModal {...props} />;
|
||||
}
|
||||
3
src/lib/ui/components/wrappers/index.ts
Normal file
3
src/lib/ui/components/wrappers/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import AlertModal, { AlertActionButton } from "./AlertModal";
|
||||
|
||||
export { AlertActionButton, AlertModal };
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
export * as alerts from "./alerts";
|
||||
export * as color from "./color";
|
||||
export * as components from "./components";
|
||||
export * as settings from "./settings";
|
||||
export * as sheets from "./sheets";
|
||||
export * as styles from "./styles";
|
||||
export * as toasts from "./toasts";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { DeviceManager } from "@lib/api/native/modules";
|
|||
import { after } from "@lib/api/patcher";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { ButtonColors } from "@lib/utils/types";
|
||||
import { Button, CompatButton, SafeAreaView } from "@metro/common/components";
|
||||
import { _lazyContextSymbol } from "@metro/lazy";
|
||||
import { LazyModuleContext } from "@metro/types";
|
||||
|
|
@ -91,7 +90,7 @@ export default () => after.await("render", getErrorBoundaryContext(), function (
|
|||
const buttons: Button[] = [
|
||||
{ text: Strings.RELOAD_DISCORD, onPress: this.handleReload },
|
||||
...!settings.safeMode?.enabled ? [{ text: Strings.RELOAD_IN_SAFE_MODE, onPress: toggleSafeMode }] : [],
|
||||
{ text: Strings.RETRY_RENDER, color: ButtonColors.RED, onPress: () => this.setState({ info: null, error: null }) },
|
||||
{ text: Strings.RETRY_RENDER, color: "red", onPress: () => this.setState({ info: null, error: null }) },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -130,7 +129,7 @@ export default () => after.await("render", getErrorBoundaryContext(), function (
|
|||
|
||||
return <CompatButton
|
||||
text={button.text}
|
||||
color={button.color ?? ButtonColors.BRAND}
|
||||
color={button.color ?? "brand"}
|
||||
size={button.size ?? "small"}
|
||||
onPress={button.onPress}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface RowConfig {
|
|||
render?: Parameters<typeof lazy>[0];
|
||||
icon?: ImageURISource | number;
|
||||
usePredicate?: () => boolean,
|
||||
useTrailing?: () => string | JSX.Element,
|
||||
rawTabsConfig?: Record<string, any>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { after } from "@lib/api/patcher";
|
||||
import { findInReactTree } from "@lib/utils";
|
||||
import { i18n, NavigationNative } from "@metro/common";
|
||||
import { LegacyFormIcon, LegacyFormRow, LegacyFormSection } from "@metro/common/components";
|
||||
import { LegacyFormDivider,LegacyFormIcon, LegacyFormRow, LegacyFormSection } from "@metro/common/components";
|
||||
import { findByNameLazy } from "@metro/wrappers";
|
||||
import { registeredSections } from "@ui/settings";
|
||||
|
||||
|
|
@ -12,16 +12,19 @@ function SettingsSection() {
|
|||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
return <>
|
||||
{Object.keys(registeredSections).map(sect => (
|
||||
{Object.keys(registeredSections).map(sect => registeredSections[sect].length > 0 && (
|
||||
<LegacyFormSection key={sect} title={sect}>
|
||||
{ /** Is usePredicate here safe? */}
|
||||
{registeredSections[sect].filter(r => r.usePredicate?.() ?? true).map(row => (
|
||||
<LegacyFormRow
|
||||
label={row.title()}
|
||||
leading={<LegacyFormIcon source={row.icon} />}
|
||||
trailing={LegacyFormRow.Arrow}
|
||||
onPress={wrapOnPress(row.onPress, navigation, row.render, row.title())}
|
||||
/>
|
||||
{registeredSections[sect].filter(r => r.usePredicate?.() ?? true).map((row, i, arr) => (
|
||||
<>
|
||||
<LegacyFormRow
|
||||
label={row.title()}
|
||||
leading={<LegacyFormIcon source={row.icon} />}
|
||||
trailing={<LegacyFormRow.Arrow label={row.useTrailing?.() || undefined} />}
|
||||
onPress={wrapOnPress(row.onPress, navigation, row.render, row.title())}
|
||||
/>
|
||||
{i !== arr.length - 1 && <LegacyFormDivider />}
|
||||
</>
|
||||
))}
|
||||
</LegacyFormSection>
|
||||
))}
|
||||
|
|
@ -55,7 +58,7 @@ export function patchPanelUI(unpatches: (() => void | boolean)[]) {
|
|||
const sections = findInReactTree(
|
||||
res.props.children,
|
||||
n => n?.children?.[1]?.type === LegacyFormSection
|
||||
)?.children;
|
||||
)?.children || res.props.children;
|
||||
|
||||
if (sections) {
|
||||
const index = sections.findIndex((c: any) => titles.includes(c?.props.label));
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
|||
title: row.title,
|
||||
icon: row.icon,
|
||||
usePredicate: row.usePredicate,
|
||||
useTrailing: row.useTrailing,
|
||||
onPress: wrapOnPress(row.onPress, null, row.render, row.title()),
|
||||
withArrow: true,
|
||||
...row.rawTabsConfig
|
||||
|
|
|
|||
|
|
@ -1,163 +1,3 @@
|
|||
import { OptionalKeys } from "@lib/utils/types";
|
||||
import { LiteralUnion } from "type-fest";
|
||||
|
||||
export type DiscordTextStyles = OptionalKeys<[
|
||||
"heading-sm/normal",
|
||||
"heading-sm/medium",
|
||||
"heading-sm/semibold",
|
||||
"heading-sm/bold",
|
||||
"heading-sm/extrabold",
|
||||
"heading-md/normal",
|
||||
"heading-md/medium",
|
||||
"heading-md/semibold",
|
||||
"heading-md/bold",
|
||||
"heading-md/extrabold",
|
||||
"heading-lg/normal",
|
||||
"heading-lg/medium",
|
||||
"heading-lg/semibold",
|
||||
"heading-lg/bold",
|
||||
"heading-lg/extrabold",
|
||||
"heading-xl/normal",
|
||||
"heading-xl/medium",
|
||||
"heading-xl/semibold",
|
||||
"heading-xl/bold",
|
||||
"heading-xl/extrabold",
|
||||
"heading-xxl/normal",
|
||||
"heading-xxl/medium",
|
||||
"heading-xxl/semibold",
|
||||
"heading-xxl/bold",
|
||||
"heading-xxl/extrabold",
|
||||
"eyebrow",
|
||||
"heading-deprecated-12/normal",
|
||||
"heading-deprecated-12/medium",
|
||||
"heading-deprecated-12/semibold",
|
||||
"heading-deprecated-12/bold",
|
||||
"heading-deprecated-12/extrabold",
|
||||
"redesign/heading-18/bold",
|
||||
"text-xxs/normal",
|
||||
"text-xxs/medium",
|
||||
"text-xxs/semibold",
|
||||
"text-xxs/bold",
|
||||
"text-xs/normal",
|
||||
"text-xs/medium",
|
||||
"text-xs/semibold",
|
||||
"text-xs/bold",
|
||||
"text-sm/normal",
|
||||
"text-sm/medium",
|
||||
"text-sm/semibold",
|
||||
"text-sm/bold",
|
||||
"text-md/normal",
|
||||
"text-md/medium",
|
||||
"text-md/semibold",
|
||||
"text-md/bold",
|
||||
"text-lg/normal",
|
||||
"text-lg/medium",
|
||||
"text-lg/semibold",
|
||||
"text-lg/bold",
|
||||
"redesign/message-preview/normal",
|
||||
"redesign/message-preview/medium",
|
||||
"redesign/message-preview/semibold",
|
||||
"redesign/message-preview/bold",
|
||||
"redesign/channel-title/normal",
|
||||
"redesign/channel-title/medium",
|
||||
"redesign/channel-title/semibold",
|
||||
"redesign/channel-title/bold",
|
||||
"display-sm",
|
||||
"display-md",
|
||||
"display-lg"
|
||||
]>;
|
||||
|
||||
export type RedesignModuleKeys = OptionalKeys<[
|
||||
"AlertActionButton",
|
||||
"AlertModal",
|
||||
"AlertModalContainer",
|
||||
"AvatarDuoPile",
|
||||
"AvatarPile",
|
||||
"BACKDROP_OPAQUE_MAX_OPACITY",
|
||||
"Backdrop",
|
||||
"Button",
|
||||
"Card",
|
||||
"ContextMenu",
|
||||
"ContextMenuContainer",
|
||||
"FauxHeader",
|
||||
"FloatingActionButton",
|
||||
"GhostInput",
|
||||
"GuildIconPile",
|
||||
"HeaderActionButton",
|
||||
"HeaderButton",
|
||||
"HeaderSubmittingIndicator",
|
||||
"IconButton",
|
||||
"Input",
|
||||
"InputButton",
|
||||
"InputContainer",
|
||||
"LayerContext",
|
||||
"LayerScope",
|
||||
"Modal",
|
||||
"ModalActionButton",
|
||||
"ModalContent",
|
||||
"ModalDisclaimer",
|
||||
"ModalFloatingAction",
|
||||
"ModalFloatingActionSpacer",
|
||||
"ModalFooter",
|
||||
"ModalScreen",
|
||||
"ModalStepIndicator",
|
||||
"NAV_BAR_HEIGHT",
|
||||
"NAV_BAR_HEIGHT_MULTILINE",
|
||||
"Navigator",
|
||||
"NavigatorHeader",
|
||||
"NavigatorScreen",
|
||||
"Pile",
|
||||
"PileOverflow",
|
||||
"RedesignCompat",
|
||||
"RedesignCompatContext",
|
||||
"RowButton",
|
||||
"STATUS_BAR_HEIGHT",
|
||||
"SceneLoadingIndicator",
|
||||
"SearchField",
|
||||
"SegmentedControl",
|
||||
"SegmentedControlPages",
|
||||
"Slider",
|
||||
"Stack",
|
||||
"StepModal",
|
||||
"StickyContext",
|
||||
"StickyHeader",
|
||||
"StickyWrapper",
|
||||
"TABLE_ROW_CONTENT_HEIGHT",
|
||||
"TABLE_ROW_HEIGHT",
|
||||
"TableCheckboxRow",
|
||||
"TableRadioGroup",
|
||||
"TableRadioRow",
|
||||
"TableRow",
|
||||
"TableRowGroup",
|
||||
"TableRowGroupTitle",
|
||||
"TableRowIcon",
|
||||
"TableSwitchRow",
|
||||
"Tabs",
|
||||
"TextArea",
|
||||
"TextField",
|
||||
"TextInput",
|
||||
"Toast",
|
||||
"dismissAlerts",
|
||||
"getHeaderBackButton",
|
||||
"getHeaderCloseButton",
|
||||
"getHeaderConditionalBackButton",
|
||||
"getHeaderNoTitle",
|
||||
"getHeaderTextButton",
|
||||
"hideContextMenu",
|
||||
"navigatorShouldCrossfade",
|
||||
"openAlert",
|
||||
"useAccessibilityNativeStackOptions",
|
||||
"useAndroidNavScrim",
|
||||
"useCoachmark",
|
||||
"useFloatingActionButtonScroll",
|
||||
"useFloatingActionButtonState",
|
||||
"useNativeStackNavigation",
|
||||
"useNavigation",
|
||||
"useNavigationTheme",
|
||||
"useNavigatorBackPressHandler",
|
||||
"useNavigatorScreens",
|
||||
"useNavigatorShouldCrossfade",
|
||||
"useSegmentedControlState",
|
||||
"useStackNavigation",
|
||||
"useTabNavigation",
|
||||
"useTooltip"
|
||||
]>;
|
||||
export type DiscordTextStyles = LiteralUnion<"heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "eyebrow" | "heading-deprecated-12/normal" | "heading-deprecated-12/medium" | "heading-deprecated-12/semibold" | "heading-deprecated-12/bold" | "heading-deprecated-12/extrabold" | "redesign/heading-18/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "redesign/message-preview/normal" | "redesign/message-preview/medium" | "redesign/message-preview/semibold" | "redesign/message-preview/bold" | "redesign/channel-title/normal" | "redesign/channel-title/medium" | "redesign/channel-title/semibold" | "redesign/channel-title/bold" | "display-sm" | "display-md" | "display-lg", string>;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ export const DISCORD_SERVER = "https://discord.com/invite/ddcQf3s2Uq";
|
|||
export const GITHUB = "https://github.com/revenge-mod";
|
||||
export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
|
||||
export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
|
||||
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
|
||||
|
||||
export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
|
||||
export const OLD_BUNNY_PROXY_PREFIX = "https://bunny-mod.github.io/plugins-proxy";
|
||||
|
||||
export const OFFICIAL_PLUGINS_REPO_URL = "https://bn-plugins.github.io/dist/repo.json";
|
||||
|
||||
// These are still Vendetta's
|
||||
export const DISCORD_SERVER_ID = "1015931589865246730";
|
||||
export const PLUGINS_CHANNEL_ID = "1091880384561684561";
|
||||
export const THEMES_CHANNEL_ID = "1091880434939482202";
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
|
||||
// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
|
||||
// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
|
||||
export function cyrb64(str: string, seed = 0) {
|
||||
export default function cyrb64(str: string, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { findInTree } from "@lib/utils";
|
||||
import { SearchFilter } from "@lib/utils/findInTree";
|
||||
|
||||
export const findInReactTree = (tree: { [key: string]: any; }, filter: SearchFilter): any => findInTree(tree, filter, {
|
||||
walkable: ["props", "children", "child", "sibling"],
|
||||
});
|
||||
export default function findInReactTree(tree: { [key: string]: any; }, filter: SearchFilter): any {
|
||||
return findInTree(tree, filter, {
|
||||
walkable: ["props", "children", "child", "sibling"],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
|
|||
|
||||
try {
|
||||
if (filter(tree)) return tree;
|
||||
} catch {}
|
||||
} catch { }
|
||||
|
||||
if (Array.isArray(tree)) {
|
||||
for (const item of tree) {
|
||||
|
|
@ -24,7 +24,7 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
|
|||
try {
|
||||
const found = treeSearch(item, filter, opts, depth + 1);
|
||||
if (found) return found;
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
} else if (typeof tree === "object") {
|
||||
for (const key of Object.keys(tree)) {
|
||||
|
|
@ -35,12 +35,12 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
|
|||
try {
|
||||
const found = treeSearch(tree[key], filter, opts, depth + 1);
|
||||
if (found) return found;
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const findInTree = (
|
||||
export default function findInTree(
|
||||
tree: SearchTree,
|
||||
filter: SearchFilter,
|
||||
{
|
||||
|
|
@ -48,4 +48,6 @@ export const findInTree = (
|
|||
ignore = [],
|
||||
maxDepth = 100
|
||||
}: FindInTreeOptions = {},
|
||||
): any | undefined => treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0);
|
||||
): any | undefined {
|
||||
return treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { LiteralUnion } from "type-fest";
|
|||
|
||||
type KeyOfOrAny<P, T extends object> = P extends keyof T ? T[P] : any;
|
||||
|
||||
export function hookDefineProperty<
|
||||
export default function hookDefineProperty<
|
||||
T extends object,
|
||||
P extends LiteralUnion<keyof T, PropertyKey>
|
||||
>(target: T, property: LiteralUnion<keyof T, PropertyKey>, cb: (val: KeyOfOrAny<P, T>) => KeyOfOrAny<P, T>) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
export * as constants from "./constants";
|
||||
export { cyrb64 } from "./cyrb64";
|
||||
export { Emitter } from "./Emitter";
|
||||
export { findInReactTree } from "./findInReactTree";
|
||||
export { findInTree } from "./findInTree";
|
||||
export { hookDefineProperty } from "./hookDefineProperty";
|
||||
export { default as invariant } from "./invariant";
|
||||
export * as lazy from "./lazy";
|
||||
export * as logger from "./logger";
|
||||
export { safeFetch } from "./safeFetch";
|
||||
import * as constants from "./constants";
|
||||
import cyrb64 from "./cyrb64";
|
||||
import findInReactTree from "./findInReactTree";
|
||||
import findInTree from "./findInTree";
|
||||
import hookDefineProperty from "./hookDefineProperty";
|
||||
import invariant from "./invariant";
|
||||
import * as lazy from "./lazy";
|
||||
import * as logger from "./logger";
|
||||
import safeFetch from "./safeFetch";
|
||||
|
||||
export {
|
||||
constants,
|
||||
cyrb64,
|
||||
findInReactTree,
|
||||
findInTree,
|
||||
hookDefineProperty,
|
||||
invariant,
|
||||
lazy,
|
||||
logger,
|
||||
safeFetch };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { findByNameLazy } from "@metro/wrappers";
|
||||
|
||||
export type LoggerFunction = (...messages: any[]) => void;
|
||||
type LoggerFunction = (...messages: any[]) => void;
|
||||
export interface Logger {
|
||||
log: LoggerFunction;
|
||||
info: LoggerFunction;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// A really basic fetch wrapper which throws on non-ok response codes
|
||||
export async function safeFetch(input: RequestInfo | URL, options?: RequestInit, timeout = 10000) {
|
||||
export default async function safeFetch(input: RequestInfo | URL, options?: RequestInit, timeout = 10000) {
|
||||
const req = await fetch(input, {
|
||||
signal: timeoutSignal(timeout),
|
||||
...options
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import { LiteralUnion } from "type-fest";
|
||||
|
||||
export type Nullish = null | undefined;
|
||||
|
||||
export type OptionalKeys<T extends Array<string>> = LiteralUnion<T[number], string>;
|
||||
|
||||
export interface Author {
|
||||
name: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export enum ButtonColors {
|
||||
BRAND = "brand",
|
||||
RED = "red",
|
||||
GREEN = "green",
|
||||
PRIMARY = "primary",
|
||||
TRANSPARENT = "transparent",
|
||||
GREY = "grey",
|
||||
LIGHTGREY = "lightgrey",
|
||||
WHITE = "white",
|
||||
LINK = "link"
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import { findByFilePathLazy, findByProps, findByPropsLazy } from "@metro/wrapper
|
|||
|
||||
import type { Dispatcher } from "./types/flux";
|
||||
|
||||
export * as components from "./components";
|
||||
|
||||
// Discord
|
||||
export const constants = findByPropsLazy("Fonts", "Permissions");
|
||||
export const channels = findByPropsLazy("getVoiceChannelId");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Nullish } from "@lib/utils/types";
|
||||
import { DiscordTextStyles } from "@ui/types";
|
||||
import { MutableRefObject, ReactNode, RefObject } from "react";
|
||||
import type * as RN from "react-native";
|
||||
|
|
@ -25,6 +24,7 @@ interface ButtonProps {
|
|||
scaleAmountInPx?: number;
|
||||
icon?: ImageSourcePropType | ReactNode;
|
||||
style?: Style;
|
||||
grow?: boolean;
|
||||
}
|
||||
|
||||
export type Button = React.ForwardRefExoticComponent<ButtonProps>;
|
||||
|
|
@ -103,7 +103,7 @@ interface TextInputProps extends Omit<RN.TextInputProps, "onChange" | "onChangeT
|
|||
trailingIcon?: React.FC<any>;
|
||||
trailingPressableProps?: PressableProps;
|
||||
trailingText?: string;
|
||||
value?: string | Nullish;
|
||||
value?: string | RN.Falsy;
|
||||
}
|
||||
|
||||
export type TextInput = React.FC<TextInputProps>;
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
|
|||
patchedNativeComponentRegistry = true;
|
||||
}
|
||||
|
||||
|
||||
// Hook DeveloperExperimentStore
|
||||
if (moduleExports?.default?.constructor?.displayName === "DeveloperExperimentStore") {
|
||||
moduleExports.default = new Proxy(moduleExports.default, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Nullish } from "@types";
|
||||
type Nullish = null | undefined;
|
||||
|
||||
/** @see {@link https://github.com/facebook/metro/blob/c2d7539dfc10aacb2f99fcc2f268a3b53e867a90/packages/metro-runtime/src/polyfills/require.js} */
|
||||
export namespace Metro {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue