Plugin

Decor

Create and use your own custom avatar decorations, or pick your favorite from the presets.

Appearance Customisation
index.tsx
Download

Source

src/plugins/decor/index.tsx
1import "./ui/styles.css";
2
3import ErrorBoundary from "@components/ErrorBoundary";
4import { Devs } from "@utils/constants";
5import definePlugin from "@utils/types";
6import { UserStore } from "@webpack/common";
7
8import { CDN_URL, RAW_SKU_ID, SKU_ID } from "./lib/constants";
9import { useAuthorizationStore } from "./lib/stores/AuthorizationStore";
10import { useCurrentUserDecorationsStore } from "./lib/stores/CurrentUserDecorationsStore";
11import { useUserDecorAvatarDecoration, useUsersDecorationsStore } from "./lib/stores/UsersDecorationsStore";
12import { settings } from "./settings";
13import { setAvatarDecorationModalPreview, setDecorationGridDecoration, setDecorationGridItem } from "./ui/components";
14import DecorSection from "./ui/components/DecorSection";
15
16export interface AvatarDecoration {
17 asset: string;
18 skuId: string;
19}
20
21export default definePlugin({
22 name: "Decor",
23 description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.",
24 tags: ["Appearance", "Customisation"],
25 authors: [Devs.FieryFlames],
26 patches: [
27 // Patch MediaResolver to return correct URL for Decor avatar decorations
28 {
29 find: "getAvatarDecorationURL:",
30 replacement: {
31 match: /(?<=function \i\(\i\){)(?=let{avatarDecoration)/,
32 replace: "const vcDecorDecoration=$self.getDecorAvatarDecorationURL(arguments[0]);if(vcDecorDecoration)return vcDecorDecoration;"
33 }
34 },
35 // Patch profile customization settings to include Decor section
36 {
37 find: "DefaultCustomizationSections",
38 replacement: {
39 match: /(?<=#{intl::USER_SETTINGS_AVATAR_DECORATION}\)},"decoration"\),)/,
40 replace: "$self.DecorSection(),"
41 }
42 },
43 // Decoration modal module
44 {
45 find: "80,onlyAnimateOnHoverOrFocus:!",
46 replacement: [
47 {
48 match: /(?<==)\i=>{let{children.{20,200}isSelected:\i.{0,5}\}=\i/,
49 replace: "$self.DecorationGridItem=$&",
50 },
51 {
52 match: /(?<==)\i=>{let{user:\i,avatarDecoration/,
53 replace: "$self.DecorationGridDecoration=$&",
54 },
55 // Remove NEW label from decor avatar decorations
56 {
57 match: /(?<=\i\.PURCHASE)(?=,)(?<=avatarDecoration:(\i).+?)/,
58 replace: "||$1.skuId===$self.SKU_ID"
59 }
60 ]
61 },
62 {
63 find: "isAvatarDecorationAnimating:",
64 group: true,
65 replacement: [
66 // Add Decor avatar decoration hook to avatar decoration hook
67 {
68 match: /(?<=\.avatarDecoration,guildId:\i\}\)\),)(?<=user:(\i).+?)/,
69 replace: "vcDecorAvatarDecoration=$self.useUserDecorAvatarDecoration($1),"
70 },
71 // Use added hook
72 {
73 match: /(?<={avatarDecoration:).{1,20}?(?=,)(?<=avatarDecorationOverride:(\i).+?)/,
74 replace: "$1??vcDecorAvatarDecoration??($&)"
75 },
76 // Make memo depend on added hook
77 {
78 match: /(?<=size:\i}\),\[)/,
79 replace: "vcDecorAvatarDecoration,"
80 }
81 ]
82 },
83 // Current user area, at bottom of channels/dm list
84 {
85 find: ".DISPLAY_NAME_STYLES_COACHMARK)",
86 replacement: [
87 // Use Decor avatar decoration hook
88 {
89 match: /(?<=\i\)\({avatarDecoration:)\i(?=,)(?<=currentUser:(\i).+?)/,
90 replace: "$self.useUserDecorAvatarDecoration($1)??$&"
91 }
92 ]
93 },
94 ...[
95 "#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}", class="ts-cmt">// Messages
96 "#{intl::COLLECTIBLES_NAMEPLATE_PREVIEW_A11Y}", class="ts-cmt">// Nameplate preview
97 "#{intl::COLLECTIBLES_PROFILE_PREVIEW_A11Y}", class="ts-cmt">// Avatar preview
98 ].map(find => ({
99 find,
100 replacement: {
101 match: /(?<=userValue:)((\i(?:\.author)?)\?\.avatarDecoration)/,
102 replace: "$self.useUserDecorAvatarDecoration($2)??$1"
103 }
104 })),
105 // Patch avatar decoration preview to display Decor avatar decorations as if they are purchased
106 {
107 find: "#{intl::PREMIUM_UPSELL_PROFILE_AVATAR_DECO_INLINE_UPSELL_DESCRIPTION}",
108 replacement: [
109 {
110 match: /(?<==)\i=>{let{user:\i,guildId:\i,avatarDecoration:/,
111 replace: "$self.AvatarDecorationModalPreview=$&"
112 }
113 ]
114 }
115 ],
116 settings,
117
118 flux: {
119 CONNECTION_OPEN: () => {
120 useAuthorizationStore.getState().init();
121 useCurrentUserDecorationsStore.getState().clear();
122 useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);
123 },
124 USER_PROFILE_MODAL_OPEN: data => {
125 useUsersDecorationsStore.getState().fetch(data.userId, true);
126 },
127 },
128
129 set DecorationGridItem(e: any) {
130 setDecorationGridItem(e);
131 },
132
133 set DecorationGridDecoration(e: any) {
134 setDecorationGridDecoration(e);
135 },
136
137 set AvatarDecorationModalPreview(e: any) {
138 setAvatarDecorationModalPreview(e);
139 },
140
141 SKU_ID,
142 RAW_SKU_ID,
143
144 useUserDecorAvatarDecoration,
145
146 async start() {
147 useUsersDecorationsStore.getState().fetch(UserStore.getCurrentUser().id, true);
148 },
149
150 getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) {
151 // Only Decor avatar decorations have this SKU ID
152 if (avatarDecoration?.skuId === SKU_ID) {
153 const parts = avatarDecoration.asset.split("_");
154 // Remove a_ prefix if it's animated and animation is disabled
155 if (avatarDecoration.asset.startsWith("a_") && !canAnimate) parts.shift();
156 return `${CDN_URL}/${parts.join("_")}.png`;
157 } else if (avatarDecoration?.skuId === RAW_SKU_ID) {
158 return avatarDecoration.asset;
159 }
160 },
161
162 DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
163});
164