mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-20 15:18:50 -05:00
E ER
This commit is contained in:
parent
8cc9c4855b
commit
9fc1303f97
14 changed files with 267 additions and 967 deletions
|
@ -43,7 +43,13 @@ export default function ActivityTooltip({ activity, application, user, cl }: Rea
|
|||
return icon?.image.src;
|
||||
}, [activity]);
|
||||
const timestamps = useMemo(() => getValidTimestamps(activity), [activity]);
|
||||
const startTime = useMemo(() => getValidStartTimeStamp(activity), [activity]);
|
||||
let startTime = useMemo(() => getValidStartTimeStamp(activity), [activity]);
|
||||
|
||||
startTime = Number(String(startTime).slice(0, -3));
|
||||
|
||||
if (Number.isNaN(startTime)) {
|
||||
startTime = 9999999999;
|
||||
}
|
||||
|
||||
const hasDetails = activity.details ?? activity.state;
|
||||
return (
|
||||
|
|
|
@ -255,7 +255,7 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Show all activities in the profile panel
|
||||
find: "Profile Panel: user cannot be undefined",
|
||||
find: /.UserProfileTypes.PANEL,themeOverride:\i\i/,
|
||||
replacement: {
|
||||
match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:.+?,user:\i,channelId:\i.id,)/,
|
||||
replace: "$self.showAllActivitiesComponent"
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { Button, showToast, Switch, UserStore, useState } from "@webpack/common";
|
||||
|
||||
import { buildFPTE } from "../lib/fpte";
|
||||
import { useAccentColor, usePrimaryColor, useProfileEffect, useShowPreview } from "../lib/profilePreview";
|
||||
import { BuilderButton, BuilderColorButton, CustomizationSection, openProfileEffectModal, useAvatarColors } from ".";
|
||||
|
||||
export interface BuilderProps {
|
||||
guildId?: string | undefined;
|
||||
}
|
||||
|
||||
export function Builder({ guildId }: BuilderProps) {
|
||||
const [primaryColor, setPrimaryColor] = usePrimaryColor(null);
|
||||
const [accentColor, setAccentColor] = useAccentColor(null);
|
||||
const [effect, setEffect] = useProfileEffect(null);
|
||||
const [preview, setPreview] = useShowPreview(true);
|
||||
const [buildLegacy, setBuildLegacy] = useState(false);
|
||||
|
||||
const avatarColors = useAvatarColors(UserStore.getCurrentUser().getAvatarURL(guildId, 80));
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomizationSection title="FPTE Builder">
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<BuilderColorButton
|
||||
label="Primary"
|
||||
color={primaryColor}
|
||||
setColor={setPrimaryColor}
|
||||
suggestedColors={avatarColors}
|
||||
/>
|
||||
<BuilderColorButton
|
||||
label="Accent"
|
||||
color={accentColor}
|
||||
setColor={setAccentColor}
|
||||
suggestedColors={avatarColors}
|
||||
/>
|
||||
<BuilderButton
|
||||
label="Effect"
|
||||
tooltip={effect?.title}
|
||||
selectedStyle={effect ? {
|
||||
background: `top / cover url(${effect.thumbnailPreviewSrc}), top / cover url(/assets/f328a6f8209d4f1f5022.png)`
|
||||
} : undefined}
|
||||
buttonProps={{
|
||||
onClick() {
|
||||
openProfileEffectModal(effect?.id, setEffect);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={() => {
|
||||
const strToCopy = buildFPTE(primaryColor ?? -1, accentColor ?? -1, effect?.id ?? "", buildLegacy);
|
||||
if (strToCopy)
|
||||
copyWithToast(strToCopy, "FPTE copied to clipboard!");
|
||||
else
|
||||
showToast("FPTE Builder is empty; nothing to copy!");
|
||||
}}
|
||||
>
|
||||
Copy FPTE
|
||||
</Button>
|
||||
<Button
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.PRIMARY}
|
||||
size={Button.Sizes.SMALL}
|
||||
style={primaryColor === null && accentColor === null && !effect ? { visibility: "hidden" } : undefined}
|
||||
onClick={() => {
|
||||
setPrimaryColor(null);
|
||||
setAccentColor(null);
|
||||
setEffect(null);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CustomizationSection>
|
||||
<Switch
|
||||
value={preview}
|
||||
onChange={setPreview}
|
||||
>
|
||||
FPTE Builder Preview
|
||||
</Switch>
|
||||
<Switch
|
||||
value={buildLegacy}
|
||||
note="Will use more characters"
|
||||
onChange={setBuildLegacy}
|
||||
>
|
||||
Build backwards compatible FPTE
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Text, Tooltip } from "@webpack/common";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
export interface BuilderButtonProps {
|
||||
label?: string | undefined;
|
||||
tooltip?: string | undefined;
|
||||
selectedStyle?: ComponentProps<"div">["style"];
|
||||
buttonProps?: ComponentProps<"div"> | undefined;
|
||||
}
|
||||
|
||||
export const BuilderButton = ({ label, tooltip, selectedStyle, buttonProps }: BuilderButtonProps) => (
|
||||
<Tooltip text={tooltip} shouldShow={!!tooltip}>
|
||||
{tooltipProps => (
|
||||
<div style={{ width: "60px" }}>
|
||||
<div
|
||||
{...tooltipProps}
|
||||
{...buttonProps}
|
||||
aria-label={label}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{
|
||||
...selectedStyle ?? { border: "2px dashed var(--header-secondary)" },
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
height: "60px",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
>
|
||||
{!selectedStyle && (
|
||||
<svg
|
||||
fill="var(--header-secondary)"
|
||||
width="40%"
|
||||
height="40%"
|
||||
viewBox="0 0 144 144"
|
||||
>
|
||||
<path d="M144 64H80V0H64v64H0v16h64v64h16V80h64Z" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{!!label && (
|
||||
<Text
|
||||
color="header-secondary"
|
||||
variant="text-xs/normal"
|
||||
tag="div"
|
||||
style={{
|
||||
marginTop: "4px",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Popout } from "@webpack/common";
|
||||
|
||||
import { BuilderButton, type BuilderButtonProps, CustomColorPicker, type CustomColorPickerProps } from ".";
|
||||
|
||||
export interface BuilderColorButtonProps extends Pick<BuilderButtonProps, "label">, Pick<CustomColorPickerProps, "suggestedColors"> {
|
||||
color: number | null;
|
||||
setColor: (color: number | null) => void;
|
||||
}
|
||||
|
||||
export const BuilderColorButton = ({ label, color, setColor, suggestedColors }: BuilderColorButtonProps) => (
|
||||
<Popout
|
||||
position="bottom"
|
||||
renderPopout={() => (
|
||||
<CustomColorPicker
|
||||
value={color}
|
||||
onChange={setColor}
|
||||
showEyeDropper={true}
|
||||
suggestedColors={suggestedColors}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{popoutProps => {
|
||||
const hexColor = color?.toString(16).padStart(6, "0").padStart(7, "#");
|
||||
|
||||
return (
|
||||
<BuilderButton
|
||||
label={label}
|
||||
tooltip={hexColor}
|
||||
selectedStyle={hexColor ? { background: hexColor } : undefined}
|
||||
buttonProps={popoutProps}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Popout>
|
||||
);
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { type ModalProps, openModal } from "@utils/modal";
|
||||
import { extractAndLoadChunksLazy } from "@webpack";
|
||||
import type { ComponentType, FunctionComponent, PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
import type { ProfileEffectConfig } from "../lib/profileEffects";
|
||||
|
||||
export * from "./Builder";
|
||||
export * from "./BuilderButton";
|
||||
export * from "./BuilderColorButton";
|
||||
export * from "./settingsAboutComponent";
|
||||
|
||||
export interface CustomColorPickerProps {
|
||||
value?: number | null | undefined;
|
||||
onChange: (color: number) => void;
|
||||
onClose?: (() => void) | undefined;
|
||||
suggestedColors?: string[] | undefined;
|
||||
middle?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
showEyeDropper?: boolean | undefined;
|
||||
}
|
||||
|
||||
export let CustomColorPicker: ComponentType<CustomColorPickerProps> = () => null;
|
||||
|
||||
export function setCustomColorPicker(comp: typeof CustomColorPicker) {
|
||||
CustomColorPicker = comp;
|
||||
}
|
||||
|
||||
export let useAvatarColors: (avatarURL: string, fillerColor?: string | undefined, desaturateColors?: boolean | undefined) => string[] = () => [];
|
||||
|
||||
export function setUseAvatarColors(hook: typeof useAvatarColors) {
|
||||
useAvatarColors = hook;
|
||||
}
|
||||
|
||||
export interface CustomizationSectionProps {
|
||||
title?: ReactNode;
|
||||
titleIcon?: ReactNode;
|
||||
titleId?: string | undefined;
|
||||
description?: ReactNode;
|
||||
className?: string | undefined;
|
||||
errors?: string[] | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
hideDivider?: boolean | undefined;
|
||||
showBorder?: boolean | undefined;
|
||||
borderType?: "limited" | "premium" | undefined;
|
||||
hasBackground?: boolean | undefined;
|
||||
forcedDivider?: boolean | undefined;
|
||||
showPremiumIcon?: boolean | undefined;
|
||||
}
|
||||
|
||||
export let CustomizationSection: ComponentType<PropsWithChildren<CustomizationSectionProps>> = () => null;
|
||||
|
||||
export function setCustomizationSection(comp: typeof CustomizationSection) {
|
||||
CustomizationSection = comp;
|
||||
}
|
||||
|
||||
export interface ProfileEffectModalProps extends ModalProps {
|
||||
initialSelectedEffectId?: string | undefined;
|
||||
onApply: (effect: ProfileEffectConfig | null) => void;
|
||||
}
|
||||
|
||||
export let ProfileEffectModal: FunctionComponent<ProfileEffectModalProps> = () => null;
|
||||
|
||||
export function setProfileEffectModal(comp: typeof ProfileEffectModal) {
|
||||
ProfileEffectModal = comp;
|
||||
}
|
||||
|
||||
const requireProfileEffectModal = extractAndLoadChunksLazy(["openProfileEffectModal:function(){"]);
|
||||
|
||||
export function openProfileEffectModal(initialEffectId: ProfileEffectModalProps["initialSelectedEffectId"], onApply: ProfileEffectModalProps["onApply"]) {
|
||||
requireProfileEffectModal().then(() => {
|
||||
openModal(modalProps => (
|
||||
<ProfileEffectModal
|
||||
{...modalProps}
|
||||
initialSelectedEffectId={initialEffectId}
|
||||
onApply={onApply}
|
||||
/>
|
||||
));
|
||||
});
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Margins } from "@utils/margins";
|
||||
import { Forms } from "@webpack/common";
|
||||
|
||||
export const settingsAboutComponent = () => (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
After enabling this plugin, you will see custom theme colors and effects in the profiles of other people using this plugin.
|
||||
<div className={Margins.top8}>
|
||||
<b>To set your own profile theme colors and effect:</b>
|
||||
</div>
|
||||
<ol
|
||||
className={Margins.bottom8}
|
||||
style={{ listStyle: "decimal", paddingLeft: "40px" }}
|
||||
>
|
||||
<li>Go to your profile settings</li>
|
||||
<li>Use the FPTE Builder to choose your profile theme colors and effect</li>
|
||||
<li>Click the "Copy FPTE" button</li>
|
||||
<li>Paste the invisible text anywhere in your About Me</li>
|
||||
</ol>
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>
|
||||
);
|
|
@ -1,235 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { useMemo } from "@webpack/common";
|
||||
|
||||
import { Builder, type BuilderProps, setCustomColorPicker, setCustomizationSection, setProfileEffectModal, settingsAboutComponent, setUseAvatarColors } from "./components";
|
||||
import { ProfileEffectRecord, ProfileEffectStore, setProfileEffectRecord, setProfileEffectStore } from "./lib/profileEffects";
|
||||
import { profilePreviewHook } from "./lib/profilePreview";
|
||||
import { decodeAboutMeFPTEHook } from "./lib/userProfile";
|
||||
|
||||
function replaceHelper(string: string, replaceArgs: [searchRegExp: RegExp, replaceString: string][]) {
|
||||
let result = string;
|
||||
replaceArgs.forEach(([searchRegExp, replaceString]) => {
|
||||
const beforeReplace = result;
|
||||
result = result.replace(
|
||||
canonicalizeMatch(searchRegExp),
|
||||
canonicalizeReplace(replaceString, "FakeProfileThemesAndEffects") as string
|
||||
);
|
||||
if (beforeReplace === result)
|
||||
throw new Error("Replace had no effect: " + searchRegExp);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
prioritizeNitro: {
|
||||
description: "Source to prioritize",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Nitro", value: true },
|
||||
{ label: "About Me", value: false, default: true }
|
||||
]
|
||||
},
|
||||
hideBuilder: {
|
||||
description: "Hide the FPTE Builder in the User Profile and Server Profiles settings pages",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "FakeProfileThemesAndEffects",
|
||||
description: "Allows profile theming and the usage of profile effects by hiding the colors and effect ID in your About Me using invisible, zero-width characters",
|
||||
authors: [EquicordDevs.ryan],
|
||||
patches: [
|
||||
// Patches UserProfileStore.getUserProfile()
|
||||
{
|
||||
find: '"UserProfileStore"',
|
||||
replacement: {
|
||||
match: /(?<=getUserProfile\(\i\){return )\i\[\i](?=})/,
|
||||
replace: "$self.decodeAboutMeFPTEHook($&)"
|
||||
}
|
||||
},
|
||||
// Patches ProfileCustomizationPreview
|
||||
{
|
||||
find: '"ProfileCustomizationPreview"',
|
||||
replacement: {
|
||||
match: /(?:var|let|const){(?=(?:[^}]+,)?pendingThemeColors:)(?:[^}]+,)?pendingProfileEffectId:[^}]+}=(\i)/,
|
||||
replace: "$self.profilePreviewHook($1);$&"
|
||||
}
|
||||
},
|
||||
// Adds the FPTE Builder to the User Profile settings page
|
||||
{
|
||||
find: '"DefaultCustomizationSections"',
|
||||
replacement: {
|
||||
match: /\.sectionsContainer,.*?children:\[/,
|
||||
replace: "$&$self.addFPTEBuilder(),"
|
||||
}
|
||||
},
|
||||
// Adds the FPTE Builder to the Server Profiles settings page
|
||||
{
|
||||
find: ".setNewPendingGuildIdentity",
|
||||
replacement: {
|
||||
match: /\.sectionsContainer,.*?children:\[(?=.+?[{,]guild:(\i))/,
|
||||
replace: "$&$self.addFPTEBuilder($1),"
|
||||
}
|
||||
},
|
||||
// CustomizationSection
|
||||
{
|
||||
find: ".customizationSectionBackground",
|
||||
replacement: {
|
||||
match: /default:function\(\){return (\i)}.+?;/,
|
||||
replace: "$&$self.CustomizationSection=$1;"
|
||||
}
|
||||
},
|
||||
// CustomColorPicker
|
||||
{
|
||||
find: "CustomColorPicker:function(){",
|
||||
replacement: {
|
||||
match: /CustomColorPicker:function\(\){return (\i)}.+?[ ,;}]\1=(?!=)/,
|
||||
replace: "$&$self.CustomColorPicker="
|
||||
}
|
||||
},
|
||||
// useAvatarColors
|
||||
{
|
||||
find: "useAvatarColors:function(){",
|
||||
replacement: {
|
||||
match: /useAvatarColors:function\(\){return (\i)}.+?;/,
|
||||
replace: "$&$self.useAvatarColors=$1;"
|
||||
}
|
||||
},
|
||||
// ProfileEffectRecord
|
||||
{
|
||||
find: "isProfileEffectRecord:function(){",
|
||||
replacement: {
|
||||
match: /default:function\(\){return (\i)}.+(?=}$)/,
|
||||
replace: "$&;$self.ProfileEffectRecord=$1"
|
||||
}
|
||||
},
|
||||
// ProfileEffectStore
|
||||
{
|
||||
find: '"ProfileEffectStore"',
|
||||
replacement: {
|
||||
match: /function\(\i,(\i),.+[,;}]\1\.default=(?!=)/,
|
||||
replace: "$&$self.ProfileEffectStore="
|
||||
}
|
||||
},
|
||||
// ProfileEffectModal
|
||||
{
|
||||
find: "initialSelectedProfileEffectId",
|
||||
replacement: {
|
||||
match: /default:function\(\){return (\i)}(?=.+?(function \1\((?:.(?!function |}$))+\.jsxs?\)\((\i),.+?})(?:function |}$)).+?(function \3\(.+?})(?=function |}$).*(?=}$)/,
|
||||
replace: (wpModule, modalRootName, ModalRoot, _modalInnerName, ModalInner) => (
|
||||
`${wpModule}{$self.ProfileEffectModal=${modalRootName};`
|
||||
+ replaceHelper(ModalRoot, [
|
||||
// Required for the profile preview to show profile effects
|
||||
[
|
||||
/(?<=[{,]purchases:.+?}=).+?(?=,\i=|,{\i:|;)/,
|
||||
"{isFetching:!1,categories:new Map,purchases:$self.getPurchases()}"
|
||||
]
|
||||
])
|
||||
+ replaceHelper(ModalInner, [
|
||||
// Required to show the apply button
|
||||
[
|
||||
/(?<=[{,]purchase:.+?}=).+?(?=,\i=|,{\i:|;)/,
|
||||
"{purchase:{purchasedAt:new Date}}"
|
||||
],
|
||||
// Replaces the profile effect list with the modified version
|
||||
[
|
||||
/(?<=\.jsxs?\)\()[^,]+(?=,{(?:(?:.(?!\.jsxs?\)))+,)?onSelect:)/,
|
||||
"$self.ProfileEffectModalList"
|
||||
],
|
||||
// Replaces the apply profile effect function with the modified version
|
||||
[
|
||||
/(?<=[{,]onApply:).+?\.setNewPendingProfileEffectId\)\((\i).+?(?=,\i:|}\))/,
|
||||
"()=>$self.onApply($1)"
|
||||
],
|
||||
// Required to show the apply button
|
||||
[
|
||||
/(?<=[{,]canUseCollectibles:).+?(?=,\i:|}\))/,
|
||||
"!0"
|
||||
],
|
||||
// Required to enable the apply button
|
||||
[
|
||||
/(?<=[{,]disableApplyButton:).+?(?=,\i:|}\))/,
|
||||
"!1"
|
||||
]
|
||||
])
|
||||
+ "}"
|
||||
)
|
||||
}
|
||||
},
|
||||
// ProfileEffectModalList
|
||||
{
|
||||
find: "selectedProfileEffectRef",
|
||||
replacement: {
|
||||
match: /function\(\i,(\i),.+[,;}]\1\.default=([^=].+?})(?=;|}$).*(?=}$)/,
|
||||
replace: (wpModule, _wpModuleVar, List) => (
|
||||
`${wpModule};$self.ProfileEffectModalList=`
|
||||
+ replaceHelper(List, [
|
||||
// Removes the "Exclusive to Nitro" and "Preview The Shop" sections
|
||||
// Adds every profile effect to the "Your Decorations" section and removes the "Shop" button
|
||||
[
|
||||
/(?<=[ ,](\i)=).+?(?=(?:,\i=|,{\i:|;).+?:\1\.map\()/,
|
||||
"$self.getListSections($&)"
|
||||
]
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
addFPTEBuilder: (guildId?: BuilderProps["guildId"]) => settings.store.hideBuilder ? null : <Builder guildId={guildId} />,
|
||||
|
||||
onApply(_effectId: string | undefined) { },
|
||||
set ProfileEffectModal(comp: Parameters<typeof setProfileEffectModal>[0]) {
|
||||
setProfileEffectModal(props => {
|
||||
this.onApply = effectId => {
|
||||
props.onApply(effectId ? ProfileEffectStore.getProfileEffectById(effectId)!.config : null);
|
||||
props.onClose();
|
||||
};
|
||||
return comp(props);
|
||||
});
|
||||
},
|
||||
|
||||
ProfileEffectModalList: () => null,
|
||||
|
||||
getPurchases: () => useMemo(
|
||||
() => new Map(ProfileEffectStore.profileEffects.map(effect => [
|
||||
effect.id,
|
||||
{ items: new ProfileEffectRecord(effect) }
|
||||
])),
|
||||
[ProfileEffectStore.profileEffects]
|
||||
),
|
||||
|
||||
getListSections: (origSections: any[]) => useMemo(
|
||||
() => {
|
||||
origSections.splice(1);
|
||||
origSections[0].items.splice(1);
|
||||
ProfileEffectStore.profileEffects.forEach(effect => {
|
||||
origSections[0].items.push(new ProfileEffectRecord(effect));
|
||||
});
|
||||
return origSections;
|
||||
},
|
||||
[ProfileEffectStore.profileEffects]
|
||||
),
|
||||
|
||||
set CustomizationSection(comp: Parameters<typeof setCustomizationSection>[0]) { setCustomizationSection(comp); },
|
||||
set CustomColorPicker(comp: Parameters<typeof setCustomColorPicker>[0]) { setCustomColorPicker(comp); },
|
||||
set useAvatarColors(hook: Parameters<typeof setUseAvatarColors>[0]) { setUseAvatarColors(hook); },
|
||||
set ProfileEffectRecord(obj: Parameters<typeof setProfileEffectRecord>[0]) { setProfileEffectRecord(obj); },
|
||||
set ProfileEffectStore(store: Parameters<typeof setProfileEffectStore>[0]) { setProfileEffectStore(store); },
|
||||
|
||||
settingsAboutComponent,
|
||||
settings,
|
||||
decodeAboutMeFPTEHook,
|
||||
profilePreviewHook
|
||||
});
|
|
@ -1,201 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/** The FPTE delimiter codepoint (codepoint of zero-width space). */
|
||||
const DELIMITER_CODEPOINT = 0x200B;
|
||||
/** The FPTE delimiter (zero-width space). */
|
||||
const DELIMITER = String.fromCodePoint(DELIMITER_CODEPOINT);
|
||||
/** The FPTE radix (number of default-ignorable codepoints in the SSP plane). */
|
||||
const RADIX = 0x1000;
|
||||
/** The FPTE starting codepoint (first codepoint in the SSP plane). */
|
||||
const STARTING_CODEPOINT = 0xE0000;
|
||||
/** The FPTE ending codepoint (last default-ignorable codepoint in the SSP plane). */
|
||||
const ENDING_CODEPOINT = STARTING_CODEPOINT + RADIX - 1;
|
||||
|
||||
/**
|
||||
* Builds a theme color string in the legacy format: `[#primary,#accent]`, where primary and accent are
|
||||
* 24-bit colors as base-16 strings, with each codepoint of the string offset by +{@link STARTING_CODEPOINT}.
|
||||
* @param primary The 24-bit primary color.
|
||||
* @param accent The 24-bit accent color.
|
||||
* @returns The built legacy-format theme color string.
|
||||
*/
|
||||
export function encodeColorsLegacy(primary: number, accent: number) {
|
||||
return String.fromCodePoint(...[...`[#${primary.toString(16)},#${accent.toString(16)}]`]
|
||||
.map(c => c.codePointAt(0)! + STARTING_CODEPOINT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the theme colors from a legacy-format string.
|
||||
* @param str The legacy-format string to extract the theme colors from.
|
||||
* @returns The profile theme colors. Colors will be -1 if not found.
|
||||
* @see {@link encodeColorsLegacy}
|
||||
*/
|
||||
export function decodeColorsLegacy(str: string): [primaryColor: number, accentColor: number] {
|
||||
const [primary, accent] = str.matchAll(/(?<=#)[\dA-Fa-f]{1,6}/g);
|
||||
return [primary ? parseInt(primary[0], 16) : -1, accent ? parseInt(accent[0], 16) : -1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a 24-bit color to a base-{@link RADIX} string with each codepoint offset by +{@link STARTING_CODEPOINT}.
|
||||
* @param color The 24-bit color to be converted.
|
||||
* @returns The converted base-{@link RADIX} string with +{@link STARTING_CODEPOINT} offset.
|
||||
*/
|
||||
export function encodeColor(color: number) {
|
||||
if (color === 0) return String.fromCodePoint(STARTING_CODEPOINT);
|
||||
let str = "";
|
||||
for (; color > 0; color = Math.trunc(color / RADIX))
|
||||
str = String.fromCodePoint(color % RADIX + STARTING_CODEPOINT) + str;
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a no-offset base-{@link RADIX} string to a 24-bit color.
|
||||
* @param str The no-offset base-{@link RADIX} string to be converted.
|
||||
* @returns The converted 24-bit color.
|
||||
* Will be -1 if `str` is empty and -2 if the color is greater than the maximum 24-bit color, 0xFFFFFF.
|
||||
*/
|
||||
export function decodeColor(str: string) {
|
||||
if (str === "") return -1;
|
||||
let color = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (color > 0xFFF_FFF) return -2;
|
||||
color += str.codePointAt(i)! * RADIX ** (str.length - 1 - i);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an effect ID to a base-{@link RADIX} string with each code point offset by +{@link STARTING_CODEPOINT}.
|
||||
* @param id The effect ID to be converted.
|
||||
* @returns The converted base-{@link RADIX} string with +{@link STARTING_CODEPOINT} offset.
|
||||
*/
|
||||
export function encodeEffect(id: bigint) {
|
||||
if (id === 0n) return String.fromCodePoint(STARTING_CODEPOINT);
|
||||
let str = "";
|
||||
for (; id > 0n; id /= BigInt(RADIX))
|
||||
str = String.fromCodePoint(Number(id % BigInt(RADIX)) + STARTING_CODEPOINT) + str;
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a no-offset base-{@link RADIX} string to an effect ID.
|
||||
* @param str The no-offset base-{@link RADIX} string to be converted.
|
||||
* @returns The converted effect ID.
|
||||
* Will be -1n if `str` is empty and -2n if the color is greater than the maximum effect ID.
|
||||
*/
|
||||
export function decodeEffect(str: string) {
|
||||
if (str === "") return -1n;
|
||||
let id = 0n;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (id >= 10_000_000_000_000_000_000n) return -2n;
|
||||
id += BigInt(str.codePointAt(i)!) * BigInt(RADIX) ** BigInt(str.length - 1 - i);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a FPTE string containing the given primary/accent colors and effect ID. If the FPTE Builder is NOT set to backwards
|
||||
* compatibility mode, the primary and accent colors will be converted to base-{@link RADIX} before they are encoded.
|
||||
* @param primary The primary profile theme color. Must be negative if unset.
|
||||
* @param accent The accent profile theme color. Must be negative if unset.
|
||||
* @param effect The profile effect ID. Must be empty if unset.
|
||||
* @param legacy Whether the primary and accent colors should be legacy encoded.
|
||||
* @returns The built FPTE string. Will be empty if the given colors and effect are all unset.
|
||||
*/
|
||||
export function buildFPTE(primary: number, accent: number, effect: string, legacy: boolean) {
|
||||
/** The FPTE string to be returned. */
|
||||
let fpte = "";
|
||||
|
||||
// If the FPTE Builder is set to backwards compatibility mode,
|
||||
// the primary and accent colors, if set, will be legacy encoded.
|
||||
if (legacy) {
|
||||
// Legacy FPTE strings must include both the primary and accent colors, even if they are the same.
|
||||
|
||||
if (primary >= 0) {
|
||||
// If both the primary and accent colors are set, they will be legacy encoded and added to the
|
||||
// string; otherwise, if the accent color is unset, the primary color will be used in its place.
|
||||
if (accent >= 0)
|
||||
fpte = encodeColorsLegacy(primary, accent);
|
||||
else
|
||||
fpte = encodeColorsLegacy(primary, primary);
|
||||
|
||||
// If the effect ID is set, it will be encoded and added to the string prefixed by one delimiter.
|
||||
if (effect)
|
||||
fpte += DELIMITER + encodeEffect(BigInt(effect));
|
||||
|
||||
return fpte;
|
||||
}
|
||||
|
||||
// Since the primary color is unset, the accent color, if set, will be used in its place.
|
||||
if (accent >= 0) {
|
||||
fpte = encodeColorsLegacy(accent, accent);
|
||||
|
||||
// If the effect ID is set, it will be encoded and added to the string prefixed by one delimiter.
|
||||
if (effect)
|
||||
fpte += DELIMITER + encodeEffect(BigInt(effect));
|
||||
|
||||
return fpte;
|
||||
}
|
||||
}
|
||||
// If the primary color is set, it will be encoded and added to the string.
|
||||
else if (primary >= 0) {
|
||||
fpte = encodeColor(primary);
|
||||
|
||||
// If the accent color is set and different from the primary color, it
|
||||
// will be encoded and added to the string prefixed by one delimiter.
|
||||
if (accent >= 0 && primary !== accent) {
|
||||
fpte += DELIMITER + encodeColor(accent);
|
||||
|
||||
// If the effect ID is set, it will be encoded and added to the string prefixed by one delimiter.
|
||||
if (effect)
|
||||
fpte += DELIMITER + encodeEffect(BigInt(effect));
|
||||
|
||||
return fpte;
|
||||
}
|
||||
}
|
||||
// If only the accent color is set, it will be encoded and added to the string.
|
||||
else if (accent >= 0)
|
||||
fpte = encodeColor(accent);
|
||||
|
||||
// Since either the primary/accent colors are the same, both are unset, or just one is set, only one color will be added
|
||||
// to the string; therefore, the effect ID, if set, will be encoded and added to the string prefixed by two delimiters.
|
||||
if (effect)
|
||||
fpte += DELIMITER + DELIMITER + encodeEffect(BigInt(effect));
|
||||
|
||||
return fpte;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the delimiter-separated values of the first FPTE substring in a string.
|
||||
* @param str The string to be searched for a FPTE substring.
|
||||
* @returns An array of the found FPTE substring's extracted values. Values will be empty if not found.
|
||||
*/
|
||||
export function extractFPTE(str: string) {
|
||||
/** The array of extracted FPTE values to be returned. */
|
||||
const fpte: [maybePrimaryOrLegacy: string, maybeAccentOrEffect: string, maybeEffect: string] = ["", "", ""];
|
||||
/** The current index of {@link fpte} getting extracted. */
|
||||
let i = 0;
|
||||
|
||||
for (const char of str) {
|
||||
/** The current character's codepoint. */
|
||||
const cp = char.codePointAt(0)!;
|
||||
|
||||
// If the current character is a delimiter, then the current index of fpte has been completed.
|
||||
if (cp === DELIMITER_CODEPOINT) {
|
||||
// If the current index of fpte is the last, then the extraction is done.
|
||||
if (i >= 2) break;
|
||||
i++; // Start extracting the next index of fpte.
|
||||
}
|
||||
// If the current character is not a delimiter but a valid FPTE
|
||||
// character, it will be added to the current index of fpte.
|
||||
else if (cp >= STARTING_CODEPOINT && cp <= ENDING_CODEPOINT)
|
||||
fpte[i] += String.fromCodePoint(cp - STARTING_CODEPOINT);
|
||||
// If an FPTE string has been found and its end has been reached, then the extraction is done.
|
||||
else if (i > 0 || fpte[0]) break;
|
||||
}
|
||||
|
||||
return fpte;
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { FluxStore } from "@webpack/types";
|
||||
|
||||
export interface ProfileEffectConfig {
|
||||
accessibilityLabel: string;
|
||||
animationType: number;
|
||||
description: string;
|
||||
effects: {
|
||||
duartion: number;
|
||||
height: number;
|
||||
loop: boolean;
|
||||
loopDelay: number;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
src: string;
|
||||
start: number;
|
||||
width: number;
|
||||
zIndex: number;
|
||||
}[];
|
||||
id: string;
|
||||
reducedMotionSrc: string;
|
||||
sku_id: string;
|
||||
staticFrameSrc?: string;
|
||||
thumbnailPreviewSrc: string;
|
||||
title: string;
|
||||
type: 1;
|
||||
}
|
||||
|
||||
export interface ProfileEffect extends Pick<ProfileEffectConfig, "id"> {
|
||||
config: ProfileEffectConfig;
|
||||
skuId: ProfileEffectConfig["sku_id"];
|
||||
}
|
||||
|
||||
export let ProfileEffectRecord: {
|
||||
new(effect: Omit<ProfileEffect, "config">): typeof effect & Pick<ProfileEffectConfig, "type">;
|
||||
fromServer: (effect: Pick<ProfileEffectConfig, "id" | "sku_id">) => Omit<ProfileEffect, "config"> & Pick<ProfileEffectConfig, "type">;
|
||||
};
|
||||
|
||||
export function setProfileEffectRecord(obj: typeof ProfileEffectRecord) {
|
||||
ProfileEffectRecord = obj;
|
||||
}
|
||||
|
||||
export let ProfileEffectStore: FluxStore & {
|
||||
readonly isFetching: boolean;
|
||||
readonly fetchError: Error | undefined;
|
||||
readonly profileEffects: ProfileEffect[];
|
||||
readonly tryItOutId: string | null;
|
||||
getProfileEffectById: (effectId: string) => ProfileEffect | undefined;
|
||||
};
|
||||
|
||||
export function setProfileEffectStore(store: typeof ProfileEffectStore) {
|
||||
ProfileEffectStore = store;
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { FluxDispatcher, useState } from "@webpack/common";
|
||||
|
||||
import type { ProfileEffectConfig } from "./profileEffects";
|
||||
|
||||
function updatePreview() {
|
||||
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SUBMIT_SUCCESS" });
|
||||
}
|
||||
|
||||
let primaryColor: number | null = null;
|
||||
export function usePrimaryColor(initialState: typeof primaryColor) {
|
||||
const [state, setState] = useState(() => primaryColor = initialState);
|
||||
return [
|
||||
state,
|
||||
(color: typeof primaryColor) => {
|
||||
setState(primaryColor = color);
|
||||
if (showPreview) updatePreview();
|
||||
}
|
||||
] as const;
|
||||
}
|
||||
|
||||
let accentColor: number | null = null;
|
||||
export function useAccentColor(initialState: typeof accentColor) {
|
||||
const [state, setState] = useState(() => accentColor = initialState);
|
||||
return [
|
||||
state,
|
||||
(color: typeof accentColor) => {
|
||||
setState(accentColor = color);
|
||||
if (showPreview) updatePreview();
|
||||
}
|
||||
] as const;
|
||||
}
|
||||
|
||||
let profileEffect: ProfileEffectConfig | null = null;
|
||||
export function useProfileEffect(initialState: typeof profileEffect) {
|
||||
const [state, setState] = useState(() => profileEffect = initialState);
|
||||
return [
|
||||
state,
|
||||
(effect: typeof profileEffect) => {
|
||||
setState(profileEffect = effect);
|
||||
if (showPreview) updatePreview();
|
||||
}
|
||||
] as const;
|
||||
}
|
||||
|
||||
let showPreview = true;
|
||||
export function useShowPreview(initialState: typeof showPreview) {
|
||||
const [state, setState] = useState(() => showPreview = initialState);
|
||||
return [
|
||||
state,
|
||||
(preview: typeof showPreview) => {
|
||||
setState(showPreview = preview);
|
||||
updatePreview();
|
||||
}
|
||||
] as const;
|
||||
}
|
||||
|
||||
export function profilePreviewHook(props: any) {
|
||||
if (showPreview) {
|
||||
if (primaryColor !== null) {
|
||||
props.pendingThemeColors = [primaryColor, accentColor ?? primaryColor];
|
||||
props.canUsePremiumCustomization = true;
|
||||
} else if (accentColor !== null) {
|
||||
props.pendingThemeColors = [accentColor, accentColor];
|
||||
props.canUsePremiumCustomization = true;
|
||||
}
|
||||
if (!props.forProfileEffectModal && profileEffect) {
|
||||
props.pendingProfileEffectId = profileEffect.id;
|
||||
props.canUsePremiumCustomization = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { settings } from "..";
|
||||
import { decodeColor, decodeColorsLegacy, decodeEffect, extractFPTE } from "./fpte";
|
||||
|
||||
export interface UserProfile {
|
||||
bio: string;
|
||||
premiumType: number | null | undefined;
|
||||
profileEffectId: string | undefined;
|
||||
themeColors: [primaryColor: number, accentColor: number] | undefined;
|
||||
}
|
||||
|
||||
function updateProfileThemeColors(profile: UserProfile, primary: number, accent: number) {
|
||||
if (primary > -1) {
|
||||
profile.themeColors = [primary, accent > -1 ? accent : primary];
|
||||
profile.premiumType = 2;
|
||||
} else if (accent > -1) {
|
||||
profile.themeColors = [accent, accent];
|
||||
profile.premiumType = 2;
|
||||
}
|
||||
}
|
||||
|
||||
function updateProfileEffectId(profile: UserProfile, id: bigint) {
|
||||
if (id > -1n) {
|
||||
profile.profileEffectId = id.toString();
|
||||
profile.premiumType = 2;
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeAboutMeFPTEHook(profile: UserProfile | undefined) {
|
||||
if (!profile) return profile;
|
||||
|
||||
if (settings.store.prioritizeNitro) {
|
||||
if (profile.themeColors) {
|
||||
if (!profile.profileEffectId) {
|
||||
const fpte = extractFPTE(profile.bio);
|
||||
if (decodeColor(fpte[0]) === -2)
|
||||
updateProfileEffectId(profile, decodeEffect(fpte[1]));
|
||||
else
|
||||
updateProfileEffectId(profile, decodeEffect(fpte[2]));
|
||||
}
|
||||
return profile;
|
||||
} else if (profile.profileEffectId) {
|
||||
const fpte = extractFPTE(profile.bio);
|
||||
const primaryColor = decodeColor(fpte[0]);
|
||||
if (primaryColor === -2)
|
||||
updateProfileThemeColors(profile, ...decodeColorsLegacy(fpte[0]));
|
||||
else
|
||||
updateProfileThemeColors(profile, primaryColor, decodeColor(fpte[1]));
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
const fpte = extractFPTE(profile.bio);
|
||||
const primaryColor = decodeColor(fpte[0]);
|
||||
if (primaryColor === -2) {
|
||||
updateProfileThemeColors(profile, ...decodeColorsLegacy(fpte[0]));
|
||||
updateProfileEffectId(profile, decodeEffect(fpte[1]));
|
||||
} else {
|
||||
updateProfileThemeColors(profile, primaryColor, decodeColor(fpte[1]));
|
||||
updateProfileEffectId(profile, decodeEffect(fpte[2]));
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
256
src/plugins/fakeProfileThemes/index.tsx
Normal file
256
src/plugins/fakeProfileThemes/index.tsx
Normal file
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// This plugin is a port from Alyxia's Vendetta plugin
|
||||
import "./style.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, copyWithToast } from "@utils/misc";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
import virtualMerge from "virtual-merge";
|
||||
|
||||
interface UserProfile extends User {
|
||||
themeColors?: Array<number>;
|
||||
}
|
||||
|
||||
interface Colors {
|
||||
primary: number;
|
||||
accent: number;
|
||||
}
|
||||
|
||||
function encode(primary: number, accent: number): string {
|
||||
const message = `[#${primary.toString(16).padStart(6, "0")},#${accent.toString(16).padStart(6, "0")}]`;
|
||||
const padding = "";
|
||||
const encoded = Array.from(message)
|
||||
.map(x => x.codePointAt(0))
|
||||
.filter(x => x! >= 0x20 && x! <= 0x7f)
|
||||
.map(x => String.fromCodePoint(x! + 0xe0000))
|
||||
.join("");
|
||||
|
||||
return (padding || "") + " " + encoded;
|
||||
}
|
||||
|
||||
// Courtesy of Cynthia.
|
||||
function decode(bio: string): Array<number> | null {
|
||||
if (bio == null) return null;
|
||||
|
||||
const colorString = bio.match(
|
||||
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u,
|
||||
);
|
||||
if (colorString != null) {
|
||||
const parsed = [...colorString[0]]
|
||||
.map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000))
|
||||
.join("");
|
||||
const colors = parsed
|
||||
.substring(1, parsed.length - 1)
|
||||
.split(",")
|
||||
.map(x => parseInt(x.replace("#", "0x"), 16));
|
||||
|
||||
return colors;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
nitroFirst: {
|
||||
description: "Default color source if both are present",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Nitro colors", value: true, default: true },
|
||||
{ label: "Fake colors", value: false },
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
interface ColorPickerProps {
|
||||
color: number | null;
|
||||
label: React.ReactElement;
|
||||
showEyeDropper?: boolean;
|
||||
suggestedColors?: string[];
|
||||
onChange(value: number | null): void;
|
||||
}
|
||||
|
||||
// I can't be bothered to figure out the semantics of this component. The
|
||||
// functions surely get some event argument sent to them and they likely aren't
|
||||
// all required. If anyone who wants to use this component stumbles across this
|
||||
// code, you'll have to do the research yourself.
|
||||
interface ProfileModalProps {
|
||||
user: User;
|
||||
pendingThemeColors: [number, number];
|
||||
onAvatarChange: () => void;
|
||||
onBannerChange: () => void;
|
||||
canUsePremiumCustomization: boolean;
|
||||
hideExampleButton: boolean;
|
||||
hideFakeActivity: boolean;
|
||||
isTryItOutFlow: boolean;
|
||||
}
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>('"ProfileCustomizationPreview"');
|
||||
|
||||
const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i\("(.+?)"\).then\(\i\.bind\(\i,"(.+?)"\)\)/);
|
||||
|
||||
export default definePlugin({
|
||||
name: "FakeProfileThemes",
|
||||
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
|
||||
authors: [Devs.Alyxia, Devs.Remty],
|
||||
patches: [
|
||||
{
|
||||
find: "UserProfileStore",
|
||||
replacement: {
|
||||
match: /(?<=getUserProfile\(\i\){return )(\i\[\i\])/,
|
||||
replace: "$self.colorDecodeHook($1)"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".USER_SETTINGS_RESET_PROFILE_THEME",
|
||||
replacement: {
|
||||
match: /RESET_PROFILE_THEME}\)(?<=color:(\i),.{0,500}?color:(\i),.{0,500}?)/,
|
||||
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
|
||||
}
|
||||
}
|
||||
],
|
||||
settingsAboutComponent: () => {
|
||||
const existingColors = decode(
|
||||
UserProfileStore.getUserProfile(UserStore.getCurrentUser().id).bio
|
||||
) ?? [0, 0];
|
||||
const [color1, setColor1] = useState(existingColors[0]);
|
||||
const [color2, setColor2] = useState(existingColors[1]);
|
||||
|
||||
const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
After enabling this plugin, you will see custom colors in
|
||||
the profiles of other people using compatible plugins.{" "}
|
||||
<br />
|
||||
To set your own colors:
|
||||
<ul>
|
||||
<li>
|
||||
• use the color pickers below to choose your colors
|
||||
</li>
|
||||
<li>• click the "Copy 3y3" button</li>
|
||||
<li>• paste the invisible text anywhere in your bio</li>
|
||||
</ul><br />
|
||||
<Forms.FormDivider
|
||||
className={classes(Margins.top8, Margins.bottom8)}
|
||||
/>
|
||||
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
|
||||
{!loadingColorPickerChunk && (
|
||||
<Flex
|
||||
direction={Flex.Direction.HORIZONTAL}
|
||||
style={{ gap: "1rem" }}
|
||||
>
|
||||
<ColorPicker
|
||||
color={color1}
|
||||
label={
|
||||
<Text
|
||||
variant={"text-xs/normal"}
|
||||
style={{ marginTop: "4px" }}
|
||||
>
|
||||
Primary
|
||||
</Text>
|
||||
}
|
||||
onChange={(color: number) => {
|
||||
setColor1(color);
|
||||
}}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={color2}
|
||||
label={
|
||||
<Text
|
||||
variant={"text-xs/normal"}
|
||||
style={{ marginTop: "4px" }}
|
||||
>
|
||||
Accent
|
||||
</Text>
|
||||
}
|
||||
onChange={(color: number) => {
|
||||
setColor2(color);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const colorString = encode(color1, color2);
|
||||
copyWithToast(colorString);
|
||||
}}
|
||||
color={Button.Colors.PRIMARY}
|
||||
size={Button.Sizes.XLARGE}
|
||||
>
|
||||
Copy 3y3
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
<Forms.FormDivider
|
||||
className={classes(Margins.top8, Margins.bottom8)}
|
||||
/>
|
||||
<Forms.FormTitle tag="h3">Preview</Forms.FormTitle>
|
||||
<div className="vc-fpt-preview">
|
||||
<ProfileModal
|
||||
user={UserStore.getCurrentUser()}
|
||||
pendingThemeColors={[color1, color2]}
|
||||
onAvatarChange={() => { }}
|
||||
onBannerChange={() => { }}
|
||||
canUsePremiumCustomization={true}
|
||||
hideExampleButton={true}
|
||||
hideFakeActivity={true}
|
||||
isTryItOutFlow={true}
|
||||
/>
|
||||
</div>
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>);
|
||||
},
|
||||
settings,
|
||||
colorDecodeHook(user: UserProfile) {
|
||||
if (user) {
|
||||
// don't replace colors if already set with nitro
|
||||
if (settings.store.nitroFirst && user.themeColors) return user;
|
||||
const colors = decode(user.bio);
|
||||
if (colors) {
|
||||
return virtualMerge(user, {
|
||||
premiumType: 2,
|
||||
themeColors: colors
|
||||
});
|
||||
}
|
||||
}
|
||||
return user;
|
||||
},
|
||||
addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) {
|
||||
return <Button
|
||||
onClick={() => {
|
||||
const colorString = encode(primary, accent);
|
||||
copyWithToast(colorString);
|
||||
}}
|
||||
color={Button.Colors.PRIMARY}
|
||||
size={Button.Sizes.XLARGE}
|
||||
className={Margins.left16}
|
||||
>Copy 3y3
|
||||
</Button >;
|
||||
}, { noop: true }),
|
||||
});
|
||||
|
3
src/plugins/fakeProfileThemes/style.css
Normal file
3
src/plugins/fakeProfileThemes/style.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.vc-fpt-preview * {
|
||||
pointer-events: none;
|
||||
}
|
Loading…
Add table
Reference in a new issue