UserPFP: Combine ChangeFriendAvatar

This commit is contained in:
thororen1234 2025-12-20 23:26:08 -05:00
parent d4dada31ce
commit 406a9a5e3b
No known key found for this signature in database
3 changed files with 95 additions and 154 deletions

View file

@ -1,127 +0,0 @@
/*
* 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 { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings } from "@api/Settings";
import { PencilIcon } from "@components/Icons";
import userpfp from "@equicordplugins/userpfp";
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 default definePlugin({
name: "ChangeFriendAvatar",
description: "Set custom avatar URLs for any user",
authors: [EquicordDevs.soapphia],
settings,
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 (isPluginEnabled(userpfp.name) && userpfp.data?.avatars?.[user.id]) {
if (userpfp.settings.store.preferNitro && user.avatar?.startsWith("a_")) return original(user, animated, size);
const res = new URL(userpfp.data.avatars[user.id]);
res.searchParams.set("animated", animated ? "true" : "false");
if (res && !animated) {
res.pathname = res.pathname.replaceAll(/\.gifv?/g, ".png");
}
return res.toString();
}
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) || {};
}
});

View file

@ -7,16 +7,17 @@
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 { Heading } from "@components/Heading";
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 ".";
import { data, KEY_DATASTORE } from ".";
const cl = classNameFactory("vc-custom-avatars-");
export function SetAvatarModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) {
const { avatars } = data;
const initialAvatarUrl = avatars[userId] || "";
const [url, setUrl] = useState(initialAvatarUrl);

View file

@ -6,24 +6,39 @@
import "./styles.css";
import { isPluginEnabled } from "@api/PluginManager";
import { get } from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import { Button } from "@components/Button";
import { Flex } from "@components/Flex";
import { Heart } from "@components/Heart";
import changeFriendAvatar from "@equicordplugins/changeFriendAvatar";
import { PencilIcon } from "@components/Icons";
import { Margins } from "@components/margins";
import { Notice } from "@components/Notice";
import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { openInviteModal } from "@utils/discord";
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";
const cl = classNameFactory("vc-userpfp-");
const DONO_URL = "https://ko-fi.com/coolesding";
const INVITE_LINK = "userpfp-1129784704267210844";
let data = { avatars: {} as Record<string, string> };
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 const data = { avatars: {} as 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
},
preferNitro: {
description: "Which avatar to use if both default animated (Nitro) pfp and UserPFP avatars are present",
type: OptionType.SELECT,
@ -44,45 +59,71 @@ const settings = definePluginSettings({
}
return true;
})
}
},
});
export default definePlugin({
name: "UserPFP",
description: "Allows you to use an animated avatar without Nitro",
authors: [EquicordDevs.nexpid, Devs.thororen],
authors: [EquicordDevs.nexpid, Devs.thororen, EquicordDevs.soapphia],
settings,
data,
settingsAboutComponent: () => (
<Flex className={cl("settings")}>
<Button
variant="link"
className={cl("settings-button")}
onClick={() => openInviteModal(INVITE_LINK)}
>
Join UserPFP Server
</Button>
<Button
variant="secondary"
className={cl("settings-button")}
onClick={() => VencordNative.native.openExternal(DONO_URL)}
>
Support UserPFP here <Heart className={cl("settings-heart")} />
</Button>
</Flex>
<>
<Notice.Info className={Margins.bottom8}>
Using the set avatar feature is local only meaning only you see it change.
</Notice.Info>
<Flex className={cl("settings")}>
<Button
variant="link"
className={cl("settings-button")}
onClick={() => openInviteModal(INVITE_LINK)}
>
Join UserPFP Server
</Button>
<Button
variant="secondary"
className={cl("settings-button")}
onClick={() => VencordNative.native.openExternal(DONO_URL)}
>
Support UserPFP here <Heart className={cl("settings-heart")} />
</Button>
</Flex>
</>
),
patches: [
{
find: "getUserAvatarURL:",
predicate: () => !isPluginEnabled(changeFriendAvatar.name),
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 (settings.store.preferNitro && user.avatar?.startsWith("a_")) return original(user, animated, size);
if (!data.avatars[user.id]) return original(user, animated, size);
@ -95,11 +136,37 @@ export default definePlugin({
return res.toString();
},
getAvatarServerHook: (original: any) => (config: any) => {
const { userId, avatar, size, canAnimate } = config;
const { avatars } = data;
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() {
data.avatars = await get<Record<string, string>>(KEY_DATASTORE) || {};
await fetch(settings.store.databaseSource)
.then(async res => {
if (res.ok) data = await res.json();
})
.then(res => res.ok && res.json())
.then(remote => remote?.avatars && Object.assign(data.avatars, remote.avatars))
.catch(() => null);
}
});