diff --git a/scripts/build.mjs b/scripts/build.mjs index 532fd97..f85c88c 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -25,6 +25,7 @@ const { let context = null; +/** @type {import("esbuild").BuildOptions} */ const config = { entryPoints: ["src/entry.ts"], bundle: true, diff --git a/scripts/serve.mjs b/scripts/serve.mjs index addcfd8..05febd3 100644 --- a/scripts/serve.mjs +++ b/scripts/serve.mjs @@ -80,7 +80,7 @@ if (args.adb && isADBAvailableAndAppInstalled()) { if (key.name === "r") { console.info(chalk.yellow(`${chalk.bold("↻ Reloading")} ${packageName}`)); - restartAppFromADB(server.port) + restartAppFromADB(server.address().port) .then(() => console.info(chalk.greenBright(`${chalk.bold("✔ Executed")} reload command`))) .catch(e => console.error(e)); } diff --git a/src/core/debug/safeMode.ts b/src/core/debug/safeMode.ts new file mode 100644 index 0000000..f44bf06 --- /dev/null +++ b/src/core/debug/safeMode.ts @@ -0,0 +1,17 @@ +import { getCurrentTheme, writeThemeToNative } from "@lib/addons/themes"; +import { BundleUpdaterManager } from "@lib/api/native/modules"; +import { settings } from "@lib/api/settings"; + +export function isSafeMode() { + return settings.safeMode?.enabled === true; +} + +export async function toggleSafeMode({ + to = !isSafeMode(), + reload = true +} = {}) { + const enabled = (settings.safeMode ??= { enabled: to }).enabled = to; + const currentColor = getCurrentTheme(); + await writeThemeToNative(enabled ? {} : currentColor?.data ?? {}); + if (reload) setTimeout(() => BundleUpdaterManager.reload(), 500); +} diff --git a/src/core/ui/components/AddonPage.tsx b/src/core/ui/components/AddonPage.tsx index f2c5d4c..6d66b32 100644 --- a/src/core/ui/components/AddonPage.tsx +++ b/src/core/ui/components/AddonPage.tsx @@ -1,28 +1,28 @@ 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 AlertModal, { AlertActionButton } from "@lib/ui/components/wrappers/AlertModal"; +import { dismissAlert, openAlert } from "@lib/ui/alerts"; +import { showSheet } from "@lib/ui/sheets"; import isValidHttpUrl from "@lib/utils/isValidHttpUrl"; import { lazyDestructure } from "@lib/utils/lazy"; import { findByProps } from "@metro"; -import { clipboard } from "@metro/common"; -import { Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput, useSafeAreaInsets } from "@metro/common/components"; +import { clipboard, NavigationNative } from "@metro/common"; +import { AlertActionButton, AlertModal, Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput, useSafeAreaInsets } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; +import { isNotNil } from "es-toolkit"; import fuzzysort from "fuzzysort"; -import { ComponentType, ReactNode, useCallback, useMemo } from "react"; +import { ComponentType, ReactNode, useCallback, useEffect, useMemo } from "react"; import { Image, ScrollView, View } from "react-native"; const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet")); -const { openAlert, dismissAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert")); -type SearchKeywords = Array string)>; +type SearchKeywords = Array string)>; interface AddonPageProps { title: string; items: I[]; - searchKeywords: SearchKeywords; - sortOptions?: Record number>; + searchKeywords: SearchKeywords; + sortOptions?: Record number>; resolveItem?: (value: I) => T | undefined; safeModeHint?: { message?: string; @@ -34,6 +34,9 @@ interface AddonPageProps { fetchFn?: (url: string) => Promise; onPress?: () => void; }; + + OptionsActionSheetComponent?: ComponentType; + CardComponent: ComponentType>; ListHeaderComponent?: ComponentType; ListFooterComponent?: ComponentType; @@ -55,7 +58,7 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise Promise({ CardComponent, ...props }: AddonPageProps) { - useProxy(settings); - const [search, setSearch] = React.useState(""); - const [sortFn, setSortFn] = React.useState<((a: unknown, b: unknown) => number) | null>(() => null); + const [sortFn, setSortFn] = React.useState<((a: T, b: T) => number) | null>(() => null); const { bottom: bottomInset } = useSafeAreaInsets(); + const navigation = NavigationNative.useNavigation(); + + useEffect(() => { + if (props.OptionsActionSheetComponent) { + navigation.setOptions({ + headerRight: () => showSheet("AddonMoreSheet", props.OptionsActionSheetComponent!)} + /> + }); + } + }, [navigation]); const results = useMemo(() => { let values = props.items; - if (props.resolveItem) values = values.map(props.resolveItem); - const items = values.filter(i => i && typeof i === "object"); + if (props.resolveItem) values = values.map(props.resolveItem).filter(isNotNil); + const items = values.filter(i => isNotNil(i) && typeof i === "object"); if (!search && sortFn) items.sort(sortFn); return fuzzysort.go(search, items, { keys: props.searchKeywords, all: true }); @@ -135,7 +150,7 @@ export default function AddonPage({ CardComponent, ...props }: if (results.length === 0 && !search) { return - + Oops! Nothing to see here… yet! @@ -176,7 +191,7 @@ export default function AddonPage({ CardComponent, ...props }: })} />} - {props.ListHeaderComponent && !search && } + {props.ListHeaderComponent && } ); @@ -188,12 +203,12 @@ export default function AddonPage({ CardComponent, ...props }: estimatedItemSize={136} ListHeaderComponent={headerElement} ListEmptyComponent={() => - + Hmmm... could not find that! } - contentContainerStyle={{ padding: 8, paddingHorizontal: 12 }} + contentContainerStyle={{ padding: 8, paddingHorizontal: 12, paddingBottom: 90 }} ItemSeparatorComponent={() => } ListFooterComponent={props.ListFooterComponent} renderItem={({ item }: any) => } diff --git a/src/core/ui/hooks/useFS.ts b/src/core/ui/hooks/useFS.ts index 3577450..6de18c3 100644 --- a/src/core/ui/hooks/useFS.ts +++ b/src/core/ui/hooks/useFS.ts @@ -11,7 +11,7 @@ export enum CheckState { export function useFileExists(path: string, prefix?: string): [CheckState, typeof fs] { const [state, setState] = useState(CheckState.LOADING); - const check = () => fs.fileExists(path, prefix) + const check = () => fs.fileExists(path, { prefix }) .then(exists => setState(exists ? CheckState.TRUE : CheckState.FALSE)) .catch(() => setState(CheckState.ERROR)); diff --git a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx index 0b32115..aec2901 100644 --- a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx +++ b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx @@ -1,11 +1,13 @@ import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay"; -import { assetsMap } from "@lib/api/assets"; +import { iterateAssets } from "@lib/api/assets"; import { LegacyFormDivider } from "@metro/common/components"; import { ErrorBoundary, Search } from "@ui/components"; +import { useMemo } from "react"; import { FlatList, View } from "react-native"; export default function AssetBrowser() { const [search, setSearch] = React.useState(""); + const all = useMemo(() => Array.from(iterateAssets()), []); return ( @@ -15,7 +17,7 @@ export default function AssetBrowser() { onChangeText={(v: string) => setSearch(v)} /> a.name.includes(search) || a.id.toString() === search)} + data={all.filter(a => a.name.includes(search) || a.id.toString() === search)} renderItem={({ item }) => } ItemSeparatorComponent={LegacyFormDivider} keyExtractor={item => item.name} diff --git a/src/core/ui/settings/pages/General/About.tsx b/src/core/ui/settings/pages/General/About.tsx index 35ec8c8..2000c39 100644 --- a/src/core/ui/settings/pages/General/About.tsx +++ b/src/core/ui/settings/pages/General/About.tsx @@ -1,4 +1,5 @@ import { Strings } from "@core/i18n"; +import { PyoncordIcon } from "@core/ui/settings"; import Version from "@core/ui/settings/pages/General/Version"; import { useProxy } from "@core/vendetta/storage"; import { getDebugInfo } from "@lib/api/debug"; @@ -14,7 +15,7 @@ export default function About() { { label: Strings.BUNNY, version: debugInfo.bunny.version, - icon: "ic_progress_wrench_24px", + icon: { uri: PyoncordIcon }, }, { label: "Discord", @@ -24,17 +25,17 @@ export default function About() { { label: "React", version: debugInfo.react.version, - icon: "ic_category_16px", + icon: "ScienceIcon", }, { label: "React Native", version: debugInfo.react.nativeVersion, - icon: "mobile", + icon: "MobilePhoneIcon", }, { label: Strings.BYTECODE, version: debugInfo.hermes.bytecodeVersion, - icon: "ic_server_security_24px", + icon: "TopicsIcon", }, ]; @@ -42,37 +43,37 @@ export default function About() { { label: Strings.LOADER, version: `${debugInfo.bunny.loader.name} (${debugInfo.bunny.loader.version})`, - icon: "ic_download_24px", + icon: "DownloadIcon", }, { label: Strings.OPERATING_SYSTEM, version: `${debugInfo.os.name} ${debugInfo.os.version}`, - icon: "ic_cog_24px" + icon: "ScreenIcon" }, ...(debugInfo.os.sdk ? [{ label: "SDK", version: debugInfo.os.sdk, - icon: "pencil" + icon: "StaffBadgeIcon" }] : []), { label: Strings.MANUFACTURER, version: debugInfo.device.manufacturer, - icon: "ic_badge_staff" + icon: "WrenchIcon" }, { label: Strings.BRAND, version: debugInfo.device.brand, - icon: "ic_settings_boost_24px" + icon: "SparklesIcon" }, { label: Strings.MODEL, version: debugInfo.device.model, - icon: "ic_phonelink_24px" + icon: "MobilePhoneIcon" }, { label: Platform.select({ android: Strings.CODENAME, ios: Strings.MACHINE_ID })!, version: debugInfo.device.codename, - icon: "ic_compose_24px" + icon: "TagIcon" } ]; diff --git a/src/core/ui/settings/pages/General/Version.tsx b/src/core/ui/settings/pages/General/Version.tsx index 070a90f..d392d4c 100644 --- a/src/core/ui/settings/pages/General/Version.tsx +++ b/src/core/ui/settings/pages/General/Version.tsx @@ -2,18 +2,19 @@ import { findAssetId } from "@lib/api/assets"; import { clipboard } from "@metro/common"; import { LegacyFormText, TableRow } from "@metro/common/components"; import { showToast } from "@ui/toasts"; +import { ImageURISource } from "react-native"; interface VersionProps { label: string; version: string; - icon: string; + icon: string | ImageURISource; } export default function Version({ label, version, icon }: VersionProps) { return ( } + icon={} trailing={{version}} onPress={() => { clipboard.setString(`${label} - ${version}`); diff --git a/src/core/ui/settings/pages/General/index.tsx b/src/core/ui/settings/pages/General/index.tsx index 6535325..c021a85 100644 --- a/src/core/ui/settings/pages/General/index.tsx +++ b/src/core/ui/settings/pages/General/index.tsx @@ -1,14 +1,17 @@ +import { isSafeMode, toggleSafeMode } from "@core/debug/safeMode"; 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 { getDebugInfo } from "@lib/api/debug"; +import { BundleUpdaterManager } from "@lib/api/native/modules"; import { settings } from "@lib/api/settings"; +import { openAlert } from "@lib/ui/alerts"; import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants"; import { NavigationNative, url } from "@metro/common"; -import { Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components"; -import { NativeModules, ScrollView } from "react-native"; +import { AlertActionButton, AlertActions, AlertModal, Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components"; +import { ScrollView } from "react-native"; export default function General() { useProxy(settings); @@ -27,14 +30,13 @@ export default function General() { /> } + icon={} trailing={} /> } - trailing={TableRow.Arrow} + icon={} onPress={() => navigation.push("BUNNY_CUSTOM_PAGE", { title: Strings.ABOUT, render: () => , @@ -43,33 +45,51 @@ export default function General() { } - trailing={TableRow.Arrow} + icon={} onPress={() => url.openDeeplink(DISCORD_SERVER)} /> } - trailing={TableRow.Arrow} + icon={} onPress={() => url.openURL(GITHUB)} /> } - onPress={() => NativeModules.BundleUpdaterManager.reload()} + icon={} + onPress={() => BundleUpdaterManager.reload()} /> - } - onPress={toggleSafeMode} + } + value={isSafeMode()} + onValueChange={(to: boolean) => { + toggleSafeMode({ to, reload: false }); + openAlert( + "bunny-reload-safe-mode", + + BundleUpdaterManager.reload()} + /> + + } + /> + ); + }} /> } + icon={} value={settings.developerSettings} onValueChange={(v: boolean) => { settings.developerSettings = v; @@ -80,7 +100,7 @@ export default function General() { } + icon={} value={settings.enableDiscordDeveloperSettings} onValueChange={(v: boolean) => { settings.enableDiscordDeveloperSettings = v; diff --git a/src/core/ui/settings/pages/PluginBrowser/index.tsx b/src/core/ui/settings/pages/PluginBrowser/index.tsx index c1665de..51f40ae 100644 --- a/src/core/ui/settings/pages/PluginBrowser/index.tsx +++ b/src/core/ui/settings/pages/PluginBrowser/index.tsx @@ -1,42 +1,24 @@ -import { installPlugin, isPluginInstalled, uninstallPlugin } from "@lib/addons/plugins"; -import { BunnyPluginManifest } from "@lib/addons/plugins/types"; +import { deleteRepository, installPlugin, isCorePlugin, isPluginInstalled, pluginRepositories, registeredPlugins, uninstallPlugin, updateAllRepository, updateRepository } from "@lib/addons/plugins"; +import { BunnyPluginManifestInternal } from "@lib/addons/plugins/types"; import { findAssetId } from "@lib/api/assets"; +import { dismissAlert, openAlert } from "@lib/ui/alerts"; +import { AlertActionButton } from "@lib/ui/components/wrappers"; +import { hideSheet, showSheet } from "@lib/ui/sheets"; import { showToast } from "@lib/ui/toasts"; -import { safeFetch } from "@lib/utils"; import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants"; -import { Button, Card, FlashList, IconButton, Stack, Text } from "@metro/common/components"; +import isValidHttpUrl from "@lib/utils/isValidHttpUrl"; +import { clipboard, NavigationNative } from "@metro/common"; +import { ActionSheet, AlertActions, AlertModal, Button, Card, FlashList, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput } from "@metro/common/components"; import { QueryClient, QueryClientProvider, useMutation, useQuery } from "@tanstack/react-query"; -import { chunk } from "es-toolkit"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { View } from "react-native"; const queryClient = new QueryClient(); -async function arrayFromAsync(iterableOrArrayLike: AsyncIterable): Promise { - const arr: T[] = []; - for await (const element of iterableOrArrayLike) arr.push(element); - return arr; -} - -async function fetchManifest(repoURL: string, id: string) { - const url = new URL(`plugins/${id}/manifest.json`, repoURL); - const data = await safeFetch(url).then(d => d.json()); - - queryClient.setQueryData(["plugin-manifest-dist", { id }], data); - - return data as BunnyPluginManifest; -} - -async function* getManifests(repoUrl: string) { - const rawResponse = await safeFetch(repoUrl); - const pluginIds = Object.keys(await rawResponse.json()); - - for (const idChunks of chunk(pluginIds, 5)) { - const manifests = idChunks.map(id => fetchManifest(OFFICIAL_PLUGINS_REPO_URL, id)); - for (const manifest of manifests) { - yield await manifest; - } - } +async function getManifests() { + await updateAllRepository(); + const plugins = [...registeredPlugins.values()]; + return plugins.filter(p => !isCorePlugin(p.id)); } function InstallButton(props: { id: string; }) { @@ -67,7 +49,15 @@ function TrailingButtons(props: { id: string; }) { return { }} + onPress={() => { + showSheet("plugin-info", () => { + return + + + + ; + }); + }} variant="secondary" icon={findAssetId("CircleInformationIcon")} /> @@ -75,46 +65,52 @@ function TrailingButtons(props: { id: string; }) { ; } -function PluginCard(props: { repoUrl: string, id: string, manifest: BunnyPluginManifest; }) { - const { isPending, error, data: plugin } = useQuery({ - queryKey: ["plugin-manifest-dist", { id: props.id }], - queryFn: () => fetchManifest(props.repoUrl, props.id) - }); +function PluginCard(props: { manifest: BunnyPluginManifestInternal; }) { + const { display, version } = props.manifest; return ( - {!plugin && - - {isPending && "Loading..."} - {error && `An error has occured while fetching plugin: ${error.message}`} - - } - {plugin && + - {plugin.name} + {display.name} - by {plugin.authors.map(a => typeof a === "string" ? a : a.name).join(", ")} + by {display.authors?.map(a => a.name).join(", ") || "Unknown"} ({version}) - + - {plugin.description} + {display.description} - } + ); } function BrowserPage() { + const navigation = NavigationNative.useNavigation(); + useEffect(() => { + navigation.setOptions({ + title: "Plugin Browser", + headerRight: () => { + showSheet("plugin-browser-options", PluginBrowserOptions); + }} + /> + }); + }, [navigation]); + const { data, error, isPending, refetch } = useQuery({ queryKey: ["plugins-repo-fetch"], - queryFn: () => arrayFromAsync(getManifests(OFFICIAL_PLUGINS_REPO_URL)) + queryFn: () => getManifests() }); if (error) { @@ -149,12 +145,108 @@ function BrowserPage() { contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }} renderItem={({ item: manifest }: any) => ( - + )} />; } +function AddRepositoryAlert() { + const [value, setValue] = useState(""); + + return } + actions={ + { + try { + await updateRepository(value); + showToast("Added repository!", findAssetId("Check")); + } catch (e) { + showToast("Failed to add repository!", findAssetId("Small")); + } finally { + dismissAlert("bunny-add-plugin-repository"); + showSheet("plugin-browser-options", PluginBrowserOptions); + } + }} /> + } />; +} + +function PluginBrowserOptions() { + return + + {Object.keys(pluginRepositories).map(url => { + return ; + })} + } + onPress={() => { + openAlert("bunny-add-plugin-repository", ); + hideSheet("plugin-browser-options"); + }} /> + + ; +} + +function RepositoryRow(props: { url: string; }) { + const repo = pluginRepositories[props.url]; + const isOfficial = props.url === OFFICIAL_PLUGINS_REPO_URL; + + return ( + + { + clipboard.setString(props.url); + showToast.showCopyToClipboard(); + }} + /> + { + openAlert("bunny-remove-repository", + {props.url} + } + actions={ + { + await deleteRepository(props.url); + showToast("Removed repository!", findAssetId("Trash")); + dismissAlert("bunny-remove-repository"); + }} + /> + } + />); + }} + /> + + )} /> + ); +} + export default function PluginBrowser() { return ( diff --git a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx index a13330e..81ce732 100644 --- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx @@ -1,4 +1,5 @@ import { CardWrapper } from "@core/ui/components/AddonCard"; +import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models"; import { usePluginCardStyles } from "@core/ui/settings/pages/Plugins/usePluginCardStyles"; import { findAssetId } from "@lib/api/assets"; import { NavigationNative, tokens } from "@metro/common"; @@ -8,8 +9,6 @@ import chroma from "chroma-js"; import { createContext, useContext, useMemo } from "react"; import { Image, View } from "react-native"; -import { UnifiedPluginModel } from ".."; - const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult; }>(null!); const useCardContext = () => useContext(CardContext); @@ -50,6 +49,8 @@ function Title() { function Authors() { const { plugin, result } = useCardContext(); + const styles = usePluginCardStyles(); + if (!plugin.authors) return null; // could be empty if the author(s) are irrelevant with the search! @@ -59,24 +60,23 @@ function Authors() { ); - if (highlightedNode.length > 0) return ( - - by {highlightedNode} - - ); - - const children = ["by "]; + const badges = plugin.getBadges(); + const authorText = highlightedNode.length > 0 ? highlightedNode : plugin.authors.map(a => a.name).join(", "); - for (const author of plugin.authors) { - children.push(typeof author === "string" ? author : author.name); - children.push(", "); - } - - children.pop(); - - return - {children} - ; + return ( + + + by {authorText} + + {badges.length > 0 && + {badges.map((b, i) => )} + } + + ); } function Description() { diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 05f852e..41784c2 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -13,28 +13,14 @@ import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants"; import { lazyDestructure } from "@lib/utils/lazy"; import { findByProps } from "@metro"; import { NavigationNative } from "@metro/common"; -import { Card, FlashList, IconButton, Text } from "@metro/common/components"; +import { Button, Card, FlashList, IconButton, Text } from "@metro/common/components"; import { ComponentProps } from "react"; import { View } from "react-native"; +import { UnifiedPluginModel } from "./models"; import unifyBunnyPlugin from "./models/bunny"; import unifyVdPlugin from "./models/vendetta"; -export interface UnifiedPluginModel { - id: string; - name: string; - description?: string; - authors?: Author[]; - icon?: string; - - isEnabled(): boolean; - usePluginState(): void; - isInstalled(): boolean; - toggle(start: boolean): void; - resolveSheetComponent(): Promise<{ default: React.ComponentType; }>; - getPluginSettingsComponent(): React.ComponentType | null | undefined; -} - const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert")); const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions")); @@ -53,7 +39,7 @@ function PluginPage(props: PluginPageProps) { "description", p => p.authors?.map( (a: Author | string) => typeof a === "string" ? a : a.name - ).join() + ).join() || "" ]} sortOptions={{ "Name (A-Z)": (a, b) => a.name.localeCompare(b.name), @@ -119,6 +105,20 @@ export default function Plugins() { ; }} + ListFooterComponent={() => __DEV__ && ( +