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(
+