feat(themes): port more stuff from 'dev'
This commit is contained in:
parent
1eeed0f8b9
commit
0eb28e41b7
8 changed files with 194 additions and 40 deletions
|
|
@ -1,28 +1,28 @@
|
|||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import AlertModal, { AlertActionButton } from "@lib/ui/components/wrappers/AlertModal";
|
||||
import { dismissAlert, openAlert } from "@lib/ui/alerts";
|
||||
import { showSheet } from "@lib/ui/sheets";
|
||||
import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput } from "@metro/common/components";
|
||||
import { clipboard, NavigationNative } from "@metro/common";
|
||||
import { AlertActionButton, AlertModal, Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput } from "@metro/common/components";
|
||||
import { ErrorBoundary, Search } from "@ui/components";
|
||||
import { isNotNil } from "es-toolkit";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { ComponentType, ReactNode, useCallback, useMemo } from "react";
|
||||
import { ComponentType, ReactNode, useCallback, useEffect, useMemo } from "react";
|
||||
import { Image, ScrollView, View } from "react-native";
|
||||
|
||||
const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
|
||||
const { openAlert, dismissAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
|
||||
|
||||
type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
|
||||
type SearchKeywords<T> = Array<string | ((obj: T & {}) => string)>;
|
||||
|
||||
interface AddonPageProps<T extends object, I = any> {
|
||||
title: string;
|
||||
items: I[];
|
||||
searchKeywords: SearchKeywords;
|
||||
sortOptions?: Record<string, (a: I, b: I) => number>;
|
||||
searchKeywords: SearchKeywords<T>;
|
||||
sortOptions?: Record<string, (a: T, b: T) => number>;
|
||||
resolveItem?: (value: I) => T | undefined;
|
||||
safeModeHint?: {
|
||||
message?: string;
|
||||
|
|
@ -34,6 +34,9 @@ interface AddonPageProps<T extends object, I = any> {
|
|||
fetchFn?: (url: string) => Promise<void>;
|
||||
onPress?: () => void;
|
||||
};
|
||||
|
||||
OptionsActionSheetComponent?: ComponentType<any>;
|
||||
|
||||
CardComponent: ComponentType<CardWrapper<T>>;
|
||||
ListHeaderComponent?: ComponentType<any>;
|
||||
ListFooterComponent?: ComponentType<any>;
|
||||
|
|
@ -55,7 +58,7 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<vo
|
|||
|
||||
return <AlertModal
|
||||
title={props.label}
|
||||
content="Enter the URL of the source you want to install from:"
|
||||
content="Type in the source URL you want to install from:"
|
||||
extraContent={
|
||||
<Stack style={{ marginTop: -12 }}>
|
||||
<TextInput
|
||||
|
|
@ -107,15 +110,27 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<vo
|
|||
}
|
||||
|
||||
export default function AddonPage<T extends object>({ CardComponent, ...props }: AddonPageProps<T>) {
|
||||
useProxy(settings);
|
||||
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [sortFn, setSortFn] = React.useState<((a: unknown, b: unknown) => number) | null>(() => null);
|
||||
const [sortFn, setSortFn] = React.useState<((a: T, b: T) => number) | null>(() => null);
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.OptionsActionSheetComponent) {
|
||||
navigation.setOptions({
|
||||
headerRight: () => <IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("MoreHorizontalIcon")}
|
||||
onPress={() => showSheet("AddonMoreSheet", props.OptionsActionSheetComponent!)}
|
||||
/>
|
||||
});
|
||||
}
|
||||
}, [navigation]);
|
||||
|
||||
const results = useMemo(() => {
|
||||
let values = props.items;
|
||||
if (props.resolveItem) values = values.map(props.resolveItem);
|
||||
const items = values.filter(i => i && typeof i === "object");
|
||||
if (props.resolveItem) values = values.map(props.resolveItem).filter(isNotNil);
|
||||
const items = values.filter(i => isNotNil(i) && typeof i === "object");
|
||||
if (!search && sortFn) items.sort(sortFn);
|
||||
|
||||
return fuzzysort.go(search, items, { keys: props.searchKeywords, all: true });
|
||||
|
|
@ -134,7 +149,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
if (results.length === 0 && !search) {
|
||||
return <View style={{ gap: 32, flexGrow: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<View style={{ gap: 8, alignItems: "center" }}>
|
||||
<Image source={findAssetId("empty_quick_switcher")} />
|
||||
<Image source={findAssetId("empty_quick_switcher")!} />
|
||||
<Text variant="text-lg/semibold" color="text-normal">
|
||||
Oops! Nothing to see here… yet!
|
||||
</Text>
|
||||
|
|
@ -175,7 +190,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
})}
|
||||
/>}
|
||||
</View>
|
||||
{props.ListHeaderComponent && !search && <props.ListHeaderComponent />}
|
||||
{props.ListHeaderComponent && <props.ListHeaderComponent />}
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
@ -187,12 +202,12 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
|
|||
estimatedItemSize={136}
|
||||
ListHeaderComponent={headerElement}
|
||||
ListEmptyComponent={() => <View style={{ gap: 12, padding: 12, alignItems: "center" }}>
|
||||
<Image source={findAssetId("devices_not_found")} />
|
||||
<Image source={findAssetId("devices_not_found")!} />
|
||||
<Text variant="text-lg/semibold" color="text-normal">
|
||||
Hmmm... could not find that!
|
||||
</Text>
|
||||
</View>}
|
||||
contentContainerStyle={{ padding: 8, paddingHorizontal: 12 }}
|
||||
contentContainerStyle={{ padding: 8, paddingHorizontal: 12, paddingBottom: 90 }}
|
||||
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
|
||||
ListFooterComponent={props.ListFooterComponent}
|
||||
renderItem={({ item }: any) => <CardComponent item={item.obj} result={item} />}
|
||||
|
|
|
|||
|
|
@ -2,26 +2,31 @@ import { formatString, Strings } from "@core/i18n";
|
|||
import AddonPage from "@core/ui/components/AddonPage";
|
||||
import ThemeCard from "@core/ui/settings/pages/Themes/ThemeCard";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { installTheme, Theme, themes } from "@lib/addons/themes";
|
||||
import { getCurrentTheme, installTheme, themes, VdThemeInfo } from "@lib/addons/themes";
|
||||
import { colorsPref } from "@lib/addons/themes/colors/preferences";
|
||||
import { updateBunnyColor } from "@lib/addons/themes/colors/updater";
|
||||
import { Author } from "@lib/addons/types";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { Button } from "@metro/common/components";
|
||||
import { useObservable } from "@lib/api/storage";
|
||||
import { ActionSheet, BottomSheetTitleHeader, Button, TableRadioGroup, TableRadioRow, TableRowIcon } from "@metro/common/components";
|
||||
import { View } from "react-native";
|
||||
|
||||
export default function Themes() {
|
||||
useProxy(settings);
|
||||
useProxy(themes);
|
||||
|
||||
return (
|
||||
<AddonPage<Theme>
|
||||
<AddonPage<VdThemeInfo>
|
||||
title={Strings.THEMES}
|
||||
searchKeywords={[
|
||||
"data.name",
|
||||
"data.description",
|
||||
p => p.data.authors?.map((a: Author) => a.name).join(", ")
|
||||
p => p.data.authors?.map((a: Author) => a.name).join(", ") ?? ""
|
||||
]}
|
||||
sortOptions={{
|
||||
"Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
|
||||
"Name (Z-A)": (a, b) => b.name.localeCompare(a.name)
|
||||
"Name (A-Z)": (a, b) => a.data.name.localeCompare(b.data.name),
|
||||
"Name (Z-A)": (a, b) => b.data.name.localeCompare(a.data.name)
|
||||
}}
|
||||
installAction={{
|
||||
label: "Install a theme",
|
||||
|
|
@ -38,6 +43,39 @@ export default function Themes() {
|
|||
/>
|
||||
}}
|
||||
CardComponent={ThemeCard}
|
||||
OptionsActionSheetComponent={() => {
|
||||
useObservable([colorsPref]);
|
||||
|
||||
return <ActionSheet>
|
||||
<BottomSheetTitleHeader title="Options" />
|
||||
<View style={{ paddingVertical: 20, gap: 12 }}>
|
||||
<TableRadioGroup
|
||||
title="Override Theme Type"
|
||||
value={colorsPref.type ?? "auto"}
|
||||
hasIcons={true}
|
||||
onChange={type => {
|
||||
colorsPref.type = type !== "auto" ? type as "dark" | "light" : undefined;
|
||||
getCurrentTheme()?.data && updateBunnyColor(getCurrentTheme()!.data!, { update: true });
|
||||
}}
|
||||
>
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("RobotIcon")} />} label="Auto" value="auto" />
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("ThemeDarkIcon")} />} label="Dark" value="dark" />
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("ThemeLightIcon")} />} label="Light" value="light" />
|
||||
</TableRadioGroup>
|
||||
<TableRadioGroup
|
||||
title="Chat Background"
|
||||
value={colorsPref.customBackground ?? "shown"}
|
||||
hasIcons={true}
|
||||
onChange={type => {
|
||||
colorsPref.customBackground = type !== "shown" ? type as "hidden" : null;
|
||||
}}
|
||||
>
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("ImageIcon")} />} label="Show" value={"shown"} />
|
||||
<TableRadioRow icon={<TableRowIcon source={findAssetId("DenyIcon")} />} label="Hide" value={"hidden"} />
|
||||
</TableRadioGroup>
|
||||
</View>
|
||||
</ActionSheet>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
|
||||
import { writeFile } from "@lib/api/native/fs";
|
||||
import { getStoredTheme, getThemeFilePath, isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
|
||||
import { awaitStorage as newAwaitStorage } from "@lib/api/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
|
|
@ -133,7 +134,7 @@ export async function initThemes() {
|
|||
writeFile("../vendetta_theme.json", "null");
|
||||
}
|
||||
|
||||
await awaitStorage(colorsPref);
|
||||
await newAwaitStorage(colorsPref);
|
||||
|
||||
const currentTheme = getThemeFromLoader();
|
||||
initColors(currentTheme?.data ?? null);
|
||||
|
|
|
|||
4
src/lib/ui/alerts.ts
Normal file
4
src/lib/ui/alerts.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
|
||||
export const { openAlert, dismissAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
export * as alerts from "./alerts";
|
||||
export * as components from "./components";
|
||||
export * as settings from "./settings";
|
||||
export * as sheets from "./sheets";
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -12,7 +12,7 @@ const bySingularProp = createFilterDefinition<[string]>(
|
|||
);
|
||||
|
||||
const findSingular = (prop: string) => proxyLazy(() => findExports(bySingularProp(prop))?.[prop]);
|
||||
const findProp = (prop: string) => proxyLazy(() => findByProps(prop)[prop]);
|
||||
const findProp = (...props: string[]) => proxyLazy(() => findByProps(...props)[props[0]]);
|
||||
|
||||
// Discord
|
||||
export const LegacyAlert = findByDisplayNameLazy("FluxContainer(Alert)");
|
||||
|
|
@ -27,18 +27,22 @@ export const ActionSheetRow = findProp("ActionSheetRow");
|
|||
|
||||
// Buttons
|
||||
export const Button = findSingular("Button") as t.Button;
|
||||
export const TwinButtons = findProp("TwinButtons");
|
||||
export const TwinButtons = findProp("TwinButtons") as t.TwinButtons;
|
||||
export const IconButton = findSingular("IconButton") as t.IconButton;
|
||||
export const RowButton = findProp("RowButton") as t.RowButton;
|
||||
|
||||
export const PressableScale = findProp("PressableScale");
|
||||
|
||||
// Tables
|
||||
export const TableRow = findProp("TableRow");
|
||||
export const TableRowIcon = findProp("TableRowIcon");
|
||||
export const TableRowTrailingText = findProp("TableRowTrailingText");
|
||||
export const TableRowGroup = findProp("TableRowGroup");
|
||||
export const TableSwitchRow = findProp("TableSwitchRow");
|
||||
export const TableRow = findProp("TableRow") as t.TableRow;
|
||||
export const TableRowIcon = findProp("TableRowIcon") as t.TableRowIcon;
|
||||
export const TableRowTrailingText = findProp("TableRowTrailingText") as t.TableRowTrailingText;
|
||||
export const TableRowGroup = findProp("TableRowGroup") as t.TableRowGroup;
|
||||
export const TableRadioGroup = findProp("TableRadioGroup") as t.TableRadioGroup;
|
||||
export const TableRadioRow = findProp("TableRadioRow") as t.TableRadioRow;
|
||||
export const TableSwitchRow = findProp("TableSwitchRow") as t.TableSwitchRow;
|
||||
export const TableCheckboxRow = findProp("TableCheckboxRow") as t.TableCheckboxRow;
|
||||
|
||||
export const TableSwitch = findSingular("FormSwitch");
|
||||
export const TableRadio = findSingular("FormRadio");
|
||||
export const TableCheckbox = findSingular("FormCheckbox");
|
||||
|
|
@ -51,11 +55,22 @@ export const FormCheckbox = findSingular("FormCheckbox");
|
|||
export const Card = findProp("Card");
|
||||
export const RedesignCompat = proxyLazy(() => findByProps("RedesignCompat").RedesignCompat);
|
||||
|
||||
// Alert
|
||||
export const AlertModal = findProp("AlertModal");
|
||||
export const AlertActionButton = findProp("AlertActionButton");
|
||||
export const AlertActions = findProp("AlertActions");
|
||||
|
||||
// Pile
|
||||
export const AvatarPile = findSingular("AvatarPile");
|
||||
|
||||
// Misc.
|
||||
export const Stack = findProp("Stack") as t.Stack;
|
||||
export const Avatar = findProp("default", "AvatarSizes", "getStatusSize");
|
||||
|
||||
|
||||
// Inputs
|
||||
export const TextInput = findSingular("TextInput") as t.TextInput;
|
||||
export const TextArea = findSingular("TextArea");
|
||||
|
||||
// SegmentedControl
|
||||
export const SegmentedControl = findProp("SegmentedControl") as t.SegmentedControl;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { DiscordTextStyles } from "@ui/types";
|
||||
import { MutableRefObject, ReactNode, RefObject } from "react";
|
||||
import { TextStyles, ThemeColors } from "@lib/ui/types";
|
||||
import { Falsey } from "lodash";
|
||||
import { FC, MutableRefObject, PropsWithoutRef, ReactNode, RefObject } from "react";
|
||||
import type * as RN from "react-native";
|
||||
import { ImageSourcePropType, PressableProps } from "react-native";
|
||||
import { SharedValue } from "react-native-reanimated";
|
||||
|
|
@ -24,11 +25,16 @@ interface ButtonProps {
|
|||
scaleAmountInPx?: number;
|
||||
icon?: ImageSourcePropType | ReactNode;
|
||||
style?: Style;
|
||||
grow?: boolean;
|
||||
}
|
||||
|
||||
export type Button = React.ForwardRefExoticComponent<ButtonProps>;
|
||||
|
||||
interface TwinButtonsProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export type TwinButtons = React.ComponentType<TwinButtonsProps>;
|
||||
|
||||
// Segmented Control
|
||||
interface SegmentedControlItem {
|
||||
id: string;
|
||||
|
|
@ -103,7 +109,7 @@ interface TextInputProps extends Omit<RN.TextInputProps, "onChange" | "onChangeT
|
|||
trailingIcon?: React.FC<any>;
|
||||
trailingPressableProps?: PressableProps;
|
||||
trailingText?: string;
|
||||
value?: string | RN.Falsy;
|
||||
value?: string | Falsey;
|
||||
}
|
||||
|
||||
export type TextInput = React.FC<TextInputProps>;
|
||||
|
|
@ -144,8 +150,8 @@ interface ActionSheetProps {
|
|||
export type ActionSheet = React.FC<React.PropsWithChildren<ActionSheetProps>>;
|
||||
|
||||
type TextProps = React.ComponentProps<typeof RN.Text> & {
|
||||
variant?: DiscordTextStyles;
|
||||
color?: string; // TODO: type this
|
||||
variant?: TextStyles;
|
||||
color?: ThemeColors;
|
||||
lineClamp?: number;
|
||||
maxFontSizeMultiplier?: number;
|
||||
style?: RN.TextStyle;
|
||||
|
|
@ -163,3 +169,76 @@ interface IconButtonProps {
|
|||
}
|
||||
|
||||
export type IconButton = React.FC<IconButtonProps>;
|
||||
|
||||
export type PressableScale = React.FC<PropsWithoutRef<typeof RN.Pressable>>;
|
||||
|
||||
interface TableRowBaseProps {
|
||||
arrow?: boolean;
|
||||
label: string | ReactNode;
|
||||
subLabel?: string | ReactNode;
|
||||
variant?: LiteralUnion<"danger", string>;
|
||||
icon?: JSX.Element | Falsey;
|
||||
disabled?: boolean;
|
||||
trailing?: ReactNode | React.ComponentType<any>;
|
||||
}
|
||||
|
||||
interface TableRowProps extends TableRowBaseProps {
|
||||
onPress?: () => void;
|
||||
}
|
||||
|
||||
export type TableRow = React.FC<TableRowProps> & {
|
||||
Icon: TableRowIcon;
|
||||
TrailingText: TableRowTrailingText;
|
||||
Arrow: FC<{}>;
|
||||
};
|
||||
|
||||
interface TableRowTrailingTextProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type TableRowTrailingText = FC<TableRowTrailingTextProps>;
|
||||
|
||||
interface TableRowIconProps {
|
||||
style?: RN.ImageStyle;
|
||||
variant?: LiteralUnion<"danger", string>,
|
||||
source: ImageSourcePropType | undefined;
|
||||
}
|
||||
|
||||
export type TableRowIcon = React.FC<TableRowIconProps>;
|
||||
|
||||
interface TableRowGroupProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export type TableRowGroup = React.FC<TableRowGroupProps>;
|
||||
|
||||
interface TableRadioGroupProps {
|
||||
title: string;
|
||||
value: string;
|
||||
hasIcons?: boolean;
|
||||
onChange: <T extends string>(type: T) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export type TableRadioGroup = FC<TableRadioGroupProps>;
|
||||
|
||||
interface TableRadioRowProps extends TableRowBaseProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type TableRadioRow = FC<TableRadioRowProps>;
|
||||
|
||||
interface TableSwitchRowProps extends TableRowBaseProps {
|
||||
value: boolean;
|
||||
onValueChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export type TableSwitchRow = FC<TableSwitchRowProps>;
|
||||
|
||||
interface TableCheckboxRowProps extends TableRowBaseProps {
|
||||
checked: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export type TableCheckboxRow = FC<TableCheckboxRowProps>;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue