feat: highlight search keywords

This commit is contained in:
amsyarasyiq 2024-08-28 15:45:50 +08:00
parent 5afcb7b330
commit 0fc8c69d51
4 changed files with 77 additions and 70 deletions

View file

@ -71,6 +71,7 @@ interface OverflowAction extends Action {
export interface CardWrapper<T> {
item: T;
result: Fuzzysort.KeysResult<T>;
}
interface CardProps {

View file

@ -33,16 +33,13 @@ export default function AddonPage<T extends object>({ card: CardComponent, ...pr
const [search, setSearch] = React.useState("");
const items = useMemo(() => {
const results = useMemo(() => {
let values = props.items;
if (props.resolveItem) values = values.map(props.resolveItem);
return values.filter(i => i && typeof i === "object");
}, [props.items]);
const items = values.filter(i => i && typeof i === "object");
const data = useMemo(() => {
if (!search) return items;
return fuzzysort.go(search, items, { keys: props.searchKeywords }).map(r => r.obj);
}, [items, search]);
return fuzzysort.go(search, items, { keys: props.searchKeywords, all: true });
}, [props.items, search]);
const headerElement = (
<View style={{ paddingBottom: 8 }}>
@ -58,14 +55,14 @@ export default function AddonPage<T extends object>({ card: CardComponent, ...pr
return (
<ErrorBoundary>
<FlashList
data={data}
data={results}
extraData={search}
estimatedItemSize={136}
ListHeaderComponent={headerElement}
contentContainerStyle={{ padding: 8, paddingHorizontal: 12 }}
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
ListFooterComponent={props.ListFooterComponent}
renderItem={({ item }: any) => <CardComponent item={item} />}
renderItem={({ item }: any) => <CardComponent item={item.obj} result={item} />}
/>
{(props.fetchFunction ?? props.onFabPress) && <FloatingActionButton
icon={findAssetId("PlusLargeIcon")}

View file

@ -1,21 +1,70 @@
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 { NavigationNative, tokens } from "@metro/common";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import { showSheet } from "@ui/sheets";
import { createContext, memo, useContext } from "react";
import chroma from "chroma-js";
import { createContext, memo, useContext, useMemo } from "react";
import { Image, View } from "react-native";
import { UnifiedPluginModel } from "..";
const PluginContext = createContext<UnifiedPluginModel>(null!);
const usePlugin = () => useContext(PluginContext);
const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel> }>(null!);
const useCardContext = () => useContext(CardContext);
function getHighlightColor(): import("react-native").ColorValue {
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
}
function Title() {
const styles = usePluginCardStyles();
const { plugin, result } = useCardContext();
// could be empty if the plugin name is irrelevant!
const highlightedNode = result[0].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
);
const icon = plugin.icon && findAssetId(plugin.icon);
const textNode = (
<Text
numberOfLines={1}
variant="heading-lg/semibold"
>
{highlightedNode.length ? highlightedNode : plugin.name}
</Text>
);
return <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
{icon && <Image
style={styles.smallIcon}
source={icon}
/>}
{textNode}
</View>;
}
function Authors() {
const plugin = usePlugin();
const { plugin, result } = useCardContext();
if (!plugin.authors) return null;
// could be empty if the author(s) are irrelevant with the search!
const highlightedNode = result[2].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
);
if (highlightedNode.length > 0) return (
<Text variant="text-md/semibold" color="text-muted">
by {highlightedNode}
</Text>
);
const children = ["by "];
for (const author of plugin.authors) {
@ -30,60 +79,21 @@ function Authors() {
</Text>;
}
function Title() {
const styles = usePluginCardStyles();
const plugin = usePlugin();
function Description() {
const { plugin, result } = useCardContext();
const icon = plugin.icon && findAssetId(plugin.icon);
const textElement = (
<Text
numberOfLines={1}
variant="heading-lg/semibold"
>
{plugin.name}
</Text>
// could be empty if the description is irrelevant with the search!
const highlightedNode = result[1].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>{m}</Text>
);
return !icon ? textElement : <View
style={{
flexDirection: "row",
alignItems: "center",
gap: 6
}}
>
<Image
style={styles.smallIcon}
source={icon}
/>
{textElement}
</View>;
return <Text variant="text-md/medium">
{highlightedNode.length ? highlightedNode : plugin.description}
</Text>;
}
// TODO: Wrap in a Card-ish component with red bg
// TODO: Allow glacing at the error's stack
// function Status() {
// const plugin = usePlugin();
// const styles = usePluginCardStyles();
// const INTERACTIVE_NORMAL = useToken(tokens.colors.INTERACTIVE_NORMAL);
// if (!plugin.error) return null;
// return <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
// <View style={styles.smallIcon}>
// <Image
// tintColor={INTERACTIVE_NORMAL}
// source={findAssetId("WarningIcon")}
// />
// </View>
// <Text variant="text-sm/semibold">
// There was an error while attempting to start this plugin.
// </Text>
// </View>;
// }
const Actions = memo(() => {
const plugin = usePlugin();
const { plugin } = useCardContext();
const navigation = NavigationNative.useNavigation();
return <View style={{ flexDirection: "row", gap: 6 }}>
@ -110,14 +120,15 @@ const Actions = memo(() => {
</View>;
});
export default function PluginCard({ item: plugin }: CardWrapper<UnifiedPluginModel>) {
export default function PluginCard({ result, item: plugin }: CardWrapper<UnifiedPluginModel>) {
plugin.usePluginState();
const cardContextValue = useMemo(() => ({ plugin, result }), [plugin, result]);
return (
<PluginContext.Provider value={plugin}>
<CardContext.Provider value={cardContextValue}>
<Card>
<Stack spacing={16}>
{/* <Status /> */}
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<View style={{ flexShrink: 1 }}>
<Title />
@ -135,11 +146,9 @@ export default function PluginCard({ item: plugin }: CardWrapper<UnifiedPluginMo
</Stack>
</View>
</View>
<Text variant="text-md/medium">
{plugin.description}
</Text>
<Description />
</Stack>
</Card>
</PluginContext.Provider>
</CardContext.Provider>
);
}

View file

@ -17,7 +17,7 @@ export default function Themes() {
searchKeywords={[
"manifest.name",
"manifest.description",
p => p.manifest.authors?.map((a: Author) => a.name).join()
p => p.manifest.authors?.map((a: Author) => a.name).join(", ")
]}
fetchFunction={installTheme}
items={Object.values(themes)}