diff --git a/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx b/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx index 7776aa30..54b7bb0d 100644 --- a/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx +++ b/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx @@ -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 ( diff --git a/src/equicordplugins/betterActivities/index.tsx b/src/equicordplugins/betterActivities/index.tsx index 14abefab..d7ad0270 100644 --- a/src/equicordplugins/betterActivities/index.tsx +++ b/src/equicordplugins/betterActivities/index.tsx @@ -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" diff --git a/src/equicordplugins/fakeProfileThemes/components/Builder.tsx b/src/equicordplugins/fakeProfileThemes/components/Builder.tsx deleted file mode 100644 index 5f6575e6..00000000 --- a/src/equicordplugins/fakeProfileThemes/components/Builder.tsx +++ /dev/null @@ -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 ( - <> - - - - - - - { - 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 - - { - setPrimaryColor(null); - setAccentColor(null); - setEffect(null); - }} - > - Reset - - - - - - FPTE Builder Preview - - - Build backwards compatible FPTE - - > - ); -} diff --git a/src/equicordplugins/fakeProfileThemes/components/BuilderButton.tsx b/src/equicordplugins/fakeProfileThemes/components/BuilderButton.tsx deleted file mode 100644 index 9da8049b..00000000 --- a/src/equicordplugins/fakeProfileThemes/components/BuilderButton.tsx +++ /dev/null @@ -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) => ( - - {tooltipProps => ( - - - {!selectedStyle && ( - - - - )} - - {!!label && ( - - {label} - - )} - - )} - -); diff --git a/src/equicordplugins/fakeProfileThemes/components/BuilderColorButton.tsx b/src/equicordplugins/fakeProfileThemes/components/BuilderColorButton.tsx deleted file mode 100644 index c23583aa..00000000 --- a/src/equicordplugins/fakeProfileThemes/components/BuilderColorButton.tsx +++ /dev/null @@ -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, Pick { - color: number | null; - setColor: (color: number | null) => void; -} - -export const BuilderColorButton = ({ label, color, setColor, suggestedColors }: BuilderColorButtonProps) => ( - ( - - )} - > - {popoutProps => { - const hexColor = color?.toString(16).padStart(6, "0").padStart(7, "#"); - - return ( - - ); - }} - -); diff --git a/src/equicordplugins/fakeProfileThemes/components/index.tsx b/src/equicordplugins/fakeProfileThemes/components/index.tsx deleted file mode 100644 index 910f8fc1..00000000 --- a/src/equicordplugins/fakeProfileThemes/components/index.tsx +++ /dev/null @@ -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 = () => 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> = () => 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 = () => 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 => ( - - )); - }); -} diff --git a/src/equicordplugins/fakeProfileThemes/components/settingsAboutComponent.tsx b/src/equicordplugins/fakeProfileThemes/components/settingsAboutComponent.tsx deleted file mode 100644 index 4acb8a47..00000000 --- a/src/equicordplugins/fakeProfileThemes/components/settingsAboutComponent.tsx +++ /dev/null @@ -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 = () => ( - - Usage - - After enabling this plugin, you will see custom theme colors and effects in the profiles of other people using this plugin. - - To set your own profile theme colors and effect: - - - Go to your profile settings - Use the FPTE Builder to choose your profile theme colors and effect - Click the "Copy FPTE" button - Paste the invisible text anywhere in your About Me - - - -); diff --git a/src/equicordplugins/fakeProfileThemes/index.tsx b/src/equicordplugins/fakeProfileThemes/index.tsx deleted file mode 100644 index f06581b5..00000000 --- a/src/equicordplugins/fakeProfileThemes/index.tsx +++ /dev/null @@ -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 : , - - onApply(_effectId: string | undefined) { }, - set ProfileEffectModal(comp: Parameters[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[0]) { setCustomizationSection(comp); }, - set CustomColorPicker(comp: Parameters[0]) { setCustomColorPicker(comp); }, - set useAvatarColors(hook: Parameters[0]) { setUseAvatarColors(hook); }, - set ProfileEffectRecord(obj: Parameters[0]) { setProfileEffectRecord(obj); }, - set ProfileEffectStore(store: Parameters[0]) { setProfileEffectStore(store); }, - - settingsAboutComponent, - settings, - decodeAboutMeFPTEHook, - profilePreviewHook -}); diff --git a/src/equicordplugins/fakeProfileThemes/lib/fpte.ts b/src/equicordplugins/fakeProfileThemes/lib/fpte.ts deleted file mode 100644 index b733da96..00000000 --- a/src/equicordplugins/fakeProfileThemes/lib/fpte.ts +++ /dev/null @@ -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; -} diff --git a/src/equicordplugins/fakeProfileThemes/lib/profileEffects.ts b/src/equicordplugins/fakeProfileThemes/lib/profileEffects.ts deleted file mode 100644 index 2faab659..00000000 --- a/src/equicordplugins/fakeProfileThemes/lib/profileEffects.ts +++ /dev/null @@ -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 { - config: ProfileEffectConfig; - skuId: ProfileEffectConfig["sku_id"]; -} - -export let ProfileEffectRecord: { - new(effect: Omit): typeof effect & Pick; - fromServer: (effect: Pick) => Omit & Pick; -}; - -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; -} diff --git a/src/equicordplugins/fakeProfileThemes/lib/profilePreview.ts b/src/equicordplugins/fakeProfileThemes/lib/profilePreview.ts deleted file mode 100644 index ef1beccc..00000000 --- a/src/equicordplugins/fakeProfileThemes/lib/profilePreview.ts +++ /dev/null @@ -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; - } - } -} diff --git a/src/equicordplugins/fakeProfileThemes/lib/userProfile.ts b/src/equicordplugins/fakeProfileThemes/lib/userProfile.ts deleted file mode 100644 index c7676013..00000000 --- a/src/equicordplugins/fakeProfileThemes/lib/userProfile.ts +++ /dev/null @@ -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; -} diff --git a/src/plugins/fakeProfileThemes/index.tsx b/src/plugins/fakeProfileThemes/index.tsx new file mode 100644 index 00000000..cb9b6c2c --- /dev/null +++ b/src/plugins/fakeProfileThemes/index.tsx @@ -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 . +*/ + +// 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; +} + +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 | 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(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); +const ProfileModal = findComponentByCodeLazy('"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 ( + + Usage + + After enabling this plugin, you will see custom colors in + the profiles of other people using compatible plugins.{" "} + + To set your own colors: + + + • use the color pickers below to choose your colors + + • click the "Copy 3y3" button + • paste the invisible text anywhere in your bio + + + Color pickers + {!loadingColorPickerChunk && ( + + + Primary + + } + onChange={(color: number) => { + setColor1(color); + }} + /> + + Accent + + } + onChange={(color: number) => { + setColor2(color); + }} + /> + { + const colorString = encode(color1, color2); + copyWithToast(colorString); + }} + color={Button.Colors.PRIMARY} + size={Button.Sizes.XLARGE} + > + Copy 3y3 + + + )} + + Preview + + { }} + onBannerChange={() => { }} + canUsePremiumCustomization={true} + hideExampleButton={true} + hideFakeActivity={true} + isTryItOutFlow={true} + /> + + + ); + }, + 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 { + const colorString = encode(primary, accent); + copyWithToast(colorString); + }} + color={Button.Colors.PRIMARY} + size={Button.Sizes.XLARGE} + className={Margins.left16} + >Copy 3y3 + ; + }, { noop: true }), +}); + diff --git a/src/plugins/fakeProfileThemes/style.css b/src/plugins/fakeProfileThemes/style.css new file mode 100644 index 00000000..1c9bebf2 --- /dev/null +++ b/src/plugins/fakeProfileThemes/style.css @@ -0,0 +1,3 @@ +.vc-fpt-preview * { + pointer-events: none; +}