chore: bunch of changes done last week ™️

This commit is contained in:
amsyarasyiq 2024-08-13 23:39:41 +08:00
parent 1c6a1d7007
commit 516400c730
14 changed files with 249 additions and 45 deletions

View file

@ -32,17 +32,18 @@
"yargs-parser": "^21.1.1" "yargs-parser": "^21.1.1"
}, },
"dependencies": { "dependencies": {
"es-toolkit": "^1.13.1",
"fuzzysort": "^3.0.2",
"spitroast": "^1.4.4",
"@shopify/react-native-skia": "^1.3.8", "@shopify/react-native-skia": "^1.3.8",
"@tanstack/react-query": "^5.51.16",
"@types/chroma-js": "~2.4.4", "@types/chroma-js": "~2.4.4",
"@types/lodash": "~4.17.7", "@types/lodash": "~4.17.7",
"@types/react": "18.2.60", "@types/react": "18.2.60",
"@types/react-native": "0.72.3", "@types/react-native": "0.72.3",
"es-toolkit": "^1.13.1",
"fuzzysort": "^3.0.2",
"intl-messageformat": "^10.5.14", "intl-messageformat": "^10.5.14",
"moment": "2.22.2", "moment": "2.22.2",
"react-native-reanimated": "^3.6.2", "react-native-reanimated": "^3.6.2",
"spitroast": "^1.4.4",
"type-fest": "^4.22.1" "type-fest": "^4.22.1"
}, },
"pnpm": { "pnpm": {

18
pnpm-lock.yaml generated
View file

@ -8,6 +8,9 @@ dependencies:
'@shopify/react-native-skia': '@shopify/react-native-skia':
specifier: ^1.3.8 specifier: ^1.3.8
version: 1.3.8(react-native-reanimated@3.6.2) version: 1.3.8(react-native-reanimated@3.6.2)
'@tanstack/react-query':
specifier: ^5.51.16
version: 5.51.16
'@types/chroma-js': '@types/chroma-js':
specifier: ~2.4.4 specifier: ~2.4.4
version: 2.4.4 version: 2.4.4
@ -1042,6 +1045,21 @@ packages:
'@swc/counter': 0.1.3 '@swc/counter': 0.1.3
dev: true dev: true
/@tanstack/query-core@5.51.16:
resolution: {integrity: sha512-zfV+WAtBGm1dUIbL0w/x8qTqVLKU1/Bo1p19J9LF02MmIc4FxzMImMXhFzYJQl5Hx8Wit6RiQ4tB/DvN8y9zaQ==}
dev: false
/@tanstack/react-query@5.51.16:
resolution: {integrity: sha512-NZnpJ30zkwaA2ZPhxJLs/qoMbd0yNAj6yyb3JTADJx9HjSdtvnNzOY1bDa3bU1B9CZTBBb7W9E1PpWlNXdgESg==}
peerDependencies:
react: ^18.0.0
peerDependenciesMeta:
react:
optional: true
dependencies:
'@tanstack/query-core': 5.51.16
dev: false
/@types/chroma-js@2.4.4: /@types/chroma-js@2.4.4:
resolution: {integrity: sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==} resolution: {integrity: sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==}
dev: false dev: false

View file

@ -46,6 +46,7 @@ const config = {
define: { define: {
__DEV__: JSON.stringify(releaseBranch !== "main") __DEV__: JSON.stringify(releaseBranch !== "main")
}, },
inject: ["./shims/asyncIteratorSymbol.js", "./shims/promiseAllSettled.js"],
legalComments: "none", legalComments: "none",
alias: { alias: {
"!bunny-deps-shim!": "./shims/depsModule.ts", "!bunny-deps-shim!": "./shims/depsModule.ts",
@ -84,15 +85,14 @@ const config = {
env: { env: {
targets: "fully supports es6", targets: "fully supports es6",
include: [ include: [
// Pretend that arrow functions are unsupported, since hermes does not support async arrow functions for some reason
"transform-arrow-functions",
"transform-block-scoping", "transform-block-scoping",
"transform-classes" "transform-classes",
"transform-async-to-generator",
"transform-async-generator-functions"
], ],
exclude: [ exclude: [
"transform-parameters", "transform-parameters",
"transform-template-literals", "transform-template-literals",
"transform-async-to-generator",
"transform-exponentiation-operator", "transform-exponentiation-operator",
"transform-named-capturing-groups-regex", "transform-named-capturing-groups-regex",
"transform-nullish-coalescing-operator", "transform-nullish-coalescing-operator",

View file

@ -0,0 +1,5 @@
// babel/swc async generator implementation for some reason does not include assigning Symbol.asyncIterator (and won't work out of the box???)
const asyncIteratorSymbol = Symbol("Symbol.asyncIterator");
// your editor may yell at you for invalid syntax here, but this is a valid JS syntax!
export { asyncIteratorSymbol as "Symbol.asyncIterator" };

View file

@ -0,0 +1,12 @@
// @ts-nocheck
const allSettledFulfill = value => ({ status: "fulfilled", value });
const allSettledReject = reason => ({ status: "rejected", reason });
const mapAllSettled = item => Promise.resolve(item).then(allSettledFulfill, allSettledReject);
const allSettled = Promise.allSettled ??= iterator => {
return Promise.all(Array.from(iterator).map(mapAllSettled));
};
// Your editor may yell at you for this, but this is alright! It's a valid JS syntax
export { allSettled as "Promise.allSettled" };

View file

@ -1,7 +0,0 @@
const allSettledFulfill = <T>(value: T) => ({ status: "fulfilled", value } as const);
const allSettledReject = <T>(reason: T) => ({ status: "rejected", reason } as const);
const mapAllSettled = <T>(item: T) => Promise.resolve(item).then(allSettledFulfill, allSettledReject);
Promise.allSettled ??= <T extends unknown[]>(iterator: T) => {
return Promise.all(Array.from(iterator).map(mapAllSettled));
};

View file

@ -4,13 +4,12 @@ import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings"; import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage"; import { useProxy } from "@lib/api/storage";
import { HTTP_REGEX_MULTI } from "@lib/utils/constants"; import { HTTP_REGEX_MULTI } from "@lib/utils/constants";
import { findByProps } from "@metro"; import { clipboard, NavigationNative } from "@metro/common";
import { clipboard } from "@metro/common"; import { FlashList, FloatingActionButton, HelpMessage } from "@metro/common/components";
import { FloatingActionButton, HelpMessage } from "@metro/common/components";
import { showInputAlert } from "@ui/alerts"; import { showInputAlert } from "@ui/alerts";
import { ErrorBoundary, Search } from "@ui/components"; import { ErrorBoundary, Search } from "@ui/components";
import fuzzysort from "fuzzysort"; import fuzzysort from "fuzzysort";
import { ComponentType, ReactNode, useMemo } from "react"; import { ComponentType, ReactNode, useCallback, useMemo } from "react";
import { View } from "react-native"; import { View } from "react-native";
type SearchKeywords = Array<string | ((obj: any & {}) => string)>; type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
@ -25,15 +24,20 @@ interface AddonPageProps<T extends object> {
card: ComponentType<CardWrapper<T>>; card: ComponentType<CardWrapper<T>>;
searchKeywords: SearchKeywords; searchKeywords: SearchKeywords;
onFabPress?: () => void; onFabPress?: () => void;
ListFooterComponent?: ReactNode | ComponentType; ListFooterComponent?: ComponentType<any>;
} }
// TODO: Move to somewhere else
const { FlashList } = findByProps("FlashList");
export default function AddonPage<T extends object>({ card: CardComponent, ...props }: AddonPageProps<T>) { export default function AddonPage<T extends object>({ card: CardComponent, ...props }: AddonPageProps<T>) {
useProxy(settings); useProxy(settings);
const [, forceUpdate] = React.useReducer(s => ~s, 0);
NavigationNative.useFocusEffect(
useCallback(() => {
console.log("giler", items);
forceUpdate();
}, [])
);
const [search, setSearch] = React.useState(""); const [search, setSearch] = React.useState("");
const items = useMemo(() => { const items = useMemo(() => {
@ -62,16 +66,21 @@ export default function AddonPage<T extends object>({ card: CardComponent, ...pr
return ( return (
<ErrorBoundary> <ErrorBoundary>
{/*
I've spent countless hours on these, but FlashList and FlatList refuse to rerender (unless if you reopen the page)
*even* if the data totally changed. Solutions online suggest putting extraData prop,
but that doesn't work either. I wonder what could have caused this
*/}
<FlashList <FlashList
data={data} data={data}
extraData={search}
keyExtractor={(i: any) => i?.id}
estimatedItemSize={136} estimatedItemSize={136}
ListHeaderComponent={headerElement} ListHeaderComponent={headerElement}
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }} contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
ListFooterComponent={props.ListFooterComponent} ListFooterComponent={props.ListFooterComponent}
renderItem={({ item }: any) => ( renderItem={({ item }: any) => (
<View style={{ paddingVertical: 6, paddingHorizontal: 8 }}> <CardComponent item={item} />
<CardComponent item={item} />
</View>
)} )}
/> />
{(props.fetchFunction ?? props.onFabPress) && <FloatingActionButton {(props.fetchFunction ?? props.onFabPress) && <FloatingActionButton

View file

@ -1,5 +1,163 @@
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 { Button, Card, FlashList, IconButton, Stack, Text } from "@metro/common/components";
import { QueryClient, QueryClientProvider, useMutation, useQuery } from "@tanstack/react-query";
import { chunk } from "es-toolkit";
import { useState } from "react";
import { View } from "react-native";
const queryClient = new QueryClient();
async function arrayFromAsync<T>(iterableOrArrayLike: AsyncIterable<T>): Promise<T[]> {
const arr: T[] = [];
for await (const element of iterableOrArrayLike) arr.push(element);
return arr;
}
async function fetchManifest(id: string) {
const url = `https://bn-plugins.github.io/dist/plugins/${id}/manifest.json`;
const data = await safeFetch(url).then(d => d.json());
queryClient.setQueryData(["plugin-manifest-dist", { id }], data);
return data as BunnyPluginManifest;
}
async function* getManifests() {
const rawResponse = await safeFetch("https://bn-plugins.github.io/dist/repo.json");
const pluginIds = Object.keys(await rawResponse.json());
for (const idChunks of chunk(pluginIds, 5)) {
const manifests = idChunks.map(fetchManifest);
for (const manifest of manifests) {
yield await manifest;
}
}
}
function InstallButton(props: { id: string; }) {
const [installed, setInstalled] = useState(isPluginInstalled(props.id));
const installationState = useMutation<void, Error, { install: boolean }>({
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));
}
});
return <Button
size="sm"
loading={installationState.isPending}
text={!installed ? "Install" : "Uninstall"}
onPress={() => installationState.mutate({ install: !installed })}
variant={!installed ? "primary" : "destructive"}
icon={findAssetId(!installed ? "DownloadIcon" : "TrashIcon")}
/>;
}
function TrailingButtons(props: { id: string }) {
return <Stack spacing={8} direction="horizontal">
<IconButton
size="sm"
onPress={() => {}}
variant="secondary"
icon={findAssetId("CircleInformationIcon")}
/>
<InstallButton id={props.id} />
</Stack>;
}
function PluginCard(props: { id: string, manifest: BunnyPluginManifest; }) {
const { isPending, error, data: plugin } = useQuery({
queryKey: ["plugin-manifest-dist", { id: props.id }],
queryFn: () => fetchManifest(props.id)
});
return (
<Card>
{!plugin && <View style={{ justifyContent: "center", alignItems: "center" }}>
<Text color="text-muted" variant="heading-lg/semibold">
{isPending && "Loading..."}
{error && `An error has occured while fetching plugin: ${error.message}`}
</Text>
</View>}
{plugin && <Stack spacing={16}>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<View style={{ flexShrink: 1 }}>
<Text numberOfLines={1} variant="heading-lg/semibold">
{plugin.name}
</Text>
<Text variant="text-md/semibold" color="text-muted">
by {plugin.authors.map(a => typeof a === "string" ? a : a.name).join(", ")}
</Text>
</View>
<View>
<TrailingButtons id={props.id} />
</View>
</View>
<Text variant="text-md/medium">
{plugin.description}
</Text>
</Stack>}
</Card>
);
}
function BrowserPage() {
const { data, error, isPending, refetch } = useQuery({
queryKey: ["plugins-repo-fetch"],
queryFn: () => arrayFromAsync(getManifests())
});
if (error) {
return <View style={{
flex: 1,
paddingHorizontal: 8,
justifyContent: "center",
alignItems: "center",
}}>
<Card style={{ gap: 8 }}>
<Text style={{ textAlign: "center" }} variant="heading-lg/bold">
An error occured while fetching the repository!
</Text>
<Text style={{ textAlign: "center" }} variant="text-sm/medium" color="text-muted">
{error instanceof Error ? error.message : String(error)}
</Text>
<Button
size="lg"
text="Refetch"
onPress={refetch}
icon={findAssetId("RetryIcon")}
/>
</Card>
</View>;
}
return <FlashList
data={data}
refreshing={isPending}
onRefresh={refetch}
estimatedItemSize={136}
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
renderItem={({ item: manifest }: any) => (
<View style={{ paddingVertical: 6, paddingHorizontal: 8 }}>
<PluginCard id={manifest.id} manifest={manifest} />
</View>
)}
/>;
}
export default function PluginBrowser() { export default function PluginBrowser() {
return null; return (
<QueryClientProvider client={queryClient}>
<BrowserPage />
</QueryClientProvider>
);
} }

View file

@ -1,4 +1,5 @@
import { CardWrapper } from "@core/ui/components/AddonCard"; import { CardWrapper } from "@core/ui/components/AddonCard";
import { usePluginCardStyles } from "@core/ui/settings/pages/Plugins/usePluginCardStyles";
import { findAssetId } from "@lib/api/assets"; import { findAssetId } from "@lib/api/assets";
import { NavigationNative } from "@metro/common"; import { NavigationNative } from "@metro/common";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components"; import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
@ -6,8 +7,7 @@ import { showSheet } from "@ui/sheets";
import { createContext, memo, useContext } from "react"; import { createContext, memo, useContext } from "react";
import { Image, View } from "react-native"; import { Image, View } from "react-native";
import { UnifiedPluginModel } from "."; import { UnifiedPluginModel } from "..";
import { usePluginCardStyles } from "./usePluginCardStyles";
const PluginContext = createContext<UnifiedPluginModel>(null!); const PluginContext = createContext<UnifiedPluginModel>(null!);
const usePlugin = () => useContext(PluginContext); const usePlugin = () => useContext(PluginContext);

View file

@ -1,11 +1,12 @@
import { Strings } from "@core/i18n"; import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage"; import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/PluginCard"; import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
import { VdPluginManager } from "@core/vendetta/plugins"; import { VdPluginManager } from "@core/vendetta/plugins";
import { findAssetId } from "@lib/api/assets"; import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings"; import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage"; import { useProxy } from "@lib/api/storage";
import { registeredPlugins } from "@lib/plugins"; import { useProxy as useNewProxy } from "@lib/api/storage/new";
import { isPluginInstalled, pluginSettings, registeredPlugins } from "@lib/plugins";
import { Author } from "@lib/utils/types"; import { Author } from "@lib/utils/types";
import { NavigationNative } from "@metro/common"; import { NavigationNative } from "@metro/common";
import { Button, SegmentedControl, SegmentedControlPages, Text, useSegmentedControlState } from "@metro/common/components"; import { Button, SegmentedControl, SegmentedControlPages, Text, useSegmentedControlState } from "@metro/common/components";
@ -26,7 +27,7 @@ export interface UnifiedPluginModel {
usePluginState(): void; usePluginState(): void;
toggle(start: boolean): void; toggle(start: boolean): void;
resolveSheetComponent(): Promise<{ default: React.ComponentType<any>; }>; resolveSheetComponent(): Promise<{ default: React.ComponentType<any>; }>;
getPluginSettingsComponent(): React.ComponentType<any> | null; getPluginSettingsComponent(): React.ComponentType<any> | null | undefined;
} }
function navigateToPluginBrowser(navigation: any) { function navigateToPluginBrowser(navigation: any) {
@ -36,17 +37,25 @@ function navigateToPluginBrowser(navigation: any) {
}); });
} }
function Page(props: Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>>) { interface PluginPageProps extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
useItems: () => unknown[];
}
function PluginPage(props: PluginPageProps) {
const items = props.useItems();
return <AddonPage<UnifiedPluginModel> return <AddonPage<UnifiedPluginModel>
card={PluginCard} card={PluginCard}
title={Strings.PLUGINS} title={Strings.PLUGINS}
searchKeywords={[ searchKeywords={[
"name", "name",
"description", "description",
p => p.authors?.map((a: Author | string) => typeof a === "string" ? a : a.name).join() p => p.authors?.map(
(a: Author | string) => typeof a === "string" ? a : a.name
).join()
]} ]}
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS} safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}
items={props.items!} items={items}
{...props} {...props}
/>; />;
} }
@ -64,8 +73,8 @@ export default function Plugins() {
label: "Vendetta", label: "Vendetta",
id: "vendetta-plugins", id: "vendetta-plugins",
page: ( page: (
<Page <PluginPage
items={Object.values(VdPluginManager.plugins)} useItems={() => useProxy(VdPluginManager.plugins) && Object.values(VdPluginManager.plugins)}
resolveItem={unifyVdPlugin} resolveItem={unifyVdPlugin}
fetchFunction={(url: string) => VdPluginManager.installPlugin(url)} fetchFunction={(url: string) => VdPluginManager.installPlugin(url)}
/> />
@ -75,8 +84,8 @@ export default function Plugins() {
label: "Bunny", label: "Bunny",
id: "bunny-plugins", id: "bunny-plugins",
page: ( page: (
<Page <PluginPage
items={[...registeredPlugins.values()]} useItems={() => (useNewProxy(pluginSettings), [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id)))}
resolveItem={unifyBunnyPlugin} resolveItem={unifyBunnyPlugin}
ListFooterComponent={() => ( ListFooterComponent={() => (
<View style={{ alignItems: "center", justifyContent: "center", paddingTop: 16, gap: 12 }}> <View style={{ alignItems: "center", justifyContent: "center", paddingTop: 16, gap: 12 }}>

View file

@ -1,5 +1,5 @@
import { useProxy } from "@lib/api/storage/new"; import { useProxy } from "@lib/api/storage/new";
import { getId, getPluginSettingsComponent,isPluginEnabled, pluginSettings, startPlugin, stopPlugin } from "@lib/plugins"; import { disablePlugin, enablePlugin, getId, getPluginSettingsComponent,isPluginEnabled, pluginSettings } from "@lib/plugins";
import { BunnyPluginManifest } from "@lib/plugins/types"; import { BunnyPluginManifest } from "@lib/plugins/types";
import { UnifiedPluginModel } from ".."; import { UnifiedPluginModel } from "..";
@ -18,8 +18,8 @@ export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): Unified
}, },
toggle(start: boolean) { toggle(start: boolean) {
start start
? startPlugin(getId(manifest)) ? enablePlugin(getId(manifest), true)
: stopPlugin(getId(manifest)); : disablePlugin(getId(manifest));
}, },
resolveSheetComponent() { resolveSheetComponent() {
return import("../sheets/PluginInfoActionSheet"); return import("../sheets/PluginInfoActionSheet");

View file

@ -9,9 +9,6 @@ async function initializeBunny() {
// Make 'freeze' and 'seal' do nothing // Make 'freeze' and 'seal' do nothing
Object.freeze = Object.seal = Object; Object.freeze = Object.seal = Object;
// Polyfill Promise.allSettled
require("@core/polyfills/promise-all-settled");
await require("@metro/internals/caches").initMetroCache(); await require("@metro/internals/caches").initMetroCache();
await require(".").default(); await require(".").default();
} catch (e) { } catch (e) {

View file

@ -5,7 +5,7 @@ export async function safeFetch(input: RequestInfo | URL, options?: RequestInit,
...options ...options
}); });
if (!req.ok) throw new Error("Request returned non-ok"); if (!req.ok) throw new Error(`Request returned non-ok: ${req.status} ${req.statusText}`);
return req; return req;
} }

View file

@ -100,3 +100,5 @@ export const {
FormText: LegacyFormText, FormText: LegacyFormText,
FormTitle: LegacyFormTitle FormTitle: LegacyFormTitle
} = lazyDestructure(() => Forms); } = lazyDestructure(() => Forms);
export const FlashList = findProp("FlashList");