diff --git a/package.json b/package.json index baf62d4..9d35419 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba9543e..2a17291 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/scripts/build.mjs b/scripts/build.mjs index a0365ad..532fd97 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -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", diff --git a/shims/asyncIteratorSymbol.js b/shims/asyncIteratorSymbol.js new file mode 100644 index 0000000..28c8f81 --- /dev/null +++ b/shims/asyncIteratorSymbol.js @@ -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" }; \ No newline at end of file diff --git a/shims/promiseAllSettled.js b/shims/promiseAllSettled.js new file mode 100644 index 0000000..5edeb21 --- /dev/null +++ b/shims/promiseAllSettled.js @@ -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" }; diff --git a/src/core/polyfills/promise-all-settled.ts b/src/core/polyfills/promise-all-settled.ts deleted file mode 100644 index 3124777..0000000 --- a/src/core/polyfills/promise-all-settled.ts +++ /dev/null @@ -1,7 +0,0 @@ -const allSettledFulfill = (value: T) => ({ status: "fulfilled", value } as const); -const allSettledReject = (reason: T) => ({ status: "rejected", reason } as const); -const mapAllSettled = (item: T) => Promise.resolve(item).then(allSettledFulfill, allSettledReject); - -Promise.allSettled ??= (iterator: T) => { - return Promise.all(Array.from(iterator).map(mapAllSettled)); -}; diff --git a/src/core/ui/components/AddonPage.tsx b/src/core/ui/components/AddonPage.tsx index 7a7dbba..0b9c2ae 100644 --- a/src/core/ui/components/AddonPage.tsx +++ b/src/core/ui/components/AddonPage.tsx @@ -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)>; @@ -25,15 +24,20 @@ interface AddonPageProps { card: ComponentType>; searchKeywords: SearchKeywords; onFabPress?: () => void; - ListFooterComponent?: ReactNode | ComponentType; + ListFooterComponent?: ComponentType; } -// TODO: Move to somewhere else -const { FlashList } = findByProps("FlashList"); - export default function AddonPage({ card: CardComponent, ...props }: AddonPageProps) { 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({ card: CardComponent, ...pr return ( + {/* + 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 + */} i?.id} estimatedItemSize={136} ListHeaderComponent={headerElement} contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }} ListFooterComponent={props.ListFooterComponent} renderItem={({ item }: any) => ( - - - + )} /> {(props.fetchFunction ?? props.onFabPress) && (iterableOrArrayLike: AsyncIterable): Promise { + 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({ + 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