mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-21 15:48:52 -05:00
Revert to vencord fakeprofilethemes
This commit is contained in:
parent
736c0856f5
commit
7d919d3d62
5 changed files with 0 additions and 902 deletions
|
@ -1,62 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Text, Tooltip } from "@webpack/common";
|
|
||||||
import type { CSSProperties } from "react";
|
|
||||||
|
|
||||||
interface BuilderButtonProps {
|
|
||||||
label?: string;
|
|
||||||
tooltip?: string;
|
|
||||||
selectedStyle?: CSSProperties;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BuilderButton({ label, tooltip, selectedStyle, onClick }: BuilderButtonProps) {
|
|
||||||
return (
|
|
||||||
<Tooltip text={tooltip} shouldShow={!!tooltip}>
|
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
|
||||||
<div style={{ width: "60px" }}>
|
|
||||||
<div
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
style={{
|
|
||||||
...selectedStyle || { border: "2px dashed var(--header-secondary)" },
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "grid",
|
|
||||||
height: "60px",
|
|
||||||
placeItems: "center"
|
|
||||||
}}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{!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={{ textAlign: "center" }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
||||||
import { Button, Flex, Text, useRef, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
import type { ColorPicker } from "../types";
|
|
||||||
|
|
||||||
interface ColorPickerModalProps {
|
|
||||||
modalProps: ModalProps;
|
|
||||||
ColorPicker: ColorPicker;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (v: number) => void;
|
|
||||||
initialColor: number;
|
|
||||||
suggestedColors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ColorPickerModal({ modalProps, ColorPicker, onClose, onSubmit, initialColor = 0, suggestedColors = [] }: ColorPickerModalProps) {
|
|
||||||
const [color, setColor] = useState(initialColor);
|
|
||||||
const [pos, setPos] = useState<[number, number]>([-1, -1]);
|
|
||||||
const header = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: pos[0] === -1 || pos[1] === -1 ? "revert" : "fixed",
|
|
||||||
left: `clamp(0px, ${pos[0]}px, calc(100vw - ${header.current?.getBoundingClientRect().width ?? 0}px))`,
|
|
||||||
top: `clamp(22px, ${pos[1]}px, calc(100vh - ${header.current?.getBoundingClientRect().height ?? 0}px))`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalRoot {...modalProps} size={ModalSize.DYNAMIC}>
|
|
||||||
<style>{":has(>:not([class*=hidden__]) [class*=customColorPicker__])>[class*=backdrop__]{display:none!important}[class*=root_] [class*=customColorPicker__]{border:none!important;box-shadow:none!important}"}</style>
|
|
||||||
<div
|
|
||||||
ref={header}
|
|
||||||
style={{ cursor: "move" }}
|
|
||||||
onMouseDown={e => {
|
|
||||||
const ref = header.current;
|
|
||||||
if (ref === null) return;
|
|
||||||
const rect = ref.getBoundingClientRect();
|
|
||||||
const offsetX = e.pageX - rect.left;
|
|
||||||
const offsetY = e.pageY - rect.top;
|
|
||||||
const onDrag = (e: MouseEvent) => setPos([e.pageX - offsetX, e.pageY - offsetY]);
|
|
||||||
document.addEventListener("mousemove", onDrag);
|
|
||||||
document.addEventListener("mouseup",
|
|
||||||
() => { document.removeEventListener("mousemove", onDrag); },
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalHeader justify={Flex.Justify.BETWEEN}>
|
|
||||||
<Text color="header-primary" variant="heading-lg/semibold" tag="h1">
|
|
||||||
Color Picker
|
|
||||||
</Text>
|
|
||||||
<div onMouseDown={e => e.stopPropagation()}>
|
|
||||||
<ModalCloseButton onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
</ModalHeader>
|
|
||||||
</div>
|
|
||||||
<ColorPicker
|
|
||||||
value={color}
|
|
||||||
showEyeDropper={true}
|
|
||||||
suggestedColors={suggestedColors}
|
|
||||||
onChange={(e: number) => setColor(e)}
|
|
||||||
/>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onClick={() => onSubmit(color)}>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalRoot>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openColorPickerModal(
|
|
||||||
ColorPicker: ColorPicker,
|
|
||||||
onSubmit: (v: number) => void,
|
|
||||||
initialColor: number = 0,
|
|
||||||
suggestedColors: string[] = []
|
|
||||||
) {
|
|
||||||
const key = openModal(modalProps =>
|
|
||||||
<ColorPickerModal
|
|
||||||
modalProps={modalProps}
|
|
||||||
ColorPicker={ColorPicker}
|
|
||||||
onClose={() => closeModal(key)}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
initialColor={initialColor}
|
|
||||||
suggestedColors={suggestedColors}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return key;
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
||||||
import { Button, Flex, showToast, Text, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
import type { ProfileEffect } from "../types";
|
|
||||||
|
|
||||||
interface ProfileEffectModalProps {
|
|
||||||
modalProps: ModalProps;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (v: ProfileEffect) => void;
|
|
||||||
classNames: { [k: string]: string; };
|
|
||||||
profileEffects: ProfileEffect[];
|
|
||||||
initialEffectID?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProfileEffectModal({ modalProps, onClose, onSubmit, profileEffects, classNames = {}, initialEffectID }: ProfileEffectModalProps) {
|
|
||||||
const [selected, setSelected] = useState(initialEffectID ? profileEffects.findIndex(e => e.id === initialEffectID) : -1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalRoot {...modalProps} size={ModalSize.SMALL}>
|
|
||||||
<ModalHeader justify={Flex.Justify.BETWEEN}>
|
|
||||||
<Text color="header-primary" variant="heading-lg/semibold" tag="h1">
|
|
||||||
Add Profile Effect
|
|
||||||
</Text>
|
|
||||||
<ModalCloseButton onClick={onClose} />
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalContent
|
|
||||||
paddingFix={false}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "12px",
|
|
||||||
justifyContent: "center",
|
|
||||||
padding: "16px 8px 16px 16px"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{profileEffects.map((e, i) => (
|
|
||||||
<div
|
|
||||||
className={classNames.effectGridItem + (i === selected ? " " + classNames.selected : "")}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
style={{ width: "80px", height: "80px" }}
|
|
||||||
onClick={() => setSelected(i)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className={classNames.presetEffectBackground}
|
|
||||||
src="/assets/f328a6f8209d4f1f5022.png"
|
|
||||||
alt={e.accessibilityLabel}
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
className={classNames.presetEffectImg}
|
|
||||||
src={e.thumbnailPreviewSrc}
|
|
||||||
alt={e.title}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ModalContent>
|
|
||||||
<ModalFooter
|
|
||||||
justify={Flex.Justify.BETWEEN}
|
|
||||||
direction={Flex.Direction.HORIZONTAL}
|
|
||||||
align={Flex.Align.CENTER}
|
|
||||||
>
|
|
||||||
<Text color="header-primary" variant="heading-lg/semibold" tag="h1">
|
|
||||||
{selected === -1 ? "" : profileEffects[selected].title}
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (selected !== -1)
|
|
||||||
onSubmit(profileEffects[selected]);
|
|
||||||
else
|
|
||||||
showToast("No effect selected!");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalRoot>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openProfileEffectModal(
|
|
||||||
onSubmit: (v: ProfileEffect) => void,
|
|
||||||
profileEffects: ProfileEffect[],
|
|
||||||
classNames: { [k: string]: string; } = {},
|
|
||||||
initialEffectID?: string
|
|
||||||
) {
|
|
||||||
const key = openModal(modalProps =>
|
|
||||||
<ProfileEffectModal
|
|
||||||
modalProps={modalProps}
|
|
||||||
onClose={() => closeModal(key)}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
profileEffects={profileEffects}
|
|
||||||
classNames={classNames}
|
|
||||||
initialEffectID={initialEffectID}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return key;
|
|
||||||
}
|
|
|
@ -1,574 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
|
||||||
import { EquicordDevs } from "@utils/constants";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { copyWithToast } from "@utils/misc";
|
|
||||||
import { closeModal } from "@utils/modal";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { Button, FluxDispatcher, Forms, RestAPI, showToast, Switch, Toasts, useEffect, useRef, UserStore, useState } from "@webpack/common";
|
|
||||||
|
|
||||||
import { BuilderButton } from "./components/BuilderButton";
|
|
||||||
import { openColorPickerModal } from "./components/ColorPickerModal";
|
|
||||||
import { openProfileEffectModal } from "./components/ProfileEffectModal";
|
|
||||||
import type { ColorPicker, CustomizationSection, ProfileEffect, RGBColor, UserProfile } from "./types";
|
|
||||||
|
|
||||||
let CustomizationSection: CustomizationSection = () => null;
|
|
||||||
let ColorPicker: ColorPicker = () => null;
|
|
||||||
let getPaletteForAvatar = (v: string) => Promise.resolve<RGBColor[]>([]);
|
|
||||||
let getComplimentaryPaletteForColor = (v: RGBColor): RGBColor[] => [];
|
|
||||||
const profileEffectModalClassNames: { [k: string]: string; } = {};
|
|
||||||
let [primaryColor, setPrimaryColor] = [-1, (v: number) => { }];
|
|
||||||
let [accentColor, setAccentColor] = [-1, (v: number) => { }];
|
|
||||||
let [effect, setEffect]: [ProfileEffect | null, (v: ProfileEffect | null) => void] = [null, () => { }];
|
|
||||||
let [preview, setPreview] = [true, (v: boolean) => { }];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a profile theme color string in the legacy format, [#primary,#accent] where
|
|
||||||
* primary and accent are base-16 24-bit colors, with each code point offset by +0xE0000
|
|
||||||
* @param primary The base-10 24-bit primary color to be encoded
|
|
||||||
* @param accent The base-10 24-bit accent color to be encoded
|
|
||||||
* @returns The legacy encoded profile theme color string
|
|
||||||
*/
|
|
||||||
function encodeColorsLegacy(primary: number, accent: number) {
|
|
||||||
return String.fromCodePoint(...[...`[#${primary.toString(16)},#${accent.toString(16)}]`]
|
|
||||||
.map(c => c.codePointAt(0)! + 0xE0000));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts profile theme colors from given legacy-format string
|
|
||||||
* @param str The legacy-format string to extract profile theme colors from
|
|
||||||
* @returns The profile theme colors. Colors will be -1 if not found.
|
|
||||||
*/
|
|
||||||
function decodeColorsLegacy(str: string): [number, number] {
|
|
||||||
const colors = str.matchAll(/(?<=#)[\dA-Fa-f]{1,6}/g);
|
|
||||||
return [parseInt(colors.next().value?.[0], 16) || -1, parseInt(colors.next().value?.[0], 16) || -1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given base-10 24-bit color to a base-4096 string with each code point offset by +0xE0000
|
|
||||||
* @param color The base-10 24-bit color to be converted
|
|
||||||
* @returns The converted base-4096 string with +0xE0000 offset
|
|
||||||
*/
|
|
||||||
function encodeColor(color: number) {
|
|
||||||
if (color === 0) return "\u{e0000}";
|
|
||||||
let str = "";
|
|
||||||
for (; color > 0; color = Math.trunc(color / 4096))
|
|
||||||
str = String.fromCodePoint(color % 4096 + 0xE0000) + str;
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given no-offset base-4096 string to a base-10 24-bit color
|
|
||||||
* @param str The no-offset base-4096 string to be converted
|
|
||||||
* @returns The converted base-10 24-bit color
|
|
||||||
* Will be -1 if the given string is empty and -2 if greater than the maximum 24-bit color, 16,777,215
|
|
||||||
*/
|
|
||||||
function decodeColor(str: string) {
|
|
||||||
if (str === "") return -1;
|
|
||||||
let color = 0;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
if (color > 16_777_215) return -2;
|
|
||||||
color += str.codePointAt(i)! * 4096 ** (str.length - 1 - i);
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given base-10 profile effect ID to a base-4096 string with each code point offset by +0xE0000
|
|
||||||
* @param id The base-10 profile effect ID to be converted
|
|
||||||
* @returns The converted base-4096 string with +0xE0000 offset
|
|
||||||
*/
|
|
||||||
function encodeEffect(id: bigint) {
|
|
||||||
if (id === 0n) return "\u{e0000}";
|
|
||||||
let str = "";
|
|
||||||
for (; id > 0n; id /= 4096n)
|
|
||||||
str = String.fromCodePoint(Number(id % 4096n) + 0xE0000) + str;
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given no-offset base-4096 string to a base-10 profile effect ID
|
|
||||||
* @param str The no-offset base-4096 string to be converted
|
|
||||||
* @returns The converted base-10 profile effect ID
|
|
||||||
* Will be -1n if the given string is empty and -2n if greater than the maximum profile effect ID, 1.2 quintillion
|
|
||||||
*/
|
|
||||||
function decodeEffect(str: string) {
|
|
||||||
if (str === "") return -1n;
|
|
||||||
let id = 0n;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
if (id > 1_200_000_000_000_000_000n) return -2n;
|
|
||||||
id += BigInt(str.codePointAt(i)!) * 4096n ** 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-4096 before they are encoded.
|
|
||||||
* @param primary The primary profile theme color. Must be -1 if unset.
|
|
||||||
* @param accent The accent profile theme color. Must be -1 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.
|
|
||||||
*/
|
|
||||||
function buildFPTE(primary: number, accent: number, effect: string, legacy: boolean) {
|
|
||||||
const DELIM = "\u200b"; // The FPTE delimiter (zero-width space)
|
|
||||||
|
|
||||||
let fpte = ""; // The FPTE string to be returned
|
|
||||||
|
|
||||||
// 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 !== -1) {
|
|
||||||
// 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 !== -1)
|
|
||||||
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 += DELIM + encodeEffect(BigInt(effect));
|
|
||||||
|
|
||||||
return fpte;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since the primary color is unset, the accent color, if set, will be used in its place.
|
|
||||||
if (accent !== -1) {
|
|
||||||
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 += DELIM + encodeEffect(BigInt(effect));
|
|
||||||
|
|
||||||
return fpte;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the primary color is set, it will be encoded and added to the string.
|
|
||||||
else if (primary !== -1) {
|
|
||||||
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 !== -1 && primary !== accent) {
|
|
||||||
fpte += DELIM + encodeColor(accent);
|
|
||||||
|
|
||||||
// If the effect ID is set, it will be encoded and added to the string prefixed by one delimiter.
|
|
||||||
if (effect !== "")
|
|
||||||
fpte += DELIM + encodeEffect(BigInt(effect));
|
|
||||||
|
|
||||||
return fpte;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If only the accent color is set, it will be encoded and added to the string.
|
|
||||||
else if (accent !== -1)
|
|
||||||
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 += DELIM + DELIM + encodeEffect(BigInt(effect));
|
|
||||||
|
|
||||||
return fpte;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the delimiter-separated values of the first FPTE string found in the given string
|
|
||||||
* @param str The string to be searched for a FPTE string
|
|
||||||
* @returns An array of the extracted FPTE string's values. Values will be empty if not found.
|
|
||||||
*/
|
|
||||||
function extractFPTE(str: string) {
|
|
||||||
const fpte: [string, string, string] = ["", "", ""]; // The array containing extracted FPTE values
|
|
||||||
let i = 0; // The current index of fpte getting extracted
|
|
||||||
|
|
||||||
for (const char of str) {
|
|
||||||
const cp = char.codePointAt(0)!; // The current character's code point
|
|
||||||
|
|
||||||
// If the current character is a delimiter, then the current index of fpte has been completed.
|
|
||||||
if (cp === 0x200B) {
|
|
||||||
// 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 >= 0xE0000 && cp <= 0xE0FFF)
|
|
||||||
fpte[i] += String.fromCodePoint(cp - 0xE0000);
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the given RGB color to a hexadecimal string
|
|
||||||
* @param rgb The RGB color to be converted
|
|
||||||
* @returns The converted hexadecimal string
|
|
||||||
* @example
|
|
||||||
* // returns #ff0000
|
|
||||||
* RGBtoHex([255, 0, 0])
|
|
||||||
*/
|
|
||||||
function RGBtoHex(rgb: RGBColor) {
|
|
||||||
return "#" + ((rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).padStart(6, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSuggestedColors(callback: (v: string[]) => void) {
|
|
||||||
const user = UserStore.getCurrentUser();
|
|
||||||
getPaletteForAvatar(`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp?size=80`)
|
|
||||||
.then(avatarColors => {
|
|
||||||
callback([
|
|
||||||
...avatarColors.slice(0, 2),
|
|
||||||
...getComplimentaryPaletteForColor(avatarColors[0]).slice(0, 3)
|
|
||||||
].map(e => RGBtoHex(e)));
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
showToast("Unable to retrieve suggested colors.", Toasts.Type.FAILURE);
|
|
||||||
callback([]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchProfileEffects(callback: (v: ProfileEffect[]) => void) {
|
|
||||||
RestAPI.get({ url: "/user-profile-effects" })
|
|
||||||
.then(res => callback(res.body.profile_effect_configs))
|
|
||||||
.catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
showToast("Unable to retrieve the list of profile effects.", Toasts.Type.FAILURE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUserThemeColors(user: UserProfile, primary: number, accent: number) {
|
|
||||||
if (primary > -1) {
|
|
||||||
user.themeColors = [primary, accent > -1 ? accent : primary];
|
|
||||||
user.premiumType = 2;
|
|
||||||
} else if (accent > -1) {
|
|
||||||
user.themeColors = [accent, accent];
|
|
||||||
user.premiumType = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUserEffectId(user: UserProfile, id: bigint) {
|
|
||||||
if (id > -1n) {
|
|
||||||
user.profileEffectId = id.toString();
|
|
||||||
user.premiumType = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePreview() {
|
|
||||||
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SUBMIT_SUCCESS" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
prioritizeNitro: {
|
|
||||||
description: "Source to use if profile theme colors / effects are set by both Nitro and About Me",
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
options: [
|
|
||||||
{ label: "Nitro", value: true },
|
|
||||||
{ label: "About Me", value: false, default: true },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hideBuilder: {
|
|
||||||
description: "Hide the FPTE Builder in the profiles settings page",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "FakeProfileThemes",
|
|
||||||
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: [
|
|
||||||
{
|
|
||||||
find: '"UserProfileStore"',
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=getUserProfile\(\i\){return )\i\[\i](?=})/,
|
|
||||||
replace: "$self.decodeUserBioFPTEHook($&)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '"DefaultCustomizationSections"',
|
|
||||||
replacement: {
|
|
||||||
match: /\.sectionsContainer,children:\[/,
|
|
||||||
replace: "$&$self.addFPTEBuilder(),"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: ".customizationSectionBackground",
|
|
||||||
replacement: {
|
|
||||||
match: /default:function\(\){return (\i)}.*?;/,
|
|
||||||
replace: "$&$self.CustomizationSection=$1;"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "CustomColorPicker:function(){",
|
|
||||||
replacement: {
|
|
||||||
match: /CustomColorPicker:function\(\){return (\i)}.*? \1=(?=[^=])/,
|
|
||||||
replace: "$&$self.ColorPicker="
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "getPaletteForAvatar:function(){",
|
|
||||||
replacement: {
|
|
||||||
match: /getPaletteForAvatar:function\(\){return (\i)}.*? \1=(?=[^=])/,
|
|
||||||
replace: "$&$self.getPaletteForAvatar="
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "getComplimentaryPaletteForColor:function(){",
|
|
||||||
replacement: {
|
|
||||||
match: /getComplimentaryPaletteForColor:function\(\){return (\i)}.*?;/,
|
|
||||||
replace: "$&$self.getComplimentaryPaletteForColor=$1;"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: 'effectGridItem:"',
|
|
||||||
noWarn: true,
|
|
||||||
replacement: {
|
|
||||||
match: /(\i):"(.+?)"/g,
|
|
||||||
replace: (m, k, v) => { profileEffectModalClassNames[k] = v; return m; }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '"ProfileCustomizationPreview"',
|
|
||||||
replacement: {
|
|
||||||
match: /let{(?=(?:[^}]+,)?pendingThemeColors:)(?=(?:[^}]+,)?pendingProfileEffectId:)[^}]+}=(\i)[,;]/,
|
|
||||||
replace: "$self.profilePreviewHook($1);$&"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
set CustomizationSection(c: CustomizationSection) {
|
|
||||||
CustomizationSection = c;
|
|
||||||
},
|
|
||||||
set ColorPicker(c: ColorPicker) {
|
|
||||||
ColorPicker = c;
|
|
||||||
},
|
|
||||||
set getPaletteForAvatar(f: (v: string) => Promise<RGBColor[]>) {
|
|
||||||
getPaletteForAvatar = f;
|
|
||||||
},
|
|
||||||
set getComplimentaryPaletteForColor(f: (v: RGBColor) => RGBColor[]) {
|
|
||||||
getComplimentaryPaletteForColor = f;
|
|
||||||
},
|
|
||||||
settingsAboutComponent: () => {
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
settings,
|
|
||||||
decodeUserBioFPTEHook(user: UserProfile | undefined) {
|
|
||||||
if (user === undefined) return user;
|
|
||||||
|
|
||||||
if (settings.store.prioritizeNitro) {
|
|
||||||
if (user.themeColors !== undefined) {
|
|
||||||
if (user.profileEffectId === undefined) {
|
|
||||||
const fpte = extractFPTE(user.bio);
|
|
||||||
if (decodeColor(fpte[0]) === -2)
|
|
||||||
updateUserEffectId(user, decodeEffect(fpte[1]));
|
|
||||||
else
|
|
||||||
updateUserEffectId(user, decodeEffect(fpte[2]));
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
} else if (user.profileEffectId !== undefined) {
|
|
||||||
const fpte = extractFPTE(user.bio);
|
|
||||||
const primaryColor = decodeColor(fpte[0]);
|
|
||||||
if (primaryColor === -2)
|
|
||||||
updateUserThemeColors(user, ...decodeColorsLegacy(fpte[0]));
|
|
||||||
else
|
|
||||||
updateUserThemeColors(user, primaryColor, decodeColor(fpte[1]));
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fpte = extractFPTE(user.bio);
|
|
||||||
const primaryColor = decodeColor(fpte[0]);
|
|
||||||
if (primaryColor === -2) {
|
|
||||||
updateUserThemeColors(user, ...decodeColorsLegacy(fpte[0]));
|
|
||||||
updateUserEffectId(user, decodeEffect(fpte[1]));
|
|
||||||
} else {
|
|
||||||
updateUserThemeColors(user, primaryColor, decodeColor(fpte[1]));
|
|
||||||
updateUserEffectId(user, decodeEffect(fpte[2]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
},
|
|
||||||
profilePreviewHook(props: any) {
|
|
||||||
if (preview) {
|
|
||||||
if (primaryColor !== -1) {
|
|
||||||
props.pendingThemeColors = [primaryColor, accentColor === -1 ? primaryColor : accentColor];
|
|
||||||
props.canUsePremiumCustomization = true;
|
|
||||||
} else if (accentColor !== -1) {
|
|
||||||
props.pendingThemeColors = [accentColor, accentColor];
|
|
||||||
props.canUsePremiumCustomization = true;
|
|
||||||
}
|
|
||||||
if (effect) {
|
|
||||||
props.pendingProfileEffectId = effect.id;
|
|
||||||
props.canUsePremiumCustomization = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addFPTEBuilder() {
|
|
||||||
if (settings.store.hideBuilder) return null;
|
|
||||||
|
|
||||||
[primaryColor, setPrimaryColor] = useState(-1);
|
|
||||||
[accentColor, setAccentColor] = useState(-1);
|
|
||||||
[effect, setEffect] = useState<ProfileEffect | null>(null);
|
|
||||||
[preview, setPreview] = useState(true);
|
|
||||||
const [buildLegacy, setBuildLegacy] = useState(false);
|
|
||||||
const currModal = useRef("");
|
|
||||||
|
|
||||||
useEffect(() => () => closeModal(currModal.current), []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CustomizationSection title="FPTE Builder">
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
||||||
<BuilderButton
|
|
||||||
label="Primary"
|
|
||||||
{...primaryColor !== -1 ? (c => ({
|
|
||||||
tooltip: c,
|
|
||||||
selectedStyle: { background: c }
|
|
||||||
}))("#" + primaryColor.toString(16).padStart(6, "0")) : {}}
|
|
||||||
onClick={() => {
|
|
||||||
getSuggestedColors(colors => {
|
|
||||||
closeModal(currModal.current);
|
|
||||||
currModal.current = openColorPickerModal(
|
|
||||||
ColorPicker,
|
|
||||||
c => {
|
|
||||||
setPrimaryColor(c);
|
|
||||||
if (preview) updatePreview();
|
|
||||||
},
|
|
||||||
primaryColor === -1 ? parseInt(colors[0]?.slice(1), 16) || 0 : primaryColor,
|
|
||||||
colors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<BuilderButton
|
|
||||||
label="Accent"
|
|
||||||
{...accentColor !== -1 ? (c => ({
|
|
||||||
tooltip: c,
|
|
||||||
selectedStyle: { background: c }
|
|
||||||
}))("#" + accentColor.toString(16).padStart(6, "0")) : {}}
|
|
||||||
onClick={() => {
|
|
||||||
getSuggestedColors(colors => {
|
|
||||||
closeModal(currModal.current);
|
|
||||||
currModal.current = openColorPickerModal(
|
|
||||||
ColorPicker,
|
|
||||||
c => {
|
|
||||||
setAccentColor(c);
|
|
||||||
if (preview) updatePreview();
|
|
||||||
},
|
|
||||||
accentColor === -1 ? parseInt(colors[1]?.slice(1), 16) || 0 : accentColor,
|
|
||||||
colors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<BuilderButton
|
|
||||||
label="Effect"
|
|
||||||
{...effect && {
|
|
||||||
tooltip: effect.title,
|
|
||||||
selectedStyle: {
|
|
||||||
background: `top / cover url(${effect.thumbnailPreviewSrc}), top / cover url(/assets/f328a6f8209d4f1f5022.png)`
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
fetchProfileEffects(effects => {
|
|
||||||
if (effects) {
|
|
||||||
closeModal(currModal.current);
|
|
||||||
currModal.current = openProfileEffectModal(
|
|
||||||
e => {
|
|
||||||
setEffect(e);
|
|
||||||
if (preview) updatePreview();
|
|
||||||
},
|
|
||||||
effects,
|
|
||||||
profileEffectModalClassNames,
|
|
||||||
effect?.id
|
|
||||||
);
|
|
||||||
} else
|
|
||||||
showToast("The retrieved data did not match the expected format.", Toasts.Type.FAILURE);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
onClick={() => {
|
|
||||||
const strToCopy = buildFPTE(primaryColor, accentColor, effect?.id ?? "", buildLegacy);
|
|
||||||
if (strToCopy === "")
|
|
||||||
showToast("FPTE Builder is empty; nothing to copy!");
|
|
||||||
else
|
|
||||||
copyWithToast(strToCopy, "FPTE copied to clipboard!");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy FPTE
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
look={Button.Looks.LINK}
|
|
||||||
color={Button.Colors.PRIMARY}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
style={{ display: primaryColor === -1 && accentColor === -1 && !effect ? "none" : "revert" }}
|
|
||||||
onClick={() => {
|
|
||||||
setPrimaryColor(-1);
|
|
||||||
setAccentColor(-1);
|
|
||||||
setEffect(null);
|
|
||||||
if (preview) updatePreview();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CustomizationSection>
|
|
||||||
<Switch
|
|
||||||
value={preview}
|
|
||||||
onChange={value => {
|
|
||||||
setPreview(value);
|
|
||||||
updatePreview();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
FPTE Builder Preview
|
|
||||||
</Switch>
|
|
||||||
<Switch
|
|
||||||
value={buildLegacy}
|
|
||||||
note="Will use more characters"
|
|
||||||
onChange={value => setBuildLegacy(value)}
|
|
||||||
>
|
|
||||||
Build backwards compatible FPTE
|
|
||||||
</Switch>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,68 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { User } from "discord-types/general";
|
|
||||||
import type { ComponentType, PropsWithChildren, ReactNode } from "react";
|
|
||||||
|
|
||||||
export interface UserProfile extends User {
|
|
||||||
themeColors: [number, number] | undefined;
|
|
||||||
profileEffectId: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProfileEffect {
|
|
||||||
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: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CustomizationSection = ComponentType<PropsWithChildren<{
|
|
||||||
title?: ReactNode;
|
|
||||||
titleIcon?: ReactNode;
|
|
||||||
titleId?: string;
|
|
||||||
description?: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
errors?: string[];
|
|
||||||
disabled?: boolean;
|
|
||||||
hideDivider?: boolean;
|
|
||||||
showBorder?: boolean;
|
|
||||||
borderType?: "limited" | "premium";
|
|
||||||
hasBackground?: boolean;
|
|
||||||
forcedDivider?: boolean;
|
|
||||||
showPremiumIcon?: boolean;
|
|
||||||
}>>;
|
|
||||||
|
|
||||||
export type ColorPicker = ComponentType<{
|
|
||||||
value?: number | null;
|
|
||||||
onChange: (v: number) => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
suggestedColors?: string[];
|
|
||||||
middle?: ReactNode;
|
|
||||||
footer?: ReactNode;
|
|
||||||
showEyeDropper?: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type RGBColor = [number, number, number];
|
|
Loading…
Add table
Reference in a new issue