refactor(ui): restructure and begin rework on plugins ui
This commit is contained in:
parent
e8f60c92cf
commit
b0c62d1094
21 changed files with 316 additions and 363 deletions
154
src/core/ui/components/AddonCard.tsx
Normal file
154
src/core/ui/components/AddonCard.tsx
Normal 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 >
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -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}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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);
|
||||
|
||||
90
src/core/ui/settings/pages/Plugins/PluginCard.tsx
Normal file
90
src/core/ui/settings/pages/Plugins/PluginCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
37
src/core/ui/settings/pages/Plugins/usePluginCardStyles.ts
Normal file
37
src/core/ui/settings/pages/Plugins/usePluginCardStyles.ts
Normal 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
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue