/* * 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, setProfileEffectModal, settingsAboutComponent } from "./components"; import { ProfileEffectRecord, ProfileEffectStore } from "./lib/profileEffects"; import { profilePreviewHook } from "./lib/profilePreview"; import { decodeAboutMeFPTEHook } from "./lib/userProfile"; function replaceHelper( string: string, replaceArgs: readonly (readonly [searchRegExp: RegExp, replaceString: string])[] ) { let result = string; for (const [searchRegExp, replaceString] of replaceArgs) { const beforeReplace = result; result = result.replace( canonicalizeMatch(searchRegExp), canonicalizeReplace(replaceString, "FakeProfileThemesAndEffects") ); 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\([^)]*\){return) ?([^}]+)/, replace: "$1 $self.decodeAboutMeFPTEHook($2)" } }, // Patches ProfileCustomizationPreview { find: ".EDIT_PROFILE_BANNER})", replacement: { match: /:function\(\){return (\i)}.+function \1\((\i)\){/, replace: "$&$self.profilePreviewHook($2);" } }, // 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: '"guild should not be null"', replacement: { match: /\.sectionsContainer,.*?children:\[(?=.+?[{,]guild:(\i))/, replace: "$&$self.addFPTEBuilder($1)," } }, // ProfileEffectModal { find: "initialSelectedProfileEffectId:", group: true, replacement: [ // Modal root { match: /(function (\i)\([^)]*\){(?:.(?!function |}$))*\.ModalRoot,(?:.(?!function |}$))*}).*(?=})/, replace: (match, func, funcName) => `${match}(()=>{$self.ProfileEffectModal=${funcName};` + replaceHelper(func, [ // Required for the profile preview to show profile effects [ /(?<=[{,]purchases:.+?}=).+?(?=,\i=|,{\i:|;)/, "{isFetching:!1,categories:new Map,purchases:$self.usePurchases()}" ] ]) + "})()" }, // Modal content { match: /(function \i\([^)]*\){(?:.(?!function ))*\.ModalContent,(?:.(?!function ))*}).*(?=}\))/, replace: (match, func) => match + replaceHelper(func, [ // 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.ProfileEffectSelection" ], // Replaces the apply profile effect function with the modified version [ /(?<=[{,]onApply:).*?\)\((\i).*?(?=,\i:|}\))/, "()=>$self.onApply($1)" ], // Required to show the apply button [ /(?<=[{,]canUseCollectibles:).+?(?=,\i:|}\))/, "!0" ], // Required to enable the apply button [ /(?<=[{,]disableApplyButton:).+?(?=,\i:|}\))/, "!1" ] ]) } ] }, // ProfileEffectSelection { find: ".presetEffectBackground", replacement: { match: /function\(\i,(\i),.+[,;}]\1\.\i=([^=].+?})(?=;|}$).*(?=}$)/, replace: (match, _, func) => `${match};$self.ProfileEffectSelection=` + replaceHelper(func, [ // 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.useProfileEffectSections($&)" ] ]) } }, // Patches ProfileEffectPreview { find: ".effectDescriptionContainer", replacement: { // Add back removed "forProfileEffectModal" property match: /(?<=[{,])(?=pendingProfileEffectId:)/, replace: "forProfileEffectModal:!0," } } ], addFPTEBuilder: (guild?: BuilderProps["guild"]) => settings.store.hideBuilder ? null : , onApply(_effectId?: string) { }, set ProfileEffectModal(comp: Parameters[0]) { setProfileEffectModal(props => { this.onApply = effectId => { props.onApply(effectId ? ProfileEffectStore.getProfileEffectById(effectId)!.config : null); props.onClose(); }; return comp(props); }); }, ProfileEffectSelection: () => null, usePurchases: () => useMemo( () => new Map(ProfileEffectStore.profileEffects.map(effect => [ effect.id, { items: new ProfileEffectRecord(effect) } ])), [ProfileEffectStore.profileEffects] ), useProfileEffectSections: (origSections: Record[]) => useMemo( () => { origSections.splice(1); origSections[0].items.splice(1); for (const effect of ProfileEffectStore.profileEffects) origSections[0].items.push(new ProfileEffectRecord(effect)); return origSections; }, [ProfileEffectStore.profileEffects] ), settings, replaceHelper, settingsAboutComponent, decodeAboutMeFPTEHook, profilePreviewHook });