chore: bunch of changes done last week ™️
This commit is contained in:
parent
1c6a1d7007
commit
516400c730
14 changed files with 249 additions and 45 deletions
|
|
@ -32,17 +32,18 @@
|
|||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-toolkit": "^1.13.1",
|
||||
"fuzzysort": "^3.0.2",
|
||||
"spitroast": "^1.4.4",
|
||||
"@shopify/react-native-skia": "^1.3.8",
|
||||
"@tanstack/react-query": "^5.51.16",
|
||||
"@types/chroma-js": "~2.4.4",
|
||||
"@types/lodash": "~4.17.7",
|
||||
"@types/react": "18.2.60",
|
||||
"@types/react-native": "0.72.3",
|
||||
"es-toolkit": "^1.13.1",
|
||||
"fuzzysort": "^3.0.2",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
"moment": "2.22.2",
|
||||
"react-native-reanimated": "^3.6.2",
|
||||
"spitroast": "^1.4.4",
|
||||
"type-fest": "^4.22.1"
|
||||
},
|
||||
"pnpm": {
|
||||
|
|
|
|||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -8,6 +8,9 @@ dependencies:
|
|||
'@shopify/react-native-skia':
|
||||
specifier: ^1.3.8
|
||||
version: 1.3.8(react-native-reanimated@3.6.2)
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.51.16
|
||||
version: 5.51.16
|
||||
'@types/chroma-js':
|
||||
specifier: ~2.4.4
|
||||
version: 2.4.4
|
||||
|
|
@ -1042,6 +1045,21 @@ packages:
|
|||
'@swc/counter': 0.1.3
|
||||
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:
|
||||
resolution: {integrity: sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==}
|
||||
dev: false
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ const config = {
|
|||
define: {
|
||||
__DEV__: JSON.stringify(releaseBranch !== "main")
|
||||
},
|
||||
inject: ["./shims/asyncIteratorSymbol.js", "./shims/promiseAllSettled.js"],
|
||||
legalComments: "none",
|
||||
alias: {
|
||||
"!bunny-deps-shim!": "./shims/depsModule.ts",
|
||||
|
|
@ -84,15 +85,14 @@ const config = {
|
|||
env: {
|
||||
targets: "fully supports es6",
|
||||
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-classes"
|
||||
"transform-classes",
|
||||
"transform-async-to-generator",
|
||||
"transform-async-generator-functions"
|
||||
],
|
||||
exclude: [
|
||||
"transform-parameters",
|
||||
"transform-template-literals",
|
||||
"transform-async-to-generator",
|
||||
"transform-exponentiation-operator",
|
||||
"transform-named-capturing-groups-regex",
|
||||
"transform-nullish-coalescing-operator",
|
||||
|
|
|
|||
5
shims/asyncIteratorSymbol.js
Normal file
5
shims/asyncIteratorSymbol.js
Normal 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" };
|
||||
12
shims/promiseAllSettled.js
Normal file
12
shims/promiseAllSettled.js
Normal 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" };
|
||||
|
|
@ -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));
|
||||
};
|
||||
|
|
@ -4,13 +4,12 @@ import { findAssetId } from "@lib/api/assets";
|
|||
import { settings } from "@lib/api/settings";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { HTTP_REGEX_MULTI } from "@lib/utils/constants";
|
||||
import { findByProps } from "@metro";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { FloatingActionButton, HelpMessage } from "@metro/common/components";
|
||||
import { clipboard, NavigationNative } from "@metro/common";
|
||||
import { FlashList, FloatingActionButton, HelpMessage } from "@metro/common/components";
|
||||
import { showInputAlert } from "@ui/alerts";
|
||||
import { ErrorBoundary, Search } from "@ui/components";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { ComponentType, ReactNode, useMemo } from "react";
|
||||
import { ComponentType, ReactNode, useCallback, useMemo } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
|
||||
|
|
@ -25,15 +24,20 @@ interface AddonPageProps<T extends object> {
|
|||
card: ComponentType<CardWrapper<T>>;
|
||||
searchKeywords: SearchKeywords;
|
||||
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>) {
|
||||
useProxy(settings);
|
||||
|
||||
const [, forceUpdate] = React.useReducer(s => ~s, 0);
|
||||
NavigationNative.useFocusEffect(
|
||||
useCallback(() => {
|
||||
console.log("giler", items);
|
||||
forceUpdate();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const [search, setSearch] = React.useState("");
|
||||
|
||||
const items = useMemo(() => {
|
||||
|
|
@ -62,16 +66,21 @@ export default function AddonPage<T extends object>({ card: CardComponent, ...pr
|
|||
|
||||
return (
|
||||
<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
|
||||
data={data}
|
||||
extraData={search}
|
||||
keyExtractor={(i: any) => i?.id}
|
||||
estimatedItemSize={136}
|
||||
ListHeaderComponent={headerElement}
|
||||
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
|
||||
ListFooterComponent={props.ListFooterComponent}
|
||||
renderItem={({ item }: any) => (
|
||||
<View style={{ paddingVertical: 6, paddingHorizontal: 8 }}>
|
||||
<CardComponent item={item} />
|
||||
</View>
|
||||
<CardComponent item={item} />
|
||||
)}
|
||||
/>
|
||||
{(props.fetchFunction ?? props.onFabPress) && <FloatingActionButton
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
return null;
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserPage />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { usePluginCardStyles } from "@core/ui/settings/pages/Plugins/usePluginCardStyles";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
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 { Image, View } from "react-native";
|
||||
|
||||
import { UnifiedPluginModel } from ".";
|
||||
import { usePluginCardStyles } from "./usePluginCardStyles";
|
||||
import { UnifiedPluginModel } from "..";
|
||||
|
||||
const PluginContext = createContext<UnifiedPluginModel>(null!);
|
||||
const usePlugin = () => useContext(PluginContext);
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
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 { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
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 { NavigationNative } from "@metro/common";
|
||||
import { Button, SegmentedControl, SegmentedControlPages, Text, useSegmentedControlState } from "@metro/common/components";
|
||||
|
|
@ -26,7 +27,7 @@ export interface UnifiedPluginModel {
|
|||
usePluginState(): void;
|
||||
toggle(start: boolean): void;
|
||||
resolveSheetComponent(): Promise<{ default: React.ComponentType<any>; }>;
|
||||
getPluginSettingsComponent(): React.ComponentType<any> | null;
|
||||
getPluginSettingsComponent(): React.ComponentType<any> | null | undefined;
|
||||
}
|
||||
|
||||
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>
|
||||
card={PluginCard}
|
||||
title={Strings.PLUGINS}
|
||||
searchKeywords={[
|
||||
"name",
|
||||
"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}
|
||||
items={props.items!}
|
||||
items={items}
|
||||
{...props}
|
||||
/>;
|
||||
}
|
||||
|
|
@ -64,8 +73,8 @@ export default function Plugins() {
|
|||
label: "Vendetta",
|
||||
id: "vendetta-plugins",
|
||||
page: (
|
||||
<Page
|
||||
items={Object.values(VdPluginManager.plugins)}
|
||||
<PluginPage
|
||||
useItems={() => useProxy(VdPluginManager.plugins) && Object.values(VdPluginManager.plugins)}
|
||||
resolveItem={unifyVdPlugin}
|
||||
fetchFunction={(url: string) => VdPluginManager.installPlugin(url)}
|
||||
/>
|
||||
|
|
@ -75,8 +84,8 @@ export default function Plugins() {
|
|||
label: "Bunny",
|
||||
id: "bunny-plugins",
|
||||
page: (
|
||||
<Page
|
||||
items={[...registeredPlugins.values()]}
|
||||
<PluginPage
|
||||
useItems={() => (useNewProxy(pluginSettings), [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id)))}
|
||||
resolveItem={unifyBunnyPlugin}
|
||||
ListFooterComponent={() => (
|
||||
<View style={{ alignItems: "center", justifyContent: "center", paddingTop: 16, gap: 12 }}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { UnifiedPluginModel } from "..";
|
||||
|
|
@ -18,8 +18,8 @@ export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): Unified
|
|||
},
|
||||
toggle(start: boolean) {
|
||||
start
|
||||
? startPlugin(getId(manifest))
|
||||
: stopPlugin(getId(manifest));
|
||||
? enablePlugin(getId(manifest), true)
|
||||
: disablePlugin(getId(manifest));
|
||||
},
|
||||
resolveSheetComponent() {
|
||||
return import("../sheets/PluginInfoActionSheet");
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ async function initializeBunny() {
|
|||
// Make 'freeze' and 'seal' do nothing
|
||||
Object.freeze = Object.seal = Object;
|
||||
|
||||
// Polyfill Promise.allSettled
|
||||
require("@core/polyfills/promise-all-settled");
|
||||
|
||||
await require("@metro/internals/caches").initMetroCache();
|
||||
await require(".").default();
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export async function safeFetch(input: RequestInfo | URL, options?: RequestInit,
|
|||
...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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,3 +100,5 @@ export const {
|
|||
FormText: LegacyFormText,
|
||||
FormTitle: LegacyFormTitle
|
||||
} = lazyDestructure(() => Forms);
|
||||
|
||||
export const FlashList = findProp("FlashList");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue