From 0cb02af0cb8e2089e80e25e210532e4a2b898d5b Mon Sep 17 00:00:00 2001 From: nexpid <60316309+nexpid@users.noreply.github.com> Date: Sun, 11 Feb 2024 13:17:15 +0100 Subject: [PATCH] feat(settings): improve visuals (#3) https://github.com/revenge-mod/Revenge/pull/3#issuecomment-1937379747 --- src/def.d.ts | 32 +++++++++ src/lib/constants.ts | 3 +- src/ui/components/Search.tsx | 44 +++++++++++- src/ui/components/index.ts | 2 + src/ui/settings/components/AddonPage.tsx | 8 +-- src/ui/settings/components/Card.tsx | 83 ++++++++++++++++++++--- src/ui/settings/components/PluginCard.tsx | 8 ++- src/ui/settings/components/ThemeCard.tsx | 6 +- src/ui/settings/pages/Plugins.tsx | 8 +++ src/ui/settings/pages/Themes.tsx | 8 +++ 10 files changed, 179 insertions(+), 23 deletions(-) diff --git a/src/def.d.ts b/src/def.d.ts index 12c61526..69b301e2 100644 --- a/src/def.d.ts +++ b/src/def.d.ts @@ -33,6 +33,37 @@ interface SearchProps { style?: _RN.TextStyle; } +interface RedesignObj { + TextInput: React.FC<{ + style?: any, + size?: "sm" | "md" | "lg"; + label?: string; + description?: string; + editable?: boolean; + focusable?: boolean; + placeholder?: string; + placeholderTextColor?: string; + defaultValue?: string; + value?: string; + isDisabled?: boolean; + leadingPressableProps?: _RN.PressableProps; + leadingIcon?: React.FC; + leadingText?: string; + trailingPressableProps?: _RN.PressableProps; + trailingIcon?: React.FC; + trailingText?: string; + secureTextEntry?: boolean; + isClearable?: boolean; + status?: "error" | "default"; + errorMessage?: string; + spellCheck?: boolean; + isCentered?: boolean; + returnKeyType?: "search"; + grow?: boolean; + onChange?: (value: string) => void; + }>; +} + // Helper types for API functions type PropIntellisense

= Record & Record; type PropsFinder = (...props: T[]) => PropIntellisense; @@ -416,6 +447,7 @@ interface VendettaObject { PROXY_PREFIX: string; HTTP_REGEX: RegExp; HTTP_REGEX_MULTI: RegExp; + ESCAPE_REGEX: RegExp; }; utils: { findInReactTree: (tree: SearchTree, filter: SearchFilter) => any; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d5ff8a22..1c29976e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -6,4 +6,5 @@ export const THEMES_CHANNEL_ID = "1091880434939482202"; export const GITHUB = "https://github.com/revenge-mod"; export const PROXY_PREFIX = "https://vd-plugins.github.io/proxy"; export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/; -export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; \ No newline at end of file +export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; +export const ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g; \ No newline at end of file diff --git a/src/ui/components/Search.tsx b/src/ui/components/Search.tsx index 56daa6b0..31e8322b 100644 --- a/src/ui/components/Search.tsx +++ b/src/ui/components/Search.tsx @@ -1,6 +1,9 @@ import { SearchProps } from "@types"; -import { stylesheet } from "@metro/common"; +import { ReactNative as RN, stylesheet } from "@metro/common"; import { findByName } from "@metro/filters"; +import { Redesign } from "."; +import { getAssetIDByName } from "../assets"; +import { semanticColors } from "../color"; const Search = findByName("StaticSearchBarContainer"); @@ -10,7 +13,42 @@ const styles = stylesheet.createThemedStyleSheet({ padding: 0, borderBottomWidth: 0, backgroundColor: "none", - } + }, + redesignSearch: { + paddingHorizontal: 8, + marginBottom: 4 + }, + icon: { + width: 16, + height: 16, + tintColor: semanticColors.INTERACTIVE_NORMAL, + }, }); -export default ({ onChangeText, placeholder, style }: SearchProps) => \ No newline at end of file +export default ({ onChangeText, placeholder, style }: SearchProps) => { + if (Redesign.TextInput) + return ( + ( + + )} + returnKeyType="search" + /> + ); + else + return ( + + ); +} \ No newline at end of file diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index 496cbbf8..1eb4ddf7 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1,3 +1,4 @@ +import { RedesignObj } from "@/def"; import { ReactNative as RN } from "@metro/common"; import { findByDisplayName, findByName, findByProps } from "@metro/filters"; @@ -9,6 +10,7 @@ export const Button = findByProps("Looks", "Colors", "Sizes") as React.Component export const HelpMessage = findByName("HelpMessage"); // React Native's included SafeAreaView only adds padding on iOS. export const SafeAreaView = findByProps("useSafeAreaInsets").SafeAreaView as typeof RN.SafeAreaView; +export const Redesign = (findByProps("Button", "ContextMenu", "TextInput") ?? {}) as RedesignObj; // Vendetta export { default as Summary } from "@ui/components/Summary"; diff --git a/src/ui/settings/components/AddonPage.tsx b/src/ui/settings/components/AddonPage.tsx index c23d027c..4799fea5 100644 --- a/src/ui/settings/components/AddonPage.tsx +++ b/src/ui/settings/components/AddonPage.tsx @@ -9,16 +9,16 @@ interface AddonPageProps { safeModeMessage: string; safeModeExtras?: JSX.Element | JSX.Element[]; card: React.ComponentType>; + keyGetter: (item: T) => (string | undefined)[] } -export default function AddonPage({ items, safeModeMessage, safeModeExtras, card: CardComponent }: AddonPageProps) { +export default function AddonPage({ items, safeModeMessage, safeModeExtras, card: CardComponent, keyGetter }: AddonPageProps) { useProxy(settings) useProxy(items); const [search, setSearch] = React.useState(""); return ( - {/* TODO: Implement better searching than just by ID */} {settings.safeMode?.enabled && @@ -33,8 +33,8 @@ export default function AddonPage({ items, safeModeMessage, safeModeExtras, c } style={{ paddingHorizontal: 10, paddingTop: 10 }} contentContainerStyle={{ paddingBottom: 20 }} - data={Object.values(items).filter(i => i.id?.toLowerCase().includes(search))} - renderItem={({ item, index }) => } + data={Object.values(items).filter(i => keyGetter(i).some(x => x?.toLowerCase().includes(search)))} + renderItem={({ item, index }) => } /> ) diff --git a/src/ui/settings/components/Card.tsx b/src/ui/settings/components/Card.tsx index 9dc54a38..3a58709d 100644 --- a/src/ui/settings/components/Card.tsx +++ b/src/ui/settings/components/Card.tsx @@ -1,7 +1,8 @@ +import { ESCAPE_REGEX } from "@/lib/constants"; import { ReactNative as RN, stylesheet } from "@metro/common"; -import { findByProps } from "@metro/filters"; +import { findByProps, findByStoreName } from "@metro/filters"; import { getAssetIDByName } from "@ui/assets"; -import { semanticColors } from "@ui/color"; +import { rawColors, semanticColors } from "@ui/color"; import { Forms } from "@ui/components"; const { FormRow, FormSwitch, FormRadio } = Forms; @@ -9,16 +10,28 @@ const { hideActionSheet } = findByProps("openLazy", "hideActionSheet"); const { showSimpleActionSheet } = findByProps("showSimpleActionSheet"); // TODO: These styles work weirdly. iOS has cramped text, Android with low DPI probably does too. Fix? +const { TextStyleSheet } = findByProps("TextStyleSheet"); const styles = stylesheet.createThemedStyleSheet({ card: { backgroundColor: semanticColors?.BACKGROUND_SECONDARY, - borderRadius: 5, + borderRadius: 12, }, header: { padding: 0, backgroundColor: semanticColors?.BACKGROUND_TERTIARY, - borderTopLeftRadius: 5, - borderTopRightRadius: 5, + borderRadius: 12 + }, + headerChildren: { + flexDirection: "column", + justifyContent: "center" + }, + headerLabel: { + color: semanticColors?.TEXT_NORMAL, + ...TextStyleSheet["text-md/semibold"] + }, + headerSubtitle: { + color: semanticColors?.TEXT_MUTED, + ...TextStyleSheet["text-sm/semibold"] }, actions: { flexDirection: "row-reverse", @@ -30,6 +43,22 @@ const styles = stylesheet.createThemedStyleSheet({ marginLeft: 5, tintColor: semanticColors?.INTERACTIVE_NORMAL, }, + iconContainer: { + width: 33, + height: 33, + borderRadius: 17, + backgroundColor: semanticColors?.BACKGROUND_ACCENT, + justifyContent: "center", + alignItems: "center" + }, + smallerIcon: { + width: 22, + height: 22, + tintColor: semanticColors?.INTERACTIVE_NORMAL + }, + highlight: { + backgroundColor: "#F0" + rawColors.YELLOW_300.slice(1), + } }) interface Action { @@ -45,19 +74,36 @@ interface OverflowAction extends Action { export interface CardWrapper { item: T; index: number; + highlight: string } interface CardProps { index?: number; - headerLabel: string | React.ComponentType; + headerLabel: string; + headerSublabel?: string; headerIcon?: string; toggleType?: "switch" | "radio"; toggleValue?: boolean; onToggleChange?: (v: boolean) => void; - descriptionLabel?: string | React.ComponentType; + descriptionLabel?: string; actions?: Action[]; overflowTitle?: string; overflowActions?: OverflowAction[]; + highlight: string +} + +const highlighter = (str: string, highlight: string) => { + if (!highlight) return str; + + return str + .split(new RegExp("(" + highlight.replace(ESCAPE_REGEX, "\\$&") + ")", "gi")) + .map((x, i) => + i % 2 === 1 ? ( + {x} + ) : ( + x + ) + ); } export default function Card(props: CardProps) { @@ -67,8 +113,24 @@ export default function Card(props: CardProps) { } + label={ + + {highlighter(props.headerLabel, props.highlight)} + {props.headerSublabel && ( + {highlighter(props.headerSublabel, props.highlight)} + )} + + } + leading={ + props.headerIcon && ( + + + + ) + } trailing={props.toggleType && (props.toggleType === "switch" ? ( - {/* TODO: Look into making this respect brand color */} ) )} /> {props.overflowActions && ) { +export default function PluginCard({ item: plugin, index, highlight }: CardWrapper) { const settings = getSettings(plugin.id); const navigation = NavigationNative.useNavigation(); const [removed, setRemoved] = React.useState(false); // This is needed because of Reactâ„¢ if (removed) return null; + + const authors = plugin.manifest.authors; return ( i.name).join(", ")}`} + headerLabel={plugin.manifest.name} + headerSublabel={authors?.[0] && `by ${plugin.manifest.authors.map(i => i.name).join(", ")}`} headerIcon={plugin.manifest.vendetta?.icon || "ic_application_command_24px"} toggleType="switch" toggleValue={plugin.enabled} @@ -120,6 +123,7 @@ export default function PluginCard({ item: plugin, index }: CardWrapper) }) }] : []), ]} + highlight={highlight} /> ) } diff --git a/src/ui/settings/components/ThemeCard.tsx b/src/ui/settings/components/ThemeCard.tsx index f90763ac..22bcb097 100644 --- a/src/ui/settings/components/ThemeCard.tsx +++ b/src/ui/settings/components/ThemeCard.tsx @@ -14,7 +14,7 @@ async function selectAndReload(value: boolean, id: string) { BundleUpdaterManager.reload(); } -export default function ThemeCard({ item: theme, index }: CardWrapper) { +export default function ThemeCard({ item: theme, index, highlight }: CardWrapper) { useProxy(settings); const [removed, setRemoved] = React.useState(false); @@ -26,7 +26,8 @@ export default function ThemeCard({ item: theme, index }: CardWrapper) { return ( i.name).join(", ")}` : ""}`} + headerLabel={theme.data.name} + headerSublabel={authors?.[0] && `by ${authors.map(i => i.name).join(", ")}`} descriptionLabel={theme.data.description ?? "No description."} toggleType={!settings.safeMode?.enabled ? "radio" : undefined} toggleValue={theme.selected} @@ -86,6 +87,7 @@ export default function ThemeCard({ item: theme, index }: CardWrapper) { }) }, ]} + highlight={highlight} /> ) } diff --git a/src/ui/settings/pages/Plugins.tsx b/src/ui/settings/pages/Plugins.tsx index 59bf8a06..cbd3a776 100644 --- a/src/ui/settings/pages/Plugins.tsx +++ b/src/ui/settings/pages/Plugins.tsx @@ -13,6 +13,14 @@ export default function Plugins() { items={plugins} safeModeMessage="You are in Safe Mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page." card={PluginCard} + keyGetter={(i) => + [ + i.id, + i.manifest.name, + i.manifest.description, + i.manifest.authors?.map((x) => x.name), + ].flat() + } /> ) } \ No newline at end of file diff --git a/src/ui/settings/pages/Themes.tsx b/src/ui/settings/pages/Themes.tsx index a1b24898..85396076 100644 --- a/src/ui/settings/pages/Themes.tsx +++ b/src/ui/settings/pages/Themes.tsx @@ -23,6 +23,14 @@ export default function Themes() { style={{ marginTop: 8 }} /> : undefined} card={ThemeCard} + keyGetter={(i) => + [ + i.id, + i.data.name, + i.data.description, + i.data.authors?.map((x) => x.name), + ].flat() + } /> ) } \ No newline at end of file