refactor(ui): restructure and begin rework on plugins ui

This commit is contained in:
amsyarasyiq 2024-05-23 19:29:56 +08:00
parent e8f60c92cf
commit b0c62d1094
21 changed files with 316 additions and 363 deletions

View file

@ -0,0 +1,154 @@
import { requireAssetIndex } from "@lib/api/assets";
import { lazyDestructure } from "@lib/utils/lazy";
import { Card, FormRadio, FormSwitch, IconButton, LegacyFormRow, Stack, Text } from "@metro/common/components";
import { findByProps } from "@metro/utils";
import { semanticColors } from "@ui/color";
import { createStyles, TextStyleSheet } from "@ui/styles";
import { TouchableOpacity, View } from "react-native";
const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet"));
const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
// TODO: These styles work weirdly. iOS has cramped text, Android with low DPI probably does too. Fix?
const useStyles = createStyles({
card: {
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
borderRadius: 12,
overflow: "hidden"
},
header: {
padding: 0,
},
headerLeading: {
flexDirection: "column",
justifyContent: "center",
scale: 1.2
},
headerTrailing: {
display: "flex",
flexDirection: "row",
gap: 15,
alignItems: "center"
},
headerLabel: {
...TextStyleSheet["heading-md/semibold"],
color: semanticColors.TEXT_NORMAL,
},
headerSubtitle: {
...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_MUTED,
},
descriptionLabel: {
...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_NORMAL,
},
actions: {
flexDirection: "row-reverse",
alignItems: "center",
gap: 5
},
iconStyle: {
tintColor: semanticColors.LOGO_PRIMARY,
opacity: 0.2,
height: 64,
width: 64,
left: void 0,
right: "30%",
top: "-10%"
}
});
interface Action {
icon: string;
disabled?: boolean;
onPress: () => void;
}
interface OverflowAction extends Action {
label: string;
isDestructive?: boolean;
}
export interface CardWrapper<T> {
item: T;
index: number;
}
interface CardProps {
index?: number;
headerLabel: string;
headerSublabel?: string;
headerIcon?: string;
toggleType?: "switch" | "radio";
toggleValue: () => boolean;
onToggleChange?: (v: boolean) => void;
descriptionLabel?: string;
actions?: Action[];
overflowTitle?: string;
overflowActions?: OverflowAction[];
}
export default function AddonCard(props: CardProps) {
const styles = useStyles();
return (
<Card>
<Stack spacing={16}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<View style={styles.headerLeading}>
<Text style={styles.headerLabel}>{props.headerLabel}</Text>
{props.headerSublabel && (
<Text style={styles.headerSubtitle}>{props.headerSublabel}</Text>
)}
</View>
<View style={[styles.headerTrailing, { marginLeft: "auto" }]}>
<View style={styles.actions}>
{props.overflowActions &&
<IconButton
onPress={() => showSimpleActionSheet({
key: "CardOverflow",
header: {
title: props.overflowTitle,
icon: props.headerIcon && <LegacyFormRow.Icon style={{ marginRight: 8 }} source={requireAssetIndex(props.headerIcon)} />,
onClose: () => hideActionSheet(),
},
options: props.overflowActions?.map(i => ({
...i,
icon: requireAssetIndex(i.icon)
})),
})}
size="sm"
variant="secondary"
icon={requireAssetIndex("CircleInformationIcon-primary")}
/>}
{props.actions?.map(({ icon, onPress, disabled }) => (
<IconButton
onPress={onPress}
disabled={disabled}
size="sm"
variant="secondary"
icon={requireAssetIndex(icon)}
/>
))}
</View>
{props.toggleType && (props.toggleType === "switch" ?
<FormSwitch
value={props.toggleValue()}
onValueChange={props.onToggleChange}
/>
:
<TouchableOpacity onPress={() => {
props.onToggleChange?.(!props.toggleValue());
}}>
<FormRadio selected={props.toggleValue()} />
</TouchableOpacity>
)}
</View>
</View>
{props.descriptionLabel && <Text variant="text-md/medium">
{props.descriptionLabel}
</Text>}
</Stack>
</Card >
);
}

View file

@ -1,13 +1,11 @@
import { Strings } from "@core/i18n";
import { CardWrapper } from "@core/ui/components/Card";
import { CardWrapper } from "@core/ui/components/AddonCard";
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 { lazyDestructure } from "@lib/utils/lazy";
import { clipboard } from "@metro/common";
import { HelpMessage } from "@metro/common/components";
import { findByProps, findByPropsProxy } from "@metro/utils";
import { FloatingActionButton, HelpMessage } from "@metro/common/components";
import { showInputAlert } from "@ui/alerts";
import { ErrorBoundary, Search } from "@ui/components";
import fuzzysort from "fuzzysort";
@ -18,14 +16,12 @@ export const RemoveModeContext = createContext(false);
interface AddonPageProps<T> {
title: string;
floatingButtonText: string;
fetchFunction: (url: string) => Promise<void>;
items: Record<string, T & ({ id: string; } | { name: string; })>;
safeModeMessage: string;
safeModeExtras?: JSX.Element | JSX.Element[];
card: React.ComponentType<CardWrapper<T>>;
isRemoveMode?: boolean;
headerComponent?: JSX.Element;
onFABPress?: () => void;
}
@ -44,57 +40,42 @@ function getItemsByQuery<T extends AddonPageProps<unknown>["items"][string]>(ite
}).map(r => r.obj);
}
const reanimated = findByPropsProxy("useSharedValue");
const { FloatingActionButton } = lazyDestructure(() => findByProps("FloatingActionButton"));
export default function AddonPage<T>({ floatingButtonText, fetchFunction, items, safeModeMessage, safeModeExtras, card: CardComponent, isRemoveMode, headerComponent, onFABPress }: AddonPageProps<T>) {
useProxy(items);
export default function AddonPage<T>({ card: CardComponent, ...props }: AddonPageProps<T>) {
useProxy(props.items);
useProxy(settings);
const collapseText = reanimated.useSharedValue(0);
const yOffset = React.useRef<number>(0);
const [search, setSearch] = React.useState("");
return (
<ErrorBoundary>
{/* TODO: Implement better searching than just by ID */}
<FlatList
ListHeaderComponent={<>
ListHeaderComponent={<View>
{settings.safeMode?.enabled && <View style={{ marginBottom: 10 }}>
<HelpMessage messageType={0}>{safeModeMessage}</HelpMessage>
{safeModeExtras}
<HelpMessage messageType={0}>{props.safeModeMessage}</HelpMessage>
{props.safeModeExtras}
</View>}
<Search
style={{ marginBottom: 15 }}
onChangeText={(v: string) => setSearch(v.toLowerCase())}
placeholder={Strings.SEARCH}
/>
{headerComponent}
</>}
onScroll={e => {
if (e.nativeEvent.contentOffset.y <= 0) return;
collapseText.value = Number(e.nativeEvent.contentOffset.y > yOffset.current);
yOffset.current = e.nativeEvent.contentOffset.y;
}}
</View>}
style={{ paddingHorizontal: 10, paddingTop: 10 }}
contentContainerStyle={{ paddingBottom: 90, paddingHorizontal: 5 }}
data={getItemsByQuery(Object.values(items).filter(i => typeof i === "object"), search)}
renderItem={({ item, index }) => <RemoveModeContext.Provider value={!!isRemoveMode}>
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>}
/>
<FloatingActionButton
text={floatingButtonText}
icon={requireAssetIndex("PlusLargeIcon")}
state={{ collapseText }}
onPress={onFABPress ?? (() => {
onPress={props.onFABPress ?? (() => {
// from ./InstallButton.tsx
clipboard.getString().then(content =>
showInputAlert({
title: floatingButtonText,
initialValue: content.match(HTTP_REGEX_MULTI)?.[0] ?? "",
placeholder: Strings.URL_PLACEHOLDER,
onConfirm: (input: string) => fetchFunction(input),
onConfirm: (input: string) => props.fetchFunction(input),
confirmText: Strings.INSTALL,
cancelText: Strings.CANCEL,
})

View file

@ -1,165 +0,0 @@
import { requireAssetIndex } from "@lib/api/assets";
import { lazyDestructure } from "@lib/utils/lazy";
import { ReactNative as RN } from "@metro/common";
import { FormCheckbox, FormSwitch, IconButton, LegacyFormRow } from "@metro/common/components";
import { findByProps } from "@metro/utils";
import { semanticColors } from "@ui/color";
import { createStyles, TextStyleSheet } from "@ui/styles";
import { ImageBackground, View } from "react-native";
const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet"));
const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
// TODO: These styles work weirdly. iOS has cramped text, Android with low DPI probably does too. Fix?
const useStyles = createStyles({
card: {
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
borderRadius: 12,
overflow: "hidden"
},
header: {
padding: 0,
},
headerLeading: {
flexDirection: "column",
justifyContent: "center",
scale: 1.2
},
headerTrailing: {
display: "flex",
flexDirection: "row",
gap: 15,
alignItems: "center"
},
headerLabel: {
...TextStyleSheet["heading-md/semibold"],
color: semanticColors.TEXT_NORMAL,
},
headerSubtitle: {
...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_MUTED,
},
descriptionLabel: {
...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_NORMAL,
},
actions: {
flexDirection: "row-reverse",
alignItems: "center",
gap: 5
},
iconStyle: {
tintColor: semanticColors.LOGO_PRIMARY,
opacity: 0.2,
height: 64,
width: 64,
left: void 0,
right: "30%",
top: "-10%"
}
});
interface Action {
icon: string;
disabled?: boolean;
onPress: () => void;
}
interface OverflowAction extends Action {
label: string;
isDestructive?: boolean;
}
export interface CardWrapper<T> {
item: T;
index: number;
}
interface CardProps {
index?: number;
headerLabel: string;
headerSublabel?: string;
headerIcon?: string;
toggleType?: "switch" | "radio";
toggleValue?: boolean;
onToggleChange?: (v: boolean) => void;
descriptionLabel?: string;
actions?: Action[];
overflowTitle?: string;
overflowActions?: OverflowAction[];
}
export default function Card(props: CardProps) {
const styles = useStyles();
return (
<RN.View style={[styles.card, { marginTop: props.index !== 0 ? 15 : 0 }]}>
<ImageBackground
source={props.headerIcon && requireAssetIndex(props.headerIcon) || {}}
resizeMode="cover"
imageStyle={styles.iconStyle}
>
<LegacyFormRow
style={styles.header}
label={
<View style={styles.headerLeading}>
<RN.Text style={styles.headerLabel}>{props.headerLabel}</RN.Text>
{props.headerSublabel && (
<RN.Text style={styles.headerSubtitle}>{props.headerSublabel}</RN.Text>
)}
</View>
}
trailing={
<View style={styles.headerTrailing}>
<View style={styles.actions}>
{props.overflowActions &&
<IconButton
onPress={() => showSimpleActionSheet({
key: "CardOverflow",
header: {
title: props.overflowTitle,
icon: props.headerIcon && <LegacyFormRow.Icon style={{ marginRight: 8 }} source={requireAssetIndex(props.headerIcon)} />,
onClose: () => hideActionSheet(),
},
options: props.overflowActions?.map(i => ({ ...i, icon: requireAssetIndex(i.icon) })),
})}
size="sm"
variant="secondary"
icon={requireAssetIndex("CircleInformationIcon-primary")}
/>}
{props.actions?.map(({ icon, onPress, disabled }) => (
<IconButton
onPress={onPress}
disabled={disabled}
size="sm"
variant="secondary"
icon={requireAssetIndex(icon)}
/>
))}
</View>
{props.toggleType && (props.toggleType === "switch" ?
<FormSwitch
value={props.toggleValue}
onValueChange={props.onToggleChange}
/>
:
<RN.Pressable onPress={() => {
props.onToggleChange?.(!props.toggleValue);
}}>
<FormCheckbox checked={props.toggleValue} />
</RN.Pressable>
)}
</View>
}
/>
<LegacyFormRow
label={
<RN.View>
<RN.Text style={styles.descriptionLabel}>{props.descriptionLabel}</RN.Text>
</RN.View>
}
/>
</ImageBackground>
</RN.View>
);
}

View file

@ -1,136 +0,0 @@
import { formatString, Strings } from "@core/i18n";
import Card, { CardWrapper } from "@core/ui/components/Card";
import { requireAssetIndex } from "@lib/api/assets";
import { purgeStorage, useProxy } from "@lib/api/storage";
import { BunnyPlugin, fetchPlugin, getSettings, removePlugin, startPlugin, stopPlugin } from "@lib/managers/plugins";
import { ButtonColors } from "@lib/utils/types";
import { clipboard, NavigationNative } from "@metro/common";
import { showConfirmationAlert } from "@ui/alerts";
import { showToast } from "@ui/toasts";
async function stopThenStart(plugin: BunnyPlugin, callback: Function) {
if (plugin.enabled) stopPlugin(plugin.id, false);
callback();
if (plugin.enabled) await startPlugin(plugin.id);
}
export default function PluginCard({ item: plugin, index }: CardWrapper<BunnyPlugin>) {
useProxy(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;
return (
<Card
index={index}
// TODO: Actually make use of user IDs
headerLabel={`${plugin.manifest.name}`}
headerSublabel={plugin.manifest.authors?.[0] && `by ${plugin.manifest.authors.map(i => i.name).join(", ")}`}
headerIcon={plugin.manifest.vendetta?.icon}
toggleType="switch"
toggleValue={plugin.enabled}
onToggleChange={(v: boolean) => {
try {
if (v) startPlugin(plugin.id); else stopPlugin(plugin.id);
} catch (e) {
showToast((e as Error).message, requireAssetIndex("Small"));
}
}}
descriptionLabel={plugin.manifest.description}
overflowTitle={plugin.manifest.name}
actions={[{
icon: "WrenchIcon",
disabled: !settings,
onPress: () => navigation.push("VendettaCustomPage", {
title: plugin.manifest.name,
render: settings,
})
}]}
overflowActions={[
...(settings ? [{
label: Strings.OVERFLOW_PLUGIN_SETTINGS,
icon: "settings",
onPress: () => navigation.push("VendettaCustomPage", {
title: plugin.manifest.name,
render: settings,
})
}] : []),
{
icon: "ic_sync_24px",
label: Strings.REFETCH,
onPress: async () => {
stopThenStart(plugin, () => {
fetchPlugin(plugin.id).then(async () => {
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, requireAssetIndex("toast_image_saved"));
}).catch(() => {
showToast(Strings.PLUGIN_REFETCH_FAILED, requireAssetIndex("Small"));
});
});
},
},
{
icon: "copy",
label: Strings.COPY_URL,
onPress: () => {
clipboard.setString(plugin.id);
showToast.showCopyToClipboard();
}
},
{
icon: "ic_download_24px",
label: plugin.update ? Strings.DISABLE_UPDATES : Strings.ENABLE_UPDATES,
onPress: () => {
plugin.update = !plugin.update;
showToast(formatString("TOASTS_PLUGIN_UPDATE", { update: plugin.update, name: plugin.manifest.name }), requireAssetIndex("toast_image_saved"));
}
},
{
icon: "ic_duplicate",
label: Strings.CLEAR_DATA,
isDestructive: true,
onPress: () => showConfirmationAlert({
title: Strings.HOLD_UP,
content: formatString("ARE_YOU_SURE_TO_CLEAR_DATA", { name: plugin.manifest.name }),
confirmText: Strings.CLEAR,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
onConfirm: () => {
stopThenStart(plugin, () => {
try {
purgeStorage(plugin.id);
showToast(formatString("CLEAR_DATA_SUCCESSFUL", { name: plugin.manifest.name }), requireAssetIndex("trash"));
} catch {
showToast(formatString("CLEAR_DATA_FAILED", { name: plugin.manifest.name }), requireAssetIndex("Small"));
}
});
}
}),
},
{
icon: "ic_message_delete",
label: Strings.DELETE,
isDestructive: true,
onPress: () => showConfirmationAlert({
title: Strings.HOLD_UP,
content: formatString("ARE_YOU_SURE_TO_DELETE_PLUGIN", { name: plugin.manifest.name }),
confirmText: Strings.DELETE,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
onConfirm: () => {
try {
removePlugin(plugin.id);
setRemoved(true);
} catch (e) {
showToast((e as Error).message, requireAssetIndex("Small"));
}
}
}),
}
]}
/>
);
}

View file

@ -1,4 +1,4 @@
import AssetDisplay from "@core/ui/components/AssetDisplay";
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
import { assetsMap } from "@lib/api/assets";
import { LegacyFormDivider } from "@metro/common/components";
import { ErrorBoundary, Search } from "@ui/components";

View file

@ -1,6 +1,6 @@
import { Strings } from "@core/i18n";
import { CheckState, useFileExists } from "@core/ui/settings/hooks/useFS";
import AssetBrowser from "@core/ui/settings/pages/AssetBrowser";
import AssetBrowser from "@core/ui/settings/pages/Developer/AssetBrowser";
import { requireAssetIndex } from "@lib/api/assets";
import { getReactDevToolsProp, getReactDevToolsVersion, isLoaderConfigSupported, isReactDevToolsPreloaded, isVendettaLoader } from "@lib/api/native/loader";
import { useProxy } from "@lib/api/storage";

View file

@ -1,5 +1,6 @@
import { Strings } from "@core/i18n";
import { CardWrapper } from "@core/ui/components/Card";
import { CardWrapper } from "@core/ui/components/AddonCard";
import { RemoveModeContext } from "@core/ui/components/AddonPage";
import { requireAssetIndex } from "@lib/api/assets";
import { useProxy } from "@lib/api/storage";
import { FontDefinition, fonts, removeFont, selectFont } from "@lib/managers/fonts";
@ -8,8 +9,6 @@ import { showToast } from "@ui/toasts";
import { useContext } from "react";
import { Pressable, View } from "react-native";
import { RemoveModeContext } from "./AddonPage";
export default function FontCard({ item: font, index }: CardWrapper<FontDefinition>) {
useProxy(fonts);

View file

@ -47,7 +47,7 @@ function RevengeFontsExtractor({ fonts, setName }: {
placeholder={fontName || "Whitney"}
onChange={setFontName}
errorMessage={error}
status={error ? "error" : void 0}
state={error ? "error" : void 0}
/>
<Text variant="text-xs/normal" color="text-muted">
{formatString("THEME_EXTRACTOR_DESC", {
@ -96,7 +96,7 @@ function JsonFontImporter({ fonts, setName }: {
placeholder={"https://link.to/font/pack.json"}
onChange={setFontLink}
errorMessage={error}
status={error ? "error" : void 0}
state={error ? "error" : void 0}
/>
<Button
size="md"

View file

@ -1,6 +1,6 @@
import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import FontCard from "@core/ui/components/FontCard";
import FontImporter from "@core/ui/settings/pages/Fonts/FontImporter";
import { useProxy } from "@lib/api/storage";
import { FontDefinition, fonts, installFont } from "@lib/managers/fonts";
import { settings } from "@lib/settings";
@ -9,7 +9,7 @@ import { LegacyFormText } from "@metro/common/components";
import { useEffect, useState } from "react";
import { TouchableOpacity } from "react-native";
import FontImporter from "./FontImporter";
import FontCard from "./FontCard";
export default function Plugins() {
useProxy(settings);
@ -36,7 +36,6 @@ export default function Plugins() {
return (
<AddonPage<FontDefinition>
title={Strings.FONTS}
floatingButtonText={Strings.INSTALL_FONT}
fetchFunction={installFont}
items={fonts as Record<string, FontDefinition>}
safeModeMessage={Strings.SAFE_MODE_NOTICE_FONTS}

View file

@ -1,5 +1,5 @@
import { Strings } from "@core/i18n";
import Version from "@core/ui/components/Version";
import Version from "@core/ui/settings/pages/General/Version";
import { useProxy } from "@lib/api/storage";
import { getDebugInfo } from "@lib/debug";
import { settings } from "@lib/settings";

View file

@ -1,5 +1,6 @@
import { Strings } from "@core/i18n";
import { PyoncordIcon } from "@core/ui/settings";
import About from "@core/ui/settings/pages/General/About";
import { requireAssetIndex } from "@lib/api/assets";
import { useProxy } from "@lib/api/storage";
import { getDebugInfo, toggleSafeMode } from "@lib/debug";
@ -9,8 +10,6 @@ import { NavigationNative, url } from "@metro/common";
import { Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components";
import { NativeModules, ScrollView } from "react-native";
import About from "./About";
export default function General() {
useProxy(settings);

View file

@ -0,0 +1,90 @@
import { CardWrapper } from "@core/ui/components/AddonCard";
import { requireAssetIndex } from "@lib/api/assets";
import { useProxy } from "@lib/api/storage";
import { BunnyPlugin, startPlugin, stopPlugin } from "@lib/managers/plugins";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import { createContext, useContext } from "react";
import { Image, View } from "react-native";
import { usePluginCardStyles } from "./usePluginCardStyles";
const PluginContext = createContext<BunnyPlugin>(null!);
const usePlugin = () => useContext(PluginContext);
function Authors() {
const plugin = usePlugin();
const children = ["by "];
for (const author of plugin.manifest.authors) {
children.push(author.name);
children.push(", ");
}
children.pop();
return <Text variant="text-md/semibold" color="text-muted">
{children}
</Text>;
}
function Title() {
const styles = usePluginCardStyles();
const plugin = usePlugin();
return <Text variant="heading-lg/semibold">
{plugin.manifest.vendetta?.icon && <>
<Image
style={styles.iconStyle}
source={{
// This is pretty dirty but RN won't listen if I declare height and width somewhere else
...Image.resolveAssetSource(
requireAssetIndex(plugin.manifest.vendetta?.icon)
),
height: 18,
width: 18
}}
/>
{" "}
</>}
{plugin.manifest.name}
</Text>;
}
export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
useProxy(plugin);
return (
<PluginContext.Provider value={plugin}>
<Card>
<Stack spacing={16}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Stack spacing={0}>
<Title />
<Authors />
</Stack>
<View style={{ marginLeft: "auto" }}>
<Stack spacing={12} direction="horizontal">
<IconButton
onPress={() => { }}
size="sm"
variant="secondary"
icon={requireAssetIndex("SettingsIcon")}
/>
<TableSwitch
value={plugin.enabled}
onValueChange={(v: boolean) => {
if (v) startPlugin(plugin.id);
else stopPlugin(plugin.id);
}}
/>
</Stack>
</View>
</View>
<Text variant="text-md/medium">
{plugin.manifest.description}
</Text>
</Stack>
</Card>
</PluginContext.Provider>
);
}

View file

@ -1,6 +1,6 @@
import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/components/PluginCard";
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";
@ -11,7 +11,6 @@ export default function Plugins() {
return (
<AddonPage<BunnyPlugin>
title={Strings.PLUGINS}
floatingButtonText={Strings.INSTALL_PLUGIN}
fetchFunction={installPlugin}
items={plugins}
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}

View file

@ -0,0 +1,37 @@
import { createStyles, TextStyleSheet } from "@lib/ui/styles";
import { tokens } from "@metro/common";
export const usePluginCardStyles = createStyles({
header: {
paddingVertical: 2,
},
headerLeading: {
flexDirection: "column",
justifyContent: "center",
scale: 1.2
},
headerTrailing: {
display: "flex",
flexDirection: "row",
gap: 15,
alignItems: "center"
},
headerLabel: {
...TextStyleSheet["heading-md/semibold"],
color: tokens.colors.TEXT_NORMAL,
},
headerSubtitle: {
},
descriptionLabel: {
...TextStyleSheet["text-md/semibold"],
color: tokens.colors.TEXT_NORMAL,
},
actions: {
flexDirection: "row-reverse",
alignItems: "center",
gap: 5
},
iconStyle: {
tintColor: tokens.colors.LOGO_PRIMARY
}
});

View file

@ -1,8 +1,8 @@
import { formatString, Strings } from "@core/i18n";
import Card, { CardWrapper } from "@core/ui/components/Card";
import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard";
import { requireAssetIndex } from "@lib/api/assets";
import { useProxy } from "@lib/api/storage";
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme } from "@lib/managers/themes";
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/managers/themes";
import { settings } from "@lib/settings";
import { ButtonColors } from "@lib/utils/types";
import { clipboard } from "@metro/common";
@ -29,13 +29,13 @@ export default function ThemeCard({ item: theme, index }: CardWrapper<Theme>) {
const { authors } = theme.data;
return (
<Card
<AddonCard
index={index}
headerLabel={theme.data.name}
headerSublabel={authors ? `by ${authors.map(i => i.name).join(", ")}` : ""}
descriptionLabel={theme.data.description ?? "No description."}
toggleType={!settings.safeMode?.enabled ? "radio" : undefined}
toggleValue={theme.selected}
toggleValue={() => themes[theme.id].selected}
onToggleChange={(v: boolean) => {
selectAndApply(v, theme);
}}

View file

@ -1,10 +1,9 @@
import { formatString, Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import ThemeCard from "@core/ui/components/ThemeCard";
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 { ButtonColors } from "@lib/utils/types";
import { Button } from "@metro/common/components";
export default function Themes() {
@ -14,17 +13,13 @@ export default function Themes() {
return (
<AddonPage<Theme>
title={Strings.THEMES}
floatingButtonText={Strings.INSTALL_THEME}
fetchFunction={installTheme}
items={themes}
safeModeMessage={formatString("SAFE_MODE_NOTICE_THEMES", { enabled: Boolean(settings.safeMode?.currentThemeId) })}
safeModeExtras={settings.safeMode?.currentThemeId ? <Button
text={Strings.DISABLE_THEME}
color={ButtonColors.BRAND}
size="small"
onPress={() => {
delete settings.safeMode?.currentThemeId;
}}
onPress={() => delete settings.safeMode?.currentThemeId}
style={{ marginTop: 8 }}
/> : undefined}
card={ThemeCard}

View file

@ -16,6 +16,7 @@ export const toasts = proxyLazy(() => findByFilePath("modules/toast/native/Toast
export const messageUtil = findByPropsProxy("sendBotMessage");
export const navigationStack = findByPropsProxy("createStackNavigator");
export const NavigationNative = findByPropsProxy("NavigationContainer");
export const tokens = findByPropsProxy("colors", "unsafe_rawColors");
// Flux
export const Flux = findByPropsProxy("connectStores");

View file

@ -1,5 +1,5 @@
import { DiscordTextStyles } from "@lib/ui/types";
import { Nullish } from "@lib/utils/types";
import { DiscordTextStyles } from "@ui/types";
import { ReactNode } from "react";
import type * as RN from "react-native";
import { ImageSourcePropType, PressableProps } from "react-native";
@ -83,7 +83,7 @@ interface TextInputProps extends Omit<RN.TextInputProps, "onChange" | "onChangeT
leadingText?: string;
onChange?: (text: string) => void;
size?: "sm" | "md" | "lg";
status?: "error" | "default";
state?: "error" | "default";
style?: Style;
trailingIcon?: React.FC<any>;
trailingPressableProps?: PressableProps;
@ -127,7 +127,7 @@ type TextProps = React.ComponentProps<typeof RN.Text> & {
color?: string; // TODO: type this
lineClamp?: number;
maxFontSizeMultiplier?: number;
style?: Style;
style?: RN.TextStyle;
};
export type Text = React.FC<TextProps>;

View file

@ -47,7 +47,7 @@ export function createFindProxy<A extends unknown[]>(filter: FilterFn<A>) {
indexed: !!moduleId,
moduleId,
getExports(cb: (exports: any) => void) {
if (moduleId && metroModules[moduleId!]?.isInitialized) {
if (!moduleId || metroModules[moduleId]?.isInitialized) {
return cb(findExports(filter)), () => { };
}
return this.subscribe(cb);