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:
parent
0fd76ab15a
commit
a23376eed6
4 changed files with 304 additions and 5 deletions
54
src/plugins/roleColorEverywhere/blendColors.ts
Normal file
54
src/plugins/roleColorEverywhere/blendColors.ts
Normal 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;
|
||||
}
|
106
src/plugins/roleColorEverywhere/components/RolesView.tsx
Normal file
106
src/plugins/roleColorEverywhere/components/RolesView.tsx
Normal 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 >
|
||||
);
|
||||
}
|
|
@ -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} — {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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue