128 lines
5.6 KiB
TypeScript
128 lines
5.6 KiB
TypeScript
import { logger } from "@vendetta";
|
|
import { findByName, findByStoreName } from "@vendetta/metro";
|
|
import { after } from "@vendetta/patcher";
|
|
import { findInReactTree } from "@vendetta/utils";
|
|
import { clipboard } from "@vendetta/metro/common";
|
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
|
import { showToast } from "@vendetta/ui/toasts";
|
|
import { applyPatches } from "@shared/lib/patcher";
|
|
import { registerTypeDetector, registerIntercept, patchCreateElement } from "@shared/lib/createElementIntercept";
|
|
|
|
// A gradient role's real colors live at role.colorStrings ({primaryColor, secondaryColor,
|
|
// tertiaryColor}), not the flattened colorString - copying that exact JSON back into Discord's own
|
|
// role color picker recreates the gradient. A solid-color role only has primaryColor set, so this
|
|
// falls back to the plain hex for those, keeping the existing single-color behavior unchanged.
|
|
function formatReadable(colorStrings: any): string {
|
|
const lines = [`Primary: ${colorStrings.primaryColor}`, `Secondary: ${colorStrings.secondaryColor}`];
|
|
if (colorStrings.tertiaryColor) lines.push(`Tertiary: ${colorStrings.tertiaryColor}`);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
// Clipboard only holds one value, so a gradient can't be copied both ways at once - instead, a
|
|
// second long-press on the same role within this window swaps the JSON for the readable text.
|
|
const GRADIENT_TOGGLE_WINDOW_MS = 3000;
|
|
let lastGradientCopy: { key: string; time: number } | null = null;
|
|
|
|
function copyRoleColor(colorStrings: any, fallbackColor?: string | null) {
|
|
const isGradient = !!(colorStrings?.primaryColor && colorStrings?.secondaryColor);
|
|
if (!isGradient) {
|
|
const value = colorStrings?.primaryColor ?? fallbackColor;
|
|
if (!value) return;
|
|
clipboard.setString(value);
|
|
showToast("Copied role color to clipboard", getAssetIDByName("ic_message_copy"));
|
|
lastGradientCopy = null;
|
|
return;
|
|
}
|
|
|
|
const key = JSON.stringify(colorStrings);
|
|
const now = Date.now();
|
|
if (lastGradientCopy?.key === key && now - lastGradientCopy.time < GRADIENT_TOGGLE_WINDOW_MS) {
|
|
clipboard.setString(formatReadable(colorStrings));
|
|
showToast("Copied readable colors to clipboard", getAssetIDByName("ic_message_copy"));
|
|
lastGradientCopy = null;
|
|
} else {
|
|
clipboard.setString(key);
|
|
showToast("Copied gradient JSON - press again for readable text", getAssetIDByName("ic_message_copy"));
|
|
lastGradientCopy = { key, time: now };
|
|
}
|
|
}
|
|
|
|
function patchRolePill(): () => void {
|
|
// "RolePill" on current builds, "ThemedRolePill" on older ones.
|
|
const RolePillComponent = findByName("RolePill", false) ?? findByName("ThemedRolePill", false);
|
|
if (!RolePillComponent) return () => {};
|
|
|
|
const GuildRoleStore = findByStoreName("GuildRoleStore");
|
|
|
|
return after("default", RolePillComponent, (_args: any[], res: any) => {
|
|
try {
|
|
if (!res?.props?.onPress) return;
|
|
|
|
const verifiedIcon = findInReactTree(res, (m) => m?.props?.roleColor);
|
|
const roleIcon = findInReactTree(
|
|
res,
|
|
(m) => m?.props?.style?.[0]?.borderRadius && typeof m?.props?.style?.[1]?.backgroundColor === "string" && m.props.style[1].backgroundColor.startsWith("#"),
|
|
);
|
|
const color = roleIcon?.props?.style?.[1]?.backgroundColor ?? verifiedIcon?.props?.roleColor;
|
|
if (!color) return;
|
|
|
|
// The pill only gives us a flattened color from its rendered style - fetching the real
|
|
// role record (if guildId/roleId are present on it) is what actually exposes the gradient.
|
|
const guildId = res.props?.guildId;
|
|
const roleId = res.props?.roleId ?? res.props?.role?.id;
|
|
const role = guildId && roleId ? GuildRoleStore?.getRole?.(guildId, roleId) : null;
|
|
|
|
res.props.onLongPress = () => copyRoleColor(role?.colorStrings, color);
|
|
} catch {
|
|
// Leave the role pill without a long-press handler.
|
|
}
|
|
});
|
|
}
|
|
|
|
// Covers role pills in a full profile's Roles section (UserProfileRolesCard -> RolesList ->
|
|
// RoleItem, none of which are top-level exports, hence createElementIntercept).
|
|
function patchProfileRoleItem(): () => void {
|
|
const cleanups: (() => void)[] = [];
|
|
patchCreateElement(cleanups);
|
|
|
|
registerTypeDetector(
|
|
"copy-role-color-role-item",
|
|
(type) => typeof type === "function" && type.name === "RoleItem",
|
|
(RoleItem: any) => {
|
|
const PatchedRoleItem = (props: any) => {
|
|
const ret = RoleItem(props);
|
|
|
|
try {
|
|
const colorStrings = props?.role?.colorStrings;
|
|
const fallback = props?.role?.colorString;
|
|
if ((colorStrings?.primaryColor || fallback) && ret && typeof ret === "object") {
|
|
ret.props = {
|
|
...ret.props,
|
|
onLongPress: () => copyRoleColor(colorStrings, fallback)
|
|
};
|
|
}
|
|
} catch {
|
|
// Leave the role item without a long-press handler.
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
registerIntercept(RoleItem, PatchedRoleItem);
|
|
}
|
|
);
|
|
|
|
return () => cleanups.forEach((fn) => fn());
|
|
}
|
|
|
|
let unpatchAll: () => void = () => {};
|
|
|
|
export default {
|
|
onLoad: () => {
|
|
unpatchAll = applyPatches("CopyRoleColor", logger, {
|
|
"role pill long-press": patchRolePill,
|
|
"profile role item long-press": patchProfileRoleItem
|
|
});
|
|
},
|
|
onUnload: () => unpatchAll()
|
|
};
|