perf(ui): use FlashList and some tweaks

This commit is contained in:
amsyarasyiq 2024-06-04 20:46:50 +08:00
parent ada478f517
commit 43b2462e81
10 changed files with 86 additions and 99 deletions

View file

@ -26,6 +26,7 @@
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@typescript-eslint/typescript-estree": "^7.1.1",
"chalk": "^5.3.0",
"esbuild": "^0.20.2",
"eslint": "^8.57.0",
"eslint-plugin-import-alias": "^1.2.0",
@ -39,8 +40,7 @@
"yargs-parser": "^21.1.1"
},
"dependencies": {
"chalk": "^5.3.0",
"fuzzysort": "^2.0.4",
"fuzzysort": "^3.0.1",
"spitroast": "^1.4.4"
},
"pnpm": {

16
pnpm-lock.yaml generated
View file

@ -5,12 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
chalk:
specifier: ^5.3.0
version: 5.3.0
fuzzysort:
specifier: ^2.0.4
version: 2.0.4
specifier: ^3.0.1
version: 3.0.1
spitroast:
specifier: ^1.4.4
version: 1.4.4
@ -55,6 +52,9 @@ devDependencies:
'@typescript-eslint/typescript-estree':
specifier: ^7.1.1
version: 7.1.1(typescript@5.4.5)
chalk:
specifier: ^5.3.0
version: 5.3.0
esbuild:
specifier: ^0.20.2
version: 0.20.2
@ -976,7 +976,7 @@ packages:
/chalk@5.3.0:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
dev: false
dev: true
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
@ -1445,8 +1445,8 @@ packages:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: true
/fuzzysort@2.0.4:
resolution: {integrity: sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw==}
/fuzzysort@3.0.1:
resolution: {integrity: sha512-jpiSrv+j2GwtI13z3vzdBAD5m7iVH23spSZwJrD657dfr6jUV2Jyc5YuSPKirmTp9OqnJbHudPIBGIId1bCWmA==}
dev: false
/get-intrinsic@1.2.4:

View file

@ -71,7 +71,6 @@ interface OverflowAction extends Action {
export interface CardWrapper<T> {
item: T;
index: number;
}
interface CardProps {

View file

@ -4,15 +4,16 @@ import { requireAssetIndex } from "@lib/api/assets";
import { useProxy } from "@lib/api/storage";
import { settings } from "@lib/settings";
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 { showInputAlert } from "@ui/alerts";
import { ErrorBoundary, Search } from "@ui/components";
import fuzzysort from "fuzzysort";
import { createContext } from "react";
import { FlatList, View } from "react-native";
import { useMemo } from "react";
import { View } from "react-native";
export const RemoveModeContext = createContext(false);
type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
interface AddonPageProps<T> {
title: string;
@ -21,51 +22,54 @@ interface AddonPageProps<T> {
safeModeMessage: string;
safeModeExtras?: JSX.Element | JSX.Element[];
card: React.ComponentType<CardWrapper<T>>;
isRemoveMode?: boolean;
searchKeywords: SearchKeywords;
onFABPress?: () => void;
}
function getItemsByQuery<T extends AddonPageProps<unknown>["items"][string]>(items: T[], query: string): T[] {
if (!query) return items;
return fuzzysort.go(query, items, {
keys: [
"id",
"name",
"manifest.name",
"manifest.description",
"manifest.authors.0.name",
"manifest.authors.1.name"
]
}).map(r => r.obj);
}
// TODO: Move to somewhere else
const { FlashList } = findByProps("FlashList");
export default function AddonPage<T>({ card: CardComponent, ...props }: AddonPageProps<T>) {
useProxy(props.items);
useProxy(settings);
useProxy(props.items);
const [search, setSearch] = React.useState("");
const items = useMemo(() => {
return Object.values(props.items).filter(i => typeof i === "object");
}, [props.items]);
const data = useMemo(() => {
if (!search) return items;
return fuzzysort.go(search, items, { keys: props.searchKeywords }).map(r => r.obj);
}, [items, search]);
const headerElement = useMemo(() => (
<View>
{settings.safeMode?.enabled && <View style={{ marginBottom: 10 }}>
<HelpMessage messageType={0}>{props.safeModeMessage}</HelpMessage>
{props.safeModeExtras}
</View>}
<Search
style={{ padding: 8 }}
onChangeText={(v: string) => setSearch(v.toLowerCase())}
placeholder={Strings.SEARCH}
/>
</View>
), []);
return (
<ErrorBoundary>
{/* TODO: Implement better searching than just by ID */}
<FlatList
ListHeaderComponent={<View>
{settings.safeMode?.enabled && <View style={{ marginBottom: 10 }}>
<HelpMessage messageType={0}>{props.safeModeMessage}</HelpMessage>
{props.safeModeExtras}
</View>}
<Search
onChangeText={(v: string) => setSearch(v.toLowerCase())}
placeholder={Strings.SEARCH}
/>
</View>}
style={{ paddingHorizontal: 10, paddingTop: 10 }}
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5, gap: 12 }}
data={getItemsByQuery(Object.values(props.items).filter(i => typeof i === "object"), search)}
renderItem={({ item, index }) => <RemoveModeContext.Provider value={!!props.isRemoveMode}>
<CardComponent item={item} index={index} />
</RemoveModeContext.Provider>}
<FlashList
data={data}
estimatedItemSize={136}
ListHeaderComponent={headerElement}
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
renderItem={({ item }: any) => (
<View style={{ paddingVertical: 6, paddingHorizontal: 8 }}>
<CardComponent item={item} />
</View>
)}
/>
<FloatingActionButton
icon={requireAssetIndex("PlusLargeIcon")}

View file

@ -17,6 +17,7 @@ export default function Fonts() {
return (
<AddonPage<FontDefinition>
title={Strings.FONTS}
searchKeywords={["name", "description"]}
fetchFunction={installFont}
items={fonts as Record<string, FontDefinition>}
safeModeMessage={Strings.SAFE_MODE_NOTICE_FONTS}

View file

@ -6,7 +6,7 @@ import { showSheet } from "@lib/ui/sheets";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
import { NavigationNative, tokens } from "@metro/common";
import { Card, PressableScale, Stack, TableSwitch, Text } from "@metro/common/components";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import { createContext, useContext } from "react";
import { Image, View } from "react-native";
@ -41,12 +41,12 @@ function Title() {
return <Text
numberOfLines={1}
adjustsFontSizeToFit={true}
variant="heading-md/semibold"
variant="heading-lg/semibold"
>
{icon && <>
{/* TODO: Image appears invisible for some devices/installs */}
<Image
style={styles.pluginIcon}
style={styles.smallIcon}
source={icon}
/>
{" "}
@ -65,7 +65,7 @@ function Status() {
if (!plugin.error) return null;
return <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<View style={styles.actionIcon}>
<View style={styles.smallIcon}>
<Image
tintColor={INTERACTIVE_NORMAL}
source={requireAssetIndex("WarningIcon")}
@ -77,28 +77,8 @@ function Status() {
</View>;
}
function ActionIcon(props: {
onPress: () => void;
icon: string;
disabled?: boolean;
}) {
const styles = usePluginCardStyles();
return <View style={props.disabled && { opacity: 0.4, pointerEvents: "none" }}>
<PressableScale onPress={() => !props.disabled && props.onPress()}>
<View style={styles.actionIconContainer}>
<Image
source={requireAssetIndex(props.icon)}
style={styles.actionIcon}
/>
</View>
</PressableScale>
</View>;
}
export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
const navigation = NavigationNative.useNavigation();
const styles = usePluginCardStyles();
useProxy(plugin);
@ -107,24 +87,28 @@ export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
<Card>
<Stack spacing={16}>
<Status />
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Stack spacing={0}>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<View style={{ flexShrink: 1 }}>
<Title />
<Authors />
</Stack>
<View style={{ marginLeft: "auto" }}>
</View>
<View>
<Stack spacing={12} direction="horizontal">
<View style={{ flexDirection: "row", gap: 6 }}>
<ActionIcon
icon="WrenchIcon"
<IconButton
size="sm"
variant="secondary"
icon={requireAssetIndex("WrenchIcon")}
disabled={!getSettings(plugin.id)}
onPress={() => navigation.push("VendettaCustomPage", {
title: plugin.manifest.name,
render: getSettings(plugin.id),
})}
/>
<ActionIcon
icon="CircleInformationIcon-primary"
<IconButton
size="sm"
variant="secondary"
icon={requireAssetIndex("CircleInformationIcon-primary")}
onPress={() => void showSheet(
"PluginInfoActionSheet",
import("./sheets/PluginInfoActionSheet"),

View file

@ -4,6 +4,7 @@ import PluginCard from "@core/ui/settings/pages/Plugins/PluginCard";
import { useProxy } from "@lib/api/storage";
import { BunnyPlugin, installPlugin, plugins } from "@lib/managers/plugins";
import { settings } from "@lib/settings";
import { Author } from "@lib/utils/types";
export default function Plugins() {
useProxy(settings);
@ -11,6 +12,11 @@ export default function Plugins() {
return (
<AddonPage<BunnyPlugin>
title={Strings.PLUGINS}
searchKeywords={[
"manifest.name",
"manifest.description",
p => p.manifest.authors?.map((a: Author) => a.name).join()
]}
fetchFunction={installPlugin}
items={plugins}
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}

View file

@ -2,22 +2,9 @@ import { createStyles } from "@lib/ui/styles";
import { tokens } from "@metro/common";
export const usePluginCardStyles = createStyles({
pluginIcon: {
smallIcon: {
tintColor: tokens.colors.LOGO_PRIMARY,
height: 18,
width: 18,
},
actionIconContainer: {
width: 32,
height: 32,
borderRadius: 20,
backgroundColor: tokens.colors.BACKGROUND_ACCENT,
justifyContent: "center",
alignItems: "center",
},
actionIcon: {
tintColor: tokens.colors.INTERACTIVE_NORMAL,
width: 18,
height: 18,
},
}
});

View file

@ -4,6 +4,7 @@ import ThemeCard from "@core/ui/settings/pages/Themes/ThemeCard";
import { useProxy } from "@lib/api/storage";
import { installTheme, Theme, themes } from "@lib/managers/themes";
import { settings } from "@lib/settings";
import { Author } from "@lib/utils/types";
import { Button } from "@metro/common/components";
export default function Themes() {
@ -13,6 +14,11 @@ export default function Themes() {
return (
<AddonPage<Theme>
title={Strings.THEMES}
searchKeywords={[
"manifest.name",
"manifest.description",
p => p.manifest.authors?.map((a: Author) => a.name).join()
]}
fetchFunction={installTheme}
items={themes}
safeModeMessage={formatString("SAFE_MODE_NOTICE_THEMES", { enabled: Boolean(settings.safeMode?.currentThemeId) })}

View file

@ -25,12 +25,12 @@ export default ({ onChangeText, placeholder, style }: SearchProps) => {
return <ErrorBoundary>
<View style={style}>
<TextInput
grow
isClearable
grow={true}
isClearable={true}
leadingIcon={SearchIcon}
placeholder={placeholder ?? Strings.SEARCH}
onChange={onChange}
returnKeyType={"search"}
returnKeyType="search"
size="md"
autoCapitalize="none"
autoCorrect={false}