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

View file

@ -1,7 +1,7 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { useProxy } from "@vendetta/storage";
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 SettingsScaffold from "@shared/ui/SettingsScaffold";
import ListSection from "@shared/ui/ListSection";
@ -11,7 +11,6 @@ import NoteBox from "@shared/ui/NoteBox";
import { allTags, setUserTag, removeUserTag } from "../lib/tags";
const { View } = ReactNative;
const { FormSection, FormInput } = Forms;
function AddTagForm() {
const [userId, setUserId] = React.useState("");
@ -21,13 +20,13 @@ function AddTagForm() {
return (
<View style={{ paddingHorizontal: 16, paddingBottom: 12 }}>
<FormInput
title="User ID"
<TextInput
label="User ID"
placeholder="long-press a user in Discord's own menu and Copy ID"
value={userId}
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} />
<PrimaryButton
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
"Copy ID" option when you long-press a user elsewhere.
</NoteBox>
<FormSection title="Add a tag">
<TableRowGroup title="Add a tag">
<AddTagForm />
</FormSection>
</TableRowGroup>
<ListSection
title="Tagged users"
emptyText="No one's tagged yet."

View file

@ -1,12 +1,11 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { findByProps } from "@vendetta/metro";
import { Forms } from "@vendetta/ui/components";
import { TextInput } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts";
import ColorInput from "@shared/ui/ColorInput";
import { getUserTag, setUserTag, removeUserTag } from "../lib/tags";
const { View } = ReactNative;
const { FormInput } = Forms;
const ALERT_KEY = "custom-user-tags-editor";
@ -54,7 +53,7 @@ function TagEditor({ userId, username }: { userId: string; username: string }) {
title={`Tag ${username}`}
content={
<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} />
</View>
}

View file

@ -1,14 +1,15 @@
import { showToast } from "@vendetta/ui/toasts";
import { findByProps } from "@vendetta/metro";
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 { type ProfileEffectConfig, UserStore, UserProfileStore } from "@fpte/lib/stores";
import { useAccentColor, usePrimaryColor, useShowPreview } from "@fpte/patches/patchUseProfileTheme";
import { showColorPicker, showEffectPicker } from "@fpte/ui/actionSheets";
import { resolveSemanticColorSafe, useAvatarColors, useThemeContext } from "@fpte/ui/color";
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");
@ -103,23 +104,7 @@ export function Builder({ guildId }: BuilderProps) {
const applyButtonVisible = hasSelection || fpteActive;
return (
<FormCardSection
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" }}
>
<TableRowGroup title={`FPTE Builder — ${fpteActive ? "Active" : "Inactive"}`}>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<BuilderButton
fgColor={fgColor}
@ -183,6 +168,6 @@ export function Builder({ guildId }: BuilderProps) {
value={buildLegacy}
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 { ColorValue, PressableProps, ViewProps } from "react-native";
@ -69,15 +69,6 @@ export interface FormRowProps extends Pick<ViewProps, "style">,
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"> {
value?: boolean | undefined;
onValueChange?: ((value: boolean) => void) | undefined;
@ -90,19 +81,8 @@ export interface FormSwitchRowProps extends Pick<FormLabelProps, "numberOfLines"
switchProps?: FormSwitchProps | undefined;
}
export interface FormCardSectionProps extends Pick<ViewProps, "accessibilityLabel" | "accessibilityRole" | "children" | "style"> {
title?: ReactNode;
headerComponent?: ReactNode;
titleStyle?: TextProps["style"];
cardStyle?: ViewProps["style"];
}
export const FormSection: ComponentType<FormSection> = TableRowGroup;
export const FormSection: ComponentType<FormSection> = Forms.FormSection;
export const FormRow: ComponentType<FormRowProps> = TableRow;
export const FormRow: ComponentType<FormRowProps> = Forms.FormRow;
export const FormRadioRow: ComponentType<FormRadioRowProps> = Forms.FormRadioRow;
export const FormSwitchRow: ComponentType<FormSwitchRowProps> = Forms.FormSwitchRow;
export const FormCardSection: ComponentType<FormCardSectionProps> = Forms.FormCardSection;
export const FormSwitchRow: ComponentType<FormSwitchRowProps> = TableSwitchRow;

View file

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

View file

@ -1,7 +1,7 @@
import { React, ReactNative, clipboard } from "@vendetta/metro/common";
import { findByProps, findByName, findByPropsAll, findByNameAll } from "@vendetta/metro";
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 NoteBox from "@shared/ui/NoteBox";
import PrimaryButton from "@shared/ui/PrimaryButton";
@ -14,7 +14,6 @@ import { captureFluxEvents } from "../lib/fluxLogger";
import { captureComponentRenders } from "../lib/renderLogger";
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
// 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);
return (
<FormSection title="Full diagnostic scan">
<TableRowGroup title="Full diagnostic scan">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<PrimaryButton
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>
)}
</View>
</FormSection>
</TableRowGroup>
);
}
@ -63,10 +62,10 @@ function EvalSection() {
};
return (
<FormSection title="Eval">
<TableRowGroup title="Eval">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput
title="Code"
<TextInput
label="Code"
placeholder={'e.g. return findByProps("getRootNavigationRef")'}
value={code}
onChange={setCode}
@ -98,7 +97,7 @@ function EvalSection() {
FluxDispatcher, and window. Nothing runs until you tap the button.
</Text>
</View>
</FormSection>
</TableRowGroup>
);
}
@ -125,16 +124,16 @@ function FluxLoggerSection() {
};
return (
<FormSection title="Flux event logger">
<TableRowGroup title="Flux event logger">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput
title="Filter (optional substring, e.g. GUILD)"
<TextInput
label="Filter (optional substring, e.g. GUILD)"
placeholder="Leave blank to capture everything"
value={filter}
onChange={setFilter}
/>
<FormInput
title="Capture duration (seconds)"
<TextInput
label="Capture duration (seconds)"
placeholder="10"
value={seconds}
onChange={setSeconds}
@ -158,7 +157,7 @@ function FluxLoggerSection() {
instead of guessing from decompiled source.
</Text>
</View>
</FormSection>
</TableRowGroup>
);
}
@ -186,16 +185,16 @@ function RenderLoggerSection() {
};
return (
<FormSection title="Component render/props logger">
<TableRowGroup title="Component render/props logger">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput
title="Component name (findByTypeName / findByName)"
<TextInput
label="Component name (findByTypeName / findByName)"
placeholder="e.g. YouBarNotificationsButton"
value={componentName}
onChange={setComponentName}
/>
<FormInput title="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="Max renders to capture" placeholder="3" value={maxCalls} onChange={setMaxCalls} keyboardType="numeric" />
<TextInput label="Give up after (seconds)" placeholder="15" value={seconds} onChange={setSeconds} keyboardType="numeric" />
<PrimaryButton
label={capturing ? "Waiting for renders..." : "Start capture"}
style={{ marginTop: 8 }}
@ -214,7 +213,7 @@ function RenderLoggerSection() {
property to intercept its own calls through.
</Text>
</View>
</FormSection>
</TableRowGroup>
);
}
@ -242,10 +241,10 @@ function RawSearchSection() {
};
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 }}>
<FormInput
title="rawFindByProps - comma-separated prop names"
<TextInput
label="rawFindByProps - comma-separated prop names"
placeholder="e.g. getRootNavigationRef"
value={propQuery}
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
label="Search & copy result"
style={{ marginTop: 8, marginBottom: 16 }}
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
label="Search & copy result"
style={{ marginTop: 8, marginBottom: 16 }}
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
label="Search & copy result"
style={{ marginTop: 8 }}
@ -290,7 +289,7 @@ function RawSearchSection() {
that loads your target module will actually find it.
</Text>
</View>
</FormSection>
</TableRowGroup>
);
}
@ -362,7 +361,7 @@ function FiberCaptureSection() {
};
return (
<FormSection title="Fiber capture (for Eval)">
<TableRowGroup title="Fiber capture (for Eval)">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<View ref={captureRef} style={{ width: 1, height: 1 }} />
<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.
</Text>
</View>
</FormSection>
</TableRowGroup>
);
}
function ApiTreeSection() {
return (
<FormSection title="Full Vendetta API tree">
<TableRowGroup title="Full Vendetta API tree">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<PrimaryButton
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).
</Text>
</View>
</FormSection>
</TableRowGroup>
);
}
@ -477,10 +476,10 @@ function ManualSearchSection() {
};
return (
<FormSection title="One-off search">
<TableRowGroup title="One-off search">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<FormInput
title="findByProps - comma-separated prop names"
<TextInput
label="findByProps - comma-separated prop names"
placeholder="e.g. sendMessage, sendBotMessage"
value={propQuery}
onChange={setPropQuery}
@ -488,8 +487,8 @@ function ManualSearchSection() {
<PrimaryButton label="Search first match & copy" style={{ marginTop: 8 }} onPress={searchProps} />
<PrimaryButton label="Search ALL matches & copy" style={{ marginTop: 8, marginBottom: 16 }} onPress={searchPropsAll} />
<FormInput
title="findByName - component name"
<TextInput
label="findByName - component name"
placeholder="e.g. ThemedRolePill"
value={nameQuery}
onChange={setNameQuery}
@ -504,7 +503,7 @@ function ManualSearchSection() {
component actually on screen).
</Text>
</View>
</FormSection>
</TableRowGroup>
);
}

View file

@ -1,7 +1,7 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { useProxy } from "@vendetta/storage";
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 SettingsScaffold from "@shared/ui/SettingsScaffold";
import ListSection from "@shared/ui/ListSection";
@ -9,7 +9,6 @@ import PrimaryButton from "@shared/ui/PrimaryButton";
import { getSnippets, saveSnippet, deleteSnippet } from "../lib/snippets";
const { View } = ReactNative;
const { FormSection, FormInput } = Forms;
function NewSnippetForm() {
const [name, setName] = React.useState("");
@ -18,8 +17,8 @@ function NewSnippetForm() {
return (
<View style={{ paddingHorizontal: 16, paddingBottom: 12 }}>
<FormInput title="Name" placeholder="e.g. rules" value={name} onChange={setName} />
<FormInput title="Text" placeholder="What /snippet should send" value={text} onChange={setText} multiline />
<TextInput label="Name" placeholder="e.g. rules" value={name} onChange={setName} />
<TextInput label="Text" placeholder="What /snippet should send" value={text} onChange={setText} multiline />
<PrimaryButton
label="Save snippet"
disabled={!canSave}
@ -43,9 +42,9 @@ export default function Settings() {
return (
<SettingsScaffold>
<FormSection title="Add a snippet">
<TableRowGroup title="Add a snippet">
<NewSnippetForm />
</FormSection>
</TableRowGroup>
<ListSection
title="Saved snippets"
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 { Forms } from "@vendetta/ui/components";
import { TableRowGroup } from "@shared/ui/table";
import SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox";
import PrimaryButton from "@shared/ui/PrimaryButton";
const { View, Linking } = ReactNative;
const { FormSection } = Forms;
const PRONOUNDB_URL = "https://pronoundb.org/";
export default function Settings() {
return (
<SettingsScaffold>
<FormSection title="How this works">
<TableRowGroup title="How this works">
<NoteBox>
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
@ -27,7 +26,7 @@ export default function Settings() {
<View style={{ paddingHorizontal: 16, paddingTop: 4 }}>
<PrimaryButton label="Open pronoundb.org" onPress={() => Linking.openURL(PRONOUNDB_URL)} />
</View>
</FormSection>
</TableRowGroup>
</SettingsScaffold>
);
}

View file

@ -1,13 +1,11 @@
import { React } from "@vendetta/metro/common";
import { useProxy } from "@vendetta/storage";
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 ColorInput from "@shared/ui/ColorInput";
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 -
// 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
@ -21,8 +19,8 @@ const STATUSES: { key: string; label: string; defaultColor?: string }[] = [
function numberInput(title: string, key: "ringThickness", placeholder: string) {
return (
<FormInput
title={title}
<TextInput
label={title}
placeholder={placeholder}
value={String(storage[key] ?? placeholder)}
onChange={(v: string) => {
@ -34,6 +32,7 @@ function numberInput(title: string, key: "ringThickness", placeholder: string) {
);
}
export default function Settings() {
useProxy(storage);
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
left alone.
</NoteBox>
<FormSection title="Enable">
<FormSwitchRow
<TableRowGroup title="Enable">
<TableSwitchRow
label="Draw ring around avatars"
value={!!storage.enabled}
onValueChange={(v: boolean) => { storage.enabled = v; }}
/>
</FormSection>
<FormSection title="Ring colors">
</TableRowGroup>
<TableRowGroup title="Ring colors">
{STATUSES.map(({ key, label, defaultColor }) => (
<ColorInput
key={key}
@ -66,10 +65,10 @@ export default function Settings() {
}}
/>
))}
</FormSection>
<FormSection title="Ring size">
</TableRowGroup>
<TableRowGroup title="Ring size">
{numberInput("Ring thickness", "ringThickness", "2")}
</FormSection>
</TableRowGroup>
</SettingsScaffold>
);
}

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { React, ReactNative, clipboard } from "@vendetta/metro/common";
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 SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox";
@ -9,7 +9,6 @@ import modules from "../modules";
import { ModuleCategory, vstorage, type AnyModule, type ModuleSetting } from "../lib/Module";
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
// 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") {
return (
<FormSwitchRow
<TableSwitchRow
label={setting.label}
subLabel={subLabel}
value={!!value}
@ -65,7 +64,7 @@ function ModuleSettingRow({ module, settingKey }: { module: AnyModule; settingKe
if (setting.type === "button") {
return (
<FormRow
<TableRow
label={setting.label}
subLabel={subLabel}
disabled={disabled}
@ -76,8 +75,8 @@ function ModuleSettingRow({ module, settingKey }: { module: AnyModule; settingKe
if (setting.type === "text") {
return (
<FormInput
title={setting.label}
<TextInput
label={setting.label}
placeholder={setting.placeholder}
value={typeof value === "string" ? value : ""}
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.
return (
<FormRow
<TableRow
label={setting.label}
subLabel={[subLabel, value].filter(Boolean).join(" - ")}
disabled={disabled}
@ -116,8 +115,8 @@ function ModuleSection({ module }: { module: AnyModule }) {
const errorEntries = Object.entries(module.errors);
return (
<FormSection title={module.label}>
<FormSwitchRow
<TableRowGroup title={module.label}>
<TableSwitchRow
label="Enabled"
subLabel={module.meta.sublabel}
value={!!module.storage.enabled}
@ -127,7 +126,7 @@ function ModuleSection({ module }: { module: AnyModule }) {
<ModuleSettingRow key={key} module={module} settingKey={key} />
))}
{errorEntries.length > 0 && (
<FormRow
<TableRow
label={`${errorEntries.length} error${errorEntries.length === 1 ? "" : "s"} - tap to copy`}
onPress={() => {
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>
<View style={{ paddingHorizontal: 16 }}>
<FormInput
title="Search"
<TextInput
label="Search"
placeholder="Search modules and settings..."
value={query}
onChange={setQuery}

View file

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

View file

@ -1,12 +1,11 @@
import { React } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin";
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 ColorInput from "@shared/ui/ColorInput";
import { TAG_DEFINITIONS, tagSettings } from "../../lib/getTag";
const { FormSection, FormSwitchRow, FormInput } = Forms;
function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; defaultText: string; defaultColor: string }) {
const settings = tagSettings(id);
@ -15,20 +14,20 @@ function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; def
const enabled = settings.enabled !== false;
return (
<FormSection title={defaultText}>
<FormSwitchRow
<TableRowGroup title={defaultText}>
<TableSwitchRow
label="Show this tag"
value={enabled}
onValueChange={(v: boolean) => { settings.enabled = v; }}
/>
<FormInput
title="Tag text"
<TextInput
label="Tag text"
placeholder={defaultText}
value={settings.text ?? ""}
editable={enabled}
onChange={(v: string) => { settings.text = v; }}
/>
<FormSwitchRow
<TableSwitchRow
label="Custom color"
subLabel={`Default: ${defaultColor}`}
value={!!settings.useCustomColor}
@ -43,7 +42,7 @@ function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; def
onChange={(v: string) => { settings.color = v; }}
/>
)}
<FormSwitchRow
<TableSwitchRow
label="Gradient"
subLabel="Member list & profile only, chat tags stay solid"
value={!!settings.useGradient}
@ -57,7 +56,7 @@ function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; def
onChange={(v: string) => { settings.gradientColor = v; }}
/>
)}
</FormSection>
</TableRowGroup>
);
}
@ -66,14 +65,14 @@ export default function Settings() {
return (
<SettingsScaffold>
<FormSection title="General">
<FormSwitchRow
<TableRowGroup title="General">
<TableSwitchRow
label="Use top role color"
subLabel="Used when a tag has no custom color set"
value={!!storage.useRoleColor}
onValueChange={(v: boolean) => { storage.useRoleColor = v; }}
/>
</FormSection>
</TableRowGroup>
{TAG_DEFINITIONS.map((def) => (
<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 { Forms } from "@vendetta/ui/components";
import { TableRowGroup, TableRow } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts";
import { clipboard } from "@vendetta/metro/common";
import SettingsScaffold from "@shared/ui/SettingsScaffold";
@ -7,8 +7,6 @@ import NoteBox from "@shared/ui/NoteBox";
import PrimaryButton from "@shared/ui/PrimaryButton";
import { getLastDetection } from "../lib/diagnostics";
const { FormSection, FormRow } = Forms;
const STRATEGY_LABEL: Record<string, string> = {
buttons: "Strategy 1 (button list) - working normally",
actionSheetRow: "Strategy 2 (row group) - working normally",
@ -21,9 +19,9 @@ export default function Settings() {
return (
<SettingsScaffold>
<FormSection title="Diagnostics">
<TableRowGroup title="Diagnostics">
{detection ? (
<FormRow
<TableRow
label={STRATEGY_LABEL[detection.strategy] ?? detection.strategy}
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);
}}
/>
</FormSection>
</TableRowGroup>
<NoteBox>
"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.

View file

@ -1,12 +1,11 @@
import { React } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin";
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 NoteBox from "@shared/ui/NoteBox";
import { updateYouBar } from "../patches/youBarButtons";
const { FormSection, FormSwitchRow } = Forms;
export default function Settings() {
useProxy(storage);
@ -21,8 +20,8 @@ export default function Settings() {
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.
</NoteBox>
<FormSection title="Buttons">
<FormSwitchRow
<TableRowGroup title="Buttons">
<TableSwitchRow
label="Direct Messages button"
subLabel="Show the DM button in the YouBar"
value={!!storage.showDMButton}
@ -31,7 +30,7 @@ export default function Settings() {
updateYouBar();
}}
/>
<FormSwitchRow
<TableSwitchRow
label="Settings button"
subLabel="Show the Settings button in the YouBar"
value={!!storage.showSettingsButton}
@ -40,7 +39,7 @@ export default function Settings() {
updateYouBar();
}}
/>
</FormSection>
</TableRowGroup>
</SettingsScaffold>
);
}

View file

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