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