1
0
Fork 1
mirror of https://github.com/Vendicated/Vencord.git synced 2025-01-09 17:36:23 +00:00

feat(roleColorEverywhere): improovements done by EnergoStalin

This commit is contained in:
EnergoStalin 2024-06-19 11:14:30 +03:00
parent 0fd76ab15a
commit a23376eed6
4 changed files with 304 additions and 5 deletions

View file

@ -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;
}

View file

@ -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 >
);
}

View file

@ -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<string, string[]>
}>();
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 <RoleModalList
modalProps={modalProps}
roleList={roles}
header={`${guild.name} highlighted roles.`}
onRoleRemove={id => {
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} &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(guild.id, role.id)}
/>
<Menu.MenuItem
id={cl("show-color-roles")}
label="Show roles"
action={() => openModal(modalProps => (
<RoleModal
modalProps={modalProps}
guild={guild}
/>
))}
/>
</Menu.MenuItem>
);
}
}
}
});

View file

@ -579,6 +579,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "jamesbt365",
id: 158567567487795200n,
},
EnergoStalin: {
name: "EnergoStalin",
id: 283262118902497281n
}
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly