This commit is contained in:
thororen1234 2024-06-01 15:34:40 -04:00
parent 93a96bc120
commit d18e1b2415
43 changed files with 6 additions and 9034 deletions

View file

@ -1,5 +0,0 @@
.vc-allowed-mentions-popout-menu {
height: 250px;
width: 200px;
text-overflow: ellipsis;
}

View file

@ -1,382 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./AllowedMentions.css";
import { Flex } from "@components/Flex";
import { isNonNullish } from "@utils/guards";
import { useForceUpdater } from "@utils/react";
import { findByPropsLazy } from "@webpack";
import { Clickable, Forms, GuildMemberStore, GuildStore, Menu, Popout as DiscordPopout, RelationshipStore, TextInput, useEffect, UserStore, useState } from "@webpack/common";
import { Channel } from "discord-types/general";
import { CSSProperties, ReactNode } from "react";
export type AllowedMentionsParsables = "everyone" | "users" | "roles";
export interface AllowedMentions {
parse: Set<AllowedMentionsParsables>;
users?: Set<string>;
roles?: Set<string>;
meta: {
hasEveryone: boolean;
userIds: Set<string>;
roleIds: Set<string>;
tooManyUsers: boolean;
tooManyRoles: boolean;
};
}
export interface AllowedMentionsProps {
mentions: AllowedMentions,
channel: Channel;
trailingSeparator?: boolean;
}
const replyClasses = findByPropsLazy("replyBar", "replyLabel", "separator");
export const AllowedMentionsStore = {
store: new Map<string, AllowedMentions>(),
callbacks: new Map<string, (mentions: AllowedMentions) => void>,
get(channelId: string) {
return this.store.get(channelId);
},
set(channelId: string, mentions: AllowedMentions, dispatch: boolean) {
this.store.set(channelId, mentions);
dispatch && this.callbacks.get(channelId)?.(mentions);
},
delete(channelId: string) {
return this.store.delete(channelId);
},
clear() {
return this.store.clear();
},
subscribe(channelId: string, callback: (mentions: AllowedMentions) => void) {
return this.callbacks.set(channelId, callback);
},
unsubscribe(channelId: string) {
return this.callbacks.delete(channelId);
}
};
function getDisplayableUserNameParts(userId: string, guildId: string | null) {
// @ts-ignore discord-types doesn't have globalName
const { globalName, username } = UserStore.getUser(userId) ?? {};
const nickname = guildId ? GuildMemberStore.getNick(guildId, userId) : RelationshipStore.getNickname(userId);
return [nickname, globalName as string, username];
}
function getDisplayableUserName(userId: string, guildId: string | null) {
const [nickname, globalName, username] = getDisplayableUserNameParts(userId, guildId);
// Displayed name priority
// Guild/Friend Nickname > Global Name > Username
// User id if not cached in any stores
return nickname ?? globalName ?? username ?? userId;
}
function getDisplayableRoleName(roleId: string, guildId: string | null) {
// You *can* mention roles in DMs but is it really worth adding the UI for
// it in DMs
const role = guildId ? Object.values(GuildStore.getGuild(guildId).roles).find(r => r.id === roleId)?.name : undefined;
// Role id if not cached or not from current guild
return role ?? roleId;
}
function fuzzySearch(searchQuery: string, searchString: string) {
let searchIndex = 0;
let score = 0;
for (let i = 0; i < searchString.length; i++) {
if (searchString[i] === searchQuery[searchIndex]) {
score++;
searchIndex++;
} else {
score--;
}
if (searchIndex === searchQuery.length) {
return score;
}
}
return null;
}
function AtIcon({ width, height }: { width: number, height: number; }) {
return <svg width={width} height={height} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 2C6.486 2 2 6.486 2 12C2 17.515 6.486 22 12 22C14.039 22 15.993
21.398 17.652 20.259L16.521 18.611C15.195 19.519 13.633 20 12 20C7.589
20 4 16.411 4 12C4 7.589 7.589 4 12 4C16.411 4 20 7.589 20 12V12.782C20
14.17 19.402 15 18.4 15L18.398 15.018C18.338 15.005 18.273 15 18.209
15H18C17.437 15 16.6 14.182 16.6 13.631V12C16.6 9.464 14.537 7.4 12
7.4C9.463 7.4 7.4 9.463 7.4 12C7.4 14.537 9.463 16.6 12 16.6C13.234 16.6
14.35 16.106 15.177 15.313C15.826 16.269 16.93 17 18 17L18.002
16.981C18.064 16.994 18.129 17 18.195 17H18.4C20.552 17 22 15.306 22
12.782V12C22 6.486 17.514 2 12 2ZM12 14.599C10.566 14.599 9.4 13.433 9.4
11.999C9.4 10.565 10.566 9.399 12 9.399C13.434 9.399 14.6 10.565 14.6
11.999C14.6 13.433 13.434 14.599 12 14.599Z"
/>
</svg>;
}
function Title({ children, pointer, style }: { children: ReactNode; pointer?: boolean; style?: CSSProperties; }) {
return <Forms.FormTitle style={{ margin: 0, cursor: pointer ? "pointer" : "default", ...style }}>{children}</Forms.FormTitle>;
}
function TitleSwitch({ state, setState, children }: { state: boolean, setState: (value: boolean) => void; children: ReactNode; }) {
return <Clickable onClick={() => setState(!state)}>
<Title
style={{ ...(state ? { color: "var(--text-link)" } : {}), display: "flex", gap: "0.2rem", userSelect: "none" }}
pointer
>
{children}
</Title>
</Clickable>;
}
function Separator() {
return <div className={replyClasses.separator}></div>;
}
function Popout({
title,
shouldShow,
setShouldShow,
update,
fuzzy,
ids,
rawIds,
guildId,
getDisplayableName,
all,
setAll,
}: {
title: string,
shouldShow: boolean,
setShouldShow: (value: boolean) => void,
update: () => void,
fuzzy: (search: string, id: string) => number | null,
ids: Set<string>,
rawIds: Set<string>,
guildId: string | null,
getDisplayableName: (userId: string, guildId: string | null) => string | undefined,
all: boolean,
setAll: (value: boolean) => void,
}) {
const [search, setSearch] = useState(undefined as undefined | string);
return <DiscordPopout
animation={DiscordPopout.Animation.SCALE}
align="center"
position="top"
shouldShow={shouldShow}
onRequestClose={() => setShouldShow(false)}
renderPopout={() => {
return <Menu.Menu
navId={`vc-allowed-mentions-${title}-popout`}
onClose={() => setShouldShow(false)}
className="vc-allowed-mentions-popout-menu"
>
<Menu.MenuCheckboxItem
id={`vc-allowed-mentions-${title}-popout-all`}
label="All"
checked={all}
action={() => {
// If all are selected, deselect them,
// otherwise select the remaining ones.
if (ids.size === rawIds.size) {
ids.clear();
setAll(false);
update();
} else {
rawIds.forEach(id => ids.add(id));
setAll(true);
update();
}
}}
/>
<Menu.MenuSeparator />
<Menu.MenuItem
label="Search"
id={`vc-allowed-mentions-${title}-popout-search`}
render={() => {
return <TextInput
placeholder={`Search ${title.toLowerCase()}`}
type="text"
maxLength={32}
role="combobox"
value={search}
onChange={value => setSearch(value.trim())}
style={{ margin: "2px 0", padding: "6px 8px" }}
onKeyDown={e => {
if (e.key === "Escape") {
setSearch(undefined);
} else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
// Some random event listener is blocking
// left & right arrow keys so you can't
// navigate the text with arrow keys unless
// you do e.stopPropogation
e.stopPropagation();
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
// Pressing up/down arrow keys leads to a messy
// UI state, blurring the text input fixes it.
// (kinda)
e.currentTarget.blur();
}
}}
/>;
}}
/>
{(isNonNullish(search) ?
Array.from(rawIds).map(id => ({
score: fuzzy(search, id),
name: getDisplayableName(id, guildId),
id: id
})
).filter(o => isNonNullish(o.score))
.sort((a, b) => b.score! - a.score!)
: Array.from(rawIds).map(id => ({
score: 0,
name: getDisplayableName(id, guildId),
id: id
}))
)
.map(object => {
return <Menu.MenuCheckboxItem
id={`vc-allowed-mentions-${title}-popout-${object.id}`}
label={object.name!}
checked={all || ids.has(object.id)}
action={() => {
all || ids.has(object.id) ? ids.delete(object.id) : ids.add(object.id);
setAll(ids.size === rawIds.size);
update();
}}
/>;
})}
</Menu.Menu>;
}}
>
{
(_, { isShown }) => {
return <Clickable onClick={() => setShouldShow(!isShown)}>
<Title pointer>{title}</Title>
</Clickable>;
}
}
</DiscordPopout>;
}
export function AllowedMentionsBar({ mentions, channel, trailingSeparator }: AllowedMentionsProps) {
const [users, setUsers] = useState(new Set(mentions.users));
const [roles, setRoles] = useState(new Set(mentions.roles));
const [everyone, setEveryone] = useState(mentions.parse.has("everyone"));
const [allUsers, setAllUsers] = useState(users.size === mentions.meta.userIds.size);
const [allRoles, setAllRoles] = useState(roles.size === mentions.meta.roleIds.size);
useEffect(() => {
AllowedMentionsStore.subscribe(
channel.id,
mentions => {
allUsers && mentions.users && setUsers(new Set(mentions.users));
allRoles && mentions.roles && setRoles(new Set(mentions.roles));
}
);
return () => { AllowedMentionsStore.unsubscribe(channel.id); };
});
useEffect(() => {
AllowedMentionsStore.set(
channel.id,
{
parse: new Set(
[
everyone && "everyone",
allUsers && "users",
allRoles && "roles"
].filter(v => v) as AllowedMentionsParsables[]
),
users: allUsers || users.size === 0 ? undefined : users,
roles: allRoles || roles.size === 0 ? undefined : roles,
meta: {
...mentions.meta,
tooManyUsers: users.size > 100,
tooManyRoles: roles.size > 100,
}
},
false
);
}, [
mentions,
everyone,
allUsers,
allRoles,
users,
roles,
]);
const [shouldShowUsersPopout, setShouldShowUsersPopout] = useState(false);
const [shouldShowRolesPopout, setShouldShowRolesPopout] = useState(false);
const update = useForceUpdater();
const displayEveryone = mentions.meta.hasEveryone;
const displayUserIds = mentions.meta.userIds.size > 0;
const displayRoleIds = mentions.meta.roleIds.size > 0;
return <Flex style={{ gap: "1rem", alignItems: "center" }}>
{displayEveryone && <>
<TitleSwitch state={everyone} setState={setEveryone}>
<AtIcon width={16} height={16} />
everyone /
<AtIcon width={16} height={16} />
here
</TitleSwitch>
</>}
{displayUserIds && <>
{displayEveryone && <Separator />}
<Popout
title="Users"
shouldShow={shouldShowUsersPopout}
setShouldShow={setShouldShowUsersPopout}
update={update}
fuzzy={(search, userId) => {
const samples = getDisplayableUserNameParts(userId, channel.guild_id)
.filter(isNonNullish)
.map(name => fuzzySearch(search, name))
.filter(isNonNullish) as number[];
return samples.length > 0 ? Math.max(...samples) : null;
}}
ids={users}
rawIds={mentions.meta.userIds}
guildId={channel.guild_id}
getDisplayableName={getDisplayableUserName}
all={allUsers}
setAll={setAllUsers}
/>
</>}
{displayRoleIds && <>
{(displayEveryone || displayUserIds) && <Separator />}
<Popout
title="Roles"
shouldShow={shouldShowRolesPopout}
setShouldShow={setShouldShowRolesPopout}
update={update}
fuzzy={(search, roleId) => fuzzySearch(search, getDisplayableRoleName(roleId, channel.guild_id).toLowerCase())}
ids={roles}
rawIds={mentions.meta.roleIds}
guildId={channel.guild_id}
getDisplayableName={getDisplayableRoleName}
all={allRoles}
setAll={setAllRoles}
/>
</>}
{trailingSeparator && (displayEveryone || displayUserIds || displayRoleIds) && <Separator />}
</Flex>;
}

View file

@ -1,274 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, MessageExtra, removePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin, { OptionType } from "@utils/types";
import { Alerts, GuildStore, PermissionsBits, PermissionStore } from "@webpack/common";
import { Channel } from "discord-types/general";
import { AllowedMentions, AllowedMentionsBar, AllowedMentionsProps, AllowedMentionsStore as store } from "./AllowedMentions";
export default definePlugin({
name: "AllowedMentions",
authors: [Devs.arHSM, Devs.amia],
description: "Fine grained control over whom to ping when sending a message.",
dependencies: ["MessageEventsAPI"],
settings: definePluginSettings({
pingEveryone: {
type: OptionType.BOOLEAN,
description: "Mention everyone by default",
default: false,
},
pingAllUsers: {
type: OptionType.BOOLEAN,
description: "Mention all users by default",
default: true,
},
pingAllRoles: {
type: OptionType.BOOLEAN,
description: "Mention all roles by default",
default: true,
}
}),
patches: [
{
find: ".AnalyticEvents.APPLICATION_COMMAND_VALIDATION_FAILED,",
replacement: [
// Pass type prop to slate wrapper
{
match: /className:\i\(\i,\i.slateContainer\),children:\(0,\i.jsx\)\(\i.\i,{/,
replace: "$& type: arguments[0].type,"
}
]
},
{
find: '"chat input type must be set");',
replacement: [
// Set allowedMentions & populate attachments store
{
match: /"chat input type must be set"\);/,
replace: "$& $self.allowedSlateTypes.includes(arguments[0].type?.analyticsName) && $self.setAllowedMentions(arguments[0]);"
},
// Add the hasConnectedBar class when AllowedMentionsBar is visible
{
match: /.hasConnectedBar]:\i/,
replace: '$& || $self.getAllowedMentions(arguments[0].channel.id, arguments[0].type?.analyticsName === "edit"),'
},
// Pass mentions to attached bars component
// Would do a simple isEdit but the component is memo'd
{
match: /activeCommand:\i,pendingReply:\i/,
replace: '$&, mentions: $self.getAllowedMentions(arguments[0].channel.id, arguments[0].type?.analyticsName === "edit"),'
},
]
},
{
find: ".stackedAttachedBar]:!",
replacement: [
// Add AllowedMentionsBar when not replying
// Will never render if above patch fails
{
match: /(?<=pendingReply:\i}=(\i),.+?)null!=(\i)&&(\i).push\(\(0,\i.jsx\)\(\i.\i,{reply:\i,/,
replace: "null == $2 && null != $1.mentions && $3.push($self.AllowedMentionsBar({ mentions: $1.mentions, channel: $1.channel })), $&"
}
]
},
{
find: ".Messages.REPLYING_TO.format({",
replacement: [
// Add AllowedMentionsBar to reply bar when replying
{
match: /(?<="div",\{className:\i.actions,children:\[)(?=\i&&)/,
replace: "null != $self.getAllowedMentions(arguments[0].reply.channel.id, false) && $self.AllowedMentionsBarInner({ mentions: $self.getAllowedMentions(arguments[0].reply.channel.id, false), channel: arguments[0].reply.channel, trailingSeparator: true }),",
}
]
},
{
find: ".Messages.EVERYONE_POPOUT_BODY",
replacement: [
// Remove the warning popout for large server when @everyone mention is off
{
match: /(?<=shouldShowEveryoneGuard\(\i,(\i)\))/,
replace: "|| $self.skipEveryoneContentWarningPopout($1.id)"
}
]
},
{
find: '"?use_nested_fields=true"',
replacement: [
// Patch sending allowed_mentions for forum creation
{
match: /(?<=.Endpoints.CHANNEL_THREADS\((\i.id)\)\+"\?use_nested_fields=true".+?message:\{)/,
replace: "allowed_mentions: $self.patchForumAllowedMentions($1),"
}
]
},
{
find: ".ComponentActions.FOCUS_COMPOSER_TITLE,",
replacement: [
// Clear entry on cancelling new forum post
{
match: /.trackForumNewPostCleared\)\(\{guildId:\i.guild_id,channelId:(\i.id)\}\)/,
replace: "$&; $self.onForumCancel($1);"
},
// Fail creating forum if tooManyUsers or tooManyRoles
{
match: /applyChatRestrictions\)\(\{.+?channel:(\i)\}\);if\(!\i/,
replace: "$& && !$self.validateForum($1.id)"
}
]
}
],
allowedSlateTypes: ["normal", "sidebar", "thread_creation", "create_forum_post"],
getAllowedMentions(channelId: string, shouldDelete?: boolean) {
const mentions = store.get(channelId);
if (shouldDelete) { store.delete(channelId); }
return mentions;
},
setAllowedMentions({ richValue, channel: { id: channelId, guild_id: guildId } }: { richValue: any, channel: Channel; }) {
const previous = store.get(channelId);
const canMentionEveryone = isNonNullish(guildId) ? PermissionStore.can(PermissionsBits.MENTION_EVERYONE, GuildStore.getGuild(guildId)) as boolean : true;
const mentions: AllowedMentions = {
parse: new Set(),
users: previous?.users ?? new Set(),
roles: previous?.roles ?? new Set(),
meta: {
hasEveryone: false,
userIds: new Set(),
roleIds: new Set(),
tooManyUsers: false,
tooManyRoles: false,
}
};
if (!isNonNullish(richValue[0]?.children)) {
return undefined;
}
// Discord renders the slate wrapper twice
// 1. unparsed raw text
// 2. parsed text (we need this)
// We skip setting allowed mentions for unparsed text cause there can be potential unparsed mentions
if (richValue[0]?.children.length === 1 && typeof richValue[0]?.children[0].text === "string") {
// This is the case where the input is empty (no potential unparsed mentions)
if (richValue[0]?.children[0].text === "") { store.delete(channelId); }
return;
}
for (const node of richValue[0].children) {
switch (node.type) {
case "userMention":
mentions.meta.userIds.add(node.userId);
break;
case "roleMention":
mentions.meta.roleIds.add(node.roleId);
break;
case "textMention":
if (node.name === "@everyone" || node.name === "@here") {
mentions.meta.hasEveryone = canMentionEveryone;
if (canMentionEveryone && (previous?.parse.has?.("everyone") ?? this.settings.store.pingEveryone)) {
mentions.parse.add("everyone");
}
}
break;
}
}
if (this.settings.store.pingAllUsers) { mentions.users = mentions.meta.userIds; }
if (this.settings.store.pingAllRoles) { mentions.roles = mentions.meta.roleIds; }
if (
!mentions.meta.hasEveryone
&& mentions.meta.userIds.size === 0
&& mentions.meta.roleIds.size === 0
) {
store.delete(channelId);
} else {
store.set(channelId, mentions, true);
}
},
skipEveryoneContentWarningPopout(channelId: string) {
const mentions = store.get(channelId);
return isNonNullish(mentions) && !mentions.parse.has("everyone");
},
tooManyAlert(tooManyUsers: boolean, tooManyRoles: boolean) {
const type = [
tooManyUsers && "users",
tooManyRoles && "roles"
].filter(x => x).join(" and ");
Alerts.show({
title: "Uh oh!",
body: `You've selected too many individual ${type} to mention!\nYou may only select all or up to 100 items in each category.`
});
},
validateForum(channelId: string) {
const mentions = this.getAllowedMentions(channelId, true);
if (!isNonNullish(mentions)) return true;
if (mentions.meta.tooManyUsers || mentions.meta.tooManyRoles) {
this.tooManyAlert(mentions.meta.tooManyUsers, mentions.meta.tooManyRoles);
return false;
}
return true;
},
patchSendAllowedMentions(channelId: string, extra: MessageExtra) {
const mentions = this.getAllowedMentions(channelId, true);
if (!isNonNullish(mentions)) return;
if (mentions.meta.tooManyUsers || mentions.meta.tooManyRoles) {
this.tooManyAlert(mentions.meta.tooManyUsers, mentions.meta.tooManyRoles);
return { cancel: true };
}
extra.replyOptions.allowedMentions = {
parse: Array.from(mentions.parse),
users: mentions.users ? Array.from(mentions.users) : undefined,
roles: mentions.roles ? Array.from(mentions.roles) : undefined,
// Don't override this for send! Discord already has a UI for this
repliedUser: extra.replyOptions.allowedMentions?.repliedUser ?? false,
};
},
patchForumAllowedMentions(channelId: string) {
const mentions = this.getAllowedMentions(channelId, true);
if (!isNonNullish(mentions)) return;
return {
parse: Array.from(mentions.parse),
users: mentions.users ? Array.from(mentions.users) : undefined,
roles: mentions.roles ? Array.from(mentions.roles) : undefined,
};
},
onForumCancel(channelId: string) {
store.delete(channelId);
},
AllowedMentionsBar(props: AllowedMentionsProps) {
return <Flex style={{ padding: "0.45rem 1rem", lineHeight: "16px" }}>
{<this.AllowedMentionsBarInner {...props} />}
</Flex>;
},
AllowedMentionsBarInner(props: AllowedMentionsProps) {
return <AllowedMentionsBar {...props} />;
},
start() {
this.preSend = addPreSendListener((channelId, _, extra) => this.patchSendAllowedMentions(channelId, extra));
},
stop() {
removePreSendListener(this.preSend);
store.clear();
},
});

View file

@ -67,10 +67,10 @@ export default definePlugin({
settings,
patches: [
{
find: "default.Messages.COLLECTIBLES_SHOP})]})",
find: "default.Messages.COLLECTIBLES_SHOP})]}",
replacement: [{
match: "{className:en.title,children:er.default.Messages.COLLECTIBLES_SHOP}",
replace: "{className:en.title,children:[er.default.Messages.COLLECTIBLES_SHOP,$self.PreviewToggle()]}"
match: /(children:\i.default.Messages.COLLECTIBLES_SHOP)}/,
replace: "$&,$self.PreviewToggle()"
}]
}
],

View file

@ -1,62 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { ChannelStore, GuildMemberStore, useStateFromStores } from "@webpack/common";
export default definePlugin({
name: "DeadMembers",
description: "Shows when the sender of a message has left the guild",
authors: [Devs.Kyuuhachi],
patches: [
{
find: "UsernameDecorationTypes:function()",
replacement: {
match: /(\i)=\{className:\i.username,style:.*?onContextMenu:\i,children:.*?\};/,
replace: "$&$1.children=$self.wrapMessageAuthor(arguments[0],$1.children);"
}
},
{
find: "Messages.FORUM_POST_AUTHOR_A11Y_LABEL",
replacement: {
match: /(?<=\}=(\i),\{(user:\i,author:\i)\}=.{0,400}?\(\i\.Fragment,{children:)\i(?=}\),)/,
replace: "$self.wrapForumAuthor({...$1,$2},$&)"
}
},
],
wrapMessageAuthor({ message }, text) {
const channel = ChannelStore.getChannel(message.channel_id);
return message.webhookId
? text
: <DeadIndicator
channel={channel}
userId={message.author.id}
text={text}
/>;
},
wrapForumAuthor({ channel, user }, text) {
return !user
? text
: <DeadIndicator
channel={channel}
userId={user.id}
text={text}
/>;
},
});
function DeadIndicator({ channel, userId, text }) {
const isMember = useStateFromStores(
[GuildMemberStore],
() => GuildMemberStore.isMember(channel?.guild_id, userId),
);
return channel?.guild_id && !isMember ? <s className="c98-author-dead">{text}</s> : text;
}

View file

@ -1,74 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { findByProps } from "@webpack";
import { Button, Forms, Text, useState } from "@webpack/common";
import { getAutoPresets } from "../css";
export default function ({ modalProps, onChange, autoColorwayId = "" }: { modalProps: ModalProps, onChange: (autoPresetId: string) => void, autoColorwayId: string; }) {
const [autoId, setAutoId] = useState(autoColorwayId);
const { radioBar, item: radioBarItem, itemFilled: radioBarItemFilled, radioPositionLeft } = findByProps("radioBar");
return <ModalRoot {...modalProps}>
<ModalHeader>
<Text variant="heading-lg/semibold" tag="h1">
Auto Preset Settings
</Text>
</ModalHeader>
<ModalContent>
<div className="dc-info-card" style={{ marginTop: "1em" }}>
<strong>About the Auto Colorway</strong>
<span>The auto colorway allows you to use your system's accent color in combination with a selection of presets that will fully utilize it.</span>
</div>
<div style={{ marginBottom: "20px" }}>
<Forms.FormTitle>Presets:</Forms.FormTitle>
{Object.values(getAutoPresets()).map(autoPreset => {
return <div className={`${radioBarItem} ${radioBarItemFilled}`} aria-checked={autoId === autoPreset.id}>
<div
className={`${radioBar} ${radioPositionLeft}`}
style={{ padding: "10px" }}
onClick={() => {
setAutoId(autoPreset.id);
}}>
<svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor" />
{autoId === autoPreset.id && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />}
</svg>
<Text variant="eyebrow" tag="h5">{autoPreset.name}</Text>
</div>
</div>;
})}
</div>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND_NEW}
size={Button.Sizes.MEDIUM}
onClick={() => {
DataStore.set("activeAutoPreset", autoId);
onChange(autoId);
modalProps.onClose();
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => {
modalProps.onClose();
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}

View file

@ -1,91 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Flex } from "@components/Flex";
import { CopyIcon } from "@components/Icons";
import {
ModalProps,
ModalRoot,
} from "@utils/modal";
import {
Button,
Clipboard,
ScrollerThin,
TextInput,
Toasts,
useState,
} from "@webpack/common";
import { mainColors } from "../constants";
import { colorVariables } from "../css";
import { getHex } from "../utils";
export default function ({ modalProps }: { modalProps: ModalProps; }) {
const [ColorVars, setColorVars] = useState<string[]>(colorVariables);
const [collapsedSettings, setCollapsedSettings] = useState<boolean>(true);
let results: string[];
function searchToolboxItems(e: string) {
results = [];
colorVariables.find((colorVariable: string) => {
if (colorVariable.toLowerCase().includes(e.toLowerCase())) {
results.push(colorVariable);
}
});
setColorVars(results);
}
return <ModalRoot {...modalProps} className="colorwayColorpicker">
<Flex style={{ gap: "8px", marginBottom: "8px" }}>
<TextInput
className="colorwaysColorpicker-search"
placeholder="Search for a color:"
onChange={e => {
searchToolboxItems(e);
if (e) {
setCollapsedSettings(false);
} else {
setCollapsedSettings(true);
}
}}
/>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={() => setCollapsedSettings(!collapsedSettings)}
>
<svg width="32" height="24" viewBox="0 0 24 24" aria-hidden="true" role="img">
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M7 10L12 15 17 10" aria-hidden="true" />
</svg>
</Button>
</Flex>
<ScrollerThin style={{ color: "var(--text-normal)" }} orientation="vertical" className={collapsedSettings ? " colorwaysColorpicker-collapsed" : ""} paddingFix>
{ColorVars.map((colorVariable: string) => <div
id={`colorways-colorstealer-item_${colorVariable}`}
className="colorwaysCreator-settingItm colorwaysCreator-toolboxItm"
onClick={() => {
Clipboard.copy(getHex(getComputedStyle(document.body).getPropertyValue("--" + colorVariable)));
Toasts.show({ message: "Color " + colorVariable + " copied to clipboard", id: "toolbox-color-var-copied", type: 1 });
}} style={{ "--brand-experiment": `var(--${colorVariable})` } as React.CSSProperties}>
{`Copy ${colorVariable}`}
</div>)}
</ScrollerThin>
<Flex style={{ justifyContent: "space-between", marginTop: "8px" }} wrap="wrap" className={collapsedSettings ? "" : " colorwaysColorpicker-collapsed"}>
{mainColors.map(mainColor => <div
id={`colorways-toolbox_copy-${mainColor.name}`}
className="colorwayToolbox-listItem"
>
<CopyIcon onClick={() => {
Clipboard.copy(getHex(getComputedStyle(document.body).getPropertyValue(mainColor.var)));
Toasts.show({ message: `${mainColor.title} color copied to clipboard`, id: `toolbox-${mainColor.name}-color-copied`, type: 1 });
}} width={20} height={20} className="colorwayToolbox-listItemSVG" />
<span className="colorwaysToolbox-label">{`Copy ${mainColor.title} Color`}</span>
</div>
)}
</Flex>
</ModalRoot>;
}

View file

@ -1,70 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { findByProps } from "@webpack";
import { Button, Forms, ScrollerThin, Switch, Text, useState } from "@webpack/common";
import { getPreset } from "../css";
export default function ({ modalProps, onSettings, presetId, hasTintedText, hasDiscordSaturation }: { modalProps: ModalProps, presetId: string, hasTintedText: boolean, hasDiscordSaturation: boolean, onSettings: ({ presetId, tintedText, discordSaturation }: { presetId: string, tintedText: boolean, discordSaturation: boolean; }) => void; }) {
const [tintedText, setTintedText] = useState<boolean>(hasTintedText);
const [discordSaturation, setDiscordSaturation] = useState<boolean>(hasDiscordSaturation);
const [preset, setPreset] = useState<string>(presetId);
const { radioBar, item: radioBarItem, itemFilled: radioBarItemFilled, radioPositionLeft } = findByProps("radioBar");
return <ModalRoot {...modalProps} className="colorwaysPresetPicker">
<ModalHeader><Text variant="heading-lg/semibold" tag="h1">Creator Settings</Text></ModalHeader>
<ModalContent className="colorwaysPresetPicker-content">
<Forms.FormTitle>
Presets:
</Forms.FormTitle>
<ScrollerThin orientation="vertical" paddingFix style={{ paddingRight: "2px", marginBottom: "20px", maxHeight: "250px" }}>
{Object.values(getPreset()).map(pre => {
return <div className={`${radioBarItem} ${radioBarItemFilled}`} aria-checked={preset === pre.id}>
<div
className={`${radioBar} ${radioPositionLeft}`}
style={{ padding: "10px" }}
onClick={() => {
setPreset(pre.id);
}}>
<svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor" />
{preset === pre.id && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />}
</svg>
<Text variant="eyebrow" tag="h5">{pre.name}</Text>
</div>
</div>;
})}
</ScrollerThin>
<Switch value={tintedText} onChange={setTintedText}>Use colored text</Switch>
<Switch value={discordSaturation} onChange={setDiscordSaturation} hideBorder style={{ marginBottom: "0" }}>Use Discord's saturation</Switch>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND_NEW}
size={Button.Sizes.MEDIUM}
onClick={() => {
onSettings({ presetId: preset, discordSaturation: discordSaturation, tintedText: tintedText });
modalProps.onClose();
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => {
modalProps.onClose();
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}

View file

@ -1,64 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
import { openModal } from "@utils/modal";
import { FluxDispatcher, Text, Tooltip, useEffect, useState } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
import { getAutoPresets } from "../css";
import { ColorwayObject } from "../types";
import { PalleteIcon } from "./Icons";
import Selector from "./Selector";
export default function () {
const [activeColorway, setActiveColorway] = useState<string>("None");
const [visibility, setVisibility] = useState<boolean>(true);
const [isThin, setIsThin] = useState<boolean>(false);
const [autoPreset, setAutoPreset] = useState<string>("hueRotation");
useEffect(() => {
(async function () {
setVisibility(await DataStore.get("showColorwaysButton") as boolean);
setIsThin(await DataStore.get("useThinMenuButton") as boolean);
setAutoPreset(await DataStore.get("activeAutoPreset") as string);
})();
});
FluxDispatcher.subscribe("COLORWAYS_UPDATE_BUTTON_HEIGHT" as FluxEvents, ({ isTall }) => {
setIsThin(isTall);
});
FluxDispatcher.subscribe("COLORWAYS_UPDATE_BUTTON_VISIBILITY" as FluxEvents, ({ isVisible }) => {
setVisibility(isVisible);
});
return <Tooltip text={
<>
{!isThin ? <>
<span>Colorways</span>
<Text variant="text-xs/normal" style={{ color: "var(--text-muted)", fontWeight: 500 }}>{"Active Colorway: " + activeColorway}</Text>
</> : <span>{"Active Colorway: " + activeColorway}</span>}
{activeColorway === "Auto" ? <Text variant="text-xs/normal" style={{ color: "var(--text-muted)", fontWeight: 500 }}>{"Auto Preset: " + (getAutoPresets()[autoPreset].name || "None")}</Text> : <></>}
</>
} position="right" tooltipContentClassName="colorwaysBtn-tooltipContent"
>
{({ onMouseEnter, onMouseLeave, onClick }) => visibility ? <div className="ColorwaySelectorBtnContainer">
<div
className={"ColorwaySelectorBtn" + (isThin ? " ColorwaySelectorBtn_thin" : "")}
onMouseEnter={async () => {
onMouseEnter();
setActiveColorway((await DataStore.get("activeColorwayObject") as ColorwayObject).id || "None");
setAutoPreset(await DataStore.get("activeAutoPreset") as string);
}}
onMouseLeave={onMouseLeave}
onClick={() => {
onClick();
openModal((props: any) => <Selector modalProps={props} />);
}}
>{isThin ? <Text variant="text-xs/normal" style={{ color: "var(--header-primary)", fontWeight: 700, fontSize: 9 }}>Colorways</Text> : <PalleteIcon />}</div>
</div> : <></>}
</Tooltip>;
}

View file

@ -1,318 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { Button, Forms, ScrollerThin, Text, useState } from "@webpack/common";
import { knownThemeVars } from "../constants";
import { getFontOnBg, getHex } from "../utils";
export default function ({
modalProps,
onFinished
}: {
modalProps: ModalProps;
onFinished: ({ accent, primary, secondary, tertiary }: { accent: string, primary: string, secondary: string, tertiary: string; }) => void;
}) {
const [accentColor, setAccentColor] = useState<string>(getHex(
getComputedStyle(
document.body
).getPropertyValue("--brand-experiment")
));
const [primaryColor, setPrimaryColor] = useState<string>(getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-primary")
));
const [secondaryColor, setSecondaryColor] = useState<string>(getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-secondary")
));
const [tertiaryColor, setTertiaryColor] = useState<string>(getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-tertiary")
));
return <ModalRoot {...modalProps} className="colorwayCreator-modal">
<ModalHeader>
<Text variant="heading-lg/semibold" tag="h1">
Conflicting Colors Found
</Text>
</ModalHeader>
<ModalContent className="colorwayCreator-menuWrapper">
<Text className="colorwaysConflictingColors-warning">Multiple known themes have been found, select the colors you want to copy from below:</Text>
<Forms.FormTitle style={{ marginBottom: 0 }}>Colors to copy:</Forms.FormTitle>
<div className="colorwayCreator-colorPreviews">
<div className="colorwayCreator-colorPreview" style={{ backgroundColor: primaryColor, color: getFontOnBg(primaryColor) }} >Primary</div>
<div className="colorwayCreator-colorPreview" style={{ backgroundColor: secondaryColor, color: getFontOnBg(secondaryColor) }} >Secondary</div>
<div className="colorwayCreator-colorPreview" style={{ backgroundColor: tertiaryColor, color: getFontOnBg(tertiaryColor) }} >Tertiary</div>
<div className="colorwayCreator-colorPreview" style={{ backgroundColor: accentColor, color: getFontOnBg(accentColor) }} >Accent</div>
</div>
<div className="colorwaysCreator-settingCat">
<ScrollerThin orientation="vertical" className="colorwaysCreator-settingsList" paddingFix>
<div
id="colorways-colorstealer-item_Default"
className="colorwaysCreator-settingItm colorwaysCreator-colorPreviewItm"
>
<Forms.FormTitle>Discord</Forms.FormTitle>
<div className="colorwayCreator-colorPreviews">
<div
className="colorwayCreator-colorPreview" style={{
backgroundColor: getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-primary")
),
color: getFontOnBg(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-primary")
)
)
}}
onClick={() => setPrimaryColor(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-primary")
)
)}
>Primary</div>
<div
className="colorwayCreator-colorPreview" style={{
backgroundColor: getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-secondary")
),
color: getFontOnBg(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-secondary")
)
)
}}
onClick={() => setSecondaryColor(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-secondary")
)
)}
>Secondary</div>
<div
className="colorwayCreator-colorPreview" style={{
backgroundColor: getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-tertiary")
),
color: getFontOnBg(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-tertiary")
)
)
}}
onClick={() => setTertiaryColor(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-tertiary")
)
)}
>Tertiary</div>
<div
className="colorwayCreator-colorPreview" style={{
backgroundColor: getHex(
getComputedStyle(
document.body
).getPropertyValue("--brand-experiment")
),
color: getFontOnBg(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--brand-experiment")
)
)
}}
onClick={() => setAccentColor(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--brand-experiment")
)
)}
>Accent</div>
</div>
</div>
{Object.values(knownThemeVars).map((theme: any, i) => {
if (getComputedStyle(document.body).getPropertyValue(theme.variable)) {
return (
<div
id={
"colorways-colorstealer-item_" +
Object.keys(knownThemeVars)[i]
}
className="colorwaysCreator-settingItm colorwaysCreator-colorPreviewItm"
>
<Forms.FormTitle>{Object.keys(knownThemeVars)[i] + (theme.alt ? " (Main)" : "")}</Forms.FormTitle>
<div className="colorwayCreator-colorPreviews">
{theme.primary && getComputedStyle(document.body).getPropertyValue(theme.primary).match(/^\d.*%$/)
? <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_primary"
style={{
backgroundColor: getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.primary)})`),
color: getFontOnBg(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.primary)})`))
}}
onClick={() => {
setPrimaryColor(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.primary)})`));
}}
>Primary</div>
: (
theme.primary
? <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_primary"
style={{
backgroundColor: getHex(getComputedStyle(document.body).getPropertyValue(theme.primary)),
color: getFontOnBg(getHex(getComputedStyle(document.body).getPropertyValue(theme.primary)))
}}
onClick={() => {
setPrimaryColor(getHex(getComputedStyle(document.body).getPropertyValue(theme.primary)));
}}
>Primary</div>
: (theme.primaryVariables
&& <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_primary"
style={{ backgroundColor: `hsl(${getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l)})`, color: getFontOnBg(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l)})`)) }}
onClick={() => {
setPrimaryColor(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.primaryVariables.l)})`));
}}
>Primary</div>))
}
{theme.secondary && getComputedStyle(document.body).getPropertyValue(theme.secondary).match(/^\d.*%$/)
? <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_secondary"
style={{
backgroundColor: getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.secondary)})`),
color: getFontOnBg(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.secondary)})`))
}}
onClick={() => {
setSecondaryColor(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.secondary)})`));
}}
>Secondary</div>
: (theme.secondary
? <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_secondary"
style={{
backgroundColor: getHex(getComputedStyle(document.body).getPropertyValue(theme.secondary)),
color: getFontOnBg(getHex(getComputedStyle(document.body).getPropertyValue(theme.secondary)))
}}
onClick={() => {
setSecondaryColor(getHex(getComputedStyle(document.body).getPropertyValue(theme.secondary)));
}}
>Secondary</div>
: (theme.secondaryVariables
&& <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_secondary"
style={{ backgroundColor: `hsl(${getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l)})`, color: getFontOnBg(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l)})`)) }}
onClick={() => {
setSecondaryColor(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.secondaryVariables.l)})`));
}}
>Secondary</div>))
}
{theme.tertiary && getComputedStyle(document.body).getPropertyValue(theme.tertiary).match(/^\d.*%$/)
? <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_tertiary"
style={{
backgroundColor: getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.tertiary)})`),
color: getFontOnBg(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.tertiary)})`))
}}
onClick={() => {
setTertiaryColor(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.tertiary)})`));
}}
>Tertiary</div>
: (theme.tertiary
? <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_tertiary"
style={{
backgroundColor: getHex(getComputedStyle(document.body).getPropertyValue(theme.tertiary)),
color: getFontOnBg(getHex(getComputedStyle(document.body).getPropertyValue(theme.tertiary)))
}}
onClick={() => {
setTertiaryColor(getHex(getComputedStyle(document.body).getPropertyValue(theme.tertiary)));
}}
>Tertiary</div>
: (theme.tertiaryVariables
&& <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_tertiary"
style={{ backgroundColor: `hsl(${getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l)})`, color: getFontOnBg(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l)})`)) }}
onClick={() => {
setTertiaryColor(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.tertiaryVariables.l)})`));
}}
>Tertiary</div>))}
{theme.accent && getComputedStyle(document.body).getPropertyValue(theme.accent).match(/^\d.*%$/)
? <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_accent"
style={{
backgroundColor: getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.accent)})`),
color: getFontOnBg(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.accent)})`))
}}
onClick={() => {
setAccentColor(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.accent)})`));
}}
>Accent</div>
: (theme.accent
? <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_accent"
style={{
backgroundColor: getHex(getComputedStyle(document.body).getPropertyValue(theme.accent)),
color: getFontOnBg(getHex(getComputedStyle(document.body).getPropertyValue(theme.accent)))
}}
onClick={() => {
setAccentColor(getHex(getComputedStyle(document.body).getPropertyValue(theme.accent)));
}}
>Accent</div>
: (theme.accentVariables
&& <div
className="colorwayCreator-colorPreview colorwayCreator-colorPreview_accent"
style={{ backgroundColor: `hsl(${getComputedStyle(document.body).getPropertyValue(theme.accentVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l)})`, color: getFontOnBg(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.accentVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l)})`)) }}
onClick={() => {
setAccentColor(getHex(`hsl(${getComputedStyle(document.body).getPropertyValue(theme.accentVariables.h)} ${!getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s) + "%") : getComputedStyle(document.body).getPropertyValue(theme.accentVariables.s)} ${!getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l).includes("%") ? (getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l) + "%") : getComputedStyle(document.body).getPropertyValue(theme.accentVariables.l)})`));
}}
>Accent</div>))}
</div>
</div>
);
}
})}
</ScrollerThin>
</div>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => {
onFinished({
accent: accentColor,
primary: primaryColor,
secondary: secondaryColor,
tertiary: tertiaryColor
});
modalProps.onClose();
}}
>Finish</Button>
</ModalFooter>
</ModalRoot >;
}

View file

@ -1,348 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import {
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
openModal,
} from "@utils/modal";
import {
Button,
Forms,
Slider,
Text,
TextInput,
useEffect,
UserStore,
useState,
} from "@webpack/common";
import { ColorPicker, versionData } from "..";
import { knownThemeVars } from "../constants";
import { generateCss, getPreset, gradientPresetIds, PrimarySatDiffs, pureGradientBase } from "../css";
import { Colorway } from "../types";
import { colorToHex, getHex, HexToHSL, hexToString } from "../utils";
import ColorwayCreatorSettingsModal from "./ColorwayCreatorSettingsModal";
import ConflictingColorsModal from "./ConflictingColorsModal";
import InputColorwayIdModal from "./InputColorwayIdModal";
import SaveColorwayModal from "./SaveColorwayModal";
import ThemePreviewCategory from "./ThemePreview";
export default function ({
modalProps,
loadUIProps,
colorwayID
}: {
modalProps: ModalProps;
loadUIProps?: () => Promise<void>;
colorwayID?: string;
}) {
const [accentColor, setAccentColor] = useState<string>("5865f2");
const [primaryColor, setPrimaryColor] = useState<string>("313338");
const [secondaryColor, setSecondaryColor] = useState<string>("2b2d31");
const [tertiaryColor, setTertiaryColor] = useState<string>("1e1f22");
const [colorwayName, setColorwayName] = useState<string>("");
const [tintedText, setTintedText] = useState<boolean>(true);
const [discordSaturation, setDiscordSaturation] = useState<boolean>(true);
const [preset, setPreset] = useState<string>("default");
const [presetColorArray, setPresetColorArray] = useState<string[]>(["accent", "primary", "secondary", "tertiary"]);
const [mutedTextBrightness, setMutedTextBrightness] = useState<number>(Math.min(HexToHSL("#" + primaryColor)[2] + (3.6 * 3), 100));
const colorProps = {
accent: {
get: accentColor,
set: setAccentColor,
name: "Accent"
},
primary: {
get: primaryColor,
set: setPrimaryColor,
name: "Primary"
},
secondary: {
get: secondaryColor,
set: setSecondaryColor,
name: "Secondary"
},
tertiary: {
get: tertiaryColor,
set: setTertiaryColor,
name: "Tertiary"
}
};
useEffect(() => {
if (colorwayID) {
if (!colorwayID.includes(",")) {
throw new Error("Invalid Colorway ID");
} else {
const setColor = [
setAccentColor,
setPrimaryColor,
setSecondaryColor,
setTertiaryColor
];
colorwayID.split("|").forEach((prop: string) => {
if (prop.includes(",#")) {
prop.split(/,#/).forEach((color: string, i: number) => setColor[i](colorToHex(color)));
}
if (prop.includes("n:")) {
setColorwayName(prop.split("n:")[1]);
}
if (prop.includes("p:")) {
if (Object.values(getPreset()).map(preset => preset.id).includes(prop.split("p:")[1])) {
setPreset(prop.split("p:")[1]);
setPresetColorArray(getPreset()[prop.split("p:")[1]].colors);
}
}
});
}
}
});
const colorPickerProps = {
suggestedColors: [
"#313338",
"#2b2d31",
"#1e1f22",
"#5865f2",
],
showEyeDropper: true
};
return (
<ModalRoot {...modalProps} className="colorwayCreator-modal">
<ModalHeader>
<Text variant="heading-lg/semibold" tag="h1">
Create Colorway
</Text>
</ModalHeader>
<ModalContent className="colorwayCreator-menuWrapper">
<Forms.FormTitle style={{ marginBottom: 0 }}>
Name:
</Forms.FormTitle>
<TextInput
placeholder="Give your Colorway a name"
value={colorwayName}
onChange={setColorwayName}
/>
<div className="colorwaysCreator-settingCat">
<Forms.FormTitle style={{ marginBottom: "0" }}>
Colors & Values:
</Forms.FormTitle>
<div className="colorwayCreator-colorPreviews">
{presetColorArray.map(presetColor => {
return <ColorPicker
label={<Text className="colorwaysPicker-colorLabel">{colorProps[presetColor].name}</Text>}
color={parseInt(colorProps[presetColor].get, 16)}
onChange={(color: number) => {
let hexColor = color.toString(16);
while (hexColor.length < 6) {
hexColor = "0" + hexColor;
}
colorProps[presetColor].set(hexColor);
}}
{...colorPickerProps}
/>;
})}
</div>
<Forms.FormDivider style={{ margin: "10px 0" }} />
<Forms.FormTitle>Muted Text Brightness:</Forms.FormTitle>
<Slider
minValue={0}
maxValue={100}
initialValue={mutedTextBrightness}
onValueChange={setMutedTextBrightness}
/>
</div>
<div
className="colorwaysCreator-setting"
onClick={() => openModal((props: ModalProps) => <ColorwayCreatorSettingsModal
modalProps={props}
hasDiscordSaturation={discordSaturation}
hasTintedText={tintedText}
presetId={preset}
onSettings={({ presetId, tintedText, discordSaturation }) => {
setPreset(presetId);
setPresetColorArray(getPreset()[presetId].colors);
setDiscordSaturation(discordSaturation);
setTintedText(tintedText);
}} />)}>
<Forms.FormTitle style={{ marginBottom: 0 }}>Settings & Presets</Forms.FormTitle>
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" role="img" style={{ rotate: "-90deg" }}>
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M7 10L12 15 17 10" aria-hidden="true" />
</svg>
</div>
<ThemePreviewCategory
accent={"#" + accentColor}
primary={"#" + primaryColor}
secondary={"#" + secondaryColor}
tertiary={"#" + tertiaryColor}
previewCSS={gradientPresetIds.includes(getPreset()[preset].id) ? pureGradientBase + `.colorwaysPreview-modal,.colorwaysPreview-wrapper {--gradient-theme-bg: linear-gradient(${(getPreset(
primaryColor,
secondaryColor,
tertiaryColor,
accentColor
)[preset].preset(discordSaturation) as { full: string, base: string; }).base})}` : (tintedText ? `.colorwaysPreview-modal,.colorwaysPreview-wrapper {
--primary-500: hsl(${HexToHSL("#" + primaryColor)[0]} calc(var(--saturation-factor, 1)*${discordSaturation ? Math.round(((HexToHSL("#" + primaryColor)[1] / 100) * (100 + PrimarySatDiffs[500])) * 10) / 10 : HexToHSL("#" + primaryColor)[1]}%) ${mutedTextBrightness || Math.min(HexToHSL("#" + primaryColor)[2] + (3.6 * 3), 100)}%);
--primary-360: hsl(${HexToHSL("#" + secondaryColor)[0]} calc(var(--saturation-factor, 1)*${discordSaturation ? Math.round(((HexToHSL("#" + primaryColor)[1] / 100) * (100 + PrimarySatDiffs[360])) * 10) / 10 : HexToHSL("#" + primaryColor)[1]}%) 90%);
}` : "")}
/>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={async () => {
var customColorwayCSS: string = "";
if (preset === "default") {
customColorwayCSS = generateCss(
primaryColor,
secondaryColor,
tertiaryColor,
accentColor,
tintedText,
discordSaturation,
mutedTextBrightness,
(colorwayName || "Colorway")
);
} else {
gradientPresetIds.includes(getPreset()[preset].id) ?
customColorwayCSS = `/**
* @name ${colorwayName || "Colorway"}
* @version ${versionData.creatorVersion}
* @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id}
* @preset Gradient
*/
${(getPreset(primaryColor, secondaryColor, tertiaryColor, accentColor)[preset].preset(discordSaturation) as { full: string; }).full}` : customColorwayCSS = `/**
* @name ${colorwayName || "Colorway"}
* @version ${versionData.creatorVersion}
* @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id}
* @preset ${getPreset()[preset].name}
*/
${(getPreset(primaryColor, secondaryColor, tertiaryColor, accentColor)[preset].preset(discordSaturation) as string)}`;
}
const customColorway: Colorway = {
name: (colorwayName || "Colorway"),
"dc-import": customColorwayCSS,
accent: "#" + accentColor,
primary: "#" + primaryColor,
secondary: "#" + secondaryColor,
tertiary: "#" + tertiaryColor,
colors: presetColorArray,
author: UserStore.getCurrentUser().username,
authorID: UserStore.getCurrentUser().id,
isGradient: gradientPresetIds.includes(getPreset()[preset].id),
linearGradient: gradientPresetIds.includes(getPreset()[preset].id) ? (getPreset(
primaryColor,
secondaryColor,
tertiaryColor,
accentColor
)[preset].preset(discordSaturation) as { base: string; }).base : "",
preset: getPreset()[preset].id,
creatorVersion: versionData.creatorVersion
};
openModal(props => <SaveColorwayModal modalProps={props} colorways={[customColorway]} onFinish={() => {
modalProps.onClose();
loadUIProps!();
}} />);
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => {
function setAllColors({ accent, primary, secondary, tertiary }: { accent: string, primary: string, secondary: string, tertiary: string; }) {
setAccentColor(accent.split("#")[1]);
setPrimaryColor(primary.split("#")[1]);
setSecondaryColor(secondary.split("#")[1]);
setTertiaryColor(tertiary.split("#")[1]);
}
var copiedThemes = ["Discord"];
Object.values(knownThemeVars).map((theme: { variable: string; variableType?: string; }, i: number) => {
if (getComputedStyle(document.body).getPropertyValue(theme.variable)) {
copiedThemes.push(Object.keys(knownThemeVars)[i]);
}
});
if (copiedThemes.length > 1) {
openModal(props => <ConflictingColorsModal modalProps={props} onFinished={setAllColors} />);
} else {
setPrimaryColor(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-primary")
).split("#")[1]
);
setSecondaryColor(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-secondary")
).split("#")[1]
);
setTertiaryColor(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--background-tertiary")
).split("#")[1]
);
setAccentColor(
getHex(
getComputedStyle(
document.body
).getPropertyValue("--brand-experiment")
).split("#")[1]
);
}
}}
>
Copy Current Colors
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => openModal((props: any) => <InputColorwayIdModal modalProps={props} onColorwayId={colorwayID => {
const setColor = [
setAccentColor,
setPrimaryColor,
setSecondaryColor,
setTertiaryColor
];
hexToString(colorwayID).split(/,#/).forEach((color: string, i: number) => setColor[i](colorToHex(color)));
}} />)}
>
Enter Colorway ID
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => {
modalProps.onClose();
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -1,146 +0,0 @@
/*
* 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 type { PropsWithChildren, SVGProps } from "react";
interface BaseIconProps extends IconProps {
viewBox: string;
}
interface IconProps extends SVGProps<SVGSVGElement> {
className?: string;
height?: string | number;
width?: string | number;
}
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
return (
<svg
className={classes(className, "vc-icon")}
role="img"
width={width}
height={height}
viewBox={viewBox}
{...svgProps}
>
{children}
</svg>
);
}
export function PalleteIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-pallete-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M 12,0 C 5.3733333,0 0,5.3733333 0,12 c 0,6.626667 5.3733333,12 12,12 1.106667,0 2,-0.893333 2,-2 0,-0.52 -0.2,-0.986667 -0.52,-1.346667 -0.306667,-0.346666 -0.506667,-0.813333 -0.506667,-1.32 0,-1.106666 0.893334,-2 2,-2 h 2.36 C 21.013333,17.333333 24,14.346667 24,10.666667 24,4.7733333 18.626667,0 12,0 Z M 4.6666667,12 c -1.1066667,0 -2,-0.893333 -2,-2 0,-1.1066667 0.8933333,-2 2,-2 1.1066666,0 2,0.8933333 2,2 0,1.106667 -0.8933334,2 -2,2 z M 8.666667,6.6666667 c -1.106667,0 -2.0000003,-0.8933334 -2.0000003,-2 0,-1.1066667 0.8933333,-2 2.0000003,-2 1.106666,0 2,0.8933333 2,2 0,1.1066666 -0.893334,2 -2,2 z m 6.666666,0 c -1.106666,0 -2,-0.8933334 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.1066666 -0.893333,2 -2,2 z m 4,5.3333333 c -1.106666,0 -2,-0.893333 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.106667 -0.893333,2 -2,2 z"
/>
</Icon>
);
}
export function CloseIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-close-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
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"
/>
</Icon>
);
}
export function DownloadIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-download-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2a1 1 0 0 1 1 1v10.59l3.3-3.3a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.42l3.3 3.3V3a1 1 0 0 1 1-1ZM3 20a1 1 0 1 0 0 2h18a1 1 0 1 0 0-2H3Z"
/>
</Icon>
);
}
export function ImportIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-import-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M.9 3a.9.9 0 0 1 .892.778l.008.123v16.201a.9.9 0 0 1-1.792.121L0 20.102V3.899A.9.9 0 0 1 .9 3Zm14.954 2.26.1-.112a1.2 1.2 0 0 1 1.584-.1l.113.1 5.998 5.998a1.2 1.2 0 0 1 .1 1.584l-.1.112-5.997 6.006a1.2 1.2 0 0 1-1.799-1.584l.1-.113 3.947-3.954H4.8a1.2 1.2 0 0 1-1.191-1.06l-.008-.14a1.2 1.2 0 0 1 1.06-1.192l.14-.008h15.103l-3.95-3.952a1.2 1.2 0 0 1-.1-1.585l.1-.112z"
/>
</Icon>
);
}
export function IDIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-id-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M15.3 14.48c-.46.45-1.08.67-1.86.67h-1.39V9.2h1.39c.78 0 1.4.22 1.86.67.46.45.68 1.22.68 2.31 0 1.1-.22 1.86-.68 2.31Z"
/>
<path
fill="currentColor"
fill-rule="evenodd"
d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm1 15h2.04V7.34H6V17Zm4-9.66V17h3.44c1.46 0 2.6-.42 3.38-1.25.8-.83 1.2-2.02 1.2-3.58s-.4-2.75-1.2-3.58c-.79-.83-1.92-1.25-3.38-1.25H10Z"
clip-rule="evenodd"
/>
</Icon>
);
}
export function CodeIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-code-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M9.6 7.8 4 12l5.6 4.2a1 1 0 0 1 .4.8v1.98c0 .21-.24.33-.4.2l-8.1-6.4a1 1 0 0 1 0-1.56l8.1-6.4c.16-.13.4-.01.4.2V7a1 1 0 0 1-.4.8ZM14.4 7.8 20 12l-5.6 4.2a1 1 0 0 0-.4.8v1.98c0 .21.24.33.4.2l8.1-6.4a1 1 0 0 0 0-1.56l-8.1-6.4a.25.25 0 0 0-.4.2V7a1 1 0 0 0 .4.8Z"
/>
</Icon>
);
}
export function MoreIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-more-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M4 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm10-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm8 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"
clip-rule="evenodd"
/>
</Icon>
);
}

View file

@ -1,301 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
import { CodeBlock } from "@components/CodeBlock";
import { Flex } from "@components/Flex";
import {
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
openModal,
} from "@utils/modal";
import { saveFile } from "@utils/web";
import { findComponentByCodeLazy } from "@webpack";
import { Button, Clipboard, Forms, Text, TextInput, Toasts, UserStore, useState, useStateFromStores } from "@webpack/common";
import { ColorwayCSS, versionData } from "..";
import { generateCss, pureGradientBase } from "../css";
import { Colorway } from "../types";
import { colorToHex, stringToHex } from "../utils";
import SaveColorwayModal from "./SaveColorwayModal";
import ThemePreview from "./ThemePreview";
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
function RenameColorwayModal({ modalProps, ogName, onFinish, colorwayList }: { modalProps: ModalProps, ogName: string, onFinish: (name: string) => void, colorwayList: Colorway[]; }) {
const [error, setError] = useState<string>("");
const [newName, setNewName] = useState<string>(ogName);
return <ModalRoot {...modalProps}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1" style={{ marginRight: "auto" }}>
Rename Colorway...
</Text>
<ModalCloseButton onClick={() => modalProps.onClose()} />
</ModalHeader>
<ModalContent>
<TextInput
value={newName}
error={error}
onChange={setNewName}
/>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={async () => {
if (!newName) {
return setError("Error: Please enter a valid name");
}
if (colorwayList.map(c => c.name).includes(newName)) {
return setError("Error: Name already exists");
}
onFinish(newName);
modalProps.onClose();
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}
export default function ({
modalProps,
colorway,
loadUIProps
}: {
modalProps: ModalProps;
colorway: Colorway;
loadUIProps: () => Promise<void>;
}) {
const colors: string[] = colorway.colors || [
"accent",
"primary",
"secondary",
"tertiary",
];
const profile = useStateFromStores([UserStore], () => UserStore.getUser(colorway.authorID));
return <ModalRoot {...modalProps}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1" style={{ marginRight: "auto" }}>
Colorway: {colorway.name}
</Text>
<ModalCloseButton onClick={() => modalProps.onClose()} />
</ModalHeader>
<ModalContent>
<Flex style={{ gap: "8px", width: "100%" }} flexDirection="column">
<Forms.FormTitle style={{ marginBottom: 0, width: "100%" }}>Creator:</Forms.FormTitle>
<Flex style={{ gap: ".5rem" }}>
<UserSummaryItem
users={[profile]}
guildId={undefined}
renderIcon={false}
showDefaultAvatarsForNullUsers
size={32}
showUserPopout
/>
<Text style={{ lineHeight: "32px" }}>{colorway.author}</Text>
</Flex>
<Forms.FormTitle style={{ marginBottom: 0, width: "100%" }}>Colors:</Forms.FormTitle>
<Flex style={{ gap: "8px" }}>
{colors.map(color => <div className="colorwayInfo-colorSwatch" style={{ backgroundColor: colorway[color] }} />)}
</Flex>
<Forms.FormTitle style={{ marginBottom: 0, width: "100%" }}>Actions:</Forms.FormTitle>
<Flex style={{ gap: "8px" }} flexDirection="column">
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }}
onClick={() => {
const colorwayIDArray = `${colorway.accent},${colorway.primary},${colorway.secondary},${colorway.tertiary}|n:${colorway.name}${colorway.preset ? `|p:${colorway.preset}` : ""}`;
const colorwayID = stringToHex(colorwayIDArray);
Clipboard.copy(colorwayID);
Toasts.show({
message: "Copied Colorway ID Successfully",
type: 1,
id: "copy-colorway-id-notify",
});
}}
>
Copy Colorway ID
</Button>
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }}
onClick={() => {
Clipboard.copy(colorway["dc-import"]);
Toasts.show({
message: "Copied CSS to Clipboard",
type: 1,
id: "copy-colorway-css-notify",
});
}}
>
Copy CSS
</Button>
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }}
onClick={async () => {
const newColorway = {
...colorway,
"dc-import": generateCss(colorToHex(colorway.primary) || "313338", colorToHex(colorway.secondary) || "2b2d31", colorToHex(colorway.tertiary) || "1e1f22", colorToHex(colorway.accent) || "5865f2", true, true, undefined, colorway.name)
};
openModal(props => <SaveColorwayModal modalProps={props} colorways={[newColorway]} onFinish={() => { }} />);
}}
>
Update CSS
</Button>
{colorway.sourceType === "offline" && <Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }}
onClick={async () => {
const offlineSources = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).map(o => o.colorways).filter(colorArr => colorArr.map(color => color.name).includes(colorway.name))[0];
openModal(props => <RenameColorwayModal ogName={colorway.name} colorwayList={offlineSources} modalProps={props} onFinish={async (newName: string) => {
const stores = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).map(source => {
if (source.name === colorway.source) {
return {
name: source.name,
colorways: [...source.colorways.filter(colorway => colorway.name !== colorway.name), {
...colorway,
name: newName
}]
};
} else return source;
});
DataStore.set("customColorways", stores);
if ((await DataStore.get("activeColorwayObject")).id === colorway.name) {
DataStore.set("activeColorwayObject", { id: newName, css: colorway.name, sourceType: "offline", source: colorway.source });
}
modalProps.onClose();
loadUIProps();
}} />);
}}
>
Rename
</Button>}
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }}
onClick={() => {
openModal(props => <ModalRoot {...props} className="colorwayInfo-cssModal">
<ModalContent><CodeBlock lang="css" content={colorway["dc-import"]} /></ModalContent>
</ModalRoot>);
}}
>
Show CSS
</Button>
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }}
onClick={() => {
if (!colorway["dc-import"].includes("@name")) {
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(`/**
* @name ${colorway.name || "Colorway"}
* @version ${versionData.creatorVersion}
* @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id}
*/
${colorway["dc-import"].replace((colorway["dc-import"].match(/\/\*.+\*\//) || [""])[0], "").replaceAll("url(//", "url(https://").replaceAll("url(\"//", "url(\"https://")}`, `${colorway.name.replaceAll(" ", "-").toLowerCase()}.theme.css`);
} else {
saveFile(new File([`/**
* @name ${colorway.name || "Colorway"}
* @version ${versionData.creatorVersion}
* @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id}
*/
${colorway["dc-import"].replace((colorway["dc-import"].match(/\/\*.+\*\//) || [""])[0], "").replaceAll("url(//", "url(https://").replaceAll("url(\"//", "url(\"https://")}`], `${colorway.name.replaceAll(" ", "-").toLowerCase()}.theme.css`, { type: "text/plain" }));
}
} else {
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(colorway["dc-import"], `${colorway.name.replaceAll(" ", "-").toLowerCase()}.theme.css`);
} else {
saveFile(new File([colorway["dc-import"]], `${colorway.name.replaceAll(" ", "-").toLowerCase()}.theme.css`, { type: "text/plain" }));
}
}
}}
>
Download CSS
</Button>
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }}
onClick={() => {
openModal((props: ModalProps) => <ModalRoot className="colorwaysPreview-modal" {...props}>
<style>
{colorway.isGradient ? pureGradientBase + `.colorwaysPreview-modal,.colorwaysPreview-wrapper {--gradient-theme-bg: linear-gradient(${colorway.linearGradient})}` : ""}
</style>
<ThemePreview
accent={colorway.accent}
primary={colorway.primary}
secondary={colorway.secondary}
tertiary={colorway.tertiary}
isModal
modalProps={props}
/>
</ModalRoot>);
}}
>
Show preview
</Button>
{colorway.sourceType === "offline" && <Button
color={Button.Colors.RED}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
style={{ width: "100%" }}
onClick={async () => {
const oldStores = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).filter(source => source.name !== colorway.source);
const storeToModify = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).filter(source => source.name === colorway.source)[0];
const newStore = { name: storeToModify.name, colorways: storeToModify.colorways.filter(colorway => colorway.name !== colorway.name) };
DataStore.set("customColorways", [...oldStores, newStore]);
if ((await DataStore.get("activeColorwayObject")).id === colorway.name) {
DataStore.set("activeColorwayObject", { id: null, css: null, sourceType: null, source: null });
ColorwayCSS.remove();
}
modalProps.onClose();
loadUIProps();
}}
>
Delete
</Button>}
</Flex>
</Flex>
<div style={{ width: "100%", height: "20px" }} />
</ModalContent>
</ModalRoot>;
}

View file

@ -1,49 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ModalContent, ModalFooter, ModalProps, ModalRoot } from "@utils/modal";
import { Button, Forms, TextInput, useState } from "@webpack/common";
import { hexToString } from "../utils";
export default function ({ modalProps, onColorwayId }: { modalProps: ModalProps, onColorwayId: (colorwayID: string) => void; }) {
const [colorwayID, setColorwayID] = useState<string>("");
return <ModalRoot {...modalProps} className="colorwaysCreator-noMinHeight">
<ModalContent className="colorwaysCreator-noHeader colorwaysCreator-noMinHeight">
<Forms.FormTitle>Colorway ID:</Forms.FormTitle>
<TextInput placeholder="Enter Colorway ID" onInput={e => setColorwayID(e.currentTarget.value)} />
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => {
if (!colorwayID) {
throw new Error("Please enter a Colorway ID");
} else if (!hexToString(colorwayID).includes(",")) {
throw new Error("Invalid Colorway ID");
} else {
onColorwayId(colorwayID);
modalProps.onClose();
}
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}

View file

@ -1,207 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { PlusIcon } from "@components/Icons";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { Button, Text, TextInput, useEffect, useState } from "@webpack/common";
import { Colorway } from "../types";
import { StoreNameModal } from "./SettingsTabs/SourceManager";
export default function ({ modalProps, colorways, onFinish }: { modalProps: ModalProps, colorways: Colorway[], onFinish: () => void; }) {
const [offlineColorwayStores, setOfflineColorwayStores] = useState<{ name: string, colorways: Colorway[], id?: string; }[]>([]);
const [storename, setStorename] = useState<string>();
const [noStoreError, setNoStoreError] = useState<boolean>(false);
const { radioBar, item: radioBarItem, itemFilled: radioBarItemFilled, radioPositionLeft } = findByProps("radioBar");
useEffect(() => {
(async () => {
setOfflineColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]);
})();
});
return <ModalRoot {...modalProps}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1">Select Offline Colorway Source</Text>
</ModalHeader>
<ModalContent>
{noStoreError ? <Text variant="text-xs/normal" style={{ color: "var(--text-danger)" }}>Error: No store selected</Text> : <></>}
{offlineColorwayStores.map(store => {
return <div className={`${radioBarItem} ${radioBarItemFilled}`} aria-checked={storename === store.name}>
<div
className={`${radioBar} ${radioPositionLeft}`}
style={{ padding: "10px" }}
onClick={() => {
setStorename(store.name);
}}>
<svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor" />
{storename === store.name && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />}
</svg>
<Text variant="eyebrow" tag="h5">{store.name}</Text>
</div>
</div>;
})}
<div className={`${radioBarItem} ${radioBarItemFilled}`}>
<div
className={`${radioBar} ${radioPositionLeft}`}
style={{ padding: "10px" }}
onClick={() => {
openModal(props => <StoreNameModal modalProps={props} conflicting={false} originalName="" onFinish={async e => {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: [] }]);
setOfflineColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
}} />);
}}>
<PlusIcon width={24} height={24} />
<Text variant="eyebrow" tag="h5">Create new store...</Text>
</div>
</div>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND_NEW}
size={Button.Sizes.MEDIUM}
onClick={async () => {
setNoStoreError(false);
if (!storename) {
setNoStoreError(true);
} else {
const oldStores: { name: string, colorways: Colorway[], id?: string; }[] | undefined = await DataStore.get("customColorways");
const storeToModify: { name: string, colorways: Colorway[], id?: string; } | undefined = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).filter(source => source.name === storename)[0];
colorways.map((colorway, i) => {
if (storeToModify.colorways.map(colorway => colorway.name).includes(colorway.name)) {
openModal(props => <ModalRoot {...props}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1">Duplicate Colorway</Text>
</ModalHeader>
<ModalContent>
<Text>A colorway with the same name was found in this store, what do you want to do?</Text>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => {
const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways.filter(colorwayy => colorwayy.name !== colorway.name), colorway] };
DataStore.set("customColorways", [...oldStores!.filter(source => source.name !== storename), newStore]);
props.onClose();
if (i + 1 === colorways.length) {
modalProps.onClose();
onFinish!();
}
}}
>
Override
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => {
function NewColorwayNameModal({ modalProps, onSelected }: { modalProps: ModalProps, onSelected: (e: string) => void; }) {
const [errorMsg, setErrorMsg] = useState<string>();
const [newColorwayName, setNewColorwayName] = useState("");
return <ModalRoot {...modalProps}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1">Select new name</Text>
</ModalHeader>
<ModalContent>
<TextInput error={errorMsg} value={newColorwayName} onChange={e => setNewColorwayName(e)} placeholder="Enter valid colorway name" />
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => {
setErrorMsg("");
if (storeToModify!.colorways.map(colorway => colorway.name).includes(newColorwayName)) {
setErrorMsg("Error: Name already exists");
} else {
onSelected(newColorwayName);
if (i + 1 === colorways.length) {
modalProps.onClose();
}
}
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => {
if (i + 1 === colorways.length) {
modalProps.onClose();
}
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}
openModal(propss => <NewColorwayNameModal modalProps={propss} onSelected={e => {
const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways, { ...colorway, name: e }] };
DataStore.set("customColorways", [...oldStores!.filter(source => source.name !== storename), newStore]);
props.onClose();
if (i + 1 === colorways.length) {
modalProps.onClose();
onFinish!();
}
}} />);
}}
>
Rename
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => {
props.onClose();
}}
>
Select different store
</Button>
</ModalFooter>
</ModalRoot>);
} else {
const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways, colorway] };
DataStore.set("customColorways", [...oldStores!.filter(source => source.name !== storename), newStore]);
if (i + 1 === colorways.length) {
modalProps.onClose();
onFinish();
}
}
});
}
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => {
modalProps.onClose();
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}

View file

@ -1,839 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/* eslint-disable arrow-parens */
import * as DataStore from "@api/DataStore";
import { Flex } from "@components/Flex";
import { DeleteIcon, PlusIcon } from "@components/Icons";
import { SettingsTab } from "@components/VencordSettings/shared";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { findByProps, findByPropsLazy } from "@webpack";
import {
Button,
ButtonLooks,
Clipboard,
Forms,
Menu,
Popout,
ScrollerThin,
Select,
SettingsRouter,
Text,
TextInput,
Toasts,
Tooltip,
useEffect,
useState
} from "@webpack/common";
import { ReactNode } from "react";
import { ColorwayCSS } from "..";
import { generateCss, getAutoPresets, gradientBase } from "../css";
import { Colorway, ColorwayObject, SortOptions, SourceObject } from "../types";
import { colorToHex, getHex, stringToHex } from "../utils";
import AutoColorwaySelector from "./AutoColorwaySelector";
import ColorPickerModal from "./ColorPicker";
import CreatorModal from "./CreatorModal";
import { CodeIcon, IDIcon, MoreIcon, PalleteIcon } from "./Icons";
import ColorwayInfoModal from "./InfoModal";
const { SelectionCircle } = findByPropsLazy("SelectionCircle");
function SelectorContainer({ children, isSettings, modalProps }: { children: ReactNode, isSettings?: boolean, modalProps: ModalProps; }) {
if (!isSettings) {
return <ModalRoot {...modalProps} className="colorwaySelectorModal">
{children}
</ModalRoot>;
} else {
return <SettingsTab title="Colors">
<div className="colorwaysSettingsSelector-wrapper">
{children}
</div>
</SettingsTab>;
}
}
function SelectorHeader({ children, isSettings }: { children: ReactNode, isSettings?: boolean; }) {
if (!isSettings) {
return <ModalHeader separator={false}>
{children}
</ModalHeader>;
} else {
return <Flex style={{ gap: "0" }}>
{children}
</Flex>;
}
}
function SelectorContent({ children, isSettings }: { children: ReactNode, isSettings?: boolean; }) {
if (!isSettings) {
return <ModalContent className="colorwaySelectorModalContent">{children}</ModalContent>;
} else {
return <>{children}</>;
}
}
export default function ({
modalProps,
isSettings,
settings = { selectorType: "normal" }
}: {
modalProps: ModalProps,
isSettings?: boolean,
settings?: { selectorType: "preview" | "multiple-selection" | "normal", previewSource?: string, onSelected?: (colorways: Colorway[]) => void; };
}): JSX.Element | any {
const [colorwayData, setColorwayData] = useState<SourceObject[]>([]);
const [searchValue, setSearchValue] = useState<string>("");
const [sortBy, setSortBy] = useState<SortOptions>(SortOptions.NAME_AZ);
const [activeColorwayObject, setActiveColorwayObject] = useState<ColorwayObject>({ id: null, css: null, sourceType: null, source: null });
const [customColorwayData, setCustomColorwayData] = useState<SourceObject[]>([]);
const [loaderHeight, setLoaderHeight] = useState<"2px" | "0px">("2px");
const [visibleSources, setVisibleSources] = useState<string>("all");
const [showReloadMenu, setShowReloadMenu] = useState<boolean>(false);
const [viewMode, setViewMode] = useState<"list" | "grid">("grid");
const [showLabelsInSelectorGridView, setShowLabelsInSelectorGridView] = useState<boolean>(false);
const [showSortingMenu, setShowSotringMenu] = useState<boolean>(false);
const [selectedColorways, setSelectedColorways] = useState<Colorway[]>([]);
const [errorCode, setErrorCode] = useState<number>(0);
const { item: radioBarItem, itemFilled: radioBarItemFilled } = findByProps("radioBar");
const filters = [
{
name: "All",
id: "all",
sources: [...colorwayData, ...customColorwayData]
},
...colorwayData.map((source) => ({
name: source.source,
id: source.source.toLowerCase().replaceAll(" ", "-"),
sources: [source]
})),
...customColorwayData.map((source) => ({
name: source.source,
id: source.source.toLowerCase().replaceAll(" ", "-"),
sources: [source]
}))
];
async function loadUI(force?: boolean) {
setActiveColorwayObject(await DataStore.get("activeColorwayObject") as ColorwayObject);
setViewMode(await DataStore.get("selectorViewMode") as "list" | "grid");
setShowLabelsInSelectorGridView(await DataStore.get("showLabelsInSelectorGridView") as boolean);
setLoaderHeight("0px");
if (settings.previewSource) {
const res: Response = await fetch(settings.previewSource);
const dataPromise = res.json().then(data => data).catch(() => ({ colorways: [], errorCode: 1, errorMsg: "Colorway Source format is invalid" }));
const data = await dataPromise;
if (data.errorCode) {
setErrorCode(data.errorCode);
}
const colorwayList: Colorway[] = data.css ? data.css.map(customStore => customStore.colorways).flat() : data.colorways;
setColorwayData([{ colorways: colorwayList || [], source: res.url, type: "online" }] as { type: "online" | "offline" | "temporary", source: string, colorways: Colorway[]; }[]);
} else {
setCustomColorwayData((await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).map((colorSrc: { name: string, colorways: Colorway[], id?: string; }) => ({ type: "offline", source: colorSrc.name, colorways: colorSrc.colorways })));
const onlineSources: { name: string, url: string; }[] = await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[];
const responses: Response[] = await Promise.all(
onlineSources.map((source) =>
fetch(source.url, force ? { cache: "no-store" } : {})
)
);
setColorwayData(await Promise.all(
responses
.map((res, i) => ({ response: res, name: onlineSources[i].name }))
.map((res: { response: Response, name: string; }) =>
res.response.json().then(dt => ({ colorways: dt.colorways as Colorway[], source: res.name, type: "online" })).catch(() => ({ colorways: [] as Colorway[], source: res.name, type: "online" }))
)) as { type: "online" | "offline" | "temporary", source: string, colorways: Colorway[]; }[]);
}
}
useEffect(() => { loadUI(); }, [searchValue]);
function ReloadPopout(onClose: () => void) {
return (
<Menu.Menu
navId="dc-reload-menu"
onClose={onClose}
>
<Menu.MenuItem
id="dc-force-reload"
label="Force Reload"
action={() => loadUI(true)}
/>
</Menu.Menu>
);
}
function SortingPopout(onClose: () => void) {
return (
<Menu.Menu
navId="dc-selector-options-menu"
onClose={onClose}
>
<Menu.MenuGroup label="View">
<Menu.MenuRadioItem
group="selector-viewMode"
id="selector-viewMode_grid"
label="Grid"
checked={viewMode === "grid"}
action={() => {
setViewMode("grid");
DataStore.set("selectorViewMode", "grid");
}}
/>
<Menu.MenuRadioItem
group="selector-viewMode"
id="selector-viewMode_list"
label="List"
checked={viewMode === "list"}
action={() => {
setViewMode("list");
DataStore.set("selectorViewMode", "list");
}}
/>
</Menu.MenuGroup>
<Menu.MenuGroup label="Sort By">
<Menu.MenuRadioItem
group="sort-colorways"
id="sort-colorways_name-az"
label="Name (A-Z)"
checked={sortBy === SortOptions.NAME_AZ}
action={() => setSortBy(SortOptions.NAME_AZ)}
/>
<Menu.MenuRadioItem
group="sort-colorways"
id="sort-colorways_name-za"
label="Name (Z-A)"
checked={sortBy === SortOptions.NAME_ZA}
action={() => setSortBy(SortOptions.NAME_ZA)}
/>
<Menu.MenuRadioItem
group="sort-colorways"
id="sort-colorways_source-az"
label="Source (A-Z)"
checked={sortBy === SortOptions.SOURCE_AZ}
action={() => setSortBy(SortOptions.SOURCE_AZ)}
/>
<Menu.MenuRadioItem
group="sort-colorways"
id="sort-colorways_source-za"
label="Source (Z-A)"
checked={sortBy === SortOptions.SOURCE_ZA}
action={() => setSortBy(SortOptions.SOURCE_ZA)}
/>
</Menu.MenuGroup>
</Menu.Menu>
);
}
return (
<SelectorContainer modalProps={modalProps} isSettings={isSettings}>
<SelectorHeader isSettings={isSettings}>
{settings.selectorType !== "preview" ? <>
<TextInput
className="colorwaySelector-search"
placeholder="Search for Colorways..."
value={searchValue}
onChange={setSearchValue}
/>
<Tooltip text="Refresh Colorways...">
{({ onMouseEnter, onMouseLeave }) => <Popout
position="bottom"
align="right"
animation={Popout.Animation.NONE}
shouldShow={showReloadMenu}
onRequestClose={() => setShowReloadMenu(false)}
renderPopout={() => ReloadPopout(() => setShowReloadMenu(false))}
>
{(_, { isShown }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
style={{ marginLeft: "8px" }}
id="colorway-refreshcolorway"
onMouseEnter={isShown ? () => { } : onMouseEnter}
onMouseLeave={isShown ? () => { } : onMouseLeave}
onClick={() => {
setLoaderHeight("2px");
loadUI().then(() => setLoaderHeight("0px"));
}}
onContextMenu={() => { onMouseLeave(); setShowReloadMenu(!showReloadMenu); }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="20"
height="20"
style={{ padding: "6px", boxSizing: "content-box" }}
viewBox="0 0 24 24"
fill="currentColor"
>
<rect
y="0"
fill="none"
width="24"
height="24"
/>
<path
d="M6.351,6.351C7.824,4.871,9.828,4,12,4c4.411,0,8,3.589,8,8h2c0-5.515-4.486-10-10-10 C9.285,2,6.779,3.089,4.938,4.938L3,3v6h6L6.351,6.351z"
/>
<path
d="M17.649,17.649C16.176,19.129,14.173,20,12,20c-4.411,0-8-3.589-8-8H2c0,5.515,4.486,10,10,10 c2.716,0,5.221-1.089,7.062-2.938L21,21v-6h-6L17.649,17.649z"
/>
</svg>
</Button>}
</Popout>}
</Tooltip>
<Tooltip text="Create Colorway...">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
style={{ marginLeft: "8px" }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => openModal((props) => <CreatorModal
modalProps={props}
loadUIProps={loadUI}
/>)}
>
<PlusIcon width={20} height={20} style={{ padding: "6px", boxSizing: "content-box" }} />
</Button>}
</Tooltip>
<Tooltip text="Selector Options">
{({ onMouseEnter, onMouseLeave }) => <Popout
position="bottom"
align="right"
animation={Popout.Animation.NONE}
shouldShow={showSortingMenu}
onRequestClose={() => setShowSotringMenu(false)}
renderPopout={() => SortingPopout(() => setShowSotringMenu(false))}
>
{(_, { isShown }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
style={{ marginLeft: "8px" }}
onMouseEnter={isShown ? () => { } : onMouseEnter}
onMouseLeave={isShown ? () => { } : onMouseLeave}
onClick={() => { onMouseLeave(); setShowSotringMenu(!showSortingMenu); }}
>
<MoreIcon width={20} height={20} style={{ padding: "6px", boxSizing: "content-box" }} />
</Button>}
</Popout>}
</Tooltip>
<Tooltip text="Open Color Stealer">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
style={{ marginLeft: "8px" }}
id="colorway-opencolorstealer"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => openModal((props) => <ColorPickerModal modalProps={props} />)}
>
<PalleteIcon width={20} height={20} style={{ padding: "6px", boxSizing: "content-box" }} />
</Button>}
</Tooltip>
{isSettings ? <Select
className={"colorwaySelector-sources " + ButtonLooks.OUTLINED + " colorwaySelector-sources_settings"}
look={1}
popoutClassName="colorwaySelector-sourceSelect"
options={filters.map(filter => ({ label: filter.name, value: (filter.id as string) }))}
select={value => setVisibleSources(value)}
isSelected={value => visibleSources === value}
serialize={String}
popoutPosition="bottom" /> : <></>}
</> : <Text variant="heading-lg/semibold" tag="h1">
Preview...
</Text>}
</SelectorHeader>
<SelectorContent isSettings={isSettings}>
<div className="colorwaysLoader-barContainer"><div className="colorwaysLoader-bar" style={{ height: loaderHeight }} /></div>
{settings.selectorType === "multiple-selection" && <Forms.FormTitle>Available</Forms.FormTitle>}
<ScrollerThin style={{ maxHeight: settings.selectorType === "multiple-selection" ? "50%" : (isSettings ? "unset" : "450px") }} className={"ColorwaySelectorWrapper " + (viewMode === "grid" ? "ColorwaySelectorWrapper-grid" : "ColorwaySelectorWrapper-list") + (showLabelsInSelectorGridView ? " colorwaySelector-gridWithLabels" : "")}>
{(activeColorwayObject.sourceType === "temporary" && settings.selectorType === "normal" && settings.selectorType === "normal") && <Tooltip text="Temporary Colorway">
{({ onMouseEnter, onMouseLeave }) => <div
className={viewMode === "grid" ? "discordColorway" : `${radioBarItem} ${radioBarItemFilled} discordColorway-listItem`}
id="colorway-Temporary"
aria-checked={activeColorwayObject.id === "Auto" && activeColorwayObject.source === null}
onMouseEnter={viewMode === "grid" ? onMouseEnter : () => { }}
onMouseLeave={viewMode === "grid" ? onMouseLeave : () => { }}
onClick={async () => {
DataStore.set("activeColorwayObject", { id: null, css: null, sourceType: null, source: null });
setActiveColorwayObject({ id: null, css: null, sourceType: null, source: null });
ColorwayCSS.remove();
}}
>
{viewMode === "list" && <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor" />
{activeColorwayObject.id === "Temporary Colorway" && activeColorwayObject.sourceType === "temporary" && <circle cx="12" cy="12" r="5" fill="currentColor" />}
</svg>}
<div className="discordColorwayPreviewColorContainer">
<div
className="discordColorwayPreviewColor"
style={{ backgroundColor: "var(--brand-500)" }} />
<div
className="discordColorwayPreviewColor"
style={{ backgroundColor: "var(--background-primary)" }} />
<div
className="discordColorwayPreviewColor"
style={{ backgroundColor: "var(--background-secondary)" }} />
<div
className="discordColorwayPreviewColor"
style={{ backgroundColor: "var(--background-tertiary)" }} />
</div>
<div className="colorwaySelectionCircle">
{(activeColorwayObject.id === "Temporary Colorway" && activeColorwayObject.sourceType === "temporary" && viewMode === "grid") && <SelectionCircle />}
</div>
{(showLabelsInSelectorGridView || viewMode === "list") && <Text className={"colorwayLabel" + ((showLabelsInSelectorGridView && viewMode === "grid") ? " labelInGrid" : "")}>Temporary Colorway</Text>}
{viewMode === "list" && <>
<Tooltip text="Add Colorway">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={async e => {
e.stopPropagation();
const colorwayID = stringToHex(`#${colorToHex(getHex(getComputedStyle(document.body).getPropertyValue("--brand-500")))},#${colorToHex(getHex(getComputedStyle(document.body).getPropertyValue("--background-primary")))},#${colorToHex(getHex(getComputedStyle(document.body).getPropertyValue("--background-secondary")))},#${colorToHex(getHex(getComputedStyle(document.body).getPropertyValue("--background-tertiary")))}`);
openModal(props => <CreatorModal modalProps={props} colorwayID={colorwayID} loadUIProps={loadUI} />);
}}
>
<PlusIcon width={20} height={20} />
</Button>}
</Tooltip>
</>}
</div>}
</Tooltip>}
{getComputedStyle(document.body).getPropertyValue("--os-accent-color") && ["all", "official"].includes(visibleSources) && settings.selectorType === "normal" && "auto".includes(searchValue.toLowerCase()) ? <Tooltip text="Auto">
{({ onMouseEnter, onMouseLeave }) => <div
className={viewMode === "grid" ? "discordColorway" : `${radioBarItem} ${radioBarItemFilled} discordColorway-listItem`}
id="colorway-Auto"
aria-checked={activeColorwayObject.id === "Auto" && activeColorwayObject.source === null}
onMouseEnter={viewMode === "grid" ? onMouseEnter : () => { }}
onMouseLeave={viewMode === "grid" ? onMouseLeave : () => { }}
onClick={async () => {
const activeAutoPreset = await DataStore.get("activeAutoPreset");
if (activeColorwayObject.id === "Auto") {
DataStore.set("activeColorwayObject", { id: null, css: null, sourceType: null, source: null });
setActiveColorwayObject({ id: null, css: null, sourceType: null, source: null });
ColorwayCSS.remove();
} else {
if (!activeAutoPreset) {
openModal((props: ModalProps) => <AutoColorwaySelector autoColorwayId="" modalProps={props} onChange={autoPresetId => {
const demandedColorway = getAutoPresets(colorToHex(getComputedStyle(document.body).getPropertyValue("--os-accent-color")).slice(0, 6))[autoPresetId].preset();
ColorwayCSS.set(demandedColorway);
DataStore.set("activeColorwayObject", { id: "Auto", css: demandedColorway, sourceType: "online", source: null });
setActiveColorwayObject({ id: "Auto", css: demandedColorway, sourceType: "online", source: null });
}} />);
} else {
const autoColorway = getAutoPresets(colorToHex(getComputedStyle(document.body).getPropertyValue("--os-accent-color")).slice(0, 6))[activeAutoPreset].preset();
DataStore.set("activeColorwayObject", { id: "Auto", css: autoColorway, sourceType: "online", source: null });
setActiveColorwayObject({ id: "Auto", css: autoColorway, sourceType: "online", source: null });
ColorwayCSS.set(autoColorway);
}
}
}}
>
{viewMode === "list" && <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor" />
{activeColorwayObject.id === "Auto" && activeColorwayObject.source === null && <circle cx="12" cy="12" r="5" fill="currentColor" />}
</svg>}
<div className="discordColorwayPreviewColorContainer" style={{ backgroundColor: "var(--os-accent-color)" }} />
<div className="colorwaySelectionCircle">
{(activeColorwayObject.id === "Auto" && activeColorwayObject.source === null && viewMode === "grid") && <SelectionCircle />}
</div>
{(showLabelsInSelectorGridView || viewMode === "list") && <Text className={"colorwayLabel" + ((showLabelsInSelectorGridView && viewMode === "grid") ? " labelInGrid" : "")}>Auto</Text>}
<div
className="colorwayInfoIconContainer"
onClick={async (e) => {
e.stopPropagation();
const activeAutoPreset = await DataStore.get("activeAutoPreset");
openModal((props: ModalProps) => <AutoColorwaySelector autoColorwayId={activeAutoPreset} modalProps={props} onChange={autoPresetId => {
if (activeColorwayObject.id === "Auto") {
const demandedColorway = getAutoPresets(colorToHex(getComputedStyle(document.body).getPropertyValue("--os-accent-color")).slice(0, 6))[autoPresetId].preset();
DataStore.set("activeColorwayObject", { id: "Auto", css: demandedColorway, sourceType: "online", source: null });
setActiveColorwayObject({ id: "Auto", css: demandedColorway, sourceType: "online", source: null });
ColorwayCSS.set(demandedColorway);
}
}} />);
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" style={{ margin: "4px" }} viewBox="0 0 24 24" fill="currentColor">
<path d="M 21.2856,9.6 H 24 v 4.8 H 21.2868 C 20.9976,15.5172 20.52,16.5576 19.878,17.4768 L 21.6,19.2 19.2,21.6 17.478,19.8768 c -0.9216,0.642 -1.9596,1.1208 -3.078,1.4088 V 24 H 9.6 V 21.2856 C 8.4828,20.9976 7.4436,20.5188 6.5232,19.8768 L 4.8,21.6 2.4,19.2 4.1232,17.4768 C 3.4812,16.5588 3.0024,15.5184 2.7144,14.4 H 0 V 9.6 H 2.7144 C 3.0024,8.4816 3.48,7.4424 4.1232,6.5232 L 2.4,4.8 4.8,2.4 6.5232,4.1232 C 7.4424,3.48 8.4816,3.0024 9.6,2.7144 V 0 h 4.8 v 2.7132 c 1.1184,0.2892 2.1564,0.7668 3.078,1.4088 l 1.722,-1.7232 2.4,2.4 -1.7232,1.7244 c 0.642,0.9192 1.1208,1.9596 1.4088,3.0768 z M 12,16.8 c 2.65092,0 4.8,-2.14908 4.8,-4.8 0,-2.650968 -2.14908,-4.8 -4.8,-4.8 -2.650968,0 -4.8,2.149032 -4.8,4.8 0,2.65092 2.149032,4.8 4.8,4.8 z" />
</svg>
</div>
</div>}
</Tooltip> : <></>}
{(!getComputedStyle(document.body).getPropertyValue("--os-accent-color") || !["all", "official"].includes(visibleSources)) && !filters.filter(filter => filter.id === visibleSources)[0].sources.map(source => source.colorways).flat().length ? <Forms.FormTitle
style={{
marginBottom: 0,
width: "100%",
textAlign: "center"
}}
>
No colorways...
</Forms.FormTitle> : <></>}
{errorCode !== 0 && <Forms.FormTitle
style={{
marginBottom: 0,
width: "100%",
textAlign: "center"
}}
>
{errorCode === 1 && "Error: Invalid Colorway Source Format. If this error persists, contact the source author to resolve the issue."}
</Forms.FormTitle>}
{filters.map(filter => filter.id).includes(visibleSources) && (
filters
.filter(filter => filter.id === visibleSources)[0].sources
.map(({ colorways, source, type }) => colorways.map((colorway: Colorway) => ({ ...colorway, sourceType: type, source: source, preset: colorway.preset || (colorway.isGradient ? "Gradient" : "Default") })))
.flat()
.sort((a, b) => {
switch (sortBy) {
case SortOptions.NAME_AZ:
return a.name.localeCompare(b.name);
case SortOptions.NAME_ZA:
return b.name.localeCompare(a.name);
case SortOptions.SOURCE_AZ:
return a.source.localeCompare(b.source);
case SortOptions.SOURCE_ZA:
return b.source.localeCompare(a.source);
default:
return a.name.localeCompare(b.name);
}
})
.map((color: Colorway) => {
const colors: string[] = color.colors || [
"accent",
"primary",
"secondary",
"tertiary",
];
return (color.name.toLowerCase().includes(searchValue.toLowerCase()) ?
<Tooltip text={color.name}>
{({ onMouseEnter, onMouseLeave }) => {
return (
<div
className={viewMode === "grid" ? "discordColorway" : `${radioBarItem} ${radioBarItemFilled} discordColorway-listItem`}
id={"colorway-" + color.name}
onMouseEnter={viewMode === "grid" ? onMouseEnter : () => { }}
onMouseLeave={viewMode === "grid" ? onMouseLeave : () => { }}
aria-checked={activeColorwayObject.id === color.name && activeColorwayObject.source === color.source}
onClick={async () => {
if (settings.selectorType === "normal") {
const [
onDemandWays,
onDemandWaysTintedText,
onDemandWaysDiscordSaturation,
onDemandWaysOsAccentColor
] = await DataStore.getMany([
"onDemandWays",
"onDemandWaysTintedText",
"onDemandWaysDiscordSaturation",
"onDemandWaysOsAccentColor"
]);
if (activeColorwayObject.id === color.name && activeColorwayObject.source === color.source) {
DataStore.set("activeColorwayObject", { id: null, css: null, sourceType: null, source: null });
setActiveColorwayObject({ id: null, css: null, sourceType: null, source: null });
ColorwayCSS.remove();
} else {
if (onDemandWays) {
const demandedColorway = !color.isGradient ? generateCss(
colorToHex(color.primary),
colorToHex(color.secondary),
colorToHex(color.tertiary),
colorToHex(onDemandWaysOsAccentColor ? getComputedStyle(document.body).getPropertyValue("--os-accent-color") : color.accent).slice(0, 6),
onDemandWaysTintedText,
onDemandWaysDiscordSaturation,
undefined,
color.name
) : gradientBase(colorToHex(onDemandWaysOsAccentColor ? getComputedStyle(document.body).getPropertyValue("--os-accent-color") : color.accent), onDemandWaysDiscordSaturation) + `:root:root {--custom-theme-background: linear-gradient(${color.linearGradient})}`;
ColorwayCSS.set(demandedColorway);
setActiveColorwayObject({ id: color.name, css: demandedColorway, sourceType: color.type, source: color.source });
DataStore.set("activeColorwayObject", { id: color.name, css: demandedColorway, sourceType: color.type, source: color.source });
} else {
ColorwayCSS.set(color["dc-import"]);
setActiveColorwayObject({ id: color.name, css: color["dc-import"], sourceType: color.type, source: color.source });
DataStore.set("activeColorwayObject", { id: color.name, css: color["dc-import"], sourceType: color.type, source: color.source });
}
}
}
if (settings.selectorType === "multiple-selection") {
setSelectedColorways([...selectedColorways, color]);
}
}}
>
{(viewMode === "list" && settings.selectorType === "normal") && <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor" />
{activeColorwayObject.id === color.name && activeColorwayObject.source === color.source && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />}
</svg>}
<div className="discordColorwayPreviewColorContainer">
{!color.isGradient ? colors.map((colorItm) => <div
className="discordColorwayPreviewColor"
style={{
backgroundColor: color[colorItm],
}}
/>) : <div
className="discordColorwayPreviewColor"
style={{
background: `linear-gradient(${color.linearGradient})`,
}}
/>}
</div>
{settings.selectorType === "normal" && <div className="colorwaySelectionCircle">
{(activeColorwayObject.id === color.name && activeColorwayObject.source === color.source && viewMode === "grid") && <SelectionCircle />}
</div>}
{(showLabelsInSelectorGridView || viewMode === "list") && <Text className={"colorwayLabel" + ((showLabelsInSelectorGridView && viewMode === "grid") ? " labelInGrid" : "")}>{color.name}</Text>}
{settings.selectorType === "normal" && <div
className="colorwayInfoIconContainer"
onClick={(e) => {
e.stopPropagation();
openModal((props) => <ColorwayInfoModal
modalProps={props}
colorway={color}
loadUIProps={loadUI}
/>);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" />
</svg>
</div>}
{viewMode === "list" && <>
<Tooltip text="Copy Colorway CSS">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={async e => {
e.stopPropagation();
Clipboard.copy(color["dc-import"]);
Toasts.show({
message: "Copied Colorway CSS Successfully",
type: 1,
id: "copy-colorway-css-notify",
});
}}
>
<CodeIcon width={20} height={20} />
</Button>}</Tooltip>
<Tooltip text="Copy Colorway ID">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={async e => {
e.stopPropagation();
const colorwayIDArray = `${color.accent},${color.primary},${color.secondary},${color.tertiary}|n:${color.name}${color.preset ? `|p:${color.preset}` : ""}`;
const colorwayID = stringToHex(colorwayIDArray);
Clipboard.copy(colorwayID);
Toasts.show({
message: "Copied Colorway ID Successfully",
type: 1,
id: "copy-colorway-id-notify",
});
}}
>
<IDIcon width={20} height={20} />
</Button>}
</Tooltip>
{(color.sourceType === "offline" && settings.selectorType !== "preview") && <Tooltip text="Delete Colorway">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.RED}
look={Button.Looks.OUTLINED}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={async e => {
e.stopPropagation();
const oldStores = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).filter(sourcee => sourcee.name !== color.source);
const storeToModify = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).filter(sourcee => sourcee.name === color.source)[0];
const newStore = { name: storeToModify.name, colorways: storeToModify.colorways.filter(colorway => colorway.name !== color.name) };
DataStore.set("customColorways", [...oldStores, newStore]);
setCustomColorwayData([...oldStores, newStore].map((colorSrc: { name: string, colorways: Colorway[], id?: string; }) =>
({ type: "offline", source: colorSrc.name, colorways: colorSrc.colorways })));
if ((await DataStore.get("activeColorwayObject")).id === color.name) {
DataStore.set("activeColorwayObject", { id: null, css: null, sourceType: null, source: null });
setActiveColorwayObject({ id: null, css: null, sourceType: null, source: null });
ColorwayCSS.remove();
}
}}
>
<DeleteIcon width={20} height={20} />
</Button>}
</Tooltip>}
</>}
</div>
);
}}
</Tooltip> : <></>
);
})
)}
</ScrollerThin>
{settings.selectorType === "multiple-selection" && <>
<Forms.FormTitle style={{ marginTop: "8px" }}>Selected</Forms.FormTitle>
<ScrollerThin style={{ maxHeight: "50%" }} className={"ColorwaySelectorWrapper " + (viewMode === "grid" ? "ColorwaySelectorWrapper-grid" : "ColorwaySelectorWrapper-list") + (showLabelsInSelectorGridView ? " colorwaySelector-gridWithLabels" : "")}>
{selectedColorways.map((color: Colorway, i: number) => {
const colors: string[] = color.colors || [
"accent",
"primary",
"secondary",
"tertiary",
];
return <Tooltip text={color.name}>
{({ onMouseEnter, onMouseLeave }) => {
return (
<div
className={viewMode === "grid" ? "discordColorway" : `${radioBarItem} ${radioBarItemFilled} discordColorway-listItem`}
id={"colorway-" + color.name}
onMouseEnter={viewMode === "grid" ? onMouseEnter : () => { }}
onMouseLeave={viewMode === "grid" ? onMouseLeave : () => { }}
aria-checked={activeColorwayObject.id === color.name && activeColorwayObject.source === color.source}
onClick={() => setSelectedColorways(selectedColorways.filter((colorway, ii) => ii !== i))}
>
{viewMode === "list" && <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="currentColor" />
{activeColorwayObject.id === color.name && activeColorwayObject.source === color.source && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />}
</svg>}
<div className="discordColorwayPreviewColorContainer">
{!color.isGradient ? colors.map((colorItm) => <div
className="discordColorwayPreviewColor"
style={{
backgroundColor: color[colorItm],
}}
/>) : <div
className="discordColorwayPreviewColor"
style={{
background: `linear-gradient(${color.linearGradient})`,
}}
/>}
</div>
<div className="colorwaySelectionCircle">
{(activeColorwayObject.id === color.name && activeColorwayObject.source === color.source && viewMode === "grid") && <SelectionCircle />}
</div>
{(showLabelsInSelectorGridView || viewMode === "list") && <Text className={"colorwayLabel" + ((showLabelsInSelectorGridView && viewMode === "grid") ? " labelInGrid" : "")}>{color.name}</Text>}
{viewMode === "list" && <>
<Tooltip text="Copy Colorway CSS">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={async e => {
e.stopPropagation();
Clipboard.copy(color["dc-import"]);
Toasts.show({
message: "Copied Colorway CSS Successfully",
type: 1,
id: "copy-colorway-css-notify",
});
}}
>
<CodeIcon width={20} height={20} />
</Button>}</Tooltip>
<Tooltip text="Copy Colorway ID">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={async e => {
e.stopPropagation();
const colorwayIDArray = `${color.accent},${color.primary},${color.secondary},${color.tertiary}|n:${color.name}${color.preset ? `|p:${color.preset}` : ""}`;
const colorwayID = stringToHex(colorwayIDArray);
Clipboard.copy(colorwayID);
Toasts.show({
message: "Copied Colorway ID Successfully",
type: 1,
id: "copy-colorway-id-notify",
});
}}
>
<IDIcon width={20} height={20} />
</Button>}
</Tooltip>
</>}
</div>
);
}}
</Tooltip>;
})}
</ScrollerThin>
</>}
</SelectorContent>
{(!isSettings && settings.selectorType !== "preview") ? <ModalFooter>
<Button
size={Button.Sizes.MEDIUM}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
style={{ marginLeft: "8px" }}
onClick={() => {
SettingsRouter.open("ColorwaysSettings");
modalProps.onClose();
}}
>
Settings
</Button>
<Button
size={Button.Sizes.MEDIUM}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={() => modalProps.onClose()}
>
Close
</Button>
<Select
className={"colorwaySelector-sources " + ButtonLooks.OUTLINED}
look={1}
popoutClassName="colorwaySelector-sourceSelect"
options={filters.map(filter => { return { label: filter.name, value: (filter.id as string) }; })}
select={value => setVisibleSources(value)}
isSelected={value => visibleSources === value}
serialize={String}
popoutPosition="top" />
</ModalFooter> : <></>}
</SelectorContainer >
);
}

View file

@ -1,84 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { SettingsTab } from "@components/VencordSettings/shared";
import { Switch, useCallback, useEffect, useState } from "@webpack/common";
export default function () {
const [onDemand, setOnDemand] = useState<boolean>(false);
const [onDemandTinted, setOnDemandTinted] = useState<boolean>(false);
const [onDemandDiscordSat, setOnDemandDiscordSat] = useState<boolean>(false);
const [onDemandOsAccent, setOnDemandOsAccent] = useState<boolean>(false);
async function loadUI() {
const [
onDemandWays,
onDemandWaysTintedText,
onDemandWaysDiscordSaturation,
onDemandWaysOsAccentColor
] = await DataStore.getMany([
"onDemandWays",
"onDemandWaysTintedText",
"onDemandWaysDiscordSaturation",
"onDemandWaysOsAccentColor"
]);
setOnDemand(onDemandWays);
setOnDemandTinted(onDemandWaysTintedText);
setOnDemandDiscordSat(onDemandWaysDiscordSaturation);
if (getComputedStyle(document.body).getPropertyValue("--os-accent-color") !== "") {
setOnDemandOsAccent(onDemandWaysOsAccentColor);
}
}
const cached_loadUI = useCallback(loadUI, []);
useEffect(() => {
cached_loadUI();
}, []);
return <SettingsTab title="On-Demand">
<Switch
value={onDemand}
onChange={(v: boolean) => {
setOnDemand(v);
DataStore.set("onDemandWays", v);
}}
note="Always utilise the latest of what DiscordColorways has to offer. CSS is being directly generated on the device and gets applied in the place of the normal import/CSS given by the colorway."
>
Enable Colorways On Demand
</Switch>
<Switch
value={onDemandTinted}
onChange={(v: boolean) => {
setOnDemandTinted(v);
DataStore.set("onDemandWaysTintedText", v);
}}
disabled={!onDemand}
>
Use tinted text
</Switch>
<Switch
value={onDemandDiscordSat}
onChange={(v: boolean) => {
setOnDemandDiscordSat(v);
DataStore.set("onDemandWaysDiscordSaturation", v);
}}
disabled={!onDemand}
>
Use Discord's saturation
</Switch>
<Switch
hideBorder
value={onDemandOsAccent}
onChange={(v: boolean) => {
setOnDemandOsAccent(v);
DataStore.set("onDemandWaysOsAccentColor", v);
}}
disabled={!onDemand || !getComputedStyle(document.body).getPropertyValue("--os-accent-color")}
>
Use Operating System's Accent Color
</Switch>
</SettingsTab>;
}

View file

@ -1,184 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { SettingsTab } from "@components/VencordSettings/shared";
import {
FluxDispatcher,
Forms,
Switch,
Text,
useEffect,
useState
} from "@webpack/common";
import { FluxEvents } from "@webpack/types";
import { versionData } from "../../";
import { fallbackColorways } from "../../constants";
import { Colorway } from "../../types";
export default function () {
const [colorways, setColorways] = useState<Colorway[]>([]);
const [customColorways, setCustomColorways] = useState<Colorway[]>([]);
const [colorsButtonVisibility, setColorsButtonVisibility] = useState<boolean>(false);
const [isButtonThin, setIsButtonThin] = useState<boolean>(false);
const [showLabelsInSelectorGridView, setShowLabelsInSelectorGridView] = useState<boolean>(false);
useEffect(() => {
(async function () {
const [
customColorways,
colorwaySourceFiles,
showColorwaysButton,
useThinMenuButton,
showLabelsInSelectorGridView
] = await DataStore.getMany([
"customColorways",
"colorwaySourceFiles",
"showColorwaysButton",
"useThinMenuButton",
"showLabelsInSelectorGridView"
]);
const responses: Response[] = await Promise.all(
colorwaySourceFiles.map((url: string) =>
fetch(url)
)
);
const data = await Promise.all(
responses.map((res: Response) =>
res.json().catch(() => { return { colorways: [] }; })
));
const colorways = data.flatMap(json => json.colorways);
setColorways(colorways || fallbackColorways);
setCustomColorways(customColorways.map(source => source.colorways).flat(2));
setColorsButtonVisibility(showColorwaysButton);
setIsButtonThin(useThinMenuButton);
setShowLabelsInSelectorGridView(showLabelsInSelectorGridView);
})();
}, []);
return <SettingsTab title="Settings">
<div className="colorwaysSettingsPage-wrapper">
<Forms.FormTitle tag="h5">Quick Switch</Forms.FormTitle>
<Switch
value={colorsButtonVisibility}
onChange={(v: boolean) => {
setColorsButtonVisibility(v);
DataStore.set("showColorwaysButton", v);
FluxDispatcher.dispatch({
type: "COLORWAYS_UPDATE_BUTTON_VISIBILITY" as FluxEvents,
isVisible: v
});
}}
note="Shows a button on the top of the servers list that opens a colorway selector modal."
>
Enable Quick Switch
</Switch>
<Switch
value={isButtonThin}
onChange={(v: boolean) => {
setIsButtonThin(v);
DataStore.set("useThinMenuButton", v);
FluxDispatcher.dispatch({
type: "COLORWAYS_UPDATE_BUTTON_HEIGHT" as FluxEvents,
isTall: v
});
}}
note="Replaces the icon on the colorways launcher button with text, making it more compact."
>
Use thin Quick Switch button
</Switch>
<Forms.FormTitle tag="h5">Selector</Forms.FormTitle>
<Switch
value={showLabelsInSelectorGridView}
onChange={(v: boolean) => {
setShowLabelsInSelectorGridView(v);
DataStore.set("showLabelsInSelectorGridView", v);
}}
>
Show labels in Grid View
</Switch>
<Flex flexDirection="column" style={{ gap: 0 }}>
<h1 style={{
fontFamily: "var(--font-headline)",
fontSize: "24px",
color: "var(--header-primary)",
lineHeight: "31px",
marginBottom: "0"
}}>
Discord <span style={{
fontFamily: "var(--font-display)",
fontSize: "24px",
backgroundColor: "var(--brand-500)",
padding: "0 4px",
borderRadius: "4px"
}}>Colorways</span>
</h1>
<Text
variant="text-xs/normal"
style={{
color: "var(--text-normal)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "12px"
}}
>by Project Colorway</Text>
<Forms.FormTitle style={{ marginBottom: 0 }}>
Plugin Version:
</Forms.FormTitle>
<Text
variant="text-xs/normal"
style={{
color: "var(--text-muted)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "8px"
}}
>
{versionData.pluginVersion}
</Text>
<Forms.FormTitle style={{ marginBottom: 0 }}>
Creator Version:
</Forms.FormTitle>
<Text
variant="text-xs/normal"
style={{
color: "var(--text-muted)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "8px"
}}
>
{versionData.creatorVersion}{" (Stable)"}
</Text>
<Forms.FormTitle style={{ marginBottom: 0 }}>
Loaded Colorways:
</Forms.FormTitle>
<Text
variant="text-xs/normal"
style={{
color: "var(--text-muted)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "8px"
}}
>
{[...colorways, ...customColorways].length + 1}
</Text>
<Forms.FormTitle style={{ marginBottom: 0 }}>
Project Repositories:
</Forms.FormTitle>
<Forms.FormText style={{ marginBottom: "8px" }}>
<Link href="https://github.com/DaBluLite/DiscordColorways">DiscordColorways</Link>
<br />
<Link href="https://github.com/DaBluLite/ProjectColorway">Project Colorway</Link>
</Forms.FormText>
</Flex>
</div>
</SettingsTab>;
}

View file

@ -1,387 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { Flex } from "@components/Flex";
import { CopyIcon, DeleteIcon, PlusIcon } from "@components/Icons";
import { SettingsTab } from "@components/VencordSettings/shared";
import { Logger } from "@utils/Logger";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { chooseFile, saveFile } from "@utils/web";
import { findByProps } from "@webpack";
import { Button, Clipboard, Forms, ScrollerThin, Text, TextInput, useEffect, useState } from "@webpack/common";
import { defaultColorwaySource } from "../../constants";
import { Colorway } from "../../types";
import { DownloadIcon, ImportIcon } from "../Icons";
import Spinner from "../Spinner";
export function StoreNameModal({ modalProps, originalName, onFinish, conflicting }: { modalProps: ModalProps, originalName: string, onFinish: (newName: string) => Promise<void>, conflicting: boolean; }) {
const [error, setError] = useState<string>("");
const [newStoreName, setNewStoreName] = useState<string>(originalName);
return <ModalRoot {...modalProps}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1">{conflicting ? "Duplicate Store Name" : "Give this store a name"}</Text>
</ModalHeader>
<ModalContent>
{conflicting ? <Text>A store with the same name already exists. Please give a different name to the imported store:</Text> : <></>}
<Forms.FormTitle>Name:</Forms.FormTitle>
<TextInput error={error} value={newStoreName} onChange={e => setNewStoreName(e)} style={{ marginBottom: "16px" }} />
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={async () => {
setError("");
if ((await DataStore.get("customColorways")).map(store => store.name).includes(newStoreName)) {
return setError("Error: Store name already exists");
}
onFinish(newStoreName);
modalProps.onClose();
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}
function AddOnlineStoreModal({ modalProps, onFinish }: { modalProps: ModalProps, onFinish: (name: string, url: string) => void; }) {
const [colorwaySourceName, setColorwaySourceName] = useState<string>("");
const [colorwaySourceURL, setColorwaySourceURL] = useState<string>("");
const [nameError, setNameError] = useState<string>("");
const [URLError, setURLError] = useState<string>("");
const [nameReadOnly, setNameReadOnly] = useState<boolean>(false);
return <ModalRoot {...modalProps}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1">
Add a source:
</Text>
</ModalHeader>
<ModalContent>
<Forms.FormTitle>Name:</Forms.FormTitle>
<TextInput
placeholder="Enter a valid Name..."
onChange={setColorwaySourceName}
value={colorwaySourceName}
error={nameError}
readOnly={nameReadOnly}
disabled={nameReadOnly}
/>
<Forms.FormTitle style={{ marginTop: "8px" }}>URL:</Forms.FormTitle>
<TextInput
placeholder="Enter a valid URL..."
onChange={value => {
setColorwaySourceURL(value);
if (value === defaultColorwaySource) {
setNameReadOnly(true);
setColorwaySourceName("Project Colorway");
}
}}
value={colorwaySourceURL}
error={URLError}
style={{ marginBottom: "16px" }}
/>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={async () => {
const sourcesArr: { name: string, url: string; }[] = (await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
if (!colorwaySourceName) {
setNameError("Error: Please enter a valid name");
}
else if (!colorwaySourceURL) {
setURLError("Error: Please enter a valid URL");
}
else if (sourcesArr.map(s => s.name).includes(colorwaySourceName)) {
setNameError("Error: An online source with that name already exists");
}
else if (sourcesArr.map(s => s.url).includes(colorwaySourceURL)) {
setURLError("Error: An online source with that url already exists");
} else {
onFinish(colorwaySourceName, colorwaySourceURL);
modalProps.onClose();
}
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
}
export default function () {
const [colorwaySourceFiles, setColorwaySourceFiles] = useState<{ name: string, url: string; }[]>([]);
const [customColorwayStores, setCustomColorwayStores] = useState<{ name: string, colorways: Colorway[]; }[]>([]);
const { item: radioBarItem, itemFilled: radioBarItemFilled } = findByProps("radioBar");
useEffect(() => {
(async function () {
setColorwaySourceFiles(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
})();
}, []);
return <SettingsTab title="Sources">
<Flex style={{ gap: "0", marginBottom: "8px", alignItems: "center" }}>
<Forms.FormTitle tag="h5" style={{ marginBottom: 0, flexGrow: 1 }}>Online</Forms.FormTitle>
<Button
className="colorwaysSettings-colorwaySourceAction"
innerClassName="colorwaysSettings-iconButtonInner"
style={{ flexShrink: "0" }}
size={Button.Sizes.SMALL}
color={Button.Colors.TRANSPARENT}
onClick={() => {
openModal(props => <AddOnlineStoreModal modalProps={props} onFinish={async (name, url) => {
await DataStore.set("colorwaySourceFiles", [...await DataStore.get("colorwaySourceFiles"), { name: name, url: url }]);
setColorwaySourceFiles([...await DataStore.get("colorwaySourceFiles"), { name: name, url: url }]);
}} />);
}}>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="img"
width="14"
height="14"
viewBox="0 0 24 24">
<path
fill="currentColor"
d="M20 11.1111H12.8889V4H11.1111V11.1111H4V12.8889H11.1111V20H12.8889V12.8889H20V11.1111Z"
/>
</svg>
Add...
</Button>
</Flex>
<ScrollerThin orientation="vertical" style={{ maxHeight: "50%" }} className="colorwaysSettings-sourceScroller">
{!colorwaySourceFiles.length && <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }} onClick={() => {
DataStore.set("colorwaySourceFiles", [{ name: "Project Colorway", url: defaultColorwaySource }]);
setColorwaySourceFiles([{ name: "Project Colorway", url: defaultColorwaySource }]);
}}>
<PlusIcon width={24} height={24} />
<Text className="colorwaysSettings-colorwaySourceLabel">
Add Project Colorway Source
</Text>
</div>}
{colorwaySourceFiles.map((colorwaySourceFile: { name: string, url: string; }, i: number) => <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<div className="hoverRoll">
<Text className="colorwaysSettings-colorwaySourceLabel hoverRoll_normal">
{colorwaySourceFile.name} {colorwaySourceFile.url === defaultColorwaySource && <div className="colorways-badge">Built-In</div>}
</Text>
<Text className="colorwaysSettings-colorwaySourceLabel hoverRoll_hovered">
{colorwaySourceFile.url}
</Text>
</div>
<Flex style={{ marginLeft: "auto", gap: "8px" }}>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={() => { Clipboard.copy(colorwaySourceFile.url); }}
>
<CopyIcon width={14} height={14} /> Copy URL
</Button>
{colorwaySourceFile.url !== defaultColorwaySource
&& <>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={async () => {
openModal(props => <StoreNameModal conflicting={false} modalProps={props} originalName={colorwaySourceFile.name || ""} onFinish={async e => {
const modal = openModal(propss => <ModalRoot {...propss} className="colorwaysLoadingModal"><Spinner style={{ color: "#ffffff" }} /></ModalRoot>);
const res = await fetch(colorwaySourceFile.url);
const data = await res.json();
DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: data.colorways || [] }]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
closeModal(modal);
}} />);
}}
>
<DownloadIcon width={14} height={14} /> Download...
</Button>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
look={Button.Looks.OUTLINED}
onClick={async () => {
DataStore.set("colorwaySourceFiles", (await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).filter((src, ii) => ii !== i));
setColorwaySourceFiles((await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).filter((src, ii) => ii !== i));
}}
>
<DeleteIcon width={14} height={14} /> Remove
</Button>
</>}
</Flex>
</div>
)}
</ScrollerThin>
<Flex style={{ gap: "0", marginBottom: "8px", alignItems: "center" }}>
<Forms.FormTitle tag="h5" style={{ marginBottom: 0, flexGrow: 1 }}>Offline</Forms.FormTitle>
<Button
className="colorwaysSettings-colorwaySourceAction"
innerClassName="colorwaysSettings-iconButtonInner"
style={{ flexShrink: "0", marginLeft: "8px" }}
size={Button.Sizes.SMALL}
color={Button.Colors.TRANSPARENT}
onClick={async () => {
if (IS_DISCORD_DESKTOP) {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
{ name: "DiscordColorways Offline Store", extensions: ["json"] },
{ name: "all", extensions: ["*"] }
]
});
if (file) {
try {
if ((await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]).map(store => store.name).includes(JSON.parse(new TextDecoder().decode(file.data)).name)) {
openModal(props => <StoreNameModal conflicting modalProps={props} originalName={JSON.parse(new TextDecoder().decode(file.data)).name} onFinish={async e => {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: JSON.parse(new TextDecoder().decode(file.data)).colorways }]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
}} />);
} else {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), JSON.parse(new TextDecoder().decode(file.data))]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
}
} catch (err) {
new Logger("DiscordColorways").error(err);
}
}
} else {
const file = await chooseFile("application/json");
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
try {
if ((await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]).map(store => store.name).includes(JSON.parse(reader.result as string).name)) {
openModal(props => <StoreNameModal conflicting modalProps={props} originalName={JSON.parse(reader.result as string).name} onFinish={async e => {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: JSON.parse(reader.result as string).colorways }]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
}} />);
} else {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), JSON.parse(reader.result as string)]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
}
} catch (err) {
new Logger("DiscordColorways").error(err);
}
};
reader.readAsText(file);
}
}}
>
<ImportIcon width={14} height={14} />
Import...
</Button>
<Button
className="colorwaysSettings-colorwaySourceAction"
innerClassName="colorwaysSettings-iconButtonInner"
style={{ flexShrink: "0", marginLeft: "8px" }}
size={Button.Sizes.SMALL}
color={Button.Colors.TRANSPARENT}
onClick={() => {
openModal(props => <StoreNameModal conflicting={false} modalProps={props} originalName="" onFinish={async e => {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: [] }]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
props.onClose();
}} />);
}}>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="img"
width="14"
height="14"
viewBox="0 0 24 24">
<path
fill="currentColor"
d="M20 11.1111H12.8889V4H11.1111V11.1111H4V12.8889H11.1111V20H12.8889V12.8889H20V11.1111Z"
/>
</svg>
New...
</Button>
</Flex>
<ScrollerThin orientation="vertical" style={{ maxHeight: "50%" }} className="colorwaysSettings-sourceScroller">
{getComputedStyle(document.body).getPropertyValue("--os-accent-color") ? <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<Flex style={{ gap: 0, alignItems: "center", width: "100%", height: "30px" }}>
<Text className="colorwaysSettings-colorwaySourceLabel">OS Accent Color{" "}
<div className="colorways-badge">Built-In</div>
</Text>
</Flex>
</div> : <></>}
{customColorwayStores.map(({ name: customColorwaySourceName, colorways: offlineStoreColorways }) => <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<Text className="colorwaysSettings-colorwaySourceLabel">
{customColorwaySourceName}
</Text>
<Flex style={{ marginLeft: "auto", gap: "8px" }}>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={async () => {
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(JSON.stringify({ "name": customColorwaySourceName, "colorways": [...offlineStoreColorways] }), `${customColorwaySourceName.replaceAll(" ", "-").toLowerCase()}.colorways.json`);
} else {
saveFile(new File([JSON.stringify({ "name": customColorwaySourceName, "colorways": [...offlineStoreColorways] })], `${customColorwaySourceName.replaceAll(" ", "-").toLowerCase()}.colorways.json`, { type: "application/json" }));
}
}}
>
<DownloadIcon width={14} height={14} /> Export as...
</Button>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
look={Button.Looks.OUTLINED}
onClick={async () => {
var sourcesArr: { name: string, colorways: Colorway[]; }[] = [];
const customColorwaySources = await DataStore.get("customColorways");
customColorwaySources.map((source: { name: string, colorways: Colorway[]; }) => {
if (source.name !== customColorwaySourceName) {
sourcesArr.push(source);
}
});
DataStore.set("customColorways", sourcesArr);
setCustomColorwayStores(sourcesArr);
}}
>
<DeleteIcon width={20} height={20} /> Remove
</Button>
</Flex>
</div>
)}
</ScrollerThin>
</SettingsTab>;
}

View file

@ -1,149 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { SettingsTab } from "@components/VencordSettings/shared";
import { getTheme, Theme } from "@utils/discord";
import { openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { Button, ScrollerThin, Text, TextInput, Tooltip, useEffect, useState } from "@webpack/common";
import { StoreItem } from "../../types";
import { DownloadIcon, PalleteIcon } from "../Icons";
import Selector from "../Selector";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
function GithubIcon() {
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
return <img src={src} alt="GitHub" />;
}
export default function () {
const [storeObject, setStoreObject] = useState<StoreItem[]>([]);
const [colorwaySourceFiles, setColorwaySourceFiles] = useState<{ name: string, url: string; }[]>([]);
const [searchValue, setSearchValue] = useState<string>("");
useEffect(() => {
if (!searchValue) {
(async function () {
const res: Response = await fetch("https://dablulite.vercel.app/");
const data = await res.json();
setStoreObject(data.sources);
setColorwaySourceFiles(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
})();
}
}, []);
const { item: radioBarItem, itemFilled: radioBarItemFilled } = findByProps("radioBar");
return <SettingsTab title="Colorway Store">
<Flex style={{ gap: "0", marginBottom: "8px" }}>
<TextInput
className="colorwaySelector-search"
placeholder="Search for sources..."
value={searchValue}
onChange={setSearchValue}
/>
<Tooltip text="Refresh...">
{({ onMouseEnter, onMouseLeave }) => <Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
style={{ marginLeft: "8px" }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={async function () {
const res: Response = await fetch("https://dablulite.vercel.app/");
const data = await res.json();
setStoreObject(data.sources);
setColorwaySourceFiles(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="20"
height="20"
style={{ padding: "6px", boxSizing: "content-box" }}
viewBox="0 0 24 24"
fill="currentColor"
>
<rect
y="0"
fill="none"
width="24"
height="24"
/>
<path
d="M6.351,6.351C7.824,4.871,9.828,4,12,4c4.411,0,8,3.589,8,8h2c0-5.515-4.486-10-10-10 C9.285,2,6.779,3.089,4.938,4.938L3,3v6h6L6.351,6.351z"
/>
<path
d="M17.649,17.649C16.176,19.129,14.173,20,12,20c-4.411,0-8-3.589-8-8H2c0,5.515,4.486,10,10,10 c2.716,0,5.221-1.089,7.062-2.938L21,21v-6h-6L17.649,17.649z"
/>
</svg>
</Button>}
</Tooltip>
</Flex>
<ScrollerThin orientation="vertical" className="colorwaysSettings-sourceScroller">
{storeObject.map((item: StoreItem) =>
item.name.toLowerCase().includes(searchValue.toLowerCase()) ? <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<Flex flexDirection="column" style={{ gap: ".5rem", marginBottom: "8px" }}>
<Text className="colorwaysSettings-colorwaySourceLabelHeader">
{item.name}
</Text>
<Text className="colorwaysSettings-colorwaySourceDesc">
{item.description}
</Text>
<Text className="colorwaysSettings-colorwaySourceDesc" style={{ opacity: ".8" }}>
by {item.authorGh}
</Text>
</Flex>
<Flex style={{ gap: "8px", alignItems: "center", width: "100%" }}>
<Link href={"https://github.com/" + item.authorGh}><GithubIcon /></Link>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.SMALL}
color={colorwaySourceFiles.map(source => source.name).includes(item.name) ? Button.Colors.RED : Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
style={{ marginLeft: "auto" }}
onClick={async () => {
if (colorwaySourceFiles.map(source => source.name).includes(item.name)) {
const sourcesArr: { name: string, url: string; }[] = colorwaySourceFiles.filter(source => source.name !== item.name);
DataStore.set("colorwaySourceFiles", sourcesArr);
setColorwaySourceFiles(sourcesArr);
} else {
const sourcesArr: { name: string, url: string; }[] = [...colorwaySourceFiles, { name: item.name, url: item.url }];
DataStore.set("colorwaySourceFiles", sourcesArr);
setColorwaySourceFiles(sourcesArr);
}
}}
>
{colorwaySourceFiles.map(source => source.name).includes(item.name) ? <><DeleteIcon width={14} height={14} /> Remove</> : <><DownloadIcon width={14} height={14} /> Add to Sources</>}
</Button>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={async () => {
openModal(props => <Selector modalProps={props} settings={{ selectorType: "preview", previewSource: item.url }} />);
}}
>
<PalleteIcon width={14} height={14} />{" "}Preview
</Button>
</Flex>
</div> : <></>
)}
</ScrollerThin>
</SettingsTab>;
}

View file

@ -1,19 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CSSProperties } from "react";
export default function ({ className, style }: { className?: string, style?: CSSProperties; }) {
return <div className={"colorwaysBtn-spinner" + (className ? ` ${className}` : "")} role="img" aria-label="Loading" style={style}>
<div className="colorwaysBtn-spinnerInner">
<svg className="colorwaysBtn-spinnerCircular" viewBox="25 25 50 50" fill="currentColor">
<circle className="colorwaysBtn-spinnerBeam colorwaysBtn-spinnerBeam3" cx="50" cy="50" r="20" />
<circle className="colorwaysBtn-spinnerBeam colorwaysBtn-spinnerBeam2" cx="50" cy="50" r="20" />
<circle className="colorwaysBtn-spinnerBeam" cx="50" cy="50" r="20" />
</svg>
</div>
</div>;
}

View file

@ -1,150 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ModalProps, ModalRoot, openModal } from "@utils/modal";
import { Text } from "@webpack/common";
import { HexToHSL } from "../utils";
import { CloseIcon } from "./Icons";
export default function ThemePreview({
accent,
primary,
secondary,
tertiary,
previewCSS,
modalProps,
isModal
}: {
accent: string,
primary: string,
secondary: string,
tertiary: string,
previewCSS?: string,
modalProps?: ModalProps,
isModal?: boolean;
}) {
return <>
<style>
{".colorwaysPreview-wrapper {color: var(--header-secondary); box-shadow: var(--legacy-elevation-border);}" + previewCSS}
</style>
<div
className="colorwaysPreview-wrapper"
style={{ background: `var(--dc-overlay-app-frame, ${tertiary})` }}
>
<div className="colorwaysPreview-titlebar" />
<div className="colorwaysPreview-body">
<div className="colorwayPreview-guilds">
<div className="colorwayPreview-guild">
<div
className="colorwayPreview-guildItem"
style={{ background: `var(--dc-guild-button, ${primary})` }}
onMouseEnter={e => e.currentTarget.style.background = accent}
onMouseLeave={e => e.currentTarget.style.background = `var(--dc-guild-button, ${primary})`}
onClick={() => {
if (isModal) {
modalProps?.onClose();
} else {
openModal((props: ModalProps) => <ModalRoot className="colorwaysPreview-modal" {...props}>
<style>
{previewCSS}
</style>
<ThemePreview accent={accent} primary={primary} secondary={secondary} tertiary={tertiary} isModal modalProps={props} />
</ModalRoot>);
}
}}
>
{isModal ? <CloseIcon style={{ color: "var(--header-secondary)" }} /> : <svg
aria-hidden="true"
role="img"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M19,3H14V5h5v5h2V5A2,2,0,0,0,19,3Z"
/>
<path
fill="currentColor"
d="M19,19H14v2h5a2,2,0,0,0,2-2V14H19Z"
/>
<path
fill="currentColor"
d="M3,5v5H5V5h5V3H5A2,2,0,0,0,3,5Z"
/>
<path
fill="currentColor"
d="M5,14H3v5a2,2,0,0,0,2,2h5V19H5Z"
/>
</svg>}
</div>
</div>
<div className="colorwayPreview-guild">
<div className="colorwayPreview-guildSeparator" style={{ backgroundColor: primary }} />
</div>
<div className="colorwayPreview-guild">
<div
className="colorwayPreview-guildItem"
style={{ background: `var(--dc-guild-button, ${primary})` }}
onMouseEnter={e => e.currentTarget.style.background = accent}
onMouseLeave={e => e.currentTarget.style.background = `var(--dc-guild-button, ${primary})`}
/>
</div>
<div className="colorwayPreview-guild">
<div
className="colorwayPreview-guildItem"
style={{ background: `var(--dc-guild-button, ${primary})` }}
onMouseEnter={e => e.currentTarget.style.background = accent}
onMouseLeave={e => e.currentTarget.style.background = `var(--dc-guild-button, ${primary})`}
/>
</div>
</div>
<div className="colorwayPreview-channels" style={{ background: `var(--dc-overlay-3, ${secondary})` }}>
<div
className="colorwayPreview-userArea"
style={{
background: `var(--dc-secondary-alt, hsl(${HexToHSL(secondary)[0]} ${HexToHSL(secondary)[1]}% ${Math.max(HexToHSL(secondary)[2] - 3.6, 0)}%))`
}}
/>
<div className="colorwayPreview-filler">
<div className="colorwayPreview-channel" style={{ backgroundColor: "var(--white-500)" }} />
<div className="colorwayPreview-channel" style={{ backgroundColor: "var(--primary-360)" }} />
<div className="colorwayPreview-channel" style={{ backgroundColor: "var(--primary-500)" }} />
</div>
<div
className="colorwayPreview-topShadow"
style={{
"--primary-900-hsl": `${HexToHSL(tertiary)[0]} ${HexToHSL(tertiary)[1]}% ${Math.max(HexToHSL(tertiary)[2] - (3.6 * 6), 0)}%`,
"--primary-500-hsl": `${HexToHSL(primary)[0]} ${HexToHSL(primary)[1]}% ${Math.min(HexToHSL(primary)[2] + (3.6 * 3), 100)}%`
} as React.CSSProperties}
>
<Text
tag="div"
variant="text-md/semibold"
lineClamp={1}
selectable={false}
>
Preview
</Text>
</div>
</div>
<div className="colorwayPreview-chat" style={{ background: `var(--dc-overlay-chat, ${primary})` }}>
<div
className="colorwayPreview-chatBox"
style={{
background: `var(--dc-overlay-3, hsl(${HexToHSL(primary)[0]} ${HexToHSL(primary)[1]}% ${Math.min(HexToHSL(primary)[2] + 3.6, 100)}%))`
}}
/>
<div className="colorwayPreview-filler" />
<div
className="colorwayPreview-topShadow"
/>
</div>
</div>
</div>
</>;
}

View file

@ -1,313 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const defaultColorwaySource = "https://raw.githubusercontent.com/DaBluLite/ProjectColorway/master/index.json";
export const fallbackColorways = [
{
name: "Keyboard Purple",
original: false,
accent: "hsl(235 85.6% 64.7%)",
primary: "#222456",
secondary: "#1c1f48",
tertiary: "#080d1d",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/KeyboardPurple/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Eclipse",
original: false,
accent: "hsl(87 85.6% 64.7%)",
primary: "#000000",
secondary: "#181818",
tertiary: "#0a0a0a",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Eclipse/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Cyan",
original: false,
accent: "#009f88",
primary: "#202226",
secondary: "#1c1e21",
tertiary: "#141517",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Cyan/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Spotify",
original: false,
accent: "hsl(141 76% 48%)",
primary: "#121212",
secondary: "#090909",
tertiary: "#090909",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Spotify/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Bright n' Blue",
original: true,
accent: "hsl(234, 68%, 33%)",
primary: "#394aae",
secondary: "#29379d",
tertiary: "#1b278d",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/BrightBlue/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Still Young",
original: true,
accent: "hsl(58 85.6% 89%)",
primary: "#443a31",
secondary: "#7c3d3e",
tertiary: "#207578",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/StillYoung/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Sea",
original: true,
accent: "hsl(184, 100%, 50%)",
primary: "#07353b",
secondary: "#0b5e60",
tertiary: "#08201d",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Sea/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Lava",
original: true,
accent: "hsl(4, 80.4%, 32%)",
primary: "#401b17",
secondary: "#351917",
tertiary: "#230b0b",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Lava/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Solid Pink",
original: true,
accent: "hsl(340, 55.2%, 56.3%)",
primary: "#1e151c",
secondary: "#21181f",
tertiary: "#291e27",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/SolidPink/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Sand",
original: true,
accent: "hsl(41, 31%, 45%)",
primary: "#7f6c43",
secondary: "#665b33",
tertiary: "#5c5733",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Sand/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "AMOLED",
original: true,
accent: "hsl(235 85.6% 64.7%)",
primary: "#000000",
secondary: "#000000",
tertiary: "#000000",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Amoled/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Zorin",
original: false,
accent: "hsl(200, 89%, 86%)",
primary: "#171d20",
secondary: "#171d20",
tertiary: "#1e2529",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Zorin/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Desaturated",
original: false,
accent: "hsl(227, 58%, 65%)",
primary: "#35383d",
secondary: "#2c2f34",
tertiary: "#1e1f24",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Desaturated/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Crimson",
original: false,
accent: "hsl(0, 100%, 50%)",
primary: "#050000",
secondary: "#0a0000",
tertiary: "#0f0000",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Crimson/import.css);",
author: "Riddim_GLiTCH",
authorID: "801089753038061669",
},
{
name: "Jupiter",
original: true,
accent: "#ffd89b",
primary: "#ffd89b",
secondary: "#19547b",
tertiary: "#1e1f22",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Jupiter/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
isGradient: true,
colors: ["accent", "primary", "secondary"],
},
{
name: "Neon Candy",
original: true,
accent: "#FC00FF",
primary: "#00DBDE",
secondary: "#00DBDE",
tertiary: "#00DBDE",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/NeonCandy/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
isGradient: true,
colors: ["accent", "primary"],
},
{
name: "Wildberry",
original: false,
accent: "#f40172",
primary: "#180029",
secondary: "#340057",
tertiary: "#4b007a",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Wildberry/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Facebook",
original: false,
accent: "#2375e1",
primary: "#18191a",
secondary: "#242526",
tertiary: "#3a3b3c",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Facebook/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Material You",
original: false,
accent: "#004977",
primary: "#1f1f1f",
secondary: "#28292a",
tertiary: "#2d2f31",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/MaterialYou/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "Discord Teal",
original: false,
accent: "#175f6d",
primary: "#313338",
secondary: "#2b2d31",
tertiary: "#1e1f22",
"dc-import": "@import url(//dablulite.github.io/css-snippets/DiscordTeal/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
colors: ["accent"],
},
{
name: "黄昏の花 (Twilight Blossom)",
original: true,
accent: "#e100ff",
primary: "#04000a",
secondary: "#0b0024",
tertiary: "#210042",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/TwilightBlossom/import.css);",
author: "Riddim_GLiTCH",
authorID: "801089753038061669",
},
{
name: "Chai",
original: true,
accent: "#59cd51",
primary: "#1c1e15",
secondary: "#1e2118",
tertiary: "#24291e",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/Chai/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
{
name: "CS1.6",
original: false,
accent: "#929a8d",
primary: "#3f4738",
secondary: "#5b6c51",
tertiary: "#4d5945",
"dc-import": "@import url(//dablulite.github.io/DiscordColorways/CS16/import.css);",
author: "DaBluLite",
authorID: "582170007505731594",
},
];
export const knownThemeVars = {
"Cyan": {
variable: "--cyan-accent-color",
accent: "--cyan-accent-color",
primary: "--cyan-background-primary",
secondary: "--cyan-background-secondary"
},
"Virtual Boy": {
variable: "--VBaccent",
tertiary: "--VBaccent-muted",
alt: {
tertiary: "--VBaccent-dimmest"
}
},
"Modular": {
variable: "--modular-hue",
accentVariables: {
h: "--modular-hue",
s: "--modular-saturation",
l: "--modular-lightness"
}
},
"Solana": {
variable: "--accent-hue",
accentVariables: {
h: "--accent-hue",
s: "--accent-saturation",
l: "--accent-brightness"
},
primaryVariables: {
h: "--background-accent-hue",
s: "--background-accent-saturation",
l: "--background-accent-brightness"
}
}
};
export const mainColors = [
{ name: "accent", title: "Accent", var: "--brand-experiment" },
{ name: "primary", title: "Primary", var: "--background-primary" },
{ name: "secondary", title: "Secondary", var: "--background-secondary" },
{ name: "tertiary", title: "Tertiary", var: "--background-tertiary" }
];

File diff suppressed because it is too large Load diff

View file

@ -1,426 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
import { disableStyle, enableStyle } from "@api/Styles";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { ModalProps, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByProps } from "@webpack";
import {
Button,
Clipboard,
Forms,
Heading,
i18n,
SettingsRouter,
Toasts
} from "@webpack/common";
import { CSSProperties } from "react";
import { Plugins } from "Vencord";
import AutoColorwaySelector from "./components/AutoColorwaySelector";
import ColorPickerModal from "./components/ColorPicker";
import ColorwaysButton from "./components/ColorwaysButton";
import CreatorModal from "./components/CreatorModal";
import Selector from "./components/Selector";
import OnDemandWaysPage from "./components/SettingsTabs/OnDemandPage";
import SettingsPage from "./components/SettingsTabs/SettingsPage";
import SourceManager from "./components/SettingsTabs/SourceManager";
import Store from "./components/SettingsTabs/Store";
import Spinner from "./components/Spinner";
import { defaultColorwaySource } from "./constants";
import { generateCss, getAutoPresets } from "./css";
import style from "./style.css?managed";
import { ColorPickerProps, ColorwayObject } from "./types";
import { colorToHex, hexToString } from "./utils";
export let ColorPicker: React.FunctionComponent<ColorPickerProps> = () => {
return <Spinner className="colorways-creator-module-warning" />;
};
(async function () {
const [
customColorways,
colorwaySourceFiles,
showColorwaysButton,
onDemandWays,
onDemandWaysTintedText,
useThinMenuButton,
onDemandWaysDiscordSaturation,
onDemandWaysOsAccentColor,
activeColorwayObject,
selectorViewMode,
showLabelsInSelectorGridView
] = await DataStore.getMany([
"customColorways",
"colorwaySourceFiles",
"showColorwaysButton",
"onDemandWays",
"onDemandWaysTintedText",
"useThinMenuButton",
"onDemandWaysDiscordSaturation",
"onDemandWaysOsAccentColor",
"activeColorwayObject",
"selectorViewMode",
"showLabelsInSelectorGridView"
]);
const defaults = [
{ name: "showColorwaysButton", value: showColorwaysButton, default: false },
{ name: "onDemandWays", value: onDemandWays, default: false },
{ name: "onDemandWaysTintedText", value: onDemandWaysTintedText, default: true },
{ name: "useThinMenuButton", value: useThinMenuButton, default: false },
{ name: "onDemandWaysDiscordSaturation", value: onDemandWaysDiscordSaturation, default: false },
{ name: "onDemandWaysOsAccentColor", value: onDemandWaysOsAccentColor, default: false },
{ name: "activeColorwayObject", value: activeColorwayObject, default: { id: null, css: null, sourceType: null, source: null } },
{ name: "selectorViewMode", value: selectorViewMode, default: "grid" },
{ name: "showLabelsInSelectorGridView", value: showLabelsInSelectorGridView, default: false }
];
defaults.forEach(({ name, value, default: def }) => {
if (!value) DataStore.set(name, def);
});
if (customColorways) {
if (!customColorways[0].colorways) {
DataStore.set("customColorways", [{ name: "Custom", colorways: customColorways }]);
}
} else {
DataStore.set("customColorways", []);
}
if (colorwaySourceFiles) {
if (typeof colorwaySourceFiles[0] === "string") {
DataStore.set("colorwaySourceFiles", colorwaySourceFiles.map((sourceURL: string, i: number) => {
return { name: sourceURL === defaultColorwaySource ? "Project Colorway" : `Source #${i}`, url: sourceURL };
}));
}
} else {
DataStore.set("colorwaySourceFiles", [{
name: "Project Colorway",
url: defaultColorwaySource
}]);
}
})();
export const ColorwayCSS = {
get: () => document.getElementById("activeColorwayCSS")!.textContent || "",
set: (e: string) => {
if (!document.getElementById("activeColorwayCSS")) {
document.head.append(Object.assign(document.createElement("style"), {
id: "activeColorwayCSS",
textContent: e
}));
} else document.getElementById("activeColorwayCSS")!.textContent = e;
},
remove: () => document.getElementById("activeColorwayCSS")!.remove(),
};
export const versionData = {
pluginVersion: "5.7.0",
creatorVersion: "1.20",
};
export default definePlugin({
name: "DiscordColorways",
description: "A plugin that offers easy access to simple color schemes/themes for Discord, also known as Colorways",
authors: [{
name: "DaBluLite",
id: 582170007505731594n
}, Devs.ImLvna],
dependencies: ["ServerListAPI", "MessageAccessoriesAPI"],
pluginVersion: versionData.pluginVersion,
creatorVersion: versionData.creatorVersion,
toolboxActions: {
"Change Colorway": () => openModal(props => <Selector modalProps={props} />),
"Open Colorway Creator": () => openModal(props => <CreatorModal modalProps={props} />),
"Open Color Stealer": () => openModal(props => <ColorPickerModal modalProps={props} />),
"Open Settings": () => SettingsRouter.open("ColorwaysSettings"),
"Open On-Demand Settings": () => SettingsRouter.open("ColorwaysOnDemand"),
"Manage Colorways...": () => SettingsRouter.open("ColorwaysManagement"),
"Change Auto Colorway Preset": async () => {
const [
activeAutoPreset,
activeColorwayObject
] = await DataStore.getMany([
"activeAutoPreset",
"activeColorwayObject"
]);
openModal((props: ModalProps) => <AutoColorwaySelector autoColorwayId={activeAutoPreset} modalProps={props} onChange={autoPresetId => {
if (activeColorwayObject.id === "Auto") {
const demandedColorway = getAutoPresets(colorToHex(getComputedStyle(document.body).getPropertyValue("--os-accent-color")))[autoPresetId].preset();
DataStore.set("activeColorwayObject", { id: "Auto", css: demandedColorway, sourceType: "online", source: null });
ColorwayCSS.set(demandedColorway);
}
}} />);
}
},
patches: [
// Credits to Kyuuhachi for the BetterSettings plugin patches
{
find: "this.renderArtisanalHack()",
replacement: {
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
replace: "$&,_:$1",
predicate: () => true
}
},
{
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: {
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
replace: "(async ()=>$2)(),"
},
predicate: () => true
},
{
find: "colorPickerFooter:",
replacement: {
match: /function (\i).{0,200}colorPickerFooter:/,
replace: "$self.ColorPicker=$1;$&",
},
},
{
find: "Messages.ACTIVITY_SETTINGS",
replacement: {
match: /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/,
replace: "...$self.makeSettingsCategories($1),$&"
}
},
{
find: "Messages.ACTIVITY_SETTINGS",
replacement: {
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}
},
{
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
replace: "$2.default.open($1);return;"
}
}
],
set ColorPicker(e) {
ColorPicker = e;
},
isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
const firstChild = settings?.[0];
// lowest two elements... sanity backup
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
const settingsLocation = "belowNitro";
if (!header) return;
const names = {
top: i18n.Messages.USER_SETTINGS,
aboveNitro: i18n.Messages.BILLING_SETTINGS,
belowNitro: i18n.Messages.APP_SETTINGS,
aboveActivity: i18n.Messages.ACTIVITY_SETTINGS
};
return header === names[settingsLocation];
},
patchedSettings: new WeakSet(),
addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: Record<string, unknown>) {
if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return;
this.patchedSettings.add(elements);
elements.push(...this.makeSettingsCategories(sectionTypes));
},
makeSettingsCategories(SectionTypes: Record<string, unknown>) {
const { headerText, header } = findByProps("headerText", "header", "separator");
return [
{
section: SectionTypes.CUSTOM,
label: "Discord Colorways",
className: "vc-settings-header",
element: () => <div className={header} style={{
display: "flex",
justifyContent: "space-between",
padding: "6px 10px"
}}>
<Heading
variant="eyebrow"
className={headerText}
style={{
"text-wrap": "wrap",
color: "var(--channels-default)"
} as CSSProperties}
>
Discord Colorways
</Heading>
<Heading
variant="eyebrow"
className={headerText}
style={{
marginLeft: "auto",
color: "var(--channels-default)"
}}
>
v{(Plugins.plugins.DiscordColorways as any).pluginVersion}
</Heading>
</div>
},
{
section: "ColorwaysSelector",
label: "Colorways",
element: () => <Selector isSettings modalProps={{ onClose: () => new Promise(() => true), transitionState: 1 }} />,
className: "dc-colorway-selector"
},
{
section: "ColorwaysSettings",
label: "Settings",
element: SettingsPage,
className: "dc-colorway-settings"
},
{
section: "ColorwaysSourceManager",
label: "Sources",
element: SourceManager,
className: "dc-colorway-sources-manager"
},
{
section: "ColorwaysOnDemand",
label: "On-Demand",
element: OnDemandWaysPage,
className: "dc-colorway-ondemand"
},
{
section: "ColorwaysStore",
label: "Store",
element: Store,
className: "dc-colorway-store"
},
{
section: SectionTypes.DIVIDER
}
].filter(Boolean);
},
ColorwaysButton: () => <ColorwaysButton />,
async start() {
addServerListElement(ServerListRenderPosition.In, this.ColorwaysButton);
enableStyle(style);
ColorwayCSS.set((await DataStore.get("activeColorwayObject") as ColorwayObject).css || "");
addAccessory("colorways-btn", props => {
if (String(props.message.content).match(/colorway:[0-9a-f]{0,100}/)) {
return <Flex flexDirection="column">
{String(props.message.content).match(/colorway:[0-9a-f]{0,100}/g)?.map((colorID: string) => {
colorID = hexToString(colorID.split("colorway:")[1]);
return <div className="colorwayMessage">
<div className="discordColorwayPreviewColorContainer" style={{ width: "56px", height: "56px", marginRight: "16px" }}>
{(() => {
if (colorID) {
if (!colorID.includes(",")) {
throw new Error("Invalid Colorway ID");
} else {
return colorID.split("|").filter(string => string.includes(",#"))[0].split(/,#/).map((color: string) => <div className="discordColorwayPreviewColor" style={{ backgroundColor: `#${colorToHex(color)}` }} />);
}
} else return null;
})()}
</div>
<div className="colorwayMessage-contents">
<Forms.FormTitle>Colorway{/n:([A-Za-z0-9]+( [A-Za-z0-9]+)+)/i.exec(colorID) ? `: ${/n:([A-Za-z0-9]+( [A-Za-z0-9]+)+)/i.exec(colorID)![1]}` : ""}</Forms.FormTitle>
<Flex>
<Button
onClick={() => openModal(modalProps => <CreatorModal
modalProps={modalProps}
colorwayID={colorID}
/>)}
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.FILLED}
>
Add this Colorway...
</Button>
<Button
onClick={() => {
Clipboard.copy(colorID);
Toasts.show({
message: "Copied Colorway ID Successfully",
type: 1,
id: "copy-colorway-id-notify",
});
}}
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.FILLED}
>
Copy Colorway ID
</Button>
<Button
onClick={() => {
if (!colorID.includes(",")) {
throw new Error("Invalid Colorway ID");
} else {
colorID.split("|").forEach((prop: string) => {
if (prop.includes(",#")) {
DataStore.set("activeColorwayObject", {
id: "Temporary Colorway", css: generateCss(
colorToHex(prop.split(/,#/)[1]),
colorToHex(prop.split(/,#/)[2]),
colorToHex(prop.split(/,#/)[3]),
colorToHex(prop.split(/,#/)[0]),
true,
true,
32,
"Temporary Colorway"
), sourceType: "temporary", source: null
});
ColorwayCSS.set(generateCss(
colorToHex(prop.split(/,#/)[1]),
colorToHex(prop.split(/,#/)[2]),
colorToHex(prop.split(/,#/)[3]),
colorToHex(prop.split(/,#/)[0]),
true,
true,
32,
"Temporary Colorway"
));
}
});
}
}}
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.FILLED}
>
Apply temporarily
</Button>
</Flex>
</div>
</div>;
})}
</Flex>;
} else {
return null;
}
});
},
stop() {
removeServerListElement(ServerListRenderPosition.In, this.ColorwaysButton);
disableStyle(style);
ColorwayCSS.remove();
removeAccessory("colorways-btn");
},
});

File diff suppressed because it is too large Load diff

View file

@ -1,65 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface Colorway {
[key: string]: any,
name: string,
"dc-import": string,
accent: string,
primary: string,
secondary: string,
tertiary: string,
original?: boolean,
author: string,
authorID: string,
colors?: string[],
isGradient?: boolean,
sourceType?: "online" | "offline" | "temporary" | null,
source?: string,
linearGradient?: string,
preset?: string,
creatorVersion: string;
}
export interface ColorPickerProps {
color: number;
showEyeDropper: boolean;
suggestedColors: string[];
label: any;
onChange(color: number): void;
}
export interface ColorwayObject {
id: string | null,
css: string | null,
sourceType: "online" | "offline" | "temporary" | null,
source: string | null | undefined;
}
export interface SourceObject {
type: "online" | "offline" | "temporary",
source: string,
colorways: Colorway[];
}
export enum SortOptions {
NAME_AZ = 1,
NAME_ZA = 2,
SOURCE_AZ = 3,
SOURCE_ZA = 4
}
export interface StoreObject {
sources: StoreItem[];
}
export interface StoreItem {
name: string,
id: string,
description: string,
url: string,
authorGh: string;
}

View file

@ -1,150 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function HexToHSL(H: string) {
let r: any = 0, g: any = 0, b: any = 0;
if (H.length === 4) r = "0x" + H[1] + H[1], g = "0x" + H[2] + H[2], b = "0x" + H[3] + H[3];
else if (H.length === 7) {
r = "0x" + H[1] + H[2];
g = "0x" + H[3] + H[4];
b = "0x" + H[5] + H[6];
}
r /= 255, g /= 255, b /= 255;
var cmin = Math.min(r, g, b),
cmax = Math.max(r, g, b),
delta = cmax - cmin,
h = 0,
s = 0,
l = 0;
if (delta === 0) h = 0;
else if (cmax === r) h = ((g - b) / delta) % 6;
else if (cmax === g) h = (b - r) / delta + 2;
else h = (r - g) / delta + 4;
h = Math.round(h * 60);
if (h < 0) h += 360;
l = (cmax + cmin) / 2;
s = delta === 0
? 0
: delta / (1 - Math.abs(2 * l - 1));
s = +(s * 100).toFixed(1);
l = +(l * 100).toFixed(1);
return [Math.round(h), Math.round(s), Math.round(l)];
}
export const canonicalizeHex = (hex: string) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
ctx.fillStyle = hex;
hex = ctx.fillStyle;
canvas.remove();
return hex;
};
export const stringToHex = (str: string) => {
let hex = "";
for (
let i = 0;
i < str.length;
i++
) {
const charCode = str.charCodeAt(i);
const hexValue = charCode.toString(16);
hex += hexValue.padStart(2, "0");
}
return hex;
};
export const hexToString = (hex: string) => {
let str = "";
for (let i = 0; i < hex.length; i += 2) {
const hexValue = hex.substr(i, 2);
const decimalValue = parseInt(hexValue, 16);
str += String.fromCharCode(decimalValue);
}
return str;
};
export function getHex(str: string): string {
const color = Object.assign(
document.createElement("canvas").getContext("2d") as {},
{ fillStyle: str }
).fillStyle;
if (color.includes("rgba(")) {
return getHex(String([...color.split(",").slice(0, 3), ")"]).replace(",)", ")").replace("a", ""));
} else {
return color;
}
}
export function getFontOnBg(bgColor: string) {
var color = (bgColor.charAt(0) === "#") ? bgColor.substring(1, 7) : bgColor;
var r = parseInt(color.substring(0, 2), 16);
var g = parseInt(color.substring(2, 4), 16);
var b = parseInt(color.substring(4, 6), 16);
return (((r * 0.299) + (g * 0.587) + (b * 0.114)) > 186) ?
"#000000" : "#ffffff";
}
export function $e(funcArray: Array<(...vars: any) => void>, ...vars: any[]) {
funcArray.forEach(e => e(vars));
}
export function hslToHex(h: number, s: number, l: number) {
h /= 360;
s /= 100;
l /= 100;
let r: any, g: any, b: any;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = (x: number) => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
export function rgbToHex(r: number, g: number, b: number) {
const toHex = (x: number) => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
export function colorToHex(color: string) {
var colorType = "hex";
if (color.includes("hsl")) {
colorType = "hsl";
} else if (color.includes("rgb")) {
colorType = "rgb";
}
color = color.replaceAll(",", "").replace(/.+?\(/, "").replace(")", "").replaceAll(/[ \t]+\/[ \t]+/g, " ").replaceAll("%", "").replaceAll("/", "");
if (colorType === "hsl") {
color = hslToHex(Number(color.split(" ")[0]), Number(color.split(" ")[1]), Number(color.split(" ")[2]));
}
if (colorType === "rgb") {
color = rgbToHex(Number(color.split(" ")[0]), Number(color.split(" ")[1]), Number(color.split(" ")[2]));
}
return color.replace("#", "");
}

View file

@ -1,457 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton } from "@api/ChatButtons";
import {
ApplicationCommandInputType,
ApplicationCommandOptionType,
sendBotMessage,
} from "@api/Commands";
import * as DataStore from "@api/DataStore";
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";
import { removeButton } from "@api/MessagePopover";
import { Devs } from "@utils/constants";
import { sleep } from "@utils/misc";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import {
FluxDispatcher, MessageActions,
PrivateChannelsStore, RestAPI,
SnowflakeUtils,
useEffect, UserStore,
UserUtils, useState,
} from "@webpack/common";
import { Message } from "discord-types/general";
import { decryptData, encryptData, formatPemKey, generateKeys } from "./rsa-utils";
const MessageCreator = findByPropsLazy("createBotMessage");
const CloudUtils = findByPropsLazy("CloudUpload");
import { getCurrentChannel } from "@utils/discord";
let enabled;
let setEnabled;
// Interface for Message Create
interface IMessageCreate {
type: "MESSAGE_CREATE";
optimistic: boolean;
isPushNotification: boolean;
channelId: string;
message: Message;
}
// Chat Bar Icon Component
const ChatBarIcon: ChatBarButton = ({ isMainChat }) => {
[enabled, setEnabled] = useState(false);
const [buttonDisabled, setButtonDisabled] = useState(false);
useEffect(() => {
const listener: SendListener = async (_, message) => {
if (enabled) {
const groupChannel = await DataStore.get("encryptcordChannelId");
if (getCurrentChannel().id !== groupChannel) {
sendBotMessage(getCurrentChannel().id, { content: `You must be in <#${groupChannel}> to send an encrypted message!\n> If you wish to send an unencrypted message, please click the button in the chatbar.` });
message.content = "";
return;
}
const trimmedMessage = message.content.trim();
await MessageActions.receiveMessage(groupChannel, await createMessage(trimmedMessage, UserStore.getCurrentUser().id, groupChannel, 0));
const encryptcordGroupMembers = await DataStore.get("encryptcordGroupMembers");
const dmPromises = Object.keys(encryptcordGroupMembers).map(async memberId => {
const groupMember = await UserUtils.getUser(memberId).catch(() => null);
if (!groupMember) return;
const encryptedMessage = await encryptData(encryptcordGroupMembers[memberId].key, trimmedMessage);
const encryptedMessageString = JSON.stringify(encryptedMessage);
await sendTempMessage(groupMember.id, encryptedMessageString, "message");
});
await Promise.all(dmPromises);
message.content = "";
}
};
addPreSendListener(listener);
return () => void removePreSendListener(listener);
}, [enabled]);
if (!isMainChat) return null;
return (
<ChatBarButton
tooltip={enabled ? "Send Unencrypted Messages" : "Send Encrypted Messages"}
onClick={async () => {
if (await DataStore.get("encryptcordGroup") === false || (await DataStore.get("encryptcordChannelId") !== getCurrentChannel().id)) {
setButtonDisabled(true);
await sendTempMessage(getCurrentChannel().id, "", `join\`\`\`\n${await DataStore.get("encryptcordPublicKey")}\`\`\``, false);
sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created <t:${Math.floor(Date.now() / 1000) + 5}:R>\n> [Tip] You can do \`/encryptcord leave\` to leave a group` });
await sleep(5000);
if (await DataStore.get("encryptcordGroup") === true && (await DataStore.get("encryptcordChannelId") !== getCurrentChannel().id)) {
sendBotMessage(getCurrentChannel().id, { content: "*Leaving current group...*" });
await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } });
} else if (await DataStore.get("encryptcordGroup") === true) {
setButtonDisabled(false);
return;
}
await startGroup("", { channel: { id: getCurrentChannel().id } });
}
setEnabled(!enabled);
setButtonDisabled(false);
}}
buttonProps={{
style: {
transition: "transform 0.3s ease-in-out",
transform: `rotate(${enabled ? 0 : 15}deg)`,
},
disabled: buttonDisabled
}}
>
<svg
width="24"
height="24"
viewBox="0 0 129 171"
>
{!enabled && <>
<mask id="encordBarIcon">
</mask>
<path
fill="currentColor"
d="M128.93 149.231V74.907a21.142 21.142 0 00-6.195-15.1 21.165 21.165 0 00-15.101-6.195h-1.085V40.918A40.604 40.604 0 0042.214 8.065 40.602 40.602 0 0026.28 32.318h15.972a25.164 25.164 0 0128.043-15.94 25.166 25.166 0 0120.691 24.745v12.694H22.184A21.276 21.276 0 00.89 75.111v74.325a21.27 21.27 0 0013.143 19.679 21.273 21.273 0 008.152 1.615h85.388a21.455 21.455 0 0015.083-6.357 21.453 21.453 0 006.213-15.142h.062zm-63.888-15.765a21.296 21.296 0 01-15.058-36.352 21.296 21.296 0 0136.354 15.057 21.151 21.151 0 01-21.296 21.295z"
/>
</>
}
<path
mask="url(#encordBarIcon)"
fill="currentColor"
d="M129.497 149.264V75.001a21.27 21.27 0 00-21.295-21.294h-3.072V41.012a41.079 41.079 0 00-1.024-8.6A40.62 40.62 0 0070.729 1.087 5.673 5.673 0 0168.886.88h-.204c-.615 0-1.23-.205-1.844-.205h-4.095A5.672 5.672 0 0060.9.881h-.204a5.672 5.672 0 00-1.843.205A40.627 40.627 0 0025.27 32.413h.205a41.092 41.092 0 00-1.024 8.6v12.694h-3.133A21.153 21.153 0 00.023 75v74.325a21.415 21.415 0 0021.296 21.294h87.231a21.336 21.336 0 0020.886-21.294l.061-.062zm-64.91-15.97a21.317 21.317 0 01-22.069-24.804 21.316 21.316 0 0142.34 3.509 21.355 21.355 0 01-20.272 21.295zm25.185-79.649H39.604V40.951a24.283 24.283 0 016.963-17.2 25.351 25.351 0 0116.79-7.78h2.663a25.31 25.31 0 0123.752 25.184v12.49z"
/>
</svg>
</ChatBarButton>
);
};
// Export Plugin
export default definePlugin({
name: "Encryptcord",
description: "End-to-end encryption in Discord!",
authors: [Devs.Inbestigator],
dependencies: ["CommandsAPI"],
patches: [
{
find: "executeMessageComponentInteraction:",
replacement: {
match: /await\s+l\.default\.post\({\s*url:\s*A\.Endpoints\.INTERACTIONS,\s*body:\s*C,\s*timeout:\s*3e3\s*},\s*t\s*=>\s*{\s*h\(T,\s*p,\s*f,\s*t\)\s*}\s*\)/,
replace: "await $self.joinGroup(C);$&"
}
}
],
async joinGroup(interaction) {
const sender = await UserUtils.getUser(interaction.application_id).catch(() => null);
if (!sender || (sender.bot === true && sender.id !== "1")) return;
if (interaction.data.component_type !== 2) return;
switch (interaction.data.custom_id) {
case "removeFromSelf":
await handleLeaving(sender.id, await DataStore.get("encryptcordGroupMembers") ?? {}, interaction.channel_id);
await sendTempMessage(sender.id, "", "leaving");
FluxDispatcher.dispatch({
type: "MESSAGE_DELETE",
channelId: interaction.channel_id,
id: interaction.message_id,
mlDeleted: true
});
break;
case "createGroup":
await leave("", { channel: { id: interaction.channel_id } });
await startGroup("", { channel: { id: interaction.channel_id } });
break;
default:
return;
}
},
flux: {
async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) {
if (optimistic || type !== "MESSAGE_CREATE") return;
if (message.state === "SENDING") return;
if (message.author.id === UserStore.getCurrentUser().id) return;
if (!message.content) return;
const encryptcordGroupMembers = await DataStore.get("encryptcordGroupMembers");
if (!Object.keys(encryptcordGroupMembers).some(key => key === message.author.id)) {
switch (message.content.toLowerCase().split("```")[0]) {
case "groupdata":
const response = await fetch(message.attachments[0].url);
const groupdata = await response.json();
await handleGroupData(groupdata);
break;
case "join":
if (encryptcordGroupMembers[UserStore.getCurrentUser().id].child) return;
if (!await DataStore.get("encryptcordGroup")) return;
const sender = await UserUtils.getUser(message.author.id).catch(() => null);
if (!sender) return;
const userKey = message.content.split("```")[1];
await handleJoin(sender.id, userKey, encryptcordGroupMembers);
break;
default:
break;
}
return;
}
const dmChannelId = await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id);
if (channelId !== dmChannelId) return;
const sender = await UserUtils.getUser(message.author.id).catch(() => null);
if (!sender) return;
const groupChannel = await DataStore.get("encryptcordChannelId");
switch (message.content.toLowerCase()) {
case "leaving":
handleLeaving(sender.id, encryptcordGroupMembers, groupChannel);
break;
case "message":
const msgResponse = await fetch(message.attachments[0].url);
const messagedata = await msgResponse.json();
await handleMessage(messagedata, sender.id, groupChannel);
break;
case "groupdata":
const response = await fetch(message.attachments[0].url);
const groupdata = await response.json();
await handleGroupData(groupdata);
break;
default:
break;
}
},
},
commands: [
{
name: "encryptcord",
description: "End-to-end encryption in Discord!",
options: [
{
name: "leave",
description: "Leave current group",
options: [],
type: ApplicationCommandOptionType.SUB_COMMAND,
},
{
name: "data",
description: "View your keys and current group members",
options: [],
type: ApplicationCommandOptionType.SUB_COMMAND,
},
],
inputType: ApplicationCommandInputType.BOT,
execute: (opts, ctx) => {
switch (opts[0].name) {
case "start":
startGroup(opts[0].options, ctx);
break;
case "leave":
leave(opts[0].options, ctx);
break;
case "data":
data(opts[0].options, ctx);
break;
}
},
},
],
async start() {
addChatBarButton("Encryptcord", ChatBarIcon);
const pair = await generateKeys();
await DataStore.set("encryptcordPublicKey", pair.publicKey);
await DataStore.set("encryptcordPrivateKey", pair.privateKey);
if (await DataStore.get("encryptcordGroup") === true) {
await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } });
}
await DataStore.set("encryptcordGroup", false);
await DataStore.set("encryptcordChannelId", "");
await DataStore.set("encryptcordGroupMembers", {});
},
async stop() {
removeButton("Encryptcord");
if (await DataStore.get("encryptcordGroup") === true) {
await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } });
}
},
});
// Send Temporary Message
async function sendTempMessage(recipientId: string, attachment: string, content: string, dm: boolean = true) {
if (recipientId === UserStore.getCurrentUser().id) return;
const channelId = dm ? await PrivateChannelsStore.getOrEnsurePrivateChannel(recipientId) : recipientId;
if (attachment && attachment !== "") {
const upload = await new CloudUtils.CloudUpload({
file: new File([new Blob([attachment])], "file.text", { type: "text/plain; charset=utf-8" }),
isClip: false,
isThumbnail: false,
platform: 1,
}, channelId, false, 0);
upload.on("complete", async () => {
const messageId = await RestAPI.post({
url: `/channels/${channelId}/messages`,
body: {
content,
attachments: [{
id: "0",
filename: upload.filename,
uploaded_filename: upload.uploadedFilename,
}],
nonce: SnowflakeUtils.fromTimestamp(Date.now()),
},
}).then(response => response.body.id);
await sleep(500);
MessageActions.deleteMessage(channelId, messageId);
});
await upload.upload();
return;
}
const messageId = await RestAPI.post({
url: `/channels/${channelId}/messages`,
body: {
content,
nonce: SnowflakeUtils.fromTimestamp(Date.now()),
},
}).then(response => response.body.id);
await sleep(500);
MessageActions.deleteMessage(channelId, messageId);
}
// Handle leaving group
async function handleLeaving(senderId: string, encryptcordGroupMembers: object, groupChannel: string) {
const updatedMembers = Object.keys(encryptcordGroupMembers).reduce((result, memberId) => {
if (memberId !== senderId) {
result[memberId] = encryptcordGroupMembers[memberId];
if (result[memberId].child === senderId) {
result[memberId].child = encryptcordGroupMembers[senderId].child;
}
if (result[memberId].parent === senderId) {
result[memberId].parent = encryptcordGroupMembers[senderId].parent;
}
}
return result;
}, {});
await DataStore.set("encryptcordGroupMembers", updatedMembers);
await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 2));
}
// Handle receiving message
async function handleMessage(message, senderId: string, groupChannel: string) {
const decryptedMessage = await decryptData(await DataStore.get("encryptcordPrivateKey"), message);
await MessageActions.receiveMessage(groupChannel, await createMessage(decryptedMessage, senderId, groupChannel, 0));
}
// Handle receiving group data
async function handleGroupData(groupData) {
await DataStore.set("encryptcordChannelId", groupData.channel);
await DataStore.set("encryptcordGroupMembers", groupData.members);
await DataStore.set("encryptcordGroup", true);
await MessageActions.receiveMessage(groupData.channel, await createMessage("", UserStore.getCurrentUser().id, groupData.channel, 7));
setEnabled(true);
}
// Handle joining group
async function handleJoin(senderId: string, senderKey: string, encryptcordGroupMembers: object) {
encryptcordGroupMembers[senderId] = { key: senderKey, parent: UserStore.getCurrentUser().id, child: null };
encryptcordGroupMembers[UserStore.getCurrentUser().id].child = senderId;
await DataStore.set("encryptcordGroupMembers", encryptcordGroupMembers);
const groupChannel = await DataStore.get("encryptcordChannelId");
const newMember = await UserUtils.getUser(senderId).catch(() => null);
if (!newMember) return;
const membersData = {};
Object.entries(encryptcordGroupMembers)
.forEach(([memberId, value]) => {
membersData[memberId] = value;
});
const membersDataString = JSON.stringify({ members: membersData, channel: groupChannel });
const dmPromises = Object.keys(encryptcordGroupMembers).map(async memberId => {
const groupMember = await UserUtils.getUser(memberId).catch(() => null);
if (!groupMember) return;
await sendTempMessage(groupMember.id, membersDataString, "groupdata");
});
await Promise.all(dmPromises);
await MessageActions.receiveMessage(groupChannel, {
...await createMessage("", senderId, groupChannel, 7), components: [{
type: 1,
components: [{
type: 2,
style: 4,
label: "I don't want to talk to you!",
custom_id: "removeFromSelf"
},
{
type: 2,
style: 2,
label: "(Other users can still send/receive messages to/from them)",
disabled: true,
custom_id: "encryptcord"
}]
}]
});
}
// Create message for group
async function createMessage(message: string, senderId: string, channelId: string, type: number) {
const messageStart = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] });
const sender = await UserUtils.getUser(senderId).catch(() => null);
if (!sender) return;
return { ...messageStart, content: message, author: sender, type, flags: 0 };
}
// Start E2EE Group
async function startGroup(opts, ctx) {
const channelId = ctx.channel.id;
await DataStore.set("encryptcordChannelId", channelId);
await DataStore.set("encryptcordGroupMembers", {
[UserStore.getCurrentUser().id]: { key: await DataStore.get("encryptcordPublicKey"), parent: null, child: null }
});
await DataStore.set("encryptcordGroup", true);
sendBotMessage(channelId, { content: "Group created!\n> Other users can click the lock icon to join." });
await MessageActions.receiveMessage(channelId, await createMessage("", UserStore.getCurrentUser().id, channelId, 7));
setEnabled(true);
}
// Leave the Group;
async function leave(opts, ctx) {
const channelId = ctx.channel.id;
if (!(await DataStore.get("encryptcordGroup"))) {
sendBotMessage(channelId, { content: "You're not in a group!" });
return;
}
const user = UserStore.getCurrentUser();
const encryptcordGroupMembers = await DataStore.get("encryptcordGroupMembers");
const dmPromises = Object.keys(encryptcordGroupMembers).map(async memberId => {
const groupMember = await UserUtils.getUser(memberId).catch(() => null);
if (!groupMember) return;
await sendTempMessage(groupMember.id, "", "leaving");
});
await Promise.all(dmPromises);
await DataStore.set("encryptcordGroup", false);
await DataStore.set("encryptcordChannelId", "");
await DataStore.set("encryptcordGroupMembers", {});
await MessageActions.receiveMessage(channelId, await createMessage("", user.id, channelId, 2));
setEnabled(false);
}
// View user data
async function data(opts, ctx) {
const channelId = ctx.channel.id;
const encryptcordGroupMembers = await DataStore.get("encryptcordGroupMembers");
const encryptcordPublicKey = await DataStore.get("encryptcordPublicKey");
const encryptcordPrivateKey = await DataStore.get("encryptcordPrivateKey");
const exportedPrivateKey = await crypto.subtle.exportKey("pkcs8", encryptcordPrivateKey);
const groupMembers = Object.keys(encryptcordGroupMembers);
sendBotMessage(channelId, {
content: `## Public key:\n\`\`\`${encryptcordPublicKey}\`\`\`\n## Private key:\n||\`\`\`${formatPemKey(exportedPrivateKey, "private")}\`\`\`||*(DO **NOT** SHARE THIS)*\n## Group members:\n\`\`\`json\n${JSON.stringify(groupMembers)}\`\`\``
});
}

View file

@ -1,121 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const generateKeys = async () => {
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
);
const exportedPublicKey = await crypto.subtle.exportKey("spki", keyPair.publicKey);
const publicKey = formatPemKey(exportedPublicKey, "public");
return { privateKey: keyPair.privateKey, publicKey };
};
export const encryptData = async (pemPublicKey, data) => {
const publicKey = await importPemPublicKey(pemPublicKey);
const chunkSize = 446;
const encryptedChunks: any[] = [];
const encoder = new TextEncoder();
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = await data.substring(i, i + chunkSize);
const encryptedChunk = await crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
publicKey,
encoder.encode(chunk)
);
encryptedChunks.push(arrayBufferToBase64(encryptedChunk));
}
return encryptedChunks;
};
export const decryptData = async (privateKey, encArray) => {
const decryptionPromises = encArray.map(async encStr => {
const encBuffer = base64ToArrayBuffer(encStr);
const dec = await crypto.subtle.decrypt(
{
name: "RSA-OAEP",
},
privateKey,
encBuffer
);
return new TextDecoder().decode(dec);
});
const decryptedMessages = await Promise.all(decryptionPromises);
return decryptedMessages.join("");
};
// Helper functions
const arrayBufferToBase64 = buffer => {
const binary = String.fromCharCode(...new Uint8Array(buffer));
return btoa(binary);
};
const base64ToArrayBuffer = base64String => {
const binaryString = atob(base64String);
const { length } = binaryString;
const buffer = new ArrayBuffer(length);
const view = new Uint8Array(buffer);
for (let i = 0; i < length; i++) {
view[i] = binaryString.charCodeAt(i);
}
return buffer;
};
export const formatPemKey = (keyData, type) => {
const base64Key = arrayBufferToBase64(keyData);
return `-----BEGIN ${type.toUpperCase()} KEY-----\n` + base64Key + `\n-----END ${type.toUpperCase()} KEY----- `;
};
const importPemPublicKey = async pemKey => {
try {
const trimmedPemKey = pemKey.trim();
const keyBody = trimmedPemKey
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "");
const binaryDer = atob(keyBody);
const arrayBuffer = new Uint8Array(binaryDer.length);
for (let i = 0; i < binaryDer.length; i++) {
arrayBuffer[i] = binaryDer.charCodeAt(i);
}
return await crypto.subtle.importKey(
"spki",
arrayBuffer,
{
name: "RSA-OAEP",
hash: { name: "SHA-256" },
},
true,
["encrypt"]
);
} catch (error) {
console.error("Error importing PEM public key:", error);
throw error;
}
};

View file

@ -1,111 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { DataStore } from "@api/index";
import { Toasts } from "@webpack/common";
import { settings } from "./index";
import { Collection, Gif } from "./types";
import { getFormat } from "./utils/getFormat";
export const DATA_COLLECTION_NAME = "gif-collections-collections";
// this is here bec async + react class component dont play nice and stutters happen. IF theres a better way of doing it pls let me know
export let cache_collections: Collection[] = [];
export const getCollections = async (): Promise<Collection[]> => (await DataStore.get<Collection[]>(DATA_COLLECTION_NAME)) ?? [];
export const getCollection = async (name: string): Promise<Collection | undefined> => {
const collections = await getCollections();
return collections.find(c => c.name === name);
};
export const getCachedCollection = (name: string): Collection | undefined => cache_collections.find(c => c.name === name);
export const createCollection = async (name: string, gifs: Gif[]): Promise<void> => {
const collections = await getCollections();
const duplicateCollection = collections.find(c => c.name === `gc:${name}`);
if (duplicateCollection)
return Toasts.show({
message: "That collection already exists",
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
duration: 3000,
position: Toasts.Position.BOTTOM
}
});
// gifs shouldnt be empty because to create a collection you need to right click an image / gif and then create it yk. but cant hurt to have a null-conditional check RIGHT?
const latestGifSrc = gifs[gifs.length - 1]?.src ?? settings.store.defaultEmptyCollectionImage;
const collection = {
name: `gc:${name}`,
src: latestGifSrc,
format: getFormat(latestGifSrc),
type: "Category",
gifs
};
await DataStore.set(DATA_COLLECTION_NAME, [...collections, collection]);
return await refreshCacheCollection();
};
export const addToCollection = async (name: string, gif: Gif): Promise<void> => {
const collections = await getCollections();
const collectionIndex = collections.findIndex(c => c.name === name);
if (collectionIndex === -1) return console.warn("collection not found");
collections[collectionIndex].gifs.push(gif);
collections[collectionIndex].src = gif.src;
collections[collectionIndex].format = getFormat(gif.src);
await DataStore.set(DATA_COLLECTION_NAME, collections);
return await refreshCacheCollection();
};
export const removeFromCollection = async (id: string): Promise<void> => {
const collections = await getCollections();
const collectionIndex = collections.findIndex(c => c.gifs.some(g => g.id === id));
if (collectionIndex === -1) return console.warn("collection not found");
// Remove The Gif
collections[collectionIndex].gifs = collections[collectionIndex].gifs.filter(g => g.id !== id);
const collection = collections[collectionIndex];
const latestGifSrc = collection.gifs.length ? collection.gifs[collection.gifs.length - 1].src : settings.store.defaultEmptyCollectionImage;
collections[collectionIndex].src = latestGifSrc;
collections[collectionIndex].format = getFormat(latestGifSrc);
await DataStore.set(DATA_COLLECTION_NAME, collections);
return await refreshCacheCollection();
};
export const deleteCollection = async (name: string): Promise<void> => {
const collections = await getCollections();
const col = collections.filter(c => c.name !== name);
await DataStore.set(DATA_COLLECTION_NAME, col);
await refreshCacheCollection();
};
export const refreshCacheCollection = async (): Promise<void> => {
cache_collections = await getCollections();
};

View file

@ -1,21 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// cant change them now eh. My friend uses this plugin. LOVE YOU FREEZER
export const GIF_ITEM_PREFIX = "gc-moment:";
export const GIF_COLLECTION_PREFIX = "gc:";

View file

@ -1,352 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Plugin idea by brainfreeze (668137937333911553) 😎
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Alerts, Button, ContextMenuApi, FluxDispatcher, Forms, Menu, React, TextInput, useCallback, useState } from "@webpack/common";
import * as CollectionManager from "./CollectionManager";
import { GIF_COLLECTION_PREFIX, GIF_ITEM_PREFIX } from "./constants";
import { Category, Collection, Gif, Props } from "./types";
import { getFormat } from "./utils/getFormat";
import { getGif } from "./utils/getGif";
import { downloadCollections, uploadGifCollections } from "./utils/settingsUtils";
import { uuidv4 } from "./utils/uuidv4";
export const settings = definePluginSettings({
defaultEmptyCollectionImage: {
description: "The image / gif that will be shown when a collection has no images / gifs",
type: OptionType.STRING,
default: "https://i.imgur.com/TFatP8r.png"
},
importGifs: {
type: OptionType.COMPONENT,
description: "Import Collections",
component: () =>
<Button onClick={async () =>
// if they have collections show the warning
(await CollectionManager.getCollections()).length ? Alerts.show({
title: "Are you sure?",
body: "Importing collections will overwrite your current collections.",
confirmText: "Import",
// wow this works?
confirmColor: Button.Colors.RED,
cancelText: "Nevermind",
onConfirm: async () => uploadGifCollections()
}) : uploadGifCollections()}>
Import Collections
</Button>,
},
exportGifs: {
type: OptionType.COMPONENT,
description: "Export Collections",
component: () =>
<Button onClick={downloadCollections}>
Export Collections
</Button>
}
});
const addCollectionContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {
if (!props) return;
const { message, itemSrc, itemHref, target } = props;
const gif = getGif(message, itemSrc ?? itemHref, target);
if (!gif) return;
const group = findGroupChildrenByChildId("open-native-link", children) ?? findGroupChildrenByChildId("copy-link", children);
if (group && !group.some(child => child?.props?.id === "add-to-collection")) {
group.push(
// if i do it the normal way i get a invalid context menu thingy error -> Menu API only allows Items and groups of Items as children.
MenuThingy({ gif })
);
}
};
export default definePlugin({
name: "Gif Collection",
// need better description eh
description: "Allows you to have collections of gifs",
authors: [Devs.Aria],
patches: [
{
find: "renderCategoryExtras",
replacement: [
// This patch adds the collections to the gif part yk
{
match: /(\i\.render=function\(\){)(.{1,50}getItemGrid)/,
replace: "$1;$self.insertCollections(this);$2"
},
// Hides the gc: from the name gc:monkeh -> monkeh
{
match: /(\i\.renderCategoryExtras=function\((?<props>\i)\){)var (?<varName>\i)=\i\.name,/,
replace: "$1var $<varName>=$self.hidePrefix($<props>),"
},
// Replaces this.props.resultItems with the collection.gifs
{
match: /(\i\.renderContent=function\(\){)(.{1,50}resultItems)/,
replace: "$1;$self.renderContent(this);$2"
},
// Delete context menu for collection
{
match: /(\i\.render=function\(\){.{1,100}renderExtras.{1,200}onClick:this\.handleClick,)/,
replace: "$1onContextMenu: (e) => $self.collectionContextMenu(e, this),"
},
]
},
/*
problem:
when you click your collection in the gifs picker discord enters the collection name into the search bar
which causes discord to fetch the gifs from their api. This causes a tiny flash when the gifs have fetched successfully
solution:
if query starts with gc: and collection is not null then return early and prevent the fetch
*/
{
find: "type:\"GIF_PICKER_QUERY\"",
replacement: {
match: /(function \i\((?<query>\i),\i\){.{1,200}dispatch\({type:"GIF_PICKER_QUERY".{1,20};)/,
replace:
"$&if($self.shouldStopFetch($<query>)) return;"
}
},
],
settings,
start() {
CollectionManager.refreshCacheCollection();
addContextMenuPatch("message", addCollectionContextMenuPatch);
},
stop() {
removeContextMenuPatch("message", addCollectionContextMenuPatch);
},
CollectionManager,
oldTrendingCat: null as Category[] | null,
sillyInstance: null as any,
sillyContentInstance: null as any,
get collections(): Collection[] {
CollectionManager.refreshCacheCollection();
return CollectionManager.cache_collections;
},
renderContent(instance) {
if (instance.props.query.startsWith(GIF_COLLECTION_PREFIX)) {
this.sillyContentInstance = instance;
const collection = this.collections.find(c => c.name === instance.props.query);
if (!collection) return;
instance.props.resultItems = collection.gifs.map(g => ({
id: g.id,
format: getFormat(g.src),
src: g.src,
url: g.url,
width: g.width,
height: g.height
})).reverse();
}
},
hidePrefix(props: Category) {
return props.name.split(":").length > 1 ? props.name.replace(/.+?:/, "") : props.name;
},
insertCollections(instance: { props: Props; }) {
try {
this.sillyInstance = instance;
if (instance.props.trendingCategories.length && instance.props.trendingCategories[0].type === "Trending")
this.oldTrendingCat = instance.props.trendingCategories;
if (this.oldTrendingCat != null)
instance.props.trendingCategories = this.collections.reverse().concat(this.oldTrendingCat as Collection[]);
} catch (err) {
console.error(err);
}
},
shouldStopFetch(query: string) {
if (query.startsWith(GIF_COLLECTION_PREFIX)) {
const collection = this.collections.find(c => c.name === query);
if (collection != null) return true;
}
return false;
},
collectionContextMenu(e: React.UIEvent, instance) {
const { item } = instance.props;
if (item?.name?.startsWith(GIF_COLLECTION_PREFIX))
return ContextMenuApi.openContextMenu(e, () =>
<RemoveItemContextMenu
type="collection"
onConfirm={() => { this.sillyInstance && this.sillyInstance.forceUpdate(); }}
nameOrId={instance.props.item.name} />
);
if (item?.id?.startsWith(GIF_ITEM_PREFIX))
return ContextMenuApi.openContextMenu(e, () =>
<RemoveItemContextMenu
type="gif"
onConfirm={() => { this.sillyContentInstance && this.sillyContentInstance.forceUpdate(); }}
nameOrId={instance.props.item.id}
/>);
const { src, url, height, width } = item;
if (src && url && height != null && width != null && !item.id?.startsWith(GIF_ITEM_PREFIX))
return ContextMenuApi.openContextMenu(e, () =>
<Menu.Menu
navId="gif-collection-id"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Gif Collections"
>
{/* if i do it the normal way i get a invalid context menu thingy error -> Menu API only allows Items and groups of Items as children.*/}
{MenuThingy({ gif: { ...item, id: uuidv4() } })}
</Menu.Menu>
);
return null;
},
});
// stolen from spotify controls
const RemoveItemContextMenu = ({ type, nameOrId, onConfirm }: { type: "gif" | "collection", nameOrId: string, onConfirm: () => void; }) => (
<Menu.Menu
navId="gif-collection-id"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label={type === "collection" ? "Delete Collection" : "Remove"}
>
<Menu.MenuItem
key="delete-collection"
id="delete-collection"
label={type === "collection" ? "Delete Collection" : "Remove"}
action={() =>
// Stolen from Review components
type === "collection" ? Alerts.show({
title: "Are you sure?",
body: "Do you really want to delete this collection?",
confirmText: "Delete",
confirmColor: Button.Colors.RED,
cancelText: "Nevermind",
onConfirm: async () => {
await CollectionManager.deleteCollection(nameOrId);
onConfirm();
}
}) : CollectionManager.removeFromCollection(nameOrId).then(() => onConfirm())}
>
</Menu.MenuItem>
</Menu.Menu>
);
const MenuThingy: React.FC<{ gif: Gif; }> = ({ gif }) => {
CollectionManager.refreshCacheCollection();
const collections = CollectionManager.cache_collections;
return (
<Menu.MenuItem
label="Add To Collection"
key="add-to-collection"
id="add-to-collection"
>
{collections.map(col => (
<Menu.MenuItem
key={col.name}
id={col.name}
label={col.name.replace(/.+?:/, "")}
action={() => CollectionManager.addToCollection(col.name, gif)}
/>
))}
<Menu.MenuSeparator />
<Menu.MenuItem
key="create-collection"
id="create-collection"
label="Create Collection"
action={() => {
openModal(modalProps => (
<CreateCollectionModal onClose={modalProps.onClose} gif={gif} modalProps={modalProps} />
));
}}
/>
</Menu.MenuItem>
);
};
interface CreateCollectionModalProps {
gif: Gif;
onClose: () => void,
modalProps: ModalProps;
}
function CreateCollectionModal({ gif, onClose, modalProps }: CreateCollectionModalProps) {
const [name, setName] = useState("");
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
if (!name.length) return;
CollectionManager.createCollection(name, [gif]);
onClose();
}, [name]);
return (
<ModalRoot {...modalProps}>
<form onSubmit={onSubmit}>
<ModalHeader>
<Forms.FormText>Create Collection</Forms.FormText>
</ModalHeader>
<ModalContent>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Collection Name</Forms.FormTitle>
<TextInput onChange={(e: string) => setName(e)} />
</ModalContent>
<div style={{ marginTop: "1rem" }}>
<ModalFooter>
<Button
type="submit"
color={Button.Colors.GREEN}
disabled={!name.length}
onClick={onSubmit}
>
Create
</Button>
</ModalFooter>
</div>
</form>
</ModalRoot>
);
}

View file

@ -1,44 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export enum Format { NONE = 0, IMAGE = 1, VIDEO = 2 }
export interface Category {
type: "Trending" | "Category";
name: string;
src: string;
format: Format;
gifs?: Gif[];
}
export interface Gif {
id: string,
src: string;
url: string;
height: number,
width: number;
}
export interface Props {
favorites: { [src: string]: any; };
trendingCategories: Category[];
}
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
export type Collection = WithRequired<Category, "gifs">;

View file

@ -1,23 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const cleanUrl = (url: string) => {
const urlObject = new URL(url);
urlObject.search = "";
return urlObject.href;
};

View file

@ -1,27 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Format } from "../types";
import { getUrlExtension } from "./getUrlExtension";
const videoExtensions = ["mp4", "ogg", "webm", "avi", "wmv", "flv", "mov", "mkv", "m4v"];
export function getFormat(url: string) {
const extension = getUrlExtension(url);
return url.startsWith("https://media.tenor") || extension == null || videoExtensions.includes(extension) ? Format.VIDEO : Format.IMAGE;
}

View file

@ -1,123 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { MessageStore, SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general";
import { Gif } from "../types";
import { cleanUrl } from "./cleanUrl";
import { isAudio } from "./isAudio";
import { uuidv4 } from "./uuidv4";
export function getGifByTarget(url: string, target?: HTMLDivElement | null): Gif | null {
const liElement = target?.closest("li");
if (!target || !liElement || !liElement.id) return null;
const [channelId, messageId] = liElement.id.split("-").slice(2);
// the isValidSnowFlake part may not be nessesery cuse either way (valid or not) message will be undefined if it doenst find a message /shrug
if (!channelId || !messageId || !isValidSnowFlake(channelId) || !isValidSnowFlake(messageId)) return null;
const message = MessageStore.getMessage(channelId, messageId);
if (!message || !message.embeds.length && !message.attachments.length) return null;
return getGifByMessageAndUrl(url, message);
}
export function getGifByMessageAndTarget(target: HTMLDivElement, message: Message) {
const url = target.closest('[class^="imageWrapper"]')?.querySelector("video")?.src ?? target.closest('[class^="imageWrapper"]')?.querySelector("img")?.src;
if (!url) return null;
return getGifByMessageAndUrl(url, message);
}
export function getGifByMessageAndUrl(url: string, message: Message): Gif | null {
if (!message.embeds.length && !message.attachments.length || isAudio(url))
return null;
url = cleanUrl(url);
// find embed with matching url or image/thumbnail url
const embed = message.embeds.find(e => e.url === url || e.image?.url === url || e.image?.proxyURL === url || e.video?.proxyURL === url || e.thumbnail?.proxyURL === url); // no point in checking thumbnail/video url because no way of getting it eh. discord renders the img/video element with proxy urls
if (embed) {
if (embed.image)
return {
id: uuidv4(),
height: embed.image.height,
width: embed.image.width,
src: embed.image.proxyURL,
url: embed.image.url,
};
// Tennor
if (embed.video && embed.video.proxyURL) return {
id: uuidv4(),
height: embed.video.height,
width: embed.video.width,
src: embed.video.proxyURL,
url: embed.provider?.name === "Tenor" ? embed.url ?? embed.video.url : embed.video.url,
};
// Youtube thumbnails and other stuff idk
if (embed.thumbnail && embed.thumbnail.proxyURL) return {
id: uuidv4(),
height: embed.thumbnail.height,
width: embed.thumbnail.width,
src: embed.thumbnail.proxyURL,
url: embed.thumbnail.url,
};
}
const attachment = message.attachments.find(a => a.url === url || a.proxy_url === url);
if (attachment) return {
id: uuidv4(),
height: attachment.height ?? 50,
width: attachment.width ?? 50,
src: attachment.proxy_url,
url: attachment.url
};
return null;
}
export const getGif = (message: Message | null, url: string | null, target: HTMLDivElement | null) => {
if (message && url) {
const gif = getGifByMessageAndUrl(url, message);
if (!gif) return null;
return gif;
}
if (message && target && !url) {
const gif = getGifByMessageAndTarget(target, message);
if (!gif) return null;
return gif;
}
if (url && target && !message) {
// youtube thumbnail url is message link for some reason eh
const gif = getGifByTarget(url.startsWith("https://discord.com/") ? target.parentElement?.querySelector("img")?.src ?? url : url, target);
if (!gif) return null;
return gif;
}
};
function isValidSnowFlake(snowflake: string) {
return !Number.isNaN(SnowflakeUtils.extractTimestamp(snowflake));
}

View file

@ -1,23 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function getUrlExtension(url: string) {
// tennor stuff is like //media.tenor/blah/blah
if (!url.startsWith("https:")) url = "https:" + url;
return new URL(url).pathname.split(".").pop();
}

View file

@ -1,27 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getUrlExtension } from "./getUrlExtension";
const audioExtensions = ["mp3", "wav", "ogg", "aac", "m4a", "wma", "flac"];
export function isAudio(url: string) {
const extension = getUrlExtension(url);
return extension && audioExtensions.includes(extension);
}

View file

@ -1,131 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { DataStore } from "@api/index";
import { Toasts } from "@webpack/common";
import { DATA_COLLECTION_NAME, getCollections, refreshCacheCollection } from "../CollectionManager";
// 99% of this is coppied from src\utils\settingsSync.ts
export async function downloadCollections() {
const filename = "gif-collections.json";
const exportData = await exportCollections();
const data = new TextEncoder().encode(exportData);
if (IS_WEB) {
const file = new File([data], filename, { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
a.download = filename;
document.body.appendChild(a);
a.click();
setImmediate(() => {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
});
} else {
DiscordNative.fileManager.saveWithDialog(data, filename);
}
}
export async function exportCollections() {
const collections = await getCollections();
return JSON.stringify({ collections }, null, 4);
}
export async function importCollections(data: string) {
try {
var parsed = JSON.parse(data);
} catch (err) {
console.log(data);
throw new Error("Failed to parse JSON: " + String(err));
}
if ("collections" in parsed) {
await DataStore.set(DATA_COLLECTION_NAME, parsed.collections);
await refreshCacheCollection();
} else
throw new Error("Invalid Collections");
}
export async function uploadGifCollections(showToast = true): Promise<void> {
if (IS_WEB) {
const input = document.createElement("input");
input.type = "file";
input.style.display = "none";
input.accept = "application/json";
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
try {
await importCollections(reader.result as string);
if (showToast) toastSuccess();
} catch (err) {
console.error(err);
// new Logger("SettingsSync").error(err);
if (showToast) toastFailure(err);
}
};
reader.readAsText(file);
};
document.body.appendChild(input);
input.click();
setImmediate(() => document.body.removeChild(input));
} else {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
{ name: "Gif Collections", extensions: ["json"] },
{ name: "all", extensions: ["*"] }
]
});
if (file) {
try {
await importCollections(new TextDecoder().decode(file.data));
if (showToast) toastSuccess();
} catch (err) {
console.error(err);
// new Logger("SettingsSync").error(err);
if (showToast) toastFailure(err);
}
}
}
}
const toastSuccess = () => Toasts.show({
type: Toasts.Type.SUCCESS,
message: "Settings successfully imported.",
id: Toasts.genId()
});
const toastFailure = (err: any) => Toasts.show({
type: Toasts.Type.FAILURE,
message: `Failed to import settings: ${String(err)}`,
id: Toasts.genId()
});

View file

@ -1,29 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { GIF_ITEM_PREFIX } from "../constants";
export function uuidv4() {
let d = new Date().getTime();
d += performance.now();
return `${GIF_ITEM_PREFIX}xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, c => {
const r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
});
}

View file

@ -26,7 +26,7 @@ export default definePlugin({
authors: [EquicordDevs.Jaxx],
patches: [
{
find: ".displayName=\"SearchStore\";",
find: '"SearchStore",',
replacement: {
match: /(\i)\.offset=null!==\((\i)=(\i)\.offset\)&&void 0!==(\i)\?(\i):0/i,
replace: (_, v, v1, query, v3, v4) => `$self.main(${query}), ${v}.offset = null !== (${v1} = ${query}.offset) && void 0 !== ${v3} ? ${v4} : 0`

View file

@ -5,6 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { SettingsRouter } from "@webpack/common";
@ -20,10 +21,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "ThemeLibrary",
description: "A library of themes for Vencord.",
authors: [{
name: "Fafa",
id: 428188716641812481n,
}],
authors: [EquicordDevs.Fafa],
settings,
toolboxActions: {
"Open Theme Library": () => {