diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index 40cb15c9d..eede9559e 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -444,3 +444,101 @@ export function PlaceholderIcon(props: IconProps) { ); } + +export function MainSettingsIcon(props: IconProps) { + return ( + + + + ); +} + +export function PluginsIcon(props: IconProps) { + return ( + + + + ); +} + +export function CloudIcon(props: IconProps) { + return ( + + + + ); +} + +export function BackupRestoreIcon(props: IconProps) { + return ( + + + + ); +} + +export function UpdaterIcon(props: IconProps) { + return ( + + + + ); +} + +export function PatchHelperIcon(props: IconProps) { + return ( + + + + ); +} + +export function VesktopSettingsIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/settings/tabs/BaseTab.tsx b/src/components/settings/tabs/BaseTab.tsx index 5d9914c3d..b4c0ebb53 100644 --- a/src/components/settings/tabs/BaseTab.tsx +++ b/src/components/settings/tabs/BaseTab.tsx @@ -16,20 +16,15 @@ * along with this program. If not, see . */ -import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import { handleComponentFailed } from "@components/handleComponentFailed"; -import { Margins } from "@utils/margins"; import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { onlyOnce } from "@utils/onlyOnce"; import type { ComponentType, PropsWithChildren } from "react"; -export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) { +export function SettingsTab({ children }: PropsWithChildren) { return ( -
- {title} - {children} -
+
{children}
); } diff --git a/src/components/settings/tabs/patchHelper/index.tsx b/src/components/settings/tabs/patchHelper/index.tsx index 2d33a0293..bd99f5c8b 100644 --- a/src/components/settings/tabs/patchHelper/index.tsx +++ b/src/components/settings/tabs/patchHelper/index.tsx @@ -104,7 +104,7 @@ function PatchHelper() { } return ( - + Full patch + diff --git a/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx b/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx index e4e8b001f..447c1f442 100644 --- a/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx +++ b/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx @@ -27,7 +27,7 @@ import { Button, Text } from "@webpack/common"; function BackupAndRestoreTab() { return ( - + Warning diff --git a/src/components/settings/tabs/sync/CloudTab.tsx b/src/components/settings/tabs/sync/CloudTab.tsx index 73526bcdb..bab67a7fd 100644 --- a/src/components/settings/tabs/sync/CloudTab.tsx +++ b/src/components/settings/tabs/sync/CloudTab.tsx @@ -96,10 +96,8 @@ function CloudTab() { const settings = useSettings(["cloud.authenticated", "cloud.url"]); return ( - +
- Cloud Settings - Vencord comes with a cloud integration that adds goodies like settings sync across devices. It respects your privacy, and @@ -118,7 +116,7 @@ function CloudTab() { settings.cloud.authenticated = v; }} /> - Backend URL + Backend URL Which backend to use when using cloud integrations. diff --git a/src/components/settings/tabs/themes/index.tsx b/src/components/settings/tabs/themes/index.tsx index 7d2234459..2bc5e81eb 100644 --- a/src/components/settings/tabs/themes/index.tsx +++ b/src/components/settings/tabs/themes/index.tsx @@ -37,7 +37,7 @@ function ThemesTab() { const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); return ( - + + Themes are not supported on the Userscript! diff --git a/src/components/settings/tabs/updater/index.tsx b/src/components/settings/tabs/updater/index.tsx index 935f66125..c238d0772 100644 --- a/src/components/settings/tabs/updater/index.tsx +++ b/src/components/settings/tabs/updater/index.tsx @@ -44,9 +44,7 @@ function Updater() { }; return ( - - Updater Settings - + + {isDonor(user?.id) ? ( . */ -import { Settings } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; +import { BackupRestoreIcon, CloudIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons"; import { BackupAndRestoreTab, CloudTab, PatchHelperTab, PluginsTab, ThemesTab, UpdaterTab, VencordTab } from "@components/settings/tabs"; import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; -import definePlugin, { OptionType } from "@utils/types"; +import { isTruthy } from "@utils/guards"; +import definePlugin, { IconProps, OptionType } from "@utils/types"; +import { waitFor } from "@webpack"; import { React } from "@webpack/common"; +import type { ComponentType, PropsWithChildren, ReactNode } from "react"; import gitHash from "~git-hash"; -type SectionType = "HEADER" | "DIVIDER" | "CUSTOM"; -type SectionTypes = Record; +let LayoutTypes = { + SECTION: 1, + SIDEBAR_ITEM: 2, + PANEL: 3, + PANE: 4 +}; +waitFor(["SECTION", "SIDEBAR_ITEM", "PANEL", "PANE"], v => LayoutTypes = v); + +const FallbackSectionTypes = { + HEADER: "HEADER", + DIVIDER: "DIVIDER", + CUSTOM: "CUSTOM" +}; +type SectionTypes = typeof FallbackSectionTypes; + +type SettingsLocation = + | "top" + | "aboveNitro" + | "belowNitro" + | "aboveActivity" + | "belowActivity" + | "bottom"; + +interface SettingsLayoutNode { + type: number; + key?: string; + legacySearchKey?: string; + useLabel?(): string; + useTitle?(): string; + buildLayout?(): SettingsLayoutNode[]; + icon?(): ReactNode; + render?(): ReactNode; +} + +interface EntryOptions { + key: string, + title: string, + panelTitle?: string, + Component: ComponentType<{}>, + Icon: ComponentType; +} +interface SettingsLayoutBuilder { + key?: string; + buildLayout(): SettingsLayoutNode[]; +} + +const settings = definePluginSettings({ + settingsLocation: { + type: OptionType.SELECT, + description: "Where to put the Vencord settings section", + options: [ + { label: "At the very top", value: "top" }, + { label: "Above the Nitro section", value: "aboveNitro", default: true }, + { label: "Below the Nitro section", value: "belowNitro" }, + { label: "Above Activity Settings", value: "aboveActivity" }, + { label: "Below Activity Settings", value: "belowActivity" }, + { label: "At the very bottom", value: "bottom" }, + ] as { label: string; value: SettingsLocation; default?: boolean; }[] + } +}); export default definePlugin({ name: "Settings", @@ -34,19 +96,28 @@ export default definePlugin({ authors: [Devs.Ven, Devs.Megu], required: true, + settings, + patches: [ { find: ".versionHash", replacement: [ { - match: /\.info.+?\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/, - replace: (m, component, props) => { + match: /\.compactInfo.+?(?=null!=(\i)&&(.{0,20}\i\.Text.{0,200}?,children:).{0,15}?("span"),({className:\i\.versionHash,children:\["Build Override: ",\1\.id\]\})\)\}\))/, + replace: (m, _buildOverride, makeRow, component, props) => { props = props.replace(/children:\[.+\]/, ""); - return `${m},$self.makeInfoElements(${component}, ${props})`; + return `${m},$self.makeInfoElements(${component},${props}).map(e=>${makeRow}e})),`; } }, { - match: /copyValue:\i\.join\(" "\)/, + match: /\.info.+?\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/, + replace: (m, component, props) => { + props = props.replace(/children:\[.+\]/, ""); + return `${m},$self.makeInfoElements(${component},${props})`; + } + }, + { + match: /copyValue:\i\.join\(" "\)/g, replace: "$& + $self.getInfoString()" } ] @@ -73,15 +144,147 @@ export default definePlugin({ } }, { - find: "2025-09-user-settings-redesign-1", + find: ".buildLayout().map", replacement: { - match: /enabled:![01],showLegacyOpen:/g, - replace: "enabled:false,showLegacyOpen:" + match: /(\i)\.buildLayout\(\)(?=\.map)/, + replace: "$self.buildLayout($1)" } } ], + buildEntry(options: EntryOptions): SettingsLayoutNode { + const { key, title, panelTitle = title, Component, Icon } = options; + + return ({ + key, + type: LayoutTypes.SIDEBAR_ITEM, + legacySearchKey: title.toUpperCase(), + useTitle: () => title, + icon: () => , + buildLayout: () => [ + { + key: key + "_panel", + type: LayoutTypes.PANEL, + useTitle: () => panelTitle, + buildLayout: () => [ + { + key: key + "_pane", + type: LayoutTypes.PANE, + buildLayout: () => [], + render: () => , + useTitle: () => panelTitle + } + ] + } + ] + }); + }, + + buildLayout(originalLayoutBuilder: SettingsLayoutBuilder) { + const layout = originalLayoutBuilder.buildLayout(); + if (originalLayoutBuilder.key !== "$Root") return layout; + if (!Array.isArray(layout)) return layout; + + if (layout.some(s => s?.key === "vencord_section")) return layout; + + const { buildEntry } = this; + + const vencordEntries: SettingsLayoutNode[] = [ + buildEntry({ + key: "vencord_main", + title: "Vencord", + panelTitle: "Vencord Settings", + Component: VencordTab, + Icon: MainSettingsIcon + }), + buildEntry({ + key: "vencord_plugins", + title: "Plugins", + Component: PluginsTab, + Icon: PluginsIcon + }), + buildEntry({ + key: "vencord_themes", + title: "Themes", + Component: ThemesTab, + Icon: PaintbrushIcon + }), + !IS_UPDATER_DISABLED && UpdaterTab && buildEntry({ + key: "vencord_updater", + title: "Updater", + panelTitle: "Vencord Updater", + Component: UpdaterTab, + Icon: UpdaterIcon + }), + buildEntry({ + key: "vencord_cloud", + title: "Cloud", + panelTitle: "Vencord Cloud", + Component: CloudTab, + Icon: CloudIcon + }), + buildEntry({ + key: "vencord_backup_restore", + title: "Backup & Restore", + Component: BackupAndRestoreTab, + Icon: BackupRestoreIcon + }), + IS_DEV && PatchHelperTab && buildEntry({ + key: "vencord_patch_helper", + title: "Patch Helper", + Component: PatchHelperTab, + Icon: PatchHelperIcon + }), + ...this.customEntries.map(buildEntry), + // TODO: Remove deprecated customSections in a future update + ...this.customSections.map((func, i) => { + const { section, element, label } = func(FallbackSectionTypes); + if (Object.values(FallbackSectionTypes).includes(section)) return null; + + return buildEntry({ + key: `vencord_deprecated_custom_${section}`, + title: label, + Component: element, + Icon: section === "Vesktop" ? VesktopSettingsIcon : PlaceholderIcon + }); + }) + ].filter(isTruthy); + + const vencordSection: SettingsLayoutNode = { + key: "vencord_section", + type: LayoutTypes.SECTION, + useLabel: () => "Vencord", + buildLayout: () => vencordEntries + }; + + const { settingsLocation } = settings.store; + + const places: Record = { + top: "user_section", + aboveNitro: "billing_section", + belowNitro: "billing_section", + aboveActivity: "activity_section", + belowActivity: "activity_section", + bottom: "logout_section" + }; + + const key = places[settingsLocation] ?? places.top; + let idx = layout.findIndex(s => typeof s?.key === "string" && s.key === key); + + if (idx === -1) { + idx = 2; + } else if (settingsLocation.startsWith("below")) { + idx += 1; + } + + layout.splice(idx, 0, vencordSection); + + return layout; + }, + + /** @deprecated Use customEntries */ customSections: [] as ((SectionTypes: SectionTypes) => any)[], + customEntries: [] as EntryOptions[], makeSettingsCategories(SectionTypes: SectionTypes) { return [ @@ -139,12 +342,12 @@ export default definePlugin({ ].filter(Boolean); }, - isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) { - const firstChild = settings?.[0]; + isRightSpot({ header, settings: s }: { header?: string; settings?: string[]; }) { + const firstChild = s?.[0]; // lowest two elements... sanity backup if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true; - const { settingsLocation } = Settings.plugins.Settings; + const { settingsLocation } = settings.store; if (settingsLocation === "bottom") return firstChild === "LOGOUT"; if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG"; @@ -152,7 +355,7 @@ export default definePlugin({ if (!header) return; try { - const names = { + const names: Record, string> = { top: getIntlMessage("USER_SETTINGS"), aboveNitro: getIntlMessage("BILLING_SETTINGS"), belowNitro: getIntlMessage("APP_SETTINGS"), @@ -182,31 +385,12 @@ export default definePlugin({ return (...args: any[]) => { const elements = originalHook(...args); if (!this.patchedSettings.has(elements)) - elements.unshift(...this.makeSettingsCategories({ - HEADER: "HEADER", - DIVIDER: "DIVIDER", - CUSTOM: "CUSTOM" - })); + elements.unshift(...this.makeSettingsCategories(FallbackSectionTypes)); return elements; }; }, - options: { - settingsLocation: { - type: OptionType.SELECT, - description: "Where to put the Vencord settings section", - options: [ - { label: "At the very top", value: "top" }, - { label: "Above the Nitro section", value: "aboveNitro", default: true }, - { label: "Below the Nitro section", value: "belowNitro" }, - { label: "Above Activity Settings", value: "aboveActivity" }, - { label: "Below Activity Settings", value: "belowActivity" }, - { label: "At the very bottom", value: "bottom" }, - ] - }, - }, - get electronVersion() { return VencordNative.native.getVersions().electron || window.legcord?.electron || null; }, @@ -245,7 +429,7 @@ export default definePlugin({ return "\n" + this.getInfoRows().join("\n"); }, - makeInfoElements(Component: React.ComponentType, props: React.PropsWithChildren) { + makeInfoElements(Component: ComponentType, props: PropsWithChildren) { return this.getInfoRows().map((text, i) => {text} ); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 737c5eb9a..e864c651e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -612,6 +612,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ alfred: { name: "alfred", id: 1038466644353232967n + }, + vv: { + name: "VV", + id: 254866377087778816n } } satisfies Record);