Plugin

FakeProfileThemes

Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding

Appearance Customisation
index.tsx
Download

Source

src/plugins/fakeProfileThemes/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
7// This plugin is a port from Alyxia's Vendetta plugin
8import "./styles.css";
9
10import { definePluginSettings } from "@api/Settings";
11import { Divider } from "@components/Divider";
12import ErrorBoundary from "@components/ErrorBoundary";
13import { Flex } from "@components/Flex";
14import { Devs } from "@utils/constants";
15import { copyWithToast, fetchUserProfile } from "@utils/discord";
16import { Margins } from "@utils/margins";
17import { classes } from "@utils/misc";
18import { useAwaiter } from "@utils/react";
19import definePlugin, { OptionType } from "@utils/types";
20import { User, UserProfile } from "@vencord/discord-types";
21import { findComponentByCodeLazy } from "@webpack";
22import { Button, ColorPicker, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
23import virtualMerge from "virtual-merge";
24
25interface Colors {
26 primary: number;
27 accent: number;
28}
29
30function encode(primary: number, accent: number): string {
31 const message = `[#${primary.toString(16).padStart(6, "0")},#${accent.toString(16).padStart(6, "0")}]`;
32 const padding = "";
33 const encoded = Array.from(message)
34 .map(x => x.codePointAt(0))
35 .filter(x => x! >= 0x20 && x! <= 0x7f)
36 .map(x => String.fromCodePoint(x! + 0xe0000))
37 .join("");
38
39 return (padding || "") + " " + encoded;
40}
41
42// Courtesy of Cynthia.
43function decode(bio: string): Array<number> | null {
44 if (bio == null) return null;
45
46 const colorString = bio.match(
47 /\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e005d}/u,
48 );
49 if (colorString != null) {
50 const parsed = [...colorString[0]]
51 .map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000))
52 .join("");
53 const colors = parsed
54 .substring(1, parsed.length - 1)
55 .split(",")
56 .map(x => parseInt(x.replace("#", "0x"), 16));
57
58 return colors;
59 } else {
60 return null;
61 }
62}
63
64const settings = definePluginSettings({
65 nitroFirst: {
66 description: "Default color source if both are present",
67 type: OptionType.SELECT,
68 options: [
69 { label: "Nitro colors", value: true, default: true },
70 { label: "Fake colors", value: false },
71 ]
72 }
73});
74
75// I can't be bothered to figure out the semantics of this component. The
76// functions surely get some event argument sent to them and they likely aren't
77// all required. If anyone who wants to use this component stumbles across this
78// code, you'll have to do the research yourself.
79interface ProfileModalProps {
80 user: User;
81 pendingThemeColors: [number, number];
82 onAvatarChange: () => void;
83 onBannerChange: () => void;
84 canUsePremiumCustomization: boolean;
85 hideExampleButton: boolean;
86 hideFakeActivity: boolean;
87 isTryItOut: boolean;
88}
89
90const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOut:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER");
91
92function SettingsAboutComponentWrapper() {
93 const [, , userProfileLoading] = useAwaiter(() => fetchUserProfile(UserStore.getCurrentUser().id));
94
95 return !userProfileLoading && <SettingsAboutComponent />;
96}
97
98function SettingsAboutComponent() {
99 const existingColors = decode(
100 UserProfileStore.getUserProfile(UserStore.getCurrentUser().id)?.bio ?? ""
101 ) ?? [0, 0];
102 const [color1, setColor1] = useState(existingColors[0]);
103 const [color2, setColor2] = useState(existingColors[1]);
104
105 return (
106 <section>
107 <Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
108 <Forms.FormText>
109 After enabling this plugin, you will see custom colors in
110 the profiles of other people using compatible plugins.{" "}
111 </Forms.FormText>
112 <Forms.FormText className={Margins.top8}>
113 <strong>To set your own profile theme colors:</strong>
114 <ul>
115 <li>&mdash; use the color pickers below to choose your colors</li>
116 <li>&mdash; click the "Copy 3y3" button</li>
117 <li>&mdash; paste the invisible text anywhere in your bio</li>
118 </ul>
119 <Divider
120 className={classes(Margins.top8, Margins.bottom8)}
121 />
122 <Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
123 <Flex gap="1em">
124 <ColorPicker
125 color={color1}
126 label={
127 <Text
128 variant={"text-xs/normal"}
129 style={{ marginTop: "4px" }}
130 >
131 Primary
132 </Text>
133 }
134 onChange={(color: number) => {
135 setColor1(color);
136 }}
137 />
138 <ColorPicker
139 color={color2}
140 label={
141 <Text
142 variant={"text-xs/normal"}
143 style={{ marginTop: "4px" }}
144 >
145 Accent
146 </Text>
147 }
148 onChange={(color: number) => {
149 setColor2(color);
150 }}
151 />
152 <Button
153 onClick={() => {
154 const colorString = encode(color1, color2);
155 copyWithToast(colorString);
156 }}
157 color={Button.Colors.PRIMARY}
158 size={Button.Sizes.XLARGE}
159 style={{ marginBottom: "auto" }}
160 >
161 Copy 3y3
162 </Button>
163 </Flex>
164 <Divider
165 className={classes(Margins.top8, Margins.bottom8)}
166 />
167 <Forms.FormTitle tag="h3">Preview</Forms.FormTitle>
168 <div className="vc-fpt-preview">
169 <ProfileModal
170 user={UserStore.getCurrentUser()}
171 pendingThemeColors={[color1, color2]}
172 onAvatarChange={() => { }}
173 onBannerChange={() => { }}
174 canUsePremiumCustomization={true}
175 hideExampleButton={true}
176 hideFakeActivity={true}
177 isTryItOut={true}
178 />
179 </div>
180 </Forms.FormText>
181 </section>);
182}
183
184export default definePlugin({
185 name: "FakeProfileThemes",
186 description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
187 tags: ["Appearance", "Customisation"],
188 authors: [Devs.Alyxia, Devs.Remty],
189 patches: [
190 {
191 find: "UserProfileStore",
192 replacement: {
193 match: /(?<=getUserProfile\(\i\){return )(.+?)(?=})/,
194 replace: "$self.colorDecodeHook($1)"
195 },
196 },
197 {
198 find: "#{intl::USER_SETTINGS_RESET_PROFILE_THEME}),onClick:",
199 replacement: {
200 match: /#{intl::USER_SETTINGS_RESET_PROFILE_THEME}\).+?}\)(?=\])(?<=color:(\i),.{0,500}?color:(\i),.{0,500}?)/,
201 replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
202 }
203 }
204 ],
205
206 settingsAboutComponent: SettingsAboutComponentWrapper,
207
208 settings,
209 colorDecodeHook(user: UserProfile) {
210 if (user?.bio) {
211 // don't replace colors if already set with nitro
212 if (settings.store.nitroFirst && user.themeColors) return user;
213 const colors = decode(user.bio);
214 if (colors) {
215 return virtualMerge(user, {
216 premiumType: 2,
217 themeColors: colors
218 });
219 }
220 }
221 return user;
222 },
223 addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) {
224 return <Button
225 onClick={() => {
226 const colorString = encode(primary, accent);
227 copyWithToast(colorString);
228 }}
229 color={Button.Colors.PRIMARY}
230 size={Button.Sizes.XLARGE}
231 className={Margins.left16}
232 >Copy 3y3
233 </Button >;
234 }, { noop: true }),
235});
236