perf(ui): use FlashList and some tweaks
This commit is contained in:
parent
ada478f517
commit
43b2462e81
10 changed files with 86 additions and 99 deletions
|
|
@ -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
16
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ interface OverflowAction extends Action {
|
|||
|
||||
export interface CardWrapper<T> {
|
||||
item: T;
|
||||
index: number;
|
||||
}
|
||||
|
||||
interface CardProps {
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) })}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue