Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(roleColorEverywhere): color blending from multiple roles #3034

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/plugins/roleColorEverywhere/blendColors.ts
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 28 additions & 0 deletions src/plugins/roleColorEverywhere/components/RolesModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { ModalProps } from "@utils/modal";
import { GuildStore, React } from "@webpack/common";
import { Guild } from "discord-types/general";

import { toggleRole } from "../storeHelper";
import { RoleModalList } from "./RolesView";

export function RoleModal({ modalProps, guild, colorsStore }: { modalProps: ModalProps, guild: Guild, colorsStore: Record<string, string[]> }) {
const [ids, setIds] = React.useState(colorsStore[guild.id]);
const roles = React.useMemo(() => ids.map(id => GuildStore.getRole(guild.id, id)), [ids]);

return <RoleModalList
modalProps={modalProps}
roleList={roles}
header={`${guild.name} highlighted roles.`}
onRoleRemove={id => {
toggleRole(colorsStore, guild.id, id);
setIds(colorsStore[guild.id]);
}}
/>;
}

106 changes: 106 additions & 0 deletions src/plugins/roleColorEverywhere/components/RolesView.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classes(role, rolePill, border ? rolePillBorder : null)}>
<div
className={classes(roleRemoveButton, roleRemoveButtonCanRemove)}
onClick={() => onRoleRemove(data.id)}
>
<span
className={roleCircle}
style={{ backgroundColor: data.colorString }}
/>
<svg
role="img"
className={roleRemoveIcon}
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="var(--primary-630)" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z"/>
</svg>
</div>
<span>
<img alt="" className={roleIcon} height="16" src={`https://cdn.discordapp.com/role-icons/${data.id}/${data.icon}.webp?size=16&quality=lossless`}/>
</span>
<div className={roleName}>
<Text
className={roleNameOverflow}
variant="text-xs/medium"
>
{data.name}
</Text>
</div>
</div>
);
}

export function RoleList({ roleData, onRoleRemove }: { onRoleRemove: (id: string) => void, roleData: Role[] }) {
const { root, roles } = Classes;

return (
<div>
{!roleData?.length && (
<span>No roles</span>
)}

{roleData?.length !== 0 && (
<div className={classes(root, roles)}>
{roleData.map(data => (
<RoleCard
data={data}
onRoleRemove={onRoleRemove}
border={false}
/>
))}
</div>
)}
</div>
);
}

export function RoleModalList({ roleList, header, onRoleRemove, modalProps }: {
roleList: Role[]
modalProps: ModalProps
header: string
onRoleRemove: (id: string) => void
}) {
return (
<ModalRoot
{...modalProps}
size={ModalSize.SMALL}
>
<ModalHeader>
<Text className="vc-role-list-title" variant="heading-lg/semibold">{header}</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent>
<RoleList
roleData={roleList}
onRoleRemove={onRoleRemove}
/>
</ModalContent>
</ModalRoot >
);
}
80 changes: 75 additions & 5 deletions src/plugins/roleColorEverywhere/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { 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 { RoleModal } from "./components/RolesModal";
import { toggleRole } from "./storeHelper";
import { brewUserColor } from "./witchCauldron";

const cl = classNameFactory("rolecolor");
const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!;

const useMessageAuthor = findByCodeLazy('"Result cannot be null because the message is not null"');

Expand Down Expand Up @@ -70,11 +81,13 @@ const settings = definePluginSettings({
markers: makeRange(0, 100, 10),
default: 30
}
});
}).withPrivateSettings<{
userColorFromRoles: Record<string, string[]>
}>();

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,

Expand Down Expand Up @@ -166,15 +179,22 @@ 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 brewUserColor(settings.store.userColorFromRoles, member.roles, channelOrGuildId) ?? member.colorString;
} catch (e) {
new Logger("RoleColorEverywhere").error("Failed to get color string", e);
}

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);
Expand Down Expand Up @@ -221,5 +241,55 @@ export default definePlugin({
{title ?? label} &mdash; {count}
</span>
);
}, { 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(
<Menu.MenuItem
id={cl("context-menu")}
label="Coloring"
>
<Menu.MenuItem
id={cl("toggle-role-for-guild")}
label={togglelabel}
action={() => toggleRole(settings.store.userColorFromRoles, guild.id, role.id)}
/>
<Menu.MenuItem
id={cl("show-color-roles")}
label="Show roles"
action={() => openModal(modalProps => (
<RoleModal
modalProps={modalProps}
guild={guild}
colorsStore={settings.store.userColorFromRoles}
/>
))}
/>
</Menu.MenuItem>
);
}
}
}
});
31 changes: 31 additions & 0 deletions src/plugins/roleColorEverywhere/storeHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { GuildStore } from "@webpack/common";

// Using plain replaces cause i dont want sanitize regexp
export function toggleRole(colorsStore: ColorsStore, guildId: string, id: string) {
let roles = colorsStore[guildId];
const len = roles.length;

roles = roles.filter(e => e !== id);

if (len === roles.length) {
roles.push(id);
}

colorsStore[guildId] = roles;
}

export function atLeastOneOverrideAppliesToGuild(overrides: string[], guildId: string) {
for (const role of overrides) {
if (GuildStore.getRole(guildId, role)) {
return true;
}
}

return false;
}
7 changes: 7 additions & 0 deletions src/plugins/roleColorEverywhere/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

type ColorsStore = Record<string, string[]>;
37 changes: 37 additions & 0 deletions src/plugins/roleColorEverywhere/witchCauldron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { GuildStore } from "@webpack/common";

import { blendColors } from "./blendColors";
import { atLeastOneOverrideAppliesToGuild } from "./storeHelper";

export function brewUserColor(colorsStore: ColorsStore, roles: string[], guildId: string) {
const overrides = colorsStore[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;
}
Loading