diff --git a/packages/discord-types/src/common/Activity.d.ts b/packages/discord-types/src/common/Activity.d.ts index d513780a..73387205 100644 --- a/packages/discord-types/src/common/Activity.d.ts +++ b/packages/discord-types/src/common/Activity.d.ts @@ -32,5 +32,9 @@ export interface Activity { metadata?: { button_urls?: Array; }; + party?: { + id?: string; + size?: [number, number]; + }; } diff --git a/src/plugins/customRPC/RpcSettings.tsx b/src/plugins/customRPC/RpcSettings.tsx new file mode 100644 index 00000000..845c088c --- /dev/null +++ b/src/plugins/customRPC/RpcSettings.tsx @@ -0,0 +1,275 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./settings.css"; + +import { classNameFactory } from "@api/Styles"; +import { Divider } from "@components/Divider"; +import { Heading } from "@components/Heading"; +import { resolveError } from "@components/settings/tabs/plugins/components/Common"; +import { debounce } from "@shared/debounce"; +import { ActivityType } from "@vencord/discord-types/enums"; +import { Select, Text, TextInput, useState } from "@webpack/common"; + +import { setRpc, settings, TimestampMode } from "."; + +const cl = classNameFactory("vc-customRPC-settings-"); + +type SettingsKey = keyof typeof settings.store; + +interface TextOption { + settingsKey: SettingsKey; + label: string; + disabled?: boolean; + transform?: (value: string) => T; + isValid?: (value: T) => true | string; +} + +interface SelectOption { + settingsKey: SettingsKey; + label: string; + disabled?: boolean; + options: { label: string; value: T; default?: boolean; }[]; +} + +const makeValidator = (maxLength: number, isRequired = false) => (value: string) => { + if (isRequired && !value) return "This field is required."; + if (value.length > maxLength) return `Must be not longer than ${maxLength} characters.`; + return true; +}; + +const maxLength128 = makeValidator(128); + +function isAppIdValid(value: string) { + if (!/^\d{16,21}$/.test(value)) return "Must be a valid Discord ID."; + return true; +} + +const updateRPC = debounce(() => { + setRpc(true); + if (Vencord.Plugins.isPluginEnabled("CustomRPC")) setRpc(); +}); + +function isStreamLinkDisabled() { + return settings.store.type !== ActivityType.STREAMING; +} + +function isStreamLinkValid(value: string) { + if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL."; + if (value && value.length > 512) return "Streaming link must be not longer than 512 characters."; + return true; +} + +function parseNumber(value: string) { + return value ? parseInt(value, 10) : 0; +} + +function isNumberValid(value: number) { + if (isNaN(value)) return "Must be a number."; + if (value < 0) return "Must be a positive number."; + return true; +} + +function isImageKeyValid(value: string) { + if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//.test(value)) return "Don't use a Discord link. Use an Imgur image link instead."; + if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image (e.g. https://i.imgur.com/...). Right click the image and click 'Copy image address'"; + if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image (e.g. https://media.tenor.com/...). Right click the GIF and click 'Copy image address'"; + return true; +} + +function PairSetting(props: { data: [TextOption, TextOption]; }) { + const [left, right] = props.data; + + return ( +
+ + +
+ ); +} + +function SingleSetting({ settingsKey, label, disabled, isValid, transform }: TextOption) { + const [state, setState] = useState(settings.store[settingsKey] ?? ""); + const [error, setError] = useState(null); + + function handleChange(newValue: any) { + if (transform) newValue = transform(newValue); + + const valid = isValid?.(newValue) ?? true; + + setState(newValue); + setError(resolveError(valid)); + + if (valid === true) { + settings.store[settingsKey] = newValue; + updateRPC(); + } + } + + return ( +
+ {label} + + {error && {error}} +
+ ); +} + +function SelectSetting({ settingsKey, label, options, disabled }: SelectOption) { + return ( +
+ {label} +