Add DiscordColorways Back

This commit is contained in:
thororen1234 2024-06-24 17:35:16 -04:00
parent d99f5aa28a
commit 0865d4bb58
25 changed files with 6832 additions and 1 deletions

View file

@ -21,7 +21,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
- Request for plugins from Discord.
<details>
<summary>Extra included plugins (58 additional plugins)</summary>
<summary>Extra included plugins (61 additional plugins)</summary>
- AllCallTimers by MaxHerbold and D3SOX
- AltKrispSwitch by newwares
@ -33,10 +33,12 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
- CleanChannelName by AutumnVN
- CopyUserMention by Cortex and castdrian
- CustomAppIcons by Happy Enderman and SerStars
- DiscordColorways by DaBluLite
- DNDWhilePlaying by thororen
- DoNotLeak by Perny
- DoubleCounterBypass by nyx
- EmojiDumper by Cortex, Samwich, Woosh
- Encryptcord by Inbestigator
- EquicordCSS by FoxStorm1 and thororen (and all respective css developers)
- ExportContacts by dat_insanity
- FindReply by newwares
@ -68,6 +70,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
- Search by JacobTm and thororen
- SearchFix by Jaxx
- Sekai Stickers by MaiKokain
- ServerSearch by camila314
- ShowBadgesInChat by Inbestigator and KrystalSkull
- Slap by Korbo
- SoundBoardLogger by Moxxie, fres, echo, thororen

View file

@ -0,0 +1,74 @@
/*
* 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

@ -0,0 +1,91 @@
/*
* 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

@ -0,0 +1,70 @@
/*
* 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

@ -0,0 +1,64 @@
/*
* 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

@ -0,0 +1,318 @@
/*
* 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

@ -0,0 +1,349 @@
/*
* 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

@ -0,0 +1,146 @@
/*
* 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

@ -0,0 +1,301 @@
/*
* 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

@ -0,0 +1,49 @@
/*
* 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

@ -0,0 +1,207 @@
/*
* 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

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export default function () {
return <div style={{
borderRadius: "50%",
width: "calc(100% + 4px)",
height: "calc(100% + 4px)",
position: "absolute",
top: "-2px",
left: "-2px",
cursor: "default",
pointerEvents: "none",
boxShadow: "inset 0 0 0 2px var(--brand-500),inset 0 0 0 4px var(--background-primary)"
}}>
<svg style={{ position: "absolute", right: "0" }} aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" fill="var(--white-500)" />
<path fill="currentColor" fill-rule="evenodd" d="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm5.7-13.3a1 1 0 0 0-1.4-1.4L10 14.58l-2.3-2.3a1 1 0 0 0-1.4 1.42l3 3a1 1 0 0 0 1.4 0l7-7Z" clip-rule="evenodd" style={{ color: "var(--brand-500)" }} />
</svg>
</div>;
}

View file

@ -0,0 +1,838 @@
/*
* 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 } 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";
import SelectionCircle from "./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

@ -0,0 +1,84 @@
/*
* 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

@ -0,0 +1,184 @@
/*
* 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

@ -0,0 +1,387 @@
/*
* 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

@ -0,0 +1,149 @@
/*
* 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

@ -0,0 +1,19 @@
/*
* 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

@ -0,0 +1,150 @@
/*
* 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

@ -0,0 +1,313 @@
/*
* 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

@ -0,0 +1,416 @@
/*
* 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, EquicordDevs } 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.1",
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: [EquicordDevs.DaBluLite, 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:(.{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

@ -0,0 +1,65 @@
/*
* 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

@ -0,0 +1,150 @@
/*
* 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("#", "");
}