Skip to content
This repository has been archived by the owner on Apr 5, 2024. It is now read-only.

Commit

Permalink
feat(settings): improve visuals (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
nexpid committed Feb 11, 2024
1 parent 4121dc9 commit 0cb02af
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 23 deletions.
32 changes: 32 additions & 0 deletions src/def.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
leadingText?: string;
trailingPressableProps?: _RN.PressableProps;
trailingIcon?: React.FC<any>;
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<P extends string | symbol> = Record<P, any> & Record<PropertyKey, any>;
type PropsFinder = <T extends string | symbol>(...props: T[]) => PropIntellisense<T>;
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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;
44 changes: 41 additions & 3 deletions src/ui/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -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");

Expand All @@ -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) => <Search style={[styles.search, style]} placeholder={placeholder} onChangeText={onChangeText} />
export default ({ onChangeText, placeholder, style }: SearchProps) => {
if (Redesign.TextInput)
return (
<Redesign.TextInput
style={[styles.redesignSearch, style]}
size="sm"
placeholder={placeholder}
onChange={onChangeText}
isClearable={true}
leadingIcon={() => (
<RN.Image
source={getAssetIDByName("MagnifyingGlassIcon")}
style={styles.icon}
/>
)}
returnKeyType="search"
/>
);
else
return (
<Search
style={[styles.search, style]}
placeholder={placeholder}
onChangeText={onChangeText}
/>
);
}
2 changes: 2 additions & 0 deletions src/ui/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RedesignObj } from "@/def";
import { ReactNative as RN } from "@metro/common";
import { findByDisplayName, findByName, findByProps } from "@metro/filters";

Expand All @@ -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";
Expand Down
8 changes: 4 additions & 4 deletions src/ui/settings/components/AddonPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ interface AddonPageProps<T> {
safeModeMessage: string;
safeModeExtras?: JSX.Element | JSX.Element[];
card: React.ComponentType<CardWrapper<T>>;
keyGetter: (item: T) => (string | undefined)[]
}

export default function AddonPage<T>({ items, safeModeMessage, safeModeExtras, card: CardComponent }: AddonPageProps<T>) {
export default function AddonPage<T>({ items, safeModeMessage, safeModeExtras, card: CardComponent, keyGetter }: AddonPageProps<T>) {
useProxy(settings)
useProxy(items);
const [search, setSearch] = React.useState("");

return (
<ErrorBoundary>
{/* TODO: Implement better searching than just by ID */}
<RN.FlatList
ListHeaderComponent={<>
{settings.safeMode?.enabled && <RN.View style={{ marginBottom: 10 }}>
Expand All @@ -33,8 +33,8 @@ export default function AddonPage<T>({ 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 }) => <CardComponent item={item} index={index} />}
data={Object.values(items).filter(i => keyGetter(i).some(x => x?.toLowerCase().includes(search)))}
renderItem={({ item, index }) => <CardComponent item={item} index={index} highlight={search} />}
/>
</ErrorBoundary>
)
Expand Down
83 changes: 72 additions & 11 deletions src/ui/settings/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
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;
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",
Expand All @@ -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 {
Expand All @@ -45,19 +74,36 @@ interface OverflowAction extends Action {
export interface CardWrapper<T> {
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 ? (
<RN.Text style={styles.highlight}>{x}</RN.Text>
) : (
x
)
);
}

export default function Card(props: CardProps) {
Expand All @@ -67,8 +113,24 @@ export default function Card(props: CardProps) {
<RN.View style={[styles.card, { marginTop: props.index !== 0 ? 10 : 0 }]}>
<FormRow
style={styles.header}
label={props.headerLabel}
leading={props.headerIcon && <FormRow.Icon source={getAssetIDByName(props.headerIcon)} />}
label={
<RN.View style={styles.headerChildren}>
<RN.Text style={styles.headerLabel}>{highlighter(props.headerLabel, props.highlight)}</RN.Text>
{props.headerSublabel && (
<RN.Text style={styles.headerSubtitle}>{highlighter(props.headerSublabel, props.highlight)}</RN.Text>
)}
</RN.View>
}
leading={
props.headerIcon && (
<RN.View style={styles.iconContainer}>
<RN.Image
source={getAssetIDByName(props.headerIcon)}
style={styles.smallerIcon}
/>
</RN.View>
)
}
trailing={props.toggleType && (props.toggleType === "switch" ?
(<FormSwitch
style={RN.Platform.OS === "android" && { marginVertical: -15 }}
Expand All @@ -80,13 +142,12 @@ export default function Card(props: CardProps) {
pressableState = !pressableState;
props.onToggleChange?.(pressableState)
}}>
{/* TODO: Look into making this respect brand color */}
<FormRadio selected={props.toggleValue} />
</RN.Pressable>)
)}
/>
<FormRow
label={props.descriptionLabel}
label={props.descriptionLabel && highlighter(props.descriptionLabel, props.highlight)}
trailing={
<RN.View style={styles.actions}>
{props.overflowActions && <RN.TouchableOpacity
Expand Down
8 changes: 6 additions & 2 deletions src/ui/settings/components/PluginCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@ async function stopThenStart(plugin: Plugin, callback: Function) {
if (plugin.enabled) await startPlugin(plugin.id);
}

export default function PluginCard({ item: plugin, index }: CardWrapper<Plugin>) {
export default function PluginCard({ item: plugin, index, highlight }: CardWrapper<Plugin>) {
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 (
<Card
index={index}
// TODO: Actually make use of user IDs
headerLabel={`${plugin.manifest.name} by ${plugin.manifest.authors.map(i => 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}
Expand Down Expand Up @@ -120,6 +123,7 @@ export default function PluginCard({ item: plugin, index }: CardWrapper<Plugin>)
})
}] : []),
]}
highlight={highlight}
/>
)
}
6 changes: 4 additions & 2 deletions src/ui/settings/components/ThemeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async function selectAndReload(value: boolean, id: string) {
BundleUpdaterManager.reload();
}

export default function ThemeCard({ item: theme, index }: CardWrapper<Theme>) {
export default function ThemeCard({ item: theme, index, highlight }: CardWrapper<Theme>) {
useProxy(settings);
const [removed, setRemoved] = React.useState(false);

Expand All @@ -26,7 +26,8 @@ export default function ThemeCard({ item: theme, index }: CardWrapper<Theme>) {
return (
<Card
index={index}
headerLabel={`${theme.data.name} ${authors ? `by ${authors.map(i => 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}
Expand Down Expand Up @@ -86,6 +87,7 @@ export default function ThemeCard({ item: theme, index }: CardWrapper<Theme>) {
})
},
]}
highlight={highlight}
/>
)
}
8 changes: 8 additions & 0 deletions src/ui/settings/pages/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
/>
)
}
8 changes: 8 additions & 0 deletions src/ui/settings/pages/Themes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
/>
)
}

0 comments on commit 0cb02af

Please sign in to comment.