diff --git a/scripts/build.mjs b/scripts/build.mjs index 95da306..75e3ffb 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"1.2.2"` + version: `"1.3.0"` } } }, diff --git a/src/core/i18n/default.json b/src/core/i18n/default.json index af4fbb0..afd4443 100644 --- a/src/core/i18n/default.json +++ b/src/core/i18n/default.json @@ -8,6 +8,7 @@ "BRAND": "Brand", "PUPU": "Kettu", "PUPU_URL": "Kettu URL", + "BROWSER": "Browser", "BYTECODE": "Bytecode", "CANCEL": "Cancel", "CLEAR": "Clear", diff --git a/src/core/ui/settings/index.ts b/src/core/ui/settings/index.ts index d5f96d8..a671937 100644 --- a/src/core/ui/settings/index.ts +++ b/src/core/ui/settings/index.ts @@ -41,6 +41,12 @@ export default function initSettings() { render: () => import("@core/ui/settings/pages/Fonts"), usePredicate: () => isFontSupported() }, + { + key: "KETTU_BROWSER", + title: () => Strings.BROWSER, + icon: findAssetId("UploadIcon"), + render: () => import("@core/ui/settings/pages/PluginBrowser"), + }, { key: "BUNNY_DEVELOPER", title: () => Strings.DEVELOPER, diff --git a/src/core/ui/settings/pages/PluginBrowser/index.tsx b/src/core/ui/settings/pages/PluginBrowser/index.tsx index 1af3f0f..d30e9a0 100644 --- a/src/core/ui/settings/pages/PluginBrowser/index.tsx +++ b/src/core/ui/settings/pages/PluginBrowser/index.tsx @@ -1,73 +1,223 @@ -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 { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants"; -import { NEXPID_PLUGINS_REPO_URL } from "@lib/utils/constants"; -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 { useEffect, useState } from "react"; +import { React, NavigationNative } from "@metro/common"; import { View } from "react-native"; +import { Stack, Button, IconButton, Text, Card, FlashList, SegmentedControl, useSegmentedControlState } from "@metro/common/components"; +import { findAssetId } from "@lib/api/assets"; +import safeFetch from "@lib/utils/safeFetch"; +import { showToast } from "@ui/toasts"; +import Search from "@ui/components/Search"; +import { VdPluginManager } from "@core/vendetta/plugins"; +import { getCurrentTheme, installTheme, themes, fetchTheme, removeTheme, selectTheme } from "@lib/addons/themes"; +import { clipboard } from "@metro/common"; +import { hideSheet, showSheet } from "@lib/ui/sheets"; +import { AlertActionButton } from "@lib/ui/components/wrappers"; +import { dismissAlert, openAlert } from "@lib/ui/alerts"; +import { ActionSheet, AlertModal, AlertActions, TableRow, TableRowGroup } from "@metro/common/components"; +import { lazyDestructure } from "@lib/utils/lazy"; +import { findByProps } from "@metro"; -const queryClient = new QueryClient(); +const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet")); -async function getManifests() { - await updateAllRepository(); - const plugins = [...registeredPlugins.values()]; - return plugins.filter(p => !isCorePlugin(p.id)); +interface BaseAddonData { + name: string; + description: string; + authors: string[]; + installUrl: string; } -function InstallButton(props: { id: string; }) { - const [installed, setInstalled] = useState(isPluginInstalled(props.id)); - const installationState = useMutation({ - mutationFn: async ({ install }) => { - await (install ? installPlugin : uninstallPlugin)(props.id, true); - }, - onSettled() { - setInstalled(isPluginInstalled(props.id)); - }, - onError(error) { - showToast(error instanceof Error ? error.message : String(error)); +interface PluginData extends BaseAddonData { + status: "working" | "broken" | "warning" | string; + sourceUrl: string; + warningMessage?: string; +} + +interface ThemeData extends BaseAddonData {} + +type AddonData = PluginData | ThemeData; + +const PLUGIN_URL = "https://raw.githubusercontent.com/Purple-EyeZ/Plugins-List/refs/heads/main/src/plugins-data.json"; +const THEME_URL = "https://raw.githubusercontent.com/kmmiio99o/theme-marketplace/refs/heads/main/themes.json"; + +function normalizeIdFromInstallUrl(url: string) { + return url.endsWith("/") ? url : url + "/"; +} + +// @ts-ignore (i cant be bothered to type these) +function InstallButton({ addon, isPluginMode, installing, setInstalling, setRefreshTick }) { + const normId = normalizeIdFromInstallUrl(addon.installUrl); + const [installed, setInstalled] = React.useState(() => + isPluginMode ? Boolean(VdPluginManager.plugins[normId]) : Boolean(themes[addon.installUrl]) + ); + + React.useEffect(() => { + setInstalled(isPluginMode ? Boolean(VdPluginManager.plugins[normId]) : Boolean(themes[addon.installUrl])); + }, [addon.installUrl, setRefreshTick, isPluginMode]); + + const installAddon = async () => { + if (installing.has(normId)) return; + setInstalling((prev: Iterable | null | undefined) => new Set(prev).add(normId)); + try { + if (isPluginMode) { + await VdPluginManager.installPlugin(normId, true); + } else { + await installTheme(addon.installUrl); + } + showToast(`Installed ${addon.name}`, findAssetId("CheckIcon")); + setInstalled(true); + } catch (e) { + showToast(e instanceof Error ? e.message : String(e), findAssetId("CircleXIcon-primary")); + } finally { + setInstalling((prev: Iterable | null | undefined) => { const s = new Set(prev); s.delete(normId); return s; }); + setRefreshTick((t: number) => t + 1); } - }); + }; - return