Plugin
FakeProfileThemes
Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding
1
// This plugin is a port from Alyxia's Vendetta plugin2
import "./styles.css";3
4
import { definePluginSettings } from "@api/Settings";5
import { Divider } from "@components/Divider";6
import ErrorBoundary from "@components/ErrorBoundary";7
import { Flex } from "@components/Flex";8
import { Devs } from "@utils/constants";9
import { copyWithToast, fetchUserProfile } from "@utils/discord";10
import { Margins } from "@utils/margins";11
import { classes } from "@utils/misc";12
import { useAwaiter } from "@utils/react";13
import definePlugin, { OptionType } from "@utils/types";14
import { User, UserProfile } from "@vencord/discord-types";15
import { findComponentByCodeLazy } from "@webpack";16
import { Button, ColorPicker, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";17
import virtualMerge from "virtual-merge";18
19
interface Colors {20
primary: number;21
accent: number;22
}23
24
function 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.37
function 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 = parsed48
.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
58
const 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. The70
// functions surely get some event argument sent to them and they likely aren't71
// all required. If anyone who wants to use this component stumbles across this72
// code, you'll have to do the research yourself.73
interface 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
84
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOut:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER");85
86
function SettingsAboutComponentWrapper() {87
const [, , userProfileLoading] = useAwaiter(() => fetchUserProfile(UserStore.getCurrentUser().id));88
89
return !userProfileLoading && <SettingsAboutComponent />;90
}91
92
function 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 in104
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>— use the color pickers below to choose your colors</li>110
<li>— click the "Copy 3y3" button</li>111
<li>— paste the invisible text anywhere in your bio</li>112
</ul>113
<Divider114
className={classes(Margins.top8, Margins.bottom8)}115
/>116
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>117
<Flex gap="1em">118
<ColorPicker119
color={color1}120
label={121
<Text122
variant={"text-xs/normal"}123
style={{ marginTop: "4px" }}124
>125
Primary126
</Text>127
}128
onChange={(color: number) => {129
setColor1(color);130
}}131
/>132
<ColorPicker133
color={color2}134
label={135
<Text136
variant={"text-xs/normal"}137
style={{ marginTop: "4px" }}138
>139
Accent140
</Text>141
}142
onChange={(color: number) => {143
setColor2(color);144
}}145
/>146
<Button147
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 3y3156
</Button>157
</Flex>158
<Divider159
className={classes(Margins.top8, Margins.bottom8)}160
/>161
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>162
<div className="vc-fpt-preview">163
<ProfileModal164
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
178
export 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 nitro206
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: colors212
});213
}214
}215
return user;216
},217
addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) {218
return <Button219
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 3y3227
</Button >;228
}, { noop: true }),229
});230