From b5611d3cf185fd22b0a9adcffe3e473c3df7ca49 Mon Sep 17 00:00:00 2001 From: pylixonly <82711525+pylixonly@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:43:02 +0800 Subject: [PATCH] feat(ui): allow adding repository and fix some of the api --- .../ui/settings/pages/PluginBrowser/index.tsx | 211 +++++++++++------- src/lib/addons/plugins/index.ts | 15 +- src/lib/addons/plugins/types.ts | 16 +- src/lib/api/storage/index.ts | 4 +- 4 files changed, 148 insertions(+), 98 deletions(-) diff --git a/src/core/ui/settings/pages/PluginBrowser/index.tsx b/src/core/ui/settings/pages/PluginBrowser/index.tsx index 89c8a57..51f40ae 100644 --- a/src/core/ui/settings/pages/PluginBrowser/index.tsx +++ b/src/core/ui/settings/pages/PluginBrowser/index.tsx @@ -1,44 +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 { showSheet } from "@lib/ui/sheets"; +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 { NavigationNative } from "@metro/common"; -import { ActionSheet, Button, Card, FlashList, IconButton, Stack, TableRow, TableRowGroup, 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 { 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(`builds/${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; }) { @@ -69,7 +49,15 @@ function TrailingButtons(props: { id: string; }) { return { }} + onPress={() => { + showSheet("plugin-info", () => { + return + + + + ; + }); + }} variant="secondary" icon={findAssetId("CircleInformationIcon")} /> @@ -77,38 +65,29 @@ 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.display.name} + {display.name} - by {plugin.display.authors?.map(a => typeof a === "string" ? a : a.name).join(", ") ?? "Unknown"} + by {display.authors?.map(a => a.name).join(", ") || "Unknown"} ({version}) - + - {plugin.display.description} + {display.description} - } + ); } @@ -123,37 +102,7 @@ function BrowserPage() { variant="secondary" icon={findAssetId("PlusSmallIcon")} onPress={() => { - showSheet("plugin-browser-options", () => { - return - - - { }} - /> - { }} - /> - } - /> - } - onPress={() => { }} - /> - - ; - }); + showSheet("plugin-browser-options", PluginBrowserOptions); }} /> }); @@ -161,7 +110,7 @@ function BrowserPage() { const { data, error, isPending, refetch } = useQuery({ queryKey: ["plugins-repo-fetch"], - queryFn: () => arrayFromAsync(getManifests(OFFICIAL_PLUGINS_REPO_URL)) + queryFn: () => getManifests() }); if (error) { @@ -196,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/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index c82ad2a..1b5903e 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -1,6 +1,6 @@ import { getCorePlugins } from "@core/plugins"; import { readFile, removeFile, writeFile } from "@lib/api/native/fs"; -import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorage } from "@lib/api/storage"; +import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, purgeStorage, updateStorage } from "@lib/api/storage"; import { safeFetch } from "@lib/utils"; import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants"; import { semver } from "@metro/common"; @@ -36,9 +36,10 @@ function assert(condition: T, id: string, attempt: string): asserts condition /** * Checks if a version is newer than the other. However, this comes with an additional logic, * where if the version are equal, one with prerelease "tag" will be considered "newer" + * @internal * @returns Whether the version is newer */ -function newerThan(v1: string, v2: string) { +export function isGreaterVersion(v1: string, v2: string) { if (semver.gt(v1, v2)) return true; const coerced = semver.coerce(v1); if (coerced == null) return false; @@ -152,13 +153,13 @@ export async function updateRepository(repoUrl: string) { } await Promise.all(Object.keys(repo).map(async pluginId => { - if (!storedRepo || !storedRepo[pluginId] || repo[pluginId].alwaysFetch || newerThan(repo[pluginId].version, storedRepo[pluginId].version)) { + if (!storedRepo || !storedRepo[pluginId] || repo[pluginId].alwaysFetch || isGreaterVersion(repo[pluginId].version, storedRepo[pluginId].version)) { updated = true; pluginRepositories[repoUrl][pluginId] = repo[pluginId]; await updateAndWritePlugin(repoUrl, pluginId, Boolean(storedRepo && pluginSettings[pluginId])); } else { const manifest = await preloadStorageIfExists(`plugins/manifests/${pluginId}.json`); - if (manifest === undefined) { // File does not exist, so do refetch and stuff + if (!manifest) { // File does not exist, so do refetch and stuff await updateAndWritePlugin(repoUrl, pluginId, Boolean(storedRepo && pluginSettings[pluginId])); } } @@ -172,7 +173,7 @@ export async function updateRepository(repoUrl: string) { const existing = registeredPlugins.get(id); // Skip if this version isn't any higher - if (existing && !newerThan(manifest.version, existing.version)) { + if (existing && !isGreaterVersion(manifest.version, existing.version)) { continue; } @@ -200,11 +201,13 @@ export async function deleteRepository(repoUrl: string) { } // Deregister all plugins under this repository + promQueues.push(purgeStorage(`plugins/manifests/${id}.json`)); registeredPlugins.delete(id); } delete pluginRepositories[repoUrl]; await Promise.all(promQueues); + updateAllRepository(); } /** @@ -341,7 +344,7 @@ export function stopPlugin(id: string) { pluginInstances.delete(id); } -async function updateAllRepository() { +export async function updateAllRepository() { try { await updateRepository(OFFICIAL_PLUGINS_REPO_URL); } catch (error) { diff --git a/src/lib/addons/plugins/types.ts b/src/lib/addons/plugins/types.ts index 833bf3f..c6e2d22 100644 --- a/src/lib/addons/plugins/types.ts +++ b/src/lib/addons/plugins/types.ts @@ -2,14 +2,16 @@ import { createStorage } from "@core/vendetta/storage"; import { BunnyManifest } from "@lib/addons/types"; import { Logger } from "@lib/utils/logger"; -export interface PluginRepo { - [id: string]: { - version: string; - - // For plugin developing convenience, plugins with this on will always get fetched - alwaysFetch?: boolean; +export type PluginRepo = Record & { + $meta: { + name: string; + description: string; }; -} +}; export interface PluginRepoStorage { [repoUrl: string]: PluginRepo; diff --git a/src/lib/api/storage/index.ts b/src/lib/api/storage/index.ts index f123466..5746429 100644 --- a/src/lib/api/storage/index.ts +++ b/src/lib/api/storage/index.ts @@ -169,10 +169,10 @@ export async function preloadStorageIfExists(path: string): Promise { const backend = createFileBackend(path); if (await backend.exists()) { _loadedStorage[path] = await backend.get(); - return false; + return true; } - return true; + return false; } export async function purgeStorage(path: string) {