From b66f173ee3f485681cff593e88fd6d3c22bf4ac2 Mon Sep 17 00:00:00 2001 From: Sophia Date: Sat, 20 Dec 2025 22:57:36 -0500 Subject: [PATCH] feat(plugin): ChangeFriendAvatar (#574) * add changeFriendAvatar to equicordplugins * remvoe reload * styles * force set avatar? * okay it's easier to prompt reload lol * useless junk * datastore * fixes * server overrides * Flex and Heading * fixes * oops --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- .../changeFriendAvatar/AvatarModal.tsx | 69 ++++++++++ .../changeFriendAvatar/index.tsx | 120 ++++++++++++++++++ src/utils/constants.ts | 4 + 3 files changed, 193 insertions(+) create mode 100644 src/equicordplugins/changeFriendAvatar/AvatarModal.tsx create mode 100644 src/equicordplugins/changeFriendAvatar/index.tsx diff --git a/src/equicordplugins/changeFriendAvatar/AvatarModal.tsx b/src/equicordplugins/changeFriendAvatar/AvatarModal.tsx new file mode 100644 index 000000000..d77a24036 --- /dev/null +++ b/src/equicordplugins/changeFriendAvatar/AvatarModal.tsx @@ -0,0 +1,69 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { set } from "@api/DataStore"; +import { classNameFactory } from "@api/Styles"; +import { Button } from "@components/Button"; +import { Heading } from "@components/Heading"; +import { Flex } from "@components/Flex"; +import { Margins } from "@utils/margins"; +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; +import { TextInput, useState } from "@webpack/common"; + +import { avatars, KEY_DATASTORE } from "."; +const cl = classNameFactory("vc-custom-avatars-"); + +export function SetAvatarModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) { + const initialAvatarUrl = avatars[userId] || ""; + const [url, setUrl] = useState(initialAvatarUrl); + + function handleKey(e: KeyboardEvent) { + if (e.key === "Enter") saveUserAvatar(); + } + + async function saveUserAvatar() { + if (!url.trim()) { + await deleteUserAvatar(); + return; + } + + avatars[userId] = url.trim(); + await set(KEY_DATASTORE, avatars); + modalProps.onClose(); + } + + async function deleteUserAvatar() { + delete avatars[userId]; + await set(KEY_DATASTORE, avatars); + modalProps.onClose(); + } + + return ( + + + Custom Avatar + + + +
+ Enter PNG/GIF URL + +
+
+ + + {avatars[userId] && ()} + + + +
+ ); +} diff --git a/src/equicordplugins/changeFriendAvatar/index.tsx b/src/equicordplugins/changeFriendAvatar/index.tsx new file mode 100644 index 000000000..fc831de08 --- /dev/null +++ b/src/equicordplugins/changeFriendAvatar/index.tsx @@ -0,0 +1,120 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { get } from "@api/DataStore"; +import { definePluginSettings, Settings } from "@api/Settings"; +import { PencilIcon } from "@components/Icons"; +import { EquicordDevs } from "@utils/constants"; +import { openModal } from "@utils/modal"; +import definePlugin, { OptionType } from "@utils/types"; +import { User } from "@vencord/discord-types"; +import { extractAndLoadChunksLazy } from "@webpack"; +import { IconUtils, Menu, UserStore } from "@webpack/common"; + +import { SetAvatarModal } from "./AvatarModal"; + +export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/); +export const KEY_DATASTORE = "vencord-custom-avatars"; +export let avatars: Record = {}; + +const settings = definePluginSettings({ + overrideServerAvatars: { + type: OptionType.BOOLEAN, + description: "Override server avatars with custom avatars or the default user avatar if no custom avatar is set.", + default: true + } +}); + +export function getCustomAvatar(userId: string, withHash?: boolean): string | undefined { + if (!avatars[userId] || !Settings.plugins.ChangeFriendAvatar?.enabled) + return; + return avatars[userId]; +} + +export default definePlugin({ + name: "ChangeFriendAvatar", + description: "Set custom avatar URLs for any user", + authors: [EquicordDevs.soapphia], + settings, + getCustomAvatar, + + patches: [ + { + find: "getUserAvatarURL:", + replacement: [ + { + match: /(getUserAvatarURL:)(\i),/, + replace: "$1$self.getAvatarHook($2)," + }, + { + match: /(getGuildMemberAvatarURLSimple:)(\i),/, + replace: "$1$self.getAvatarServerHook($2)," + } + ] + }, + ], + + contextMenus: { + "user-context": (children, { user }) => { + if (!user?.id) return; + + children.push( + , + { + await requireSettingsMenu(); + openModal(modalProps => ); + }} + /> + ); + } + }, + + getAvatarHook: (original: any) => (user: User, animated: boolean, size: number) => { + if (!avatars[user.id]) return original(user, animated, size); + + const customUrl = avatars[user.id]; + try { + const res = new URL(customUrl); + res.searchParams.set("size", size.toString()); + return res.toString(); + } catch { + return customUrl; + } + }, + + getAvatarServerHook: (original: any) => (config: any) => { + const { userId, avatar, size, canAnimate } = config; + if (!settings.store.overrideServerAvatars) return original(config); + + if (avatars[userId]) { + const customUrl = avatars[userId]; + try { + const res = new URL(customUrl); + if (size) res.searchParams.set("size", size.toString()); + return res.toString(); + } catch { + return customUrl; + } + } + + if (avatar) { + const user = UserStore.getUser(userId); + if (user?.avatar) { + return IconUtils.getUserAvatarURL(user, canAnimate, size); + } + } + + return original(config); + }, + + async start() { + avatars = await get>(KEY_DATASTORE) || {}; + } +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index bdedaf1e8..0dd5047df 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1213,6 +1213,10 @@ export const EquicordDevs = Object.freeze({ name: "KamiRu", id: 819191621676695563n }, + soapphia: { + name: "soap phia", + id: 1012095822957133976n + }, } satisfies Record); // iife so #__PURE__ works correctly