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