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>
This commit is contained in:
parent
b161d3fd23
commit
b66f173ee3
3 changed files with 193 additions and 0 deletions
69
src/equicordplugins/changeFriendAvatar/AvatarModal.tsx
Normal file
69
src/equicordplugins/changeFriendAvatar/AvatarModal.tsx
Normal file
|
|
@ -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 (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader className={cl("modal-header")}>
|
||||
<Heading tag="h3">Custom Avatar</Heading>
|
||||
<ModalCloseButton onClick={modalProps.onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("modal-content")} onKeyDown={handleKey}>
|
||||
<section className={Margins.bottom16}>
|
||||
<Heading tag="h3">Enter PNG/GIF URL</Heading>
|
||||
<TextInput
|
||||
placeholder="https://example.com/image.png"
|
||||
value={url}
|
||||
onChange={setUrl}
|
||||
autoFocus
|
||||
/>
|
||||
</section>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Flex gap="8px">
|
||||
{avatars[userId] && (<Button variant="dangerPrimary" onClick={deleteUserAvatar}>Delete</Button>)}
|
||||
<Button onClick={saveUserAvatar}>Save</Button>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
120
src/equicordplugins/changeFriendAvatar/index.tsx
Normal file
120
src/equicordplugins/changeFriendAvatar/index.tsx
Normal file
|
|
@ -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<string, string> = {};
|
||||
|
||||
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(
|
||||
<Menu.MenuSeparator />,
|
||||
<Menu.MenuItem
|
||||
label="Set Avatar"
|
||||
id="set-avatar"
|
||||
icon={PencilIcon}
|
||||
action={async () => {
|
||||
await requireSettingsMenu();
|
||||
openModal(modalProps => <SetAvatarModal userId={user.id} modalProps={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<Record<string, string>>(KEY_DATASTORE) || {};
|
||||
}
|
||||
});
|
||||
|
|
@ -1213,6 +1213,10 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "KamiRu",
|
||||
id: 819191621676695563n
|
||||
},
|
||||
soapphia: {
|
||||
name: "soap phia",
|
||||
id: 1012095822957133976n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue