Switch every plugin's settings UI from legacy Forms to real TableRow

This commit is contained in:
Ally Founder 2026-08-03 12:27:33 -04:00
parent d5364e3a62
commit 25393c9247
19 changed files with 183 additions and 209 deletions

View file

@ -1,13 +1,12 @@
import { React, ReactNative, moment } from "@vendetta/metro/common"; import { React, ReactNative, moment } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { Forms } from "@vendetta/ui/components"; import { TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@shared/ui/table";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import { resolveSemanticColorSafe } from "@shared/lib/color"; import { resolveSemanticColorSafe } from "@shared/lib/color";
import renderTimestamp, { TimestampMode } from "../lib/renderTimestamp"; import renderTimestamp, { TimestampMode } from "../lib/renderTimestamp";
const { Text } = ReactNative; const { Text } = ReactNative;
const { FormSection, FormRow, FormDivider, FormSwitchRow, FormInput } = Forms;
const MODES: { label: string; key: TimestampMode }[] = [ const MODES: { label: string; key: TimestampMode }[] = [
{ label: "Calendar", key: "calendar" }, { label: "Calendar", key: "calendar" },
@ -21,41 +20,40 @@ export default function Settings() {
return ( return (
<SettingsScaffold> <SettingsScaffold>
<FormSection title="Format"> <TableRowGroup title="Format">
{MODES.map((mode, i) => { {MODES.map((mode) => {
const selected = storage.selected === mode.key; const selected = storage.selected === mode.key;
let preview = ""; let preview = "";
try { preview = renderTimestamp(moment(), mode.key); } catch { preview = ""; } try { preview = renderTimestamp(moment(), mode.key); } catch { preview = ""; }
return ( return (
<React.Fragment key={mode.key}> <React.Fragment key={mode.key}>
<FormRow <TableRow
label={mode.label} label={mode.label}
subLabel={preview} subLabel={preview}
trailing={selected ? <Text style={{ fontSize: 16, color: resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1") }}>✓</Text> : undefined} trailing={selected ? <Text style={{ fontSize: 16, color: resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1") }}>✓</Text> : undefined}
onPress={() => { storage.selected = mode.key; }} onPress={() => { storage.selected = mode.key; }}
/> />
{mode.key === "custom" && selected && ( {mode.key === "custom" && selected && (
<FormInput <TextInput
title="Custom format (moment.js tokens)" label="Custom format (moment.js tokens)"
placeholder="dddd, MMMM Do YYYY, h:mm:ss a" placeholder="dddd, MMMM Do YYYY, h:mm:ss a"
value={storage.customFormat} value={storage.customFormat}
onChange={(v: string) => { storage.customFormat = v; }} onChange={(v: string) => { storage.customFormat = v; }}
/> />
)} )}
{i !== MODES.length - 1 && <FormDivider />}
</React.Fragment> </React.Fragment>
); );
})} })}
</FormSection> </TableRowGroup>
<FormSection title="Layout"> <TableRowGroup title="Layout">
<FormSwitchRow <TableSwitchRow
label="Always show name & avatar" label="Always show name & avatar"
subLabel="Shows the username, avatar, and timestamp on every message instead of grouping consecutive messages" subLabel="Shows the username, avatar, and timestamp on every message instead of grouping consecutive messages"
value={!!storage.separateMessages} value={!!storage.separateMessages}
onValueChange={(v: boolean) => { storage.separateMessages = v; }} onValueChange={(v: boolean) => { storage.separateMessages = v; }}
/> />
</FormSection> </TableRowGroup>
</SettingsScaffold> </SettingsScaffold>
); );
} }

View file

@ -1,7 +1,7 @@
import { React, ReactNative } from "@vendetta/metro/common"; import { React, ReactNative } from "@vendetta/metro/common";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TextInput } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts"; import { showToast } from "@vendetta/ui/toasts";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import ListSection from "@shared/ui/ListSection"; import ListSection from "@shared/ui/ListSection";
@ -11,7 +11,6 @@ import NoteBox from "@shared/ui/NoteBox";
import { allTags, setUserTag, removeUserTag } from "../lib/tags"; import { allTags, setUserTag, removeUserTag } from "../lib/tags";
const { View } = ReactNative; const { View } = ReactNative;
const { FormSection, FormInput } = Forms;
function AddTagForm() { function AddTagForm() {
const [userId, setUserId] = React.useState(""); const [userId, setUserId] = React.useState("");
@ -21,13 +20,13 @@ function AddTagForm() {
return ( return (
<View style={{ paddingHorizontal: 16, paddingBottom: 12 }}> <View style={{ paddingHorizontal: 16, paddingBottom: 12 }}>
<FormInput <TextInput
title="User ID" label="User ID"
placeholder="long-press a user in Discord's own menu and Copy ID" placeholder="long-press a user in Discord's own menu and Copy ID"
value={userId} value={userId}
onChange={setUserId} onChange={setUserId}
/> />
<FormInput title="Tag text" placeholder="e.g. FRIEND" value={text} onChange={setText} /> <TextInput label="Tag text" placeholder="e.g. FRIEND" value={text} onChange={setText} />
<ColorInput title="Color" value={color} onChange={setColor} /> <ColorInput title="Color" value={color} onChange={setColor} />
<PrimaryButton <PrimaryButton
label="Save tag" label="Save tag"
@ -57,9 +56,9 @@ export default function Settings() {
one here by user ID - turn on Developer Mode (Discord Settings → Advanced) to get a one here by user ID - turn on Developer Mode (Discord Settings → Advanced) to get a
"Copy ID" option when you long-press a user elsewhere. "Copy ID" option when you long-press a user elsewhere.
</NoteBox> </NoteBox>
<FormSection title="Add a tag"> <TableRowGroup title="Add a tag">
<AddTagForm /> <AddTagForm />
</FormSection> </TableRowGroup>
<ListSection <ListSection
title="Tagged users" title="Tagged users"
emptyText="No one's tagged yet." emptyText="No one's tagged yet."

View file

@ -1,12 +1,11 @@
import { React, ReactNative } from "@vendetta/metro/common"; import { React, ReactNative } from "@vendetta/metro/common";
import { findByProps } from "@vendetta/metro"; import { findByProps } from "@vendetta/metro";
import { Forms } from "@vendetta/ui/components"; import { TextInput } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts"; import { showToast } from "@vendetta/ui/toasts";
import ColorInput from "@shared/ui/ColorInput"; import ColorInput from "@shared/ui/ColorInput";
import { getUserTag, setUserTag, removeUserTag } from "../lib/tags"; import { getUserTag, setUserTag, removeUserTag } from "../lib/tags";
const { View } = ReactNative; const { View } = ReactNative;
const { FormInput } = Forms;
const ALERT_KEY = "custom-user-tags-editor"; const ALERT_KEY = "custom-user-tags-editor";
@ -54,7 +53,7 @@ function TagEditor({ userId, username }: { userId: string; username: string }) {
title={`Tag ${username}`} title={`Tag ${username}`}
content={ content={
<View> <View>
<FormInput title="Tag text" placeholder="e.g. FRIEND" value={text} onChange={setText} /> <TextInput label="Tag text" placeholder="e.g. FRIEND" value={text} onChange={setText} />
<ColorInput title="Color" value={color} onChange={setColor} /> <ColorInput title="Color" value={color} onChange={setColor} />
</View> </View>
} }

View file

@ -1,14 +1,15 @@
import { showToast } from "@vendetta/ui/toasts"; import { showToast } from "@vendetta/ui/toasts";
import { findByProps } from "@vendetta/metro"; import { findByProps } from "@vendetta/metro";
import React, { useMemo, useState, useEffect } from "react"; import React, { useMemo, useState, useEffect } from "react";
import { View, Text } from "react-native"; import { View } from "react-native";
import { buildFPTE, hasFPTE, stripFPTE } from "@fpte/lib/fpte"; import { buildFPTE, hasFPTE, stripFPTE } from "@fpte/lib/fpte";
import { type ProfileEffectConfig, UserStore, UserProfileStore } from "@fpte/lib/stores"; import { type ProfileEffectConfig, UserStore, UserProfileStore } from "@fpte/lib/stores";
import { useAccentColor, usePrimaryColor, useShowPreview } from "@fpte/patches/patchUseProfileTheme"; import { useAccentColor, usePrimaryColor, useShowPreview } from "@fpte/patches/patchUseProfileTheme";
import { showColorPicker, showEffectPicker } from "@fpte/ui/actionSheets"; import { showColorPicker, showEffectPicker } from "@fpte/ui/actionSheets";
import { resolveSemanticColorSafe, useAvatarColors, useThemeContext } from "@fpte/ui/color"; import { resolveSemanticColorSafe, useAvatarColors, useThemeContext } from "@fpte/ui/color";
import { BuilderButton, Button, StaticEffect } from "@fpte/ui/components"; import { BuilderButton, Button, StaticEffect } from "@fpte/ui/components";
import { FormCardSection, FormSwitchRow } from "@fpte/ui/components/forms"; import { FormSwitchRow } from "@fpte/ui/components/forms";
import { TableRowGroup } from "@shared/ui/table";
const UserProfileActionCreators = findByProps("saveProfileChanges"); const UserProfileActionCreators = findByProps("saveProfileChanges");
@ -103,23 +104,7 @@ export function Builder({ guildId }: BuilderProps) {
const applyButtonVisible = hasSelection || fpteActive; const applyButtonVisible = hasSelection || fpteActive;
return ( return (
<FormCardSection <TableRowGroup title={`FPTE Builder — ${fpteActive ? "Active" : "Inactive"}`}>
title={
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Text style={{ fontSize: 16, color: "#FFFFFF" }}>FPTE Builder</Text>
<Text
style={{
color: fpteActive ? "#4CAF50" : "#F44336",
fontSize: 17,
marginLeft: 8,
}}
>
{fpteActive ? "Active" : "Inactive"}
</Text>
</View>
}
cardStyle={{ backgroundColor: "transparent" }}
>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}> <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<BuilderButton <BuilderButton
fgColor={fgColor} fgColor={fgColor}
@ -183,6 +168,6 @@ export function Builder({ guildId }: BuilderProps) {
value={buildLegacy} value={buildLegacy}
onValueChange={setBuildLegacy} onValueChange={setBuildLegacy}
/> */} /> */}
</FormCardSection> </TableRowGroup>
); );
} }

View file

@ -1,4 +1,4 @@
import { Forms } from "@vendetta/ui/components"; import { TableRow, TableRowGroup, TableSwitchRow } from "@shared/ui/table";
import type { ComponentType, ReactNode } from "react"; import type { ComponentType, ReactNode } from "react";
import type { ColorValue, PressableProps, ViewProps } from "react-native"; import type { ColorValue, PressableProps, ViewProps } from "react-native";
@ -69,15 +69,6 @@ export interface FormRowProps extends Pick<ViewProps, "style">,
hasError?: boolean | undefined; hasError?: boolean | undefined;
} }
export interface FormRadioProps {
selected?: boolean | undefined;
}
export interface FormRadioRowProps extends FormRadioProps, FormRowProps {
align?: "left" | "right";
value?: boolean | undefined;
}
export interface FormSwitchProps extends Pick<PressableProps, "accessible" | "accessibilityHint" | "accessibilityLabel" | "disabled" | "style"> { export interface FormSwitchProps extends Pick<PressableProps, "accessible" | "accessibilityHint" | "accessibilityLabel" | "disabled" | "style"> {
value?: boolean | undefined; value?: boolean | undefined;
onValueChange?: ((value: boolean) => void) | undefined; onValueChange?: ((value: boolean) => void) | undefined;
@ -90,19 +81,8 @@ export interface FormSwitchRowProps extends Pick<FormLabelProps, "numberOfLines"
switchProps?: FormSwitchProps | undefined; switchProps?: FormSwitchProps | undefined;
} }
export interface FormCardSectionProps extends Pick<ViewProps, "accessibilityLabel" | "accessibilityRole" | "children" | "style"> { export const FormSection: ComponentType<FormSection> = TableRowGroup;
title?: ReactNode;
headerComponent?: ReactNode;
titleStyle?: TextProps["style"];
cardStyle?: ViewProps["style"];
}
export const FormSection: ComponentType<FormSection> = Forms.FormSection; export const FormRow: ComponentType<FormRowProps> = TableRow;
export const FormRow: ComponentType<FormRowProps> = Forms.FormRow; export const FormSwitchRow: ComponentType<FormSwitchRowProps> = TableSwitchRow;
export const FormRadioRow: ComponentType<FormRadioRowProps> = Forms.FormRadioRow;
export const FormSwitchRow: ComponentType<FormSwitchRowProps> = Forms.FormSwitchRow;
export const FormCardSection: ComponentType<FormCardSectionProps> = Forms.FormCardSection;

View file

@ -3,25 +3,23 @@ import { useProxy } from "@vendetta/storage";
import React from "react"; import React from "react";
import { ScrollView } from "react-native"; import { ScrollView } from "react-native";
import { FormRadioRow, FormRow, FormSection, FormSwitchRow } from "@fpte/ui/components/forms"; import { FormSwitchRow } from "@fpte/ui/components/forms";
import { TableRadioGroup, TableRadioRow, TableRowGroup } from "@shared/ui/table";
export function Settings() { export function Settings() {
useProxy(storage); useProxy(storage);
return ( return (
<ScrollView> <ScrollView>
<FormSection title="Settings"> <TableRowGroup title="Settings">
<FormRow label="Source to prioritize" /> <TableRadioGroup
<FormRadioRow title="Source to prioritize"
label="Nitro" value={storage.prioritizeNitro ? "nitro" : "about_me"}
selected={!!storage.prioritizeNitro} onChange={(value: string) => { storage.prioritizeNitro = value === "nitro"; }}
onPress={() => { storage.prioritizeNitro = true; }} >
/> <TableRadioRow label="Nitro" value="nitro" />
<FormRadioRow <TableRadioRow label="About Me" value="about_me" />
label="About Me" </TableRadioGroup>
selected={!storage.prioritizeNitro}
onPress={() => { storage.prioritizeNitro = false; }}
/>
<FormSwitchRow <FormSwitchRow
label="Hide Builder" label="Hide Builder"
subLabel="Hide the FPTE Builder in the User Profile and Server Profiles settings pages" subLabel="Hide the FPTE Builder in the User Profile and Server Profiles settings pages"
@ -33,7 +31,7 @@ export function Settings() {
value={!!storage.forceFallbackEffectPicker} value={!!storage.forceFallbackEffectPicker}
onValueChange={value => { storage.forceFallbackEffectPicker = value; }} onValueChange={value => { storage.forceFallbackEffectPicker = value; }}
/> />
</FormSection> </TableRowGroup>
</ScrollView> </ScrollView>
); );
} }

View file

@ -1,7 +1,7 @@
import { React, ReactNative, clipboard } from "@vendetta/metro/common"; import { React, ReactNative, clipboard } from "@vendetta/metro/common";
import { findByProps, findByName, findByPropsAll, findByNameAll } from "@vendetta/metro"; import { findByProps, findByName, findByPropsAll, findByNameAll } from "@vendetta/metro";
import { showToast } from "@vendetta/ui/toasts"; import { showToast } from "@vendetta/ui/toasts";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TextInput } from "@shared/ui/table";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox"; import NoteBox from "@shared/ui/NoteBox";
import PrimaryButton from "@shared/ui/PrimaryButton"; import PrimaryButton from "@shared/ui/PrimaryButton";
@ -14,7 +14,6 @@ import { captureFluxEvents } from "../lib/fluxLogger";
import { captureComponentRenders } from "../lib/renderLogger"; import { captureComponentRenders } from "../lib/renderLogger";
const { View, Text } = ReactNative; const { View, Text } = ReactNative;
const { FormSection, FormInput } = Forms;
// Every raw Text below used to have no explicit color at all - illegible black-on-black on // Every raw Text below used to have no explicit color at all - illegible black-on-black on
// Discord's dark theme, since RN's Text defaults to black with no theming of its own. // Discord's dark theme, since RN's Text defaults to black with no theming of its own.
@ -24,7 +23,7 @@ function FullScanSection() {
const [summary, setSummary] = React.useState<string | null>(null); const [summary, setSummary] = React.useState<string | null>(null);
return ( return (
<FormSection title="Full diagnostic scan"> <TableRowGroup title="Full diagnostic scan">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<PrimaryButton <PrimaryButton
label="Run scan & copy full report" label="Run scan & copy full report"
@ -40,7 +39,7 @@ function FullScanSection() {
<Text style={{ marginTop: 8, fontSize: 12.5, opacity: 0.7, color: textColor() }}>{summary}</Text> <Text style={{ marginTop: 8, fontSize: 12.5, opacity: 0.7, color: textColor() }}>{summary}</Text>
)} )}
</View> </View>
</FormSection> </TableRowGroup>
); );
} }
@ -63,10 +62,10 @@ function EvalSection() {
}; };
return ( return (
<FormSection title="Eval"> <TableRowGroup title="Eval">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput <TextInput
title="Code" label="Code"
placeholder={'e.g. return findByProps("getRootNavigationRef")'} placeholder={'e.g. return findByProps("getRootNavigationRef")'}
value={code} value={code}
onChange={setCode} onChange={setCode}
@ -98,7 +97,7 @@ function EvalSection() {
FluxDispatcher, and window. Nothing runs until you tap the button. FluxDispatcher, and window. Nothing runs until you tap the button.
</Text> </Text>
</View> </View>
</FormSection> </TableRowGroup>
); );
} }
@ -125,16 +124,16 @@ function FluxLoggerSection() {
}; };
return ( return (
<FormSection title="Flux event logger"> <TableRowGroup title="Flux event logger">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput <TextInput
title="Filter (optional substring, e.g. GUILD)" label="Filter (optional substring, e.g. GUILD)"
placeholder="Leave blank to capture everything" placeholder="Leave blank to capture everything"
value={filter} value={filter}
onChange={setFilter} onChange={setFilter}
/> />
<FormInput <TextInput
title="Capture duration (seconds)" label="Capture duration (seconds)"
placeholder="10" placeholder="10"
value={seconds} value={seconds}
onChange={setSeconds} onChange={setSeconds}
@ -158,7 +157,7 @@ function FluxLoggerSection() {
instead of guessing from decompiled source. instead of guessing from decompiled source.
</Text> </Text>
</View> </View>
</FormSection> </TableRowGroup>
); );
} }
@ -186,16 +185,16 @@ function RenderLoggerSection() {
}; };
return ( return (
<FormSection title="Component render/props logger"> <TableRowGroup title="Component render/props logger">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput <TextInput
title="Component name (findByTypeName / findByName)" label="Component name (findByTypeName / findByName)"
placeholder="e.g. YouBarNotificationsButton" placeholder="e.g. YouBarNotificationsButton"
value={componentName} value={componentName}
onChange={setComponentName} onChange={setComponentName}
/> />
<FormInput title="Max renders to capture" placeholder="3" value={maxCalls} onChange={setMaxCalls} keyboardType="numeric" /> <TextInput label="Max renders to capture" placeholder="3" value={maxCalls} onChange={setMaxCalls} keyboardType="numeric" />
<FormInput title="Give up after (seconds)" placeholder="15" value={seconds} onChange={setSeconds} keyboardType="numeric" /> <TextInput label="Give up after (seconds)" placeholder="15" value={seconds} onChange={setSeconds} keyboardType="numeric" />
<PrimaryButton <PrimaryButton
label={capturing ? "Waiting for renders..." : "Start capture"} label={capturing ? "Waiting for renders..." : "Start capture"}
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
@ -214,7 +213,7 @@ function RenderLoggerSection() {
property to intercept its own calls through. property to intercept its own calls through.
</Text> </Text>
</View> </View>
</FormSection> </TableRowGroup>
); );
} }
@ -242,10 +241,10 @@ function RawSearchSection() {
}; };
return ( return (
<FormSection title="Raw search (bypasses the negative-result cache)"> <TableRowGroup title="Raw search (bypasses the negative-result cache)">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput <TextInput
title="rawFindByProps - comma-separated prop names" label="rawFindByProps - comma-separated prop names"
placeholder="e.g. getRootNavigationRef" placeholder="e.g. getRootNavigationRef"
value={propQuery} value={propQuery}
onChange={setPropQuery} onChange={setPropQuery}
@ -259,21 +258,21 @@ function RawSearchSection() {
}} }}
/> />
<FormInput title="rawFindByName" placeholder="e.g. YouBarNotificationsButton" value={nameQuery} onChange={setNameQuery} /> <TextInput label="rawFindByName" placeholder="e.g. YouBarNotificationsButton" value={nameQuery} onChange={setNameQuery} />
<PrimaryButton <PrimaryButton
label="Search & copy result" label="Search & copy result"
style={{ marginTop: 8, marginBottom: 16 }} style={{ marginTop: 8, marginBottom: 16 }}
onPress={() => nameQuery.trim() && run(`rawFindByName("${nameQuery.trim()}")`, () => rawFindByName(nameQuery.trim()))} onPress={() => nameQuery.trim() && run(`rawFindByName("${nameQuery.trim()}")`, () => rawFindByName(nameQuery.trim()))}
/> />
<FormInput title="rawFindByTypeName" placeholder="e.g. YouBarNotificationsButton" value={typeQuery} onChange={setTypeQuery} /> <TextInput label="rawFindByTypeName" placeholder="e.g. YouBarNotificationsButton" value={typeQuery} onChange={setTypeQuery} />
<PrimaryButton <PrimaryButton
label="Search & copy result" label="Search & copy result"
style={{ marginTop: 8, marginBottom: 16 }} style={{ marginTop: 8, marginBottom: 16 }}
onPress={() => typeQuery.trim() && run(`rawFindByTypeName("${typeQuery.trim()}")`, () => rawFindByTypeName(typeQuery.trim()))} onPress={() => typeQuery.trim() && run(`rawFindByTypeName("${typeQuery.trim()}")`, () => rawFindByTypeName(typeQuery.trim()))}
/> />
<FormInput title="rawFindByStoreName" placeholder="e.g. GuildStore" value={storeQuery} onChange={setStoreQuery} /> <TextInput label="rawFindByStoreName" placeholder="e.g. GuildStore" value={storeQuery} onChange={setStoreQuery} />
<PrimaryButton <PrimaryButton
label="Search & copy result" label="Search & copy result"
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
@ -290,7 +289,7 @@ function RawSearchSection() {
that loads your target module will actually find it. that loads your target module will actually find it.
</Text> </Text>
</View> </View>
</FormSection> </TableRowGroup>
); );
} }
@ -362,7 +361,7 @@ function FiberCaptureSection() {
}; };
return ( return (
<FormSection title="Fiber capture (for Eval)"> <TableRowGroup title="Fiber capture (for Eval)">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<View ref={captureRef} style={{ width: 1, height: 1 }} /> <View ref={captureRef} style={{ width: 1, height: 1 }} />
<Text style={{ fontSize: 12.5, opacity: 0.85, color: textColor() }} selectable>{status}</Text> <Text style={{ fontSize: 12.5, opacity: 0.85, color: textColor() }} selectable>{status}</Text>
@ -375,13 +374,13 @@ function FiberCaptureSection() {
module-registry guessing) from window.__keyInspectorFiberRoot afterward. module-registry guessing) from window.__keyInspectorFiberRoot afterward.
</Text> </Text>
</View> </View>
</FormSection> </TableRowGroup>
); );
} }
function ApiTreeSection() { function ApiTreeSection() {
return ( return (
<FormSection title="Full Vendetta API tree"> <TableRowGroup title="Full Vendetta API tree">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<PrimaryButton <PrimaryButton
label="Copy the entire plugin API tree" label="Copy the entire plugin API tree"
@ -398,7 +397,7 @@ function ApiTreeSection() {
are searchable individually below, since dumping all of them would be enormous). are searchable individually below, since dumping all of them would be enormous).
</Text> </Text>
</View> </View>
</FormSection> </TableRowGroup>
); );
} }
@ -477,10 +476,10 @@ function ManualSearchSection() {
}; };
return ( return (
<FormSection title="One-off search"> <TableRowGroup title="One-off search">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput <TextInput
title="findByProps - comma-separated prop names" label="findByProps - comma-separated prop names"
placeholder="e.g. sendMessage, sendBotMessage" placeholder="e.g. sendMessage, sendBotMessage"
value={propQuery} value={propQuery}
onChange={setPropQuery} onChange={setPropQuery}
@ -488,8 +487,8 @@ function ManualSearchSection() {
<PrimaryButton label="Search first match & copy" style={{ marginTop: 8 }} onPress={searchProps} /> <PrimaryButton label="Search first match & copy" style={{ marginTop: 8 }} onPress={searchProps} />
<PrimaryButton label="Search ALL matches & copy" style={{ marginTop: 8, marginBottom: 16 }} onPress={searchPropsAll} /> <PrimaryButton label="Search ALL matches & copy" style={{ marginTop: 8, marginBottom: 16 }} onPress={searchPropsAll} />
<FormInput <TextInput
title="findByName - component name" label="findByName - component name"
placeholder="e.g. ThemedRolePill" placeholder="e.g. ThemedRolePill"
value={nameQuery} value={nameQuery}
onChange={setNameQuery} onChange={setNameQuery}
@ -504,7 +503,7 @@ function ManualSearchSection() {
component actually on screen). component actually on screen).
</Text> </Text>
</View> </View>
</FormSection> </TableRowGroup>
); );
} }

View file

@ -1,7 +1,7 @@
import { React, ReactNative } from "@vendetta/metro/common"; import { React, ReactNative } from "@vendetta/metro/common";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TextInput } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts"; import { showToast } from "@vendetta/ui/toasts";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import ListSection from "@shared/ui/ListSection"; import ListSection from "@shared/ui/ListSection";
@ -9,7 +9,6 @@ import PrimaryButton from "@shared/ui/PrimaryButton";
import { getSnippets, saveSnippet, deleteSnippet } from "../lib/snippets"; import { getSnippets, saveSnippet, deleteSnippet } from "../lib/snippets";
const { View } = ReactNative; const { View } = ReactNative;
const { FormSection, FormInput } = Forms;
function NewSnippetForm() { function NewSnippetForm() {
const [name, setName] = React.useState(""); const [name, setName] = React.useState("");
@ -18,8 +17,8 @@ function NewSnippetForm() {
return ( return (
<View style={{ paddingHorizontal: 16, paddingBottom: 12 }}> <View style={{ paddingHorizontal: 16, paddingBottom: 12 }}>
<FormInput title="Name" placeholder="e.g. rules" value={name} onChange={setName} /> <TextInput label="Name" placeholder="e.g. rules" value={name} onChange={setName} />
<FormInput title="Text" placeholder="What /snippet should send" value={text} onChange={setText} multiline /> <TextInput label="Text" placeholder="What /snippet should send" value={text} onChange={setText} multiline />
<PrimaryButton <PrimaryButton
label="Save snippet" label="Save snippet"
disabled={!canSave} disabled={!canSave}
@ -43,9 +42,9 @@ export default function Settings() {
return ( return (
<SettingsScaffold> <SettingsScaffold>
<FormSection title="Add a snippet"> <TableRowGroup title="Add a snippet">
<NewSnippetForm /> <NewSnippetForm />
</FormSection> </TableRowGroup>
<ListSection <ListSection
title="Saved snippets" title="Saved snippets"
emptyText="None yet. Add one above, then send it anywhere with /snippet name." emptyText="None yet. Add one above, then send it anywhere with /snippet name."

View file

@ -1,18 +1,17 @@
import { React, ReactNative } from "@vendetta/metro/common"; import { React, ReactNative } from "@vendetta/metro/common";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup } from "@shared/ui/table";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox"; import NoteBox from "@shared/ui/NoteBox";
import PrimaryButton from "@shared/ui/PrimaryButton"; import PrimaryButton from "@shared/ui/PrimaryButton";
const { View, Linking } = ReactNative; const { View, Linking } = ReactNative;
const { FormSection } = Forms;
const PRONOUNDB_URL = "https://pronoundb.org/"; const PRONOUNDB_URL = "https://pronoundb.org/";
export default function Settings() { export default function Settings() {
return ( return (
<SettingsScaffold> <SettingsScaffold>
<FormSection title="How this works"> <TableRowGroup title="How this works">
<NoteBox> <NoteBox>
This plugin doesn't have anything to configure here - there's no API key or toggle, This plugin doesn't have anything to configure here - there's no API key or toggle,
because pronouns aren't Discord data. They come from PronounDB, a separate service because pronouns aren't Discord data. They come from PronounDB, a separate service
@ -27,7 +26,7 @@ export default function Settings() {
<View style={{ paddingHorizontal: 16, paddingTop: 4 }}> <View style={{ paddingHorizontal: 16, paddingTop: 4 }}>
<PrimaryButton label="Open pronoundb.org" onPress={() => Linking.openURL(PRONOUNDB_URL)} /> <PrimaryButton label="Open pronoundb.org" onPress={() => Linking.openURL(PRONOUNDB_URL)} />
</View> </View>
</FormSection> </TableRowGroup>
</SettingsScaffold> </SettingsScaffold>
); );
} }

View file

@ -1,13 +1,11 @@
import { React } from "@vendetta/metro/common"; import { React } from "@vendetta/metro/common";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TextInput, TableSwitchRow } from "@shared/ui/table";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import ColorInput from "@shared/ui/ColorInput"; import ColorInput from "@shared/ui/ColorInput";
import NoteBox from "@shared/ui/NoteBox"; import NoteBox from "@shared/ui/NoteBox";
const { FormSection, FormInput, FormSwitchRow } = Forms;
// The ring patch itself does a generic colors[status] lookup, not one hardcoded to three statuses - // The ring patch itself does a generic colors[status] lookup, not one hardcoded to three statuses -
// offline just needed its own configurable entry here to work the same way the other three already // offline just needed its own configurable entry here to work the same way the other three already
// do. Discord doesn't show a colored dot for offline at all normally, so this is opt-in (blank by // do. Discord doesn't show a colored dot for offline at all normally, so this is opt-in (blank by
@ -21,8 +19,8 @@ const STATUSES: { key: string; label: string; defaultColor?: string }[] = [
function numberInput(title: string, key: "ringThickness", placeholder: string) { function numberInput(title: string, key: "ringThickness", placeholder: string) {
return ( return (
<FormInput <TextInput
title={title} label={title}
placeholder={placeholder} placeholder={placeholder}
value={String(storage[key] ?? placeholder)} value={String(storage[key] ?? placeholder)}
onChange={(v: string) => { onChange={(v: string) => {
@ -34,6 +32,7 @@ function numberInput(title: string, key: "ringThickness", placeholder: string) {
); );
} }
export default function Settings() { export default function Settings() {
useProxy(storage); useProxy(storage);
storage.colors ??= {}; storage.colors ??= {};
@ -47,14 +46,14 @@ export default function Settings() {
avatar sizes seen in each of those contexts, so other unrelated circular UI elements are avatar sizes seen in each of those contexts, so other unrelated circular UI elements are
left alone. left alone.
</NoteBox> </NoteBox>
<FormSection title="Enable"> <TableRowGroup title="Enable">
<FormSwitchRow <TableSwitchRow
label="Draw ring around avatars" label="Draw ring around avatars"
value={!!storage.enabled} value={!!storage.enabled}
onValueChange={(v: boolean) => { storage.enabled = v; }} onValueChange={(v: boolean) => { storage.enabled = v; }}
/> />
</FormSection> </TableRowGroup>
<FormSection title="Ring colors"> <TableRowGroup title="Ring colors">
{STATUSES.map(({ key, label, defaultColor }) => ( {STATUSES.map(({ key, label, defaultColor }) => (
<ColorInput <ColorInput
key={key} key={key}
@ -66,10 +65,10 @@ export default function Settings() {
}} }}
/> />
))} ))}
</FormSection> </TableRowGroup>
<FormSection title="Ring size"> <TableRowGroup title="Ring size">
{numberInput("Ring thickness", "ringThickness", "2")} {numberInput("Ring thickness", "ringThickness", "2")}
</FormSection> </TableRowGroup>
</SettingsScaffold> </SettingsScaffold>
); );
} }

View file

@ -1,50 +1,48 @@
import { React } from "@vendetta/metro/common"; import { React } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TableSwitchRow, TextInput } from "@shared/ui/table";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox"; import NoteBox from "@shared/ui/NoteBox";
const { FormSection, FormSwitchRow, FormInput } = Forms;
export default function Settings() { export default function Settings() {
useProxy(storage); useProxy(storage);
return ( return (
<SettingsScaffold> <SettingsScaffold>
<FormSection title="Where to show the top role color"> <TableRowGroup title="Where to show the top role color">
<FormSwitchRow <TableSwitchRow
label="Typing indicator" label="Typing indicator"
subLabel='Color names in the "X is typing..." bar' subLabel='Color names in the "X is typing..." bar'
value={!storage.hideTyping} value={!storage.hideTyping}
onValueChange={(v: boolean) => { storage.hideTyping = !v; }} onValueChange={(v: boolean) => { storage.hideTyping = !v; }}
/> />
<FormSwitchRow <TableSwitchRow
label="Mentions" label="Mentions"
subLabel="Color @mentions in chat messages" subLabel="Color @mentions in chat messages"
value={!storage.noMention} value={!storage.noMention}
onValueChange={(v: boolean) => { storage.noMention = !v; }} onValueChange={(v: boolean) => { storage.noMention = !v; }}
/> />
<FormSwitchRow <TableSwitchRow
label="Member list role headers" label="Member list role headers"
subLabel="Color the role section headers in the member list" subLabel="Color the role section headers in the member list"
value={!storage.noRole} value={!storage.noRole}
onValueChange={(v: boolean) => { storage.noRole = !v; }} onValueChange={(v: boolean) => { storage.noRole = !v; }}
/> />
<FormSwitchRow <TableSwitchRow
label="Voice channel participant names" label="Voice channel participant names"
value={!storage.noVoice} value={!storage.noVoice}
onValueChange={(v: boolean) => { storage.noVoice = !v; }} onValueChange={(v: boolean) => { storage.noVoice = !v; }}
/> />
<FormSwitchRow <TableSwitchRow
label="Message text" label="Message text"
subLabel="Tints message text toward the author's role color - disables text selection on tinted messages" subLabel="Tints message text toward the author's role color - disables text selection on tinted messages"
value={storage.chatInterpolation > 0} value={storage.chatInterpolation > 0}
onValueChange={(v: boolean) => { storage.chatInterpolation = v ? 60 : 0; }} onValueChange={(v: boolean) => { storage.chatInterpolation = v ? 60 : 0; }}
/> />
{storage.chatInterpolation > 0 && ( {storage.chatInterpolation > 0 && (
<FormInput <TextInput
title="Message text strength (0-100)" label="Message text strength (0-100)"
value={String(storage.chatInterpolation)} value={String(storage.chatInterpolation)}
keyboardType="numeric" keyboardType="numeric"
onChange={(v: string) => { onChange={(v: string) => {
@ -52,7 +50,7 @@ export default function Settings() {
}} }}
/> />
)} )}
</FormSection> </TableRowGroup>
<NoteBox> <NoteBox>
Some of these surfaces (especially the member list headers) rely on component internals Some of these surfaces (especially the member list headers) rely on component internals
Discord doesn't officially expose, so a future Discord update may silently turn one of Discord doesn't officially expose, so a future Discord update may silently turn one of

View file

@ -1,6 +1,6 @@
import { findByName, findByProps } from "@vendetta/metro"; import { findByName, findByProps } from "@vendetta/metro";
import { React, ReactNative, clipboard } from "@vendetta/metro/common"; import { React, ReactNative, clipboard } from "@vendetta/metro/common";
import { Forms } from "@vendetta/ui/components"; import { TextInput } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts"; import { showToast } from "@vendetta/ui/toasts";
import { getAssetIDByName } from "@vendetta/ui/assets"; import { getAssetIDByName } from "@vendetta/ui/assets";
import { resolveSemanticColorSafe } from "@shared/lib/color"; import { resolveSemanticColorSafe } from "@shared/lib/color";
@ -17,7 +17,6 @@ const modalCloseButton =
const Navigator = findByName("Navigator") ?? findByProps("Navigator")?.Navigator; const Navigator = findByName("Navigator") ?? findByProps("Navigator")?.Navigator;
const { View, Text, ScrollView, TouchableOpacity } = ReactNative; const { View, Text, ScrollView, TouchableOpacity } = ReactNative;
const { FormInput } = Forms;
const PAGE_SIZE = 100; const PAGE_SIZE = 100;
@ -172,8 +171,8 @@ function LogList() {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<View style={{ padding: 16 }}> <View style={{ padding: 16 }}>
<FormInput <TextInput
title="Search" label="Search"
placeholder="Search content, author, channel/server/user ID..." placeholder="Search content, author, channel/server/user ID..."
value={query} value={query}
onChange={setQuery} onChange={setQuery}

View file

@ -1,6 +1,6 @@
import { React, ReactNative, clipboard } from "@vendetta/metro/common"; import { React, ReactNative, clipboard } from "@vendetta/metro/common";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TableSwitchRow, TableRow, TextInput } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts"; import { showToast } from "@vendetta/ui/toasts";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox"; import NoteBox from "@shared/ui/NoteBox";
@ -9,7 +9,6 @@ import modules from "../modules";
import { ModuleCategory, vstorage, type AnyModule, type ModuleSetting } from "../lib/Module"; import { ModuleCategory, vstorage, type AnyModule, type ModuleSetting } from "../lib/Module";
const { View, Text, TouchableOpacity } = ReactNative; const { View, Text, TouchableOpacity } = ReactNative;
const { FormSection, FormSwitchRow, FormRow, FormInput } = Forms;
// Raw Text below this point (the category header and bulk-action links) had no explicit color at // Raw Text below this point (the category header and bulk-action links) had no explicit color at
// all - illegible black-on-black on Discord's dark theme, since RN's Text defaults to black with no // all - illegible black-on-black on Discord's dark theme, since RN's Text defaults to black with no
@ -50,7 +49,7 @@ function ModuleSettingRow({ module, settingKey }: { module: AnyModule; settingKe
if (setting.type === "toggle") { if (setting.type === "toggle") {
return ( return (
<FormSwitchRow <TableSwitchRow
label={setting.label} label={setting.label}
subLabel={subLabel} subLabel={subLabel}
value={!!value} value={!!value}
@ -65,7 +64,7 @@ function ModuleSettingRow({ module, settingKey }: { module: AnyModule; settingKe
if (setting.type === "button") { if (setting.type === "button") {
return ( return (
<FormRow <TableRow
label={setting.label} label={setting.label}
subLabel={subLabel} subLabel={subLabel}
disabled={disabled} disabled={disabled}
@ -76,8 +75,8 @@ function ModuleSettingRow({ module, settingKey }: { module: AnyModule; settingKe
if (setting.type === "text") { if (setting.type === "text") {
return ( return (
<FormInput <TextInput
title={setting.label} label={setting.label}
placeholder={setting.placeholder} placeholder={setting.placeholder}
value={typeof value === "string" ? value : ""} value={typeof value === "string" ? value : ""}
editable={!disabled} editable={!disabled}
@ -90,7 +89,7 @@ function ModuleSettingRow({ module, settingKey }: { module: AnyModule; settingKe
// "choose" - cycles through choices on tap instead of a full picker sheet. // "choose" - cycles through choices on tap instead of a full picker sheet.
return ( return (
<FormRow <TableRow
label={setting.label} label={setting.label}
subLabel={[subLabel, value].filter(Boolean).join(" - ")} subLabel={[subLabel, value].filter(Boolean).join(" - ")}
disabled={disabled} disabled={disabled}
@ -116,8 +115,8 @@ function ModuleSection({ module }: { module: AnyModule }) {
const errorEntries = Object.entries(module.errors); const errorEntries = Object.entries(module.errors);
return ( return (
<FormSection title={module.label}> <TableRowGroup title={module.label}>
<FormSwitchRow <TableSwitchRow
label="Enabled" label="Enabled"
subLabel={module.meta.sublabel} subLabel={module.meta.sublabel}
value={!!module.storage.enabled} value={!!module.storage.enabled}
@ -127,7 +126,7 @@ function ModuleSection({ module }: { module: AnyModule }) {
<ModuleSettingRow key={key} module={module} settingKey={key} /> <ModuleSettingRow key={key} module={module} settingKey={key} />
))} ))}
{errorEntries.length > 0 && ( {errorEntries.length > 0 && (
<FormRow <TableRow
label={`${errorEntries.length} error${errorEntries.length === 1 ? "" : "s"} - tap to copy`} label={`${errorEntries.length} error${errorEntries.length === 1 ? "" : "s"} - tap to copy`}
onPress={() => { onPress={() => {
const report = errorEntries.map(([label, stack]) => `[${module.label}] ${label}\n${stack}`).join("\n\n"); const report = errorEntries.map(([label, stack]) => `[${module.label}] ${label}\n${stack}`).join("\n\n");
@ -136,7 +135,7 @@ function ModuleSection({ module }: { module: AnyModule }) {
}} }}
/> />
)} )}
</FormSection> </TableRowGroup>
); );
} }
@ -212,8 +211,8 @@ export default function Settings() {
</NoteBox> </NoteBox>
<View style={{ paddingHorizontal: 16 }}> <View style={{ paddingHorizontal: 16 }}>
<FormInput <TextInput
title="Search" label="Search"
placeholder="Search modules and settings..." placeholder="Search modules and settings..."
value={query} value={query}
onChange={setQuery} onChange={setQuery}

View file

@ -1,12 +1,11 @@
import { React } from "@vendetta/metro/common"; import { React } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TableSwitchRow } from "@shared/ui/table";
import SettingsScaffold from "./SettingsScaffold"; import SettingsScaffold from "./SettingsScaffold";
import NoteBox from "./NoteBox"; import NoteBox from "./NoteBox";
import { restart } from "../index"; import { restart } from "../index";
const { FormSection, FormSwitchRow } = Forms;
export default function Settings() { export default function Settings() {
useProxy(storage); useProxy(storage);
@ -19,8 +18,8 @@ export default function Settings() {
menu, the chat bubble jumps to DMs, the + button creates or joins a server through menu, the chat bubble jumps to DMs, the + button creates or joins a server through
Discord's own modal, and the check button marks every channel in every server as read. Discord's own modal, and the check button marks every channel in every server as read.
</NoteBox> </NoteBox>
<FormSection title="Layout"> <TableRowGroup title="Layout">
<FormSwitchRow <TableSwitchRow
label="Hide the left server rail" label="Hide the left server rail"
subLabel="Your servers and DMs already live in the drawer, so the rail is redundant" subLabel="Your servers and DMs already live in the drawer, so the rail is redundant"
value={storage.hideGuildsBar !== false} value={storage.hideGuildsBar !== false}
@ -29,39 +28,39 @@ export default function Settings() {
restart(); restart();
}} }}
/> />
<FormSwitchRow <TableSwitchRow
label="Unread badges" label="Unread badges"
subLabel="Show mention counts and unread dots on server icons in the drawer" subLabel="Show mention counts and unread dots on server icons in the drawer"
value={storage.showUnreadBadges !== false} value={storage.showUnreadBadges !== false}
onValueChange={(v: boolean) => { storage.showUnreadBadges = v; }} onValueChange={(v: boolean) => { storage.showUnreadBadges = v; }}
/> />
<FormSwitchRow <TableSwitchRow
label="Show server names" label="Show server names"
subLabel="Word-wrapped name below each server's icon" subLabel="Word-wrapped name below each server's icon"
value={storage.showGuildNames !== false} value={storage.showGuildNames !== false}
onValueChange={(v: boolean) => { storage.showGuildNames = v; }} onValueChange={(v: boolean) => { storage.showGuildNames = v; }}
/> />
<FormSwitchRow <TableSwitchRow
label="Hide the DMs tile" label="Hide the DMs tile"
subLabel="Remove the DMs tile from the drawer entirely" subLabel="Remove the DMs tile from the drawer entirely"
value={!!storage.hideDmTile} value={!!storage.hideDmTile}
onValueChange={(v: boolean) => { storage.hideDmTile = v; }} onValueChange={(v: boolean) => { storage.hideDmTile = v; }}
/> />
</FormSection> </TableRowGroup>
<FormSection title="Folders"> <TableRowGroup title="Folders">
<FormSwitchRow <TableSwitchRow
label="Auto-collapse other folders" label="Auto-collapse other folders"
subLabel="Opening a folder collapses any other folder that was already open" subLabel="Opening a folder collapses any other folder that was already open"
value={!!storage.autoCollapseFolders} value={!!storage.autoCollapseFolders}
onValueChange={(v: boolean) => { storage.autoCollapseFolders = v; }} onValueChange={(v: boolean) => { storage.autoCollapseFolders = v; }}
/> />
<FormSwitchRow <TableSwitchRow
label="Hide icons in collapsed folders" label="Hide icons in collapsed folders"
subLabel="Show a plain folder icon instead of a 4-server preview" subLabel="Show a plain folder icon instead of a 4-server preview"
value={!!storage.hideFolderIcons} value={!!storage.hideFolderIcons}
onValueChange={(v: boolean) => { storage.hideFolderIcons = v; }} onValueChange={(v: boolean) => { storage.hideFolderIcons = v; }}
/> />
</FormSection> </TableRowGroup>
<NoteBox> <NoteBox>
The folder options above are ported from fres621's BetterFolders - its other half The folder options above are ported from fres621's BetterFolders - its other half
(hiding icons in the native left server rail) patched a component that no longer (hiding icons in the native left server rail) patched a component that no longer

View file

@ -1,12 +1,11 @@
import { React } from "@vendetta/metro/common"; import { React } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TableSwitchRow, TextInput } from "@shared/ui/table";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import ColorInput from "@shared/ui/ColorInput"; import ColorInput from "@shared/ui/ColorInput";
import { TAG_DEFINITIONS, tagSettings } from "../../lib/getTag"; import { TAG_DEFINITIONS, tagSettings } from "../../lib/getTag";
const { FormSection, FormSwitchRow, FormInput } = Forms;
function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; defaultText: string; defaultColor: string }) { function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; defaultText: string; defaultColor: string }) {
const settings = tagSettings(id); const settings = tagSettings(id);
@ -15,20 +14,20 @@ function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; def
const enabled = settings.enabled !== false; const enabled = settings.enabled !== false;
return ( return (
<FormSection title={defaultText}> <TableRowGroup title={defaultText}>
<FormSwitchRow <TableSwitchRow
label="Show this tag" label="Show this tag"
value={enabled} value={enabled}
onValueChange={(v: boolean) => { settings.enabled = v; }} onValueChange={(v: boolean) => { settings.enabled = v; }}
/> />
<FormInput <TextInput
title="Tag text" label="Tag text"
placeholder={defaultText} placeholder={defaultText}
value={settings.text ?? ""} value={settings.text ?? ""}
editable={enabled} editable={enabled}
onChange={(v: string) => { settings.text = v; }} onChange={(v: string) => { settings.text = v; }}
/> />
<FormSwitchRow <TableSwitchRow
label="Custom color" label="Custom color"
subLabel={`Default: ${defaultColor}`} subLabel={`Default: ${defaultColor}`}
value={!!settings.useCustomColor} value={!!settings.useCustomColor}
@ -43,7 +42,7 @@ function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; def
onChange={(v: string) => { settings.color = v; }} onChange={(v: string) => { settings.color = v; }}
/> />
)} )}
<FormSwitchRow <TableSwitchRow
label="Gradient" label="Gradient"
subLabel="Member list & profile only, chat tags stay solid" subLabel="Member list & profile only, chat tags stay solid"
value={!!settings.useGradient} value={!!settings.useGradient}
@ -57,7 +56,7 @@ function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; def
onChange={(v: string) => { settings.gradientColor = v; }} onChange={(v: string) => { settings.gradientColor = v; }}
/> />
)} )}
</FormSection> </TableRowGroup>
); );
} }
@ -66,14 +65,14 @@ export default function Settings() {
return ( return (
<SettingsScaffold> <SettingsScaffold>
<FormSection title="General"> <TableRowGroup title="General">
<FormSwitchRow <TableSwitchRow
label="Use top role color" label="Use top role color"
subLabel="Used when a tag has no custom color set" subLabel="Used when a tag has no custom color set"
value={!!storage.useRoleColor} value={!!storage.useRoleColor}
onValueChange={(v: boolean) => { storage.useRoleColor = v; }} onValueChange={(v: boolean) => { storage.useRoleColor = v; }}
/> />
</FormSection> </TableRowGroup>
{TAG_DEFINITIONS.map((def) => ( {TAG_DEFINITIONS.map((def) => (
<TagSettingsSection key={def.id} id={def.id} defaultText={def.defaultText} defaultColor={def.defaultColor} /> <TagSettingsSection key={def.id} id={def.id} defaultText={def.defaultText} defaultColor={def.defaultColor} />
))} ))}

View file

@ -1,5 +1,5 @@
import { React } from "@vendetta/metro/common"; import { React } from "@vendetta/metro/common";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TableRow } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts"; import { showToast } from "@vendetta/ui/toasts";
import { clipboard } from "@vendetta/metro/common"; import { clipboard } from "@vendetta/metro/common";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
@ -7,8 +7,6 @@ import NoteBox from "@shared/ui/NoteBox";
import PrimaryButton from "@shared/ui/PrimaryButton"; import PrimaryButton from "@shared/ui/PrimaryButton";
import { getLastDetection } from "../lib/diagnostics"; import { getLastDetection } from "../lib/diagnostics";
const { FormSection, FormRow } = Forms;
const STRATEGY_LABEL: Record<string, string> = { const STRATEGY_LABEL: Record<string, string> = {
buttons: "Strategy 1 (button list) - working normally", buttons: "Strategy 1 (button list) - working normally",
actionSheetRow: "Strategy 2 (row group) - working normally", actionSheetRow: "Strategy 2 (row group) - working normally",
@ -21,9 +19,9 @@ export default function Settings() {
return ( return (
<SettingsScaffold> <SettingsScaffold>
<FormSection title="Diagnostics"> <TableRowGroup title="Diagnostics">
{detection ? ( {detection ? (
<FormRow <TableRow
label={STRATEGY_LABEL[detection.strategy] ?? detection.strategy} label={STRATEGY_LABEL[detection.strategy] ?? detection.strategy}
subLabel={`Last checked ${new Date(detection.timestamp).toLocaleString()}${detection.detail ? ` - ${detection.detail}` : ""}`} subLabel={`Last checked ${new Date(detection.timestamp).toLocaleString()}${detection.detail ? ` - ${detection.detail}` : ""}`}
/> />
@ -41,7 +39,7 @@ export default function Settings() {
showToast("Copied - paste this if you're reporting an issue", undefined); showToast("Copied - paste this if you're reporting an issue", undefined);
}} }}
/> />
</FormSection> </TableRowGroup>
<NoteBox> <NoteBox>
"View Raw" is added to the message long-press menu by detecting Discord's internal "View Raw" is added to the message long-press menu by detecting Discord's internal
action-sheet layout, which isn't an official API and can change between app versions. action-sheet layout, which isn't an official API and can change between app versions.

View file

@ -1,12 +1,11 @@
import { React } from "@vendetta/metro/common"; import { React } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage"; import { useProxy } from "@vendetta/storage";
import { Forms } from "@vendetta/ui/components"; import { TableRowGroup, TableSwitchRow } from "@shared/ui/table";
import SettingsScaffold from "@shared/ui/SettingsScaffold"; import SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox"; import NoteBox from "@shared/ui/NoteBox";
import { updateYouBar } from "../patches/youBarButtons"; import { updateYouBar } from "../patches/youBarButtons";
const { FormSection, FormSwitchRow } = Forms;
export default function Settings() { export default function Settings() {
useProxy(storage); useProxy(storage);
@ -21,8 +20,8 @@ export default function Settings() {
original, this patches the YouBar the moment it loads - if you enable it while original, this patches the YouBar the moment it loads - if you enable it while
Discord's already running, you may need to restart once for the buttons to appear. Discord's already running, you may need to restart once for the buttons to appear.
</NoteBox> </NoteBox>
<FormSection title="Buttons"> <TableRowGroup title="Buttons">
<FormSwitchRow <TableSwitchRow
label="Direct Messages button" label="Direct Messages button"
subLabel="Show the DM button in the YouBar" subLabel="Show the DM button in the YouBar"
value={!!storage.showDMButton} value={!!storage.showDMButton}
@ -31,7 +30,7 @@ export default function Settings() {
updateYouBar(); updateYouBar();
}} }}
/> />
<FormSwitchRow <TableSwitchRow
label="Settings button" label="Settings button"
subLabel="Show the Settings button in the YouBar" subLabel="Show the Settings button in the YouBar"
value={!!storage.showSettingsButton} value={!!storage.showSettingsButton}
@ -40,7 +39,7 @@ export default function Settings() {
updateYouBar(); updateYouBar();
}} }}
/> />
</FormSection> </TableRowGroup>
</SettingsScaffold> </SettingsScaffold>
); );
} }

View file

@ -1,9 +1,8 @@
import { React, ReactNative } from "@vendetta/metro/common"; import { React, ReactNative } from "@vendetta/metro/common";
import { Forms } from "@vendetta/ui/components"; import { TableRow, TableRowGroup } from "./table";
import NoteBox from "./NoteBox"; import NoteBox from "./NoteBox";
const { Text } = ReactNative; const { Text } = ReactNative;
const { FormSection, FormRow } = Forms;
export interface ListItem { export interface ListItem {
key: string; key: string;
@ -19,12 +18,12 @@ export interface ListItem {
*/ */
export default function ListSection({ title, items, emptyText }: { title: string; items: ListItem[]; emptyText: string }) { export default function ListSection({ title, items, emptyText }: { title: string; items: ListItem[]; emptyText: string }) {
return ( return (
<FormSection title={title}> <TableRowGroup title={title}>
{items.length === 0 ? ( {items.length === 0 ? (
<NoteBox>{emptyText}</NoteBox> <NoteBox>{emptyText}</NoteBox>
) : ( ) : (
items.map((item) => ( items.map((item) => (
<FormRow <TableRow
key={item.key} key={item.key}
label={item.label} label={item.label}
subLabel={item.subLabel} subLabel={item.subLabel}
@ -33,6 +32,6 @@ export default function ListSection({ title, items, emptyText }: { title: string
/> />
)) ))
)} )}
</FormSection> </TableRowGroup>
); );
} }

29
shared/ui/table.ts Normal file
View file

@ -0,0 +1,29 @@
import { findByProps } from "@vendetta/metro";
import { Forms } from "@vendetta/ui/components";
/**
* Discord's redesigned "Table" row family (TableRow/TableRowGroup/TableSwitchRow/...) replaced
* the old Forms.Form* components in the app itself a while ago - Forms.* still works (Vendetta's
* compat layer maps it to the legacy components, see metro/common/components.ts in the loader),
* but it's the deprecated skin. These are found the same way ViewRaw finds ActionSheetRow: a
* direct findByProps lookup for the real component, since the vendetta.ui.components compat
* object never exposed the Table family at all. Falls back to the matching legacy Form
* component if a lookup ever comes back empty, same "isolate the failure" approach as the rest
* of this repo - the fallback's prop names don't perfectly match (e.g. `icon` vs `leading`), so a
* few things would render without an icon/arrow in that fallback path, but nothing crashes.
*/
const find = (prop: string): any => findByProps(prop)?.[prop];
const RealTableRow: any = find("TableRow");
export const TableRow: any = RealTableRow ?? Forms.FormRow;
export const TableRowGroup: any = find("TableRowGroup") ?? Forms.FormSection;
export const TableSwitchRow: any = find("TableSwitchRow") ?? Forms.FormSwitchRow;
export const TableRadioRow: any = find("TableRadioRow") ?? Forms.FormRadioRow;
export const TableRadioGroup: any = find("TableRadioGroup") ?? Forms.FormRadioGroup;
export const TableCheckboxRow: any = find("TableCheckboxRow") ?? Forms.FormCheckboxRow;
export const TextInput: any = find("TextInput") ?? Forms.FormInput;
export const TableRowIcon: any = RealTableRow?.Icon ?? Forms.FormRow.Icon;
export const TableRowArrow: any = RealTableRow?.Arrow ?? Forms.FormRow.Arrow;
export const TableRowTrailingText: any = RealTableRow?.TrailingText;