diff --git a/src/plugins/roleColorEverywhere/blendColors.ts b/src/plugins/roleColorEverywhere/blendColors.ts new file mode 100644 index 000000000..d5d5f25c6 --- /dev/null +++ b/src/plugins/roleColorEverywhere/blendColors.ts @@ -0,0 +1,54 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export function blendColors(color1hex: string, color2hex: string, percentage: number) { + // check input + color1hex = color1hex || "#000000"; + color2hex = color2hex || "#ffffff"; + percentage = percentage || 0.5; + + // 2: check to see if we need to convert 3 char hex to 6 char hex, else slice off hash + // the three character hex is just a representation of the 6 hex where each character is repeated + // ie: #060 => #006600 (green) + if (color1hex.length === 4) + color1hex = color1hex[1] + color1hex[1] + color1hex[2] + color1hex[2] + color1hex[3] + color1hex[3]; + else + color1hex = color1hex.substring(1); + if (color2hex.length === 4) + color2hex = color2hex[1] + color2hex[1] + color2hex[2] + color2hex[2] + color2hex[3] + color2hex[3]; + else + color2hex = color2hex.substring(1); + + // 3: we have valid input, convert colors to rgb + const color1rgb = [parseInt(color1hex[0] + color1hex[1], 16), parseInt(color1hex[2] + color1hex[3], 16), parseInt(color1hex[4] + color1hex[5], 16)]; + const color2rgb = [parseInt(color2hex[0] + color2hex[1], 16), parseInt(color2hex[2] + color2hex[3], 16), parseInt(color2hex[4] + color2hex[5], 16)]; + + // 4: blend + const color3rgb = [ + (1 - percentage) * color1rgb[0] + percentage * color2rgb[0], + (1 - percentage) * color1rgb[1] + percentage * color2rgb[1], + (1 - percentage) * color1rgb[2] + percentage * color2rgb[2] + ]; + + // 5: convert to hex + const color3hex = "#" + intToHex(color3rgb[0]) + intToHex(color3rgb[1]) + intToHex(color3rgb[2]); + + return color3hex; +} + +/* + convert a Number to a two character hex string + must round, or we will end up with more digits than expected (2) + note: can also result in single digit, which will need to be padded with a 0 to the left + @param: num => the number to conver to hex + @returns: string => the hex representation of the provided number +*/ +function intToHex(num: number) { + var hex = Math.round(num).toString(16); + if (hex.length === 1) + hex = "0" + hex; + return hex; +} diff --git a/src/plugins/roleColorEverywhere/components/RolesView.tsx b/src/plugins/roleColorEverywhere/components/RolesView.tsx new file mode 100644 index 000000000..281a4413d --- /dev/null +++ b/src/plugins/roleColorEverywhere/components/RolesView.tsx @@ -0,0 +1,106 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classes } from "@utils/misc"; +import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; +import { filters, findBulk, proxyLazyWebpack } from "@webpack"; +import { Text } from "@webpack/common"; +import { Role } from "discord-types/general"; + +const Classes = proxyLazyWebpack(() => + Object.assign({}, ...findBulk( + filters.byProps("roles", "rolePill", "rolePillBorder"), + filters.byProps("roleCircle", "dotBorderBase", "dotBorderColor"), + filters.byProps("roleNameOverflow", "root", "roleName", "roleRemoveButton", "roleRemoveButtonCanRemove", "roleRemoveIcon", "roleIcon") + )) +) as Record<"roles" | "rolePill" | "rolePillBorder" | "desaturateUserColors" | "flex" | "alignCenter" | "justifyCenter" | "svg" | "background" | "dot" | "dotBorderColor" | "roleCircle" | "dotBorderBase" | "flex" | "alignCenter" | "justifyCenter" | "wrap" | "root" | "role" | "roleRemoveButton" | "roleDot" | "roleFlowerStar" | "roleRemoveIcon" | "roleRemoveIconFocused" | "roleVerifiedIcon" | "roleName" | "roleNameOverflow" | "actionButton" | "overflowButton" | "addButton" | "addButtonIcon" | "overflowRolesPopout" | "overflowRolesPopoutArrowWrapper" | "overflowRolesPopoutArrow" | "popoutBottom" | "popoutTop" | "overflowRolesPopoutHeader" | "overflowRolesPopoutHeaderIcon" | "overflowRolesPopoutHeaderText" | "roleIcon" | "roleRemoveButtonCanRemove" | "roleRemoveIcon" | "roleIcon", string>; + +export function RoleCard({ onRoleRemove, data, border }: { onRoleRemove: (id: string) => void, data: Role, border: boolean }) { + const { role, roleRemoveButton, roleRemoveButtonCanRemove, roleRemoveIcon, roleIcon, roleNameOverflow, rolePill, rolePillBorder, roleCircle, roleName } = Classes; + + return ( +
+
onRoleRemove(data.id)} + > + + + + +
+ + + +
+ + {data.name} + +
+
+ ); +} + +export function RoleList({ roleData, onRoleRemove }: { onRoleRemove: (id: string) => void, roleData: Role[] }) { + const { root, roles } = Classes; + + return ( +
+ {!roleData?.length && ( + No roles + )} + + {roleData?.length !== 0 && ( +
+ {roleData.map(data => ( + + ))} +
+ )} +
+ ); +} + +export function RoleModalList({ roleList, header, onRoleRemove, modalProps }: { + roleList: Role[] + modalProps: ModalProps + header: string + onRoleRemove: (id: string) => void +}) { + return ( + + + {header} + + + + + + + ); +} diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index 7b811943d..208a563e6 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -17,13 +17,24 @@ */ import { definePluginSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import { getUserSettingLazy } from "@api/UserSettings"; import ErrorBoundary from "@components/ErrorBoundary"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; +import { getCurrentGuild } from "@utils/discord"; +import { ModalProps, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; import { findByCodeLazy } from "@webpack"; -import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common"; +import { ChannelStore, GuildMemberStore, GuildStore, Menu, React } from "@webpack/common"; +import { Guild } from "discord-types/general"; + +import { blendColors } from "./blendColors"; +import { RoleModalList } from "./components/RolesView"; + +const cl = classNameFactory("rolecolor"); +const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!; const useMessageAuthor = findByCodeLazy('"Result cannot be null because the message is not null"'); @@ -70,11 +81,79 @@ const settings = definePluginSettings({ markers: makeRange(0, 100, 10), default: 30 } -}); +}).withPrivateSettings<{ + userColorFromRoles: Record +}>(); + +function atLeastOneOverrideAppliesToGuild(overrides: string[], guildId: string) { + for (const role of overrides) { + if (GuildStore.getRole(guildId, role)) { + return true; + } + } + + return false; +} + +function getPrimaryRoleOverrideColor(roles: string[], guildId: string) { + const overrides = settings.store.userColorFromRoles[guildId]; + if (!overrides?.length) return null; + + if (atLeastOneOverrideAppliesToGuild(overrides, guildId!)) { + const memberRoles = roles.map(role => GuildStore.getRole(guildId!, role)).filter(e => e); + const blendColorsFromRoles = memberRoles + .filter(role => overrides.includes(role.id)) + .sort((a, b) => b.color - a.color); + + // if only one override apply, return the first role color + if (blendColorsFromRoles.length < 2) + return blendColorsFromRoles[0]?.colorString ?? null; + + const color = blendColorsFromRoles + .slice(1) + .reduce( + (p, c) => blendColors(p, c!.colorString!, .5), + blendColorsFromRoles[0].colorString! + ); + + return color; + } + + return null; +} + +// Using plain replaces cause i dont want sanitize regexp +function toggleRole(guildId: string, id: string) { + let roles = settings.store.userColorFromRoles[guildId]; + const len = roles.length; + + roles = roles.filter(e => e !== id); + + if (len === roles.length) { + roles.push(id); + } + + settings.store.userColorFromRoles[guildId] = roles; +} + +function RoleModal({ modalProps, guild }: { modalProps: ModalProps, guild: Guild }) { + const [ids, setIds] = React.useState(settings.store.userColorFromRoles[guild.id]); + const roles = React.useMemo(() => ids.map(id => GuildStore.getRole(guild.id, id)), [ids]); + + return { + toggleRole(guild.id, id); + setIds(settings.store.userColorFromRoles[guild.id]); + }} + />; +} export default definePlugin({ name: "RoleColorEverywhere", - authors: [Devs.KingFish, Devs.lewisakura, Devs.AutumnVN, Devs.Kyuuhachi, Devs.jamesbt365], + authors: [Devs.KingFish, Devs.lewisakura, Devs.AutumnVN, Devs.Kyuuhachi, Devs.jamesbt365, Devs.EnergoStalin], description: "Adds the top role color anywhere possible", settings, @@ -166,8 +245,9 @@ export default definePlugin({ try { const guildId = ChannelStore.getChannel(channelOrGuildId)?.guild_id ?? GuildStore.getGuild(channelOrGuildId)?.id; if (guildId == null) return null; + const member = GuildMemberStore.getMember(guildId, userId); - return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null; + return getPrimaryRoleOverrideColor(member.roles, channelOrGuildId) ?? member.colorString; } catch (e) { new Logger("RoleColorEverywhere").error("Failed to get color string", e); } @@ -175,6 +255,12 @@ export default definePlugin({ return null; }, + start() { + DeveloperMode.updateSetting(true); + + settings.store.userColorFromRoles ??= {}; + }, + getColorInt(userId: string, channelOrGuildId: string) { const colorString = this.getColorString(userId, channelOrGuildId); return colorString && parseInt(colorString.slice(1), 16); @@ -221,5 +307,54 @@ export default definePlugin({ {title ?? label} — {count} ); - }, { noop: true }) + }, { noop: true }), + + getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) { + return { + style: { + color: this.getColor(userId, { guildId }) + } + }; + }, + + contextMenus: { + "dev-context"(children, { id }: { id: string; }) { + const guild = getCurrentGuild(); + if (!guild) return; + + settings.store.userColorFromRoles[guild.id] ??= []; + + const role = GuildStore.getRole(guild.id, id); + if (!role) return; + + const togglelabel = (settings.store.userColorFromRoles[guild.id]?.includes(role.id) ? + "Remove role from" : + "Add role to") + " coloring list"; + + if (role.colorString) { + children.push( + + toggleRole(guild.id, role.id)} + /> + openModal(modalProps => ( + + ))} + /> + + ); + } + } + } }); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e75825912..79c88980c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -579,6 +579,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "jamesbt365", id: 158567567487795200n, }, + EnergoStalin: { + name: "EnergoStalin", + id: 283262118902497281n + } } satisfies Record); // iife so #__PURE__ works correctly