Merge branch 'dev'

This commit is contained in:
thororen1234 2024-08-21 21:39:50 -04:00
commit 64fe43281a
40 changed files with 4493 additions and 2364 deletions

View file

@ -0,0 +1,18 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
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") as any).textContent = e;
},
remove: () => document.getElementById("activeColorwayCSS")?.remove(),
};

View file

@ -4,52 +4,49 @@
* 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 { DataStore, useEffect, useState } from "../";
import { getAutoPresets } from "../css";
import { ModalProps } from "../types";
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>
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Auto Preset Settings
</h2>
<div className="colorwaysModalContent">
<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>;
})}
<span className="colorwaysModalSectionHeader">Presets:</span>
{Object.values(getAutoPresets()).map(autoPreset => <div
className="discordColorway"
aria-checked={autoId === autoPreset.id}
style={{ padding: "10px", marginBottom: "8px" }}
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>
<span className="colorwayLabel">{autoPreset.name}</span>
</div>)}
</div>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND_NEW}
size={Button.Sizes.MEDIUM}
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={() => {
DataStore.set("activeAutoPreset", autoId);
onChange(autoId);
@ -57,18 +54,15 @@ export default function ({ modalProps, onChange, autoColorwayId = "" }: { modalP
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
onClick={() => {
modalProps.onClose();
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
</button>
</div>
</div>;
}

View file

@ -4,28 +4,25 @@
* 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 { DataStore, Toasts, useEffect, useState } from "..";
import { mainColors } from "../constants";
import { colorVariables } from "../css";
import { ModalProps } from "../types";
import { getHex } from "../utils";
import { CopyIcon } from "./Icons";
export default function ({ modalProps }: { modalProps: ModalProps; }) {
const [ColorVars, setColorVars] = useState<string[]>(colorVariables);
const [collapsedSettings, setCollapsedSettings] = useState<boolean>(true);
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
let results: string[];
function searchToolboxItems(e: string) {
results = [];
@ -37,55 +34,53 @@ export default function ({ modalProps }: { modalProps: ModalProps; }) {
setColorVars(results);
}
return <ModalRoot {...modalProps} className="colorwayColorpicker">
<Flex style={{ gap: "8px", marginBottom: "8px" }}>
<TextInput
className="colorwaysColorpicker-search"
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<div style={{ gap: "8px", marginBottom: "8px", display: "flex" }}>
<input
type="text"
className="colorwaySelector-search"
placeholder="Search for a color:"
onChange={e => {
searchToolboxItems(e);
if (e) {
onChange={({ currentTarget: { value } }) => {
searchToolboxItems(value);
if (value) {
setCollapsedSettings(false);
} else {
setCollapsedSettings(true);
}
}}
/>
<Button
innerClassName="colorwaysSettings-iconButtonInner"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
<button
className="colorwaysPillButton"
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>
</button>
</div>
<div style={{ color: "var(--text-normal)", overflow: "hidden auto", scrollbarWidth: "none" }} className={collapsedSettings ? " colorwaysColorpicker-collapsed" : ""}>
{ColorVars.map((colorVariable: string) => <div
id={`colorways-colorstealer-item_${colorVariable}`}
className="colorwaysCreator-settingItm colorwaysCreator-toolboxItm"
onClick={() => {
Clipboard.copy(getHex(getComputedStyle(document.body).getPropertyValue("--" + colorVariable)));
navigator.clipboard.writeText(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"}>
</div>
<div style={{ justifyContent: "space-between", marginTop: "8px", flexWrap: "wrap", gap: "1em" }} 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)));
navigator.clipboard.writeText(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>;
</div>
</div>;
}

View file

@ -4,67 +4,70 @@
* 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 { DataStore, useEffect, useState } from "..";
import { getPreset } from "../css";
import { ModalProps } from "../types";
import Setting from "./Setting";
import Switch from "./Switch";
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>
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">Creator Settings</h2>
<div className="colorwaysModalContent" style={{
minWidth: "500px"
}}>
<span className="colorwaysModalSectionHeader">
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}
</span>
<div className="colorwaysScroller" style={{ paddingRight: "2px", marginBottom: "20px", maxHeight: "250px" }}>
{Object.values(getPreset()).map(pre => <div
aria-checked={preset === pre.id}
className="discordColorway"
style={{ padding: "10px", marginBottom: "8px" }}
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>
<span className="colorwayLabel">{pre.name}</span>
</div>)}
</div>
<Setting divider>
<Switch value={tintedText} onChange={setTintedText} label="Use colored text" />
</Setting>
<Switch value={discordSaturation} onChange={setDiscordSaturation} label="Use Discord's saturation" />
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
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}
</button>
<button
className="colorwaysPillButton"
onClick={() => {
modalProps.onClose();
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
</button>
</div>
</div>;
}

View file

@ -0,0 +1,117 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore, openModal, Toasts, useEffect, useState } from "..";
import { ColorwayCSS } from "../colorwaysAPI";
import { generateCss } from "../css";
import { colorToHex, hexToString } from "../utils";
import CreatorModal from "./CreatorModal";
export let changeThemeIDCard: (theme: string) => void = () => { };
export default function ({ props }) {
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
changeThemeIDCard = theme => setTheme(theme);
load();
return () => {
changeThemeIDCard = () => { };
};
}, []);
if (String(props.message.content).match(/colorway:[0-9a-f]{0,100}/)) {
return <div className="colorwayIDCard" data-theme={theme}>
{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">
<span className="colorwaysModalSectionHeader">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]}` : ""}</span>
<div style={{
display: "flex",
gap: "1em"
}}>
<button
className="colorwaysPillButton"
onClick={() => openModal(modalProps => <CreatorModal
modalProps={modalProps}
colorwayID={colorID}
/>)}
>
Add this Colorway...
</button>
<button
className="colorwaysPillButton"
onClick={() => {
navigator.clipboard.writeText(colorID);
Toasts.show({
message: "Copied Colorway ID Successfully",
type: 1,
id: "copy-colorway-id-notify",
});
}}
>
Copy Colorway ID
</button>
<button
className="colorwaysPillButton"
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"
));
}
});
}
}}
>
Apply temporarily
</button>
</div>
</div>
</div>;
})}
</div>;
} else {
return null;
}
}

View file

@ -4,15 +4,12 @@
* 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 { DataStore, FluxDispatcher, FluxEvents, openModal, useEffect, useState } from "..";
import { getAutoPresets } from "../css";
import { ColorwayObject } from "../types";
import { PalleteIcon } from "./Icons";
import Selector from "./Selector";
import Selector from "./MainModal";
import Tooltip from "./Tooltip";
export default function () {
const [activeColorway, setActiveColorway] = useState<string>("None");
@ -39,11 +36,11 @@ export default function () {
<>
{!isThin ? <>
<span>Colorways</span>
<Text variant="text-xs/normal" style={{ color: "var(--text-muted)", fontWeight: 500 }}>{"Active Colorway: " + activeColorway}</Text>
<span style={{ color: "var(--text-muted)", fontWeight: 500, fontSize: 12 }}>{"Active Colorway: " + activeColorway}</span>
</> : <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> : <></>}
{activeColorway === "Auto" ? <span style={{ color: "var(--text-muted)", fontWeight: 500, fontSize: 12 }}>{"Auto Preset: " + (getAutoPresets()[autoPreset].name || "None")}</span> : <></>}
</>
} position="right" tooltipContentClassName="colorwaysBtn-tooltipContent"
} position="right"
>
{({ onMouseEnter, onMouseLeave, onClick }) => visibility ? <div className="ColorwaySelectorBtnContainer">
<div
@ -58,7 +55,7 @@ export default function () {
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>
>{isThin ? <span style={{ color: "var(--header-primary)", fontWeight: 700, fontSize: 9 }}>Colorways</span> : <PalleteIcon />}</div>
</div> : <></>}
</Tooltip>;
}

View file

@ -4,10 +4,9 @@
* 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 { DataStore, useEffect, useState } from "..";
import { knownThemeVars } from "../constants";
import { ModalProps } from "../types";
import { getFontOnBg, getHex } from "../utils";
export default function ({
@ -37,15 +36,22 @@ export default function ({
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>
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Conflicting Colors Found
</h2>
<div className="colorwaysModalContent">
<span className="colorwaysConflictingColors-warning">Multiple known themes have been found, select the colors you want to copy from below:</span>
<span className="colorwaysModalSectionHeader">Colors to copy:</span>
<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>
@ -53,12 +59,12 @@ export default function ({
<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 className="colorwaysCreator-settingsList">
<div
id="colorways-colorstealer-item_Default"
className="colorwaysCreator-settingItm colorwaysCreator-colorPreviewItm"
>
<Forms.FormTitle>Discord</Forms.FormTitle>
<span className="colorwaysModalSectionHeader">Discord</span>
<div className="colorwayCreator-colorPreviews">
<div
className="colorwayCreator-colorPreview" style={{
@ -164,7 +170,7 @@ export default function ({
}
className="colorwaysCreator-settingItm colorwaysCreator-colorPreviewItm"
>
<Forms.FormTitle>{Object.keys(knownThemeVars)[i] + (theme.alt ? " (Main)" : "")}</Forms.FormTitle>
<span className="colorwaysModalSectionHeader">{Object.keys(knownThemeVars)[i] + (theme.alt ? " (Main)" : "")}</span>
<div className="colorwayCreator-colorPreviews">
{theme.primary && getComputedStyle(document.body).getPropertyValue(theme.primary).match(/^\d.*%$/)
? <div
@ -294,15 +300,12 @@ export default function ({
);
}
})}
</ScrollerThin>
</div>
</div>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={() => {
onFinished({
accent: accentColor,
@ -312,7 +315,7 @@ export default function ({
});
modalProps.onClose();
}}
>Finish</Button>
</ModalFooter>
</ModalRoot >;
>Finish</button>
</div>
</div >;
}

View file

@ -4,30 +4,12 @@
* 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 { ColorPicker, DataStore, openModal, PluginProps, Slider, useEffect, useReducer, UserStore, useState } from "..";
import { knownThemeVars } from "../constants";
import { generateCss, getPreset, gradientPresetIds, PrimarySatDiffs, pureGradientBase } from "../css";
import { Colorway } from "../types";
import { Colorway, ModalProps } from "../types";
import { colorToHex, getHex, HexToHSL, hexToString } from "../utils";
import { updateRemoteSources } from "../wsClient";
import ColorwayCreatorSettingsModal from "./ColorwayCreatorSettingsModal";
import ConflictingColorsModal from "./ConflictingColorsModal";
import InputColorwayIdModal from "./InputColorwayIdModal";
@ -35,61 +17,98 @@ import SaveColorwayModal from "./SaveColorwayModal";
import ThemePreviewCategory from "./ThemePreview";
export default function ({
modalProps,
loadUIProps,
loadUIProps = () => new Promise(() => { }),
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 [colors, updateColors] = useReducer((colors: {
accent: string,
primary: string,
secondary: string,
tertiary: string;
}, action: {
task: "accent" | "primary" | "secondary" | "tertiary" | "all",
color?: string;
colorObj?: {
accent: string,
primary: string,
secondary: string,
tertiary: string;
};
}) => {
if (action.task === "all") {
return { ...action.colorObj } as {
accent: string,
primary: string,
secondary: string,
tertiary: string;
};
} else {
return { ...colors, [action.task as "accent" | "primary" | "secondary" | "tertiary"]: action.color } as {
accent: string,
primary: string,
secondary: string,
tertiary: string;
};
}
}, {
accent: "5865f2",
primary: "313338",
secondary: "2b2d31",
tertiary: "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 [mutedTextBrightness, setMutedTextBrightness] = useState<number>(Math.min(HexToHSL("#" + colors.primary)[2] + (3.6 * 3), 100));
const [theme, setTheme] = useState("discord");
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(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
};
load();
}, []);
const setColor = [
"accent",
"primary",
"secondary",
"tertiary"
] as ("accent" | "primary" | "secondary" | "tertiary")[];
const colorProps = [
{
name: "Accent",
id: "accent"
},
{
name: "Primary",
id: "primary"
},
{
name: "Secondary",
id: "secondary"
},
{
name: "Tertiary",
id: "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)));
prop.split(/,#/).forEach((color: string, i: number) => updateColors({ task: setColor[i], color: colorToHex(color) }));
}
if (prop.includes("n:")) {
setColorwayName(prop.split("n:")[1]);
@ -115,43 +134,54 @@ export default function ({
};
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
<div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">Create a Colorway</h2>
<div className="colorwaysModalContent" style={{ minWidth: 500 }}>
<span className="colorwaysModalSectionHeader">Name:</span>
<input
type="text"
className="colorwaySelector-search"
placeholder="Give your Colorway a name"
value={colorwayName}
onChange={setColorwayName}
onInput={e => setColorwayName(e.currentTarget.value)}
/>
<div className="colorwaysCreator-settingCat">
<Forms.FormTitle style={{ marginBottom: "0" }}>
Colors & Values:
</Forms.FormTitle>
<span className="colorwaysModalSectionHeader">Colors & Values:</span>
<div className="colorwayCreator-colorPreviews">
{presetColorArray.map(presetColor => {
{colorProps.filter(color => presetColorArray.includes(color.id) || Object.keys(getPreset()[preset].calculated! || {}).includes(color.id)).map(presetColor => {
return <ColorPicker
label={<Text className="colorwaysPicker-colorLabel">{colorProps[presetColor].name}</Text>}
color={parseInt(colorProps[presetColor].get, 16)}
label={<span className="colorwaysPicker-colorLabel">{Object.keys(getPreset()[preset].calculated! || {}).includes(presetColor.id) ? (presetColor.name + " (Calculated)") : presetColor.name}</span>}
color={!Object.keys(
getPreset()[preset].calculated! || {}
).includes(presetColor.id) ?
parseInt(colors[presetColor.id], 16) :
parseInt(
colorToHex(
getPreset(
colors.primary,
colors.secondary,
colors.tertiary,
colors.accent
)[preset].calculated![presetColor.id]
),
16
)
}
onChange={(color: number) => {
let hexColor = color.toString(16);
while (hexColor.length < 6) {
hexColor = "0" + hexColor;
if (!Object.keys(getPreset()[preset].calculated! || {}).includes(presetColor.id)) {
let hexColor = color.toString(16);
while (hexColor.length < 6) {
hexColor = "0" + hexColor;
}
updateColors({ task: presetColor.id as "accent" | "primary" | "secondary" | "tertiary", color: hexColor });
}
colorProps[presetColor].set(hexColor);
}}
{...colorPickerProps}
/>;
})}
</div>
<Forms.FormDivider style={{ margin: "10px 0" }} />
<Forms.FormTitle>Muted Text Brightness:</Forms.FormTitle>
<div className="colorwaysSettingsDivider" style={{ margin: "10px 0" }} />
<span className="colorwaysModalSectionHeader">Muted Text Brightness:</span>
<Slider
minValue={0}
maxValue={100}
@ -172,41 +202,38 @@ export default function ({
setDiscordSaturation(discordSaturation);
setTintedText(tintedText);
}} />)}>
<Forms.FormTitle style={{ marginBottom: 0 }}>Settings & Presets</Forms.FormTitle>
<span className="colorwaysModalSectionHeader">Settings & Presets</span>
<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}
accent={"#" + colors.accent}
primary={"#" + colors.primary}
secondary={"#" + colors.secondary}
tertiary={"#" + colors.tertiary}
previewCSS={gradientPresetIds.includes(getPreset()[preset].id) ? pureGradientBase + `.colorwaysPreview-modal,.colorwaysPreview-wrapper {--gradient-theme-bg: linear-gradient(${(getPreset(
primaryColor,
secondaryColor,
tertiaryColor,
accentColor
colors.primary,
colors.secondary,
colors.tertiary,
colors.accent
)[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%);
--primary-500: hsl(${HexToHSL("#" + colors.primary)[0]} calc(var(--saturation-factor, 1)*${discordSaturation ? Math.round(((HexToHSL("#" + colors.primary)[1] / 100) * (100 + PrimarySatDiffs[500])) * 10) / 10 : HexToHSL("#" + colors.primary)[1]}%) ${mutedTextBrightness || Math.min(HexToHSL("#" + colors.primary)[2] + (3.6 * 3), 100)}%);
--primary-360: hsl(${HexToHSL("#" + colors.secondary)[0]} calc(var(--saturation-factor, 1)*${discordSaturation ? Math.round(((HexToHSL("#" + colors.primary)[1] / 100) * (100 + PrimarySatDiffs[360])) * 10) / 10 : HexToHSL("#" + colors.primary)[1]}%) 90%);
}` : "")}
/>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
var customColorwayCSS: string = "";
if (preset === "default") {
customColorwayCSS = generateCss(
primaryColor,
secondaryColor,
tertiaryColor,
accentColor,
colors.primary,
colors.secondary,
colors.tertiary,
colors.accent,
tintedText,
discordSaturation,
mutedTextBrightness,
@ -216,61 +243,64 @@ export default function ({
gradientPresetIds.includes(getPreset()[preset].id) ?
customColorwayCSS = `/**
* @name ${colorwayName || "Colorway"}
* @version ${versionData.creatorVersion}
* @version ${PluginProps.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 = `/**
${(getPreset(colors.primary, colors.secondary, colors.tertiary, colors.accent)[preset].preset(discordSaturation) as { full: string; }).full}` : customColorwayCSS = `/**
* @name ${colorwayName || "Colorway"}
* @version ${versionData.creatorVersion}
* @version ${PluginProps.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)}`;
${(getPreset(colors.primary, colors.secondary, colors.tertiary, colors.accent)[preset].preset(discordSaturation) as string)}`;
}
const customColorway: Colorway = {
name: (colorwayName || "Colorway"),
"dc-import": customColorwayCSS,
accent: "#" + accentColor,
primary: "#" + primaryColor,
secondary: "#" + secondaryColor,
tertiary: "#" + tertiaryColor,
accent: "#" + colors.accent,
primary: "#" + colors.primary,
secondary: "#" + colors.secondary,
tertiary: "#" + colors.tertiary,
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
colors.primary,
colors.secondary,
colors.tertiary,
colors.accent
)[preset].preset(discordSaturation) as { base: string; }).base : "",
preset: getPreset()[preset].id,
creatorVersion: versionData.creatorVersion
creatorVersion: PluginProps.creatorVersion
};
openModal(props => <SaveColorwayModal modalProps={props} colorways={[customColorway]} onFinish={() => {
modalProps.onClose();
loadUIProps!();
loadUIProps();
updateRemoteSources();
}} />);
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
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]);
updateColors({
task: "all",
colorObj: {
accent: accent.split("#")[1],
primary: primary.split("#")[1],
secondary: secondary.split("#")[1],
tertiary: tertiary.split("#")[1]
}
});
}
var copiedThemes = ["Discord"];
Object.values(knownThemeVars).map((theme: { variable: string; variableType?: string; }, i: number) => {
@ -281,69 +311,53 @@ export default function ({
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]
);
updateColors({
task: "all", colorObj: {
primary: getHex(
getComputedStyle(
document.body
).getPropertyValue("--primary-600")
).split("#")[1],
secondary: getHex(
getComputedStyle(
document.body
).getPropertyValue("--primary-630")
).split("#")[1],
tertiary: getHex(
getComputedStyle(
document.body
).getPropertyValue("--primary-700")
).split("#")[1],
accent: 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}
</button>
<button
className="colorwaysPillButton"
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)));
hexToString(colorwayID).split(/,#/).forEach((color: string, i: number) => updateColors({ task: setColor[i], color: colorToHex(color) }));
}} />)}
>
Enter Colorway ID
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
onClick={() => {
modalProps.onClose();
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>
</button>
</div>
</div>
);
}

View file

@ -0,0 +1,85 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useEffect, useState } from "..";
import { SortOptions } from "../types";
import { SortIcon } from "./Icons";
export default function ({ sort, onSortChange }: { sort: SortOptions, onSortChange: (newSort: SortOptions) => void; }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
const [showMenu, setShowMenu] = useState(false);
function rightClickContextMenu(e) {
e.stopPropagation();
window.dispatchEvent(new Event("click"));
setShowMenu(!showMenu);
setPos({
x: e.currentTarget.getBoundingClientRect().x,
y: e.currentTarget.getBoundingClientRect().y + e.currentTarget.offsetHeight + 8
});
}
function onPageClick(this: Window, e: globalThis.MouseEvent) {
setShowMenu(false);
}
useEffect(() => {
window.addEventListener("click", onPageClick);
return () => {
window.removeEventListener("click", onPageClick);
};
}, []);
function onSortChange_internal(newSort: SortOptions) {
onSortChange(newSort);
setShowMenu(false);
}
return <>
{showMenu ? <nav className="colorwaysContextMenu" style={{
position: "fixed",
top: `${pos.y}px`,
left: `${pos.x}px`
}}>
<button onClick={() => onSortChange_internal(1)} className="colorwaysContextMenuItm">
Name (A-Z)
<svg aria-hidden="true" role="img" width="18" height="18" viewBox="0 0 24 24" style={{
marginLeft: "8px"
}}>
<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" />
{sort === 1 ? <circle className="colorwaysRadioSelected" cx="12" cy="12" r="5" /> : null}
</svg>
</button>
<button onClick={() => onSortChange_internal(2)} className="colorwaysContextMenuItm">
Name (Z-A)
<svg aria-hidden="true" role="img" width="18" height="18" viewBox="0 0 24 24" style={{
marginLeft: "8px"
}}>
<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" />
{sort === 2 ? <circle className="colorwaysRadioSelected" cx="12" cy="12" r="5" /> : null}
</svg>
</button>
<button onClick={() => onSortChange_internal(3)} className="colorwaysContextMenuItm">
Source (A-Z)
<svg aria-hidden="true" role="img" width="18" height="18" viewBox="0 0 24 24" style={{
marginLeft: "8px"
}}>
<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" />
{sort === 3 ? <circle className="colorwaysRadioSelected" cx="12" cy="12" r="5" /> : null}
</svg>
</button>
<button onClick={() => onSortChange_internal(4)} className="colorwaysContextMenuItm">
Source (Z-A)
<svg aria-hidden="true" role="img" width="18" height="18" viewBox="0 0 24 24" style={{
marginLeft: "8px"
}}>
<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" />
{sort === 4 ? <circle className="colorwaysRadioSelected" cx="12" cy="12" r="5" /> : null}
</svg>
</button>
</nav> : null}
<button className="colorwaysPillButton" onClick={rightClickContextMenu}><SortIcon width={14} height={14} /> Sort By...</button>
</>;
}

View file

@ -1,26 +1,35 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classes } from "@utils/misc";
import type { PropsWithChildren, SVGProps } from "react";
import type { PropsWithChildren } from "react";
import { classes } from "../utils";
interface BaseIconProps extends IconProps {
viewBox: string;
}
interface IconProps extends SVGProps<SVGSVGElement> {
className?: string;
height?: string | number;
width?: string | number;
}
type IconProps = JSX.IntrinsicElements["svg"];
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
return (
<svg
className={classes(className, "vc-icon")}
className={classes(className, "dc-icon")}
role="img"
width={width}
height={height}
@ -32,16 +41,221 @@ function Icon({ height = 24, width = 24, className, children, viewBox, ...svgPro
);
}
export function PalleteIcon(props: IconProps) {
/**
* Discord's link icon, as seen in the Message context menu "Copy Message Link" option
*/
export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
return (
<Icon
height={height}
width={width}
className={classes(className, "dc-link-icon")}
viewBox="0 0 24 24"
>
<g fill="none" fill-rule="evenodd">
<path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z" />
<rect width={width} height={height} />
</g>
</Icon>
);
}
/**
* Discord's copy icon, as seen in the user popout right of the username when clicking
* your own username in the bottom left user panel
*/
export function CopyIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-pallete-icon")}
className={classes(props.className, "dc-copy-icon")}
viewBox="0 0 24 24"
>
<g fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" />
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" />
</g>
</Icon>
);
}
/**
* Discord's open external icon, as seen in the user profile connections
*/
export function OpenExternalIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-open-external-icon")}
viewBox="0 0 24 24"
>
<polygon
fill="currentColor"
fillRule="nonzero"
points="13 20 11 20 11 8 5.5 13.5 4.08 12.08 12 4.16 19.92 12.08 18.5 13.5 13 8"
/>
</Icon>
);
}
export function ImageIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-image-icon")}
viewBox="0 0 24 24"
>
<path fill="currentColor" d="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
</Icon>
);
}
export function InfoIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-info-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"
transform="translate(2 2)"
d="M9,7 L11,7 L11,5 L9,5 L9,7 Z M10,18 C5.59,18 2,14.41 2,10 C2,5.59 5.59,2 10,2 C14.41,2 18,5.59 18,10 C18,14.41 14.41,18 10,18 L10,18 Z M10,4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16,4.4771525 0,10 C-1.33226763e-15,12.6521649 1.0535684,15.195704 2.92893219,17.0710678 C4.80429597,18.9464316 7.3478351,20 10,20 C12.6521649,20 15.195704,18.9464316 17.0710678,17.0710678 C18.9464316,15.195704 20,12.6521649 20,10 C20,7.3478351 18.9464316,4.80429597 17.0710678,2.92893219 C15.195704,1.0535684 12.6521649,2.22044605e-16 10,0 L10,4.4408921e-16 Z M9,15 L11,15 L11,9 L9,9 L9,15 L9,15 Z"
/>
</Icon>
);
}
/**
* Discord's screenshare icon, as seen in the connection panel
*/
export function ScreenshareIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-screenshare-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 4.5C2 3.397 2.897 2.5 4 2.5H20C21.103 2.5 22 3.397 22 4.5V15.5C22 16.604 21.103 17.5 20 17.5H13V19.5H17V21.5H7V19.5H11V17.5H4C2.897 17.5 2 16.604 2 15.5V4.5ZM13.2 14.3375V11.6C9.864 11.6 7.668 12.6625 6 15C6.672 11.6625 8.532 8.3375 13.2 7.6625V5L18 9.6625L13.2 14.3375Z"
/>
</Icon>
);
}
export function ImageVisible(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-image-visible")}
viewBox="0 0 24 24"
>
<path fill="currentColor" d="M5 21q-.825 0-1.413-.587Q3 19.825 3 19V5q0-.825.587-1.413Q4.175 3 5 3h14q.825 0 1.413.587Q21 4.175 21 5v14q0 .825-.587 1.413Q19.825 21 19 21Zm0-2h14V5H5v14Zm1-2h12l-3.75-5-3 4L9 13Zm-1 2V5v14Z" />
</Icon>
);
}
export function ImageInvisible(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-image-invisible")}
viewBox="0 0 24 24"
>
<path fill="currentColor" d="m21 18.15-2-2V5H7.85l-2-2H19q.825 0 1.413.587Q21 4.175 21 5Zm-1.2 4.45L18.2 21H5q-.825 0-1.413-.587Q3 19.825 3 19V5.8L1.4 4.2l1.4-1.4 18.4 18.4ZM6 17l3-4 2.25 3 .825-1.1L5 7.825V19h11.175l-2-2Zm7.425-6.425ZM10.6 13.4Z" />
</Icon>
);
}
export function Microphone(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-microphone")}
viewBox="0 0 24 24"
>
<path fillRule="evenodd" clipRule="evenodd" d="M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V21H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1ZM12 4C11.2 4 11 4.66667 11 5V11C11 11.3333 11.2 12 12 12C12.8 12 13 11.3333 13 11V5C13 4.66667 12.8 4 12 4Z" fill="currentColor" />
<path fillRule="evenodd" clipRule="evenodd" d="M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V22H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1Z" fill="currentColor" />
</Icon >
);
}
export function CogWheel(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-cog-wheel")}
viewBox="0 0 24 24"
>
<path
clipRule="evenodd"
fill="currentColor"
d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z"
/>
</Icon>
);
}
export function ReplyIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-reply-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10 8.26667V4L3 11.4667L10 18.9333V14.56C15 14.56 18.5 16.2667 21 20C20 14.6667 17 9.33333 10 8.26667Z"
/>
</Icon>
);
}
export function DeleteIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-delete-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"
/>
<path
fill="currentColor"
d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"
/>
</Icon>
);
}
export function SearchIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-search-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M21.707 20.293L16.314 14.9C17.403 13.504 18 11.799 18 10C18 7.863 17.167 5.854 15.656 4.344C14.146 2.832 12.137 2 10 2C7.863 2 5.854 2.832 4.344 4.344C2.833 5.854 2 7.863 2 10C2 12.137 2.833 14.146 4.344 15.656C5.854 17.168 7.863 18 10 18C11.799 18 13.504 17.404 14.9 16.314L20.293 21.706L21.707 20.293ZM10 16C8.397 16 6.891 15.376 5.758 14.243C4.624 13.11 4 11.603 4 10C4 8.398 4.624 6.891 5.758 5.758C6.891 4.624 8.397 4 10 4C11.603 4 13.109 4.624 14.242 5.758C15.376 6.891 16 8.398 16 10C16 11.603 15.376 13.11 14.242 14.243C13.109 15.376 11.603 16 10 16Z"
/>
</Icon>
);
}
export function PlusIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-plus-icon")}
viewBox="0 0 18 18"
>
<polygon
fill-rule="nonzero"
fill="currentColor"
points="15 10 10 10 10 15 8 15 8 10 3 10 3 8 8 8 8 3 10 3 10 8 15 8"
/>
</Icon>
);
@ -51,7 +265,7 @@ export function CloseIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-close-icon")}
className={classes(props.className, "dc-close-icon")}
viewBox="0 0 24 24"
>
<path
@ -62,11 +276,76 @@ export function CloseIcon(props: IconProps) {
);
}
export function SwatchIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-swatch-icon")}
viewBox="0 0 16 16"
style={{ padding: "4px" }}
>
<path fill="currentColor" d="M0 .5A.5.5 0 0 1 .5 0h5a.5.5 0 0 1 .5.5v5.277l4.147-4.131a.5.5 0 0 1 .707 0l3.535 3.536a.5.5 0 0 1 0 .708L10.261 10H15.5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5H3a2.99 2.99 0 0 1-2.121-.879A2.99 2.99 0 0 1 0 13.044m6-.21 7.328-7.3-2.829-2.828L6 7.188v5.647zM4.5 13a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0zM15 15v-4H9.258l-4.015 4H15zM0 .5v12.495V.5z" />
<path fill="currentColor" d="M0 12.995V13a3.07 3.07 0 0 0 0-.005z" />
</Icon>
);
}
export function PalleteIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-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 NoEntrySignIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-no-entry-sign-icon")}
viewBox="0 0 24 24"
>
<path
d="M0 0h24v24H0z"
fill="none"
/>
<path
fill="currentColor"
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z"
/>
</Icon>
);
}
export function DownloadIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-download-icon")}
className={classes(props.className, "dc-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 SafetyIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-safety-icon")}
viewBox="0 0 24 24"
>
<path
@ -81,7 +360,7 @@ export function ImportIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-import-icon")}
className={classes(props.className, "dc-import-icon")}
viewBox="0 0 24 24"
>
<path
@ -96,7 +375,22 @@ export function IDIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-id-icon")}
viewBox="0 0 24 24"
className={classes(props.className, "dc-id-icon")}
fillRule="evenodd"
clipRule="evenodd"
>
<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 NotesIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-notes-icon")}
viewBox="0 0 24 24"
>
<path
@ -117,7 +411,7 @@ export function CodeIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-code-icon")}
className={classes(props.className, "dc-code-icon")}
viewBox="0 0 24 24"
>
<path
@ -132,7 +426,7 @@ export function MoreIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-more-icon")}
className={classes(props.className, "dc-more-icon")}
viewBox="0 0 24 24"
>
<path
@ -144,3 +438,112 @@ export function MoreIcon(props: IconProps) {
</Icon>
);
}
export function SortIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-sort-icon")}
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M3.5 3.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 12.293zm4 .5a.5.5 0 0 1 0-1h1a.5.5 0 0 1 0 1zm0 3a.5.5 0 0 1 0-1h3a.5.5 0 0 1 0 1zm0 3a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1zM7 12.5a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 0-1h-7a.5.5 0 0 0-.5.5"
/>
</Icon>
);
}
export function FolderIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-folder-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 5a3 3 0 0 1 3-3h3.93a2 2 0 0 1 1.66.9L12 5h7a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Z"
/>
</Icon>
);
}
export function LogIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-log-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M3.11 8H6v10.82c0 .86.37 1.68 1 2.27.46.43 1.02.71 1.63.84A1 1 0 0 0 9 22h10a4 4 0 0 0 4-4v-1a2 2 0 0 0-2-2h-1V5a3 3 0 0 0-3-3H4.67c-.87 0-1.7.32-2.34.9-.63.6-1 1.42-1 2.28 0 .71.3 1.35.52 1.75a5.35 5.35 0 0 0 .48.7l.01.01h.01L3.11 7l-.76.65a1 1 0 0 0 .76.35Zm1.56-4c-.38 0-.72.14-.97.37-.24.23-.37.52-.37.81a1.69 1.69 0 0 0 .3.82H6v-.83c0-.29-.13-.58-.37-.8C5.4 4.14 5.04 4 4.67 4Zm5 13a3.58 3.58 0 0 1 0 3H19a2 2 0 0 0 2-2v-1H9.66ZM3.86 6.35ZM11 8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm-1 5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1Z"
/>
</Icon>
);
}
export function RestartIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-restart-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z"
/>
</Icon>
);
}
export function PaintbrushIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-paintbrush-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M15.35 7.24C15.9 6.67 16 5.8 16 5a3 3 0 1 1 3 3c-.8 0-1.67.09-2.24.65a1.5 1.5 0 0 0 0 2.11l1.12 1.12a3 3 0 0 1 0 4.24l-5 5a3 3 0 0 1-4.25 0l-5.76-5.75a3 3 0 0 1 0-4.24l4.04-4.04.97-.97a3 3 0 0 1 4.24 0l1.12 1.12c.58.58 1.52.58 2.1 0ZM6.9 9.9 4.3 12.54a1 1 0 0 0 0 1.42l2.17 2.17.83-.84a1 1 0 0 1 1.42 1.42l-.84.83.59.59 1.83-1.84a1 1 0 0 1 1.42 1.42l-1.84 1.83.17.17a1 1 0 0 0 1.42 0l2.63-2.62L6.9 9.9Z"
/>
</Icon>
);
}
export function PencilIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-pencil-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z"
/>
</Icon>
);
}
export function InternetIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "dc-internet-icon")}
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855q-.215.403-.395.872c.705.157 1.472.257 2.282.287zM4.249 3.539q.214-.577.481-1.078a7 7 0 0 1 .597-.933A7 7 0 0 0 3.051 3.05q.544.277 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9 9 0 0 1-1.565-.667A6.96 6.96 0 0 0 1.018 7.5zm1.4-2.741a12.3 12.3 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332M8.5 5.09V7.5h2.99a12.3 12.3 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.6 13.6 0 0 1 7.5 10.91V8.5zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741zm-3.282 3.696q.18.469.395.872c.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a7 7 0 0 1-.598-.933 9 9 0 0 1-.481-1.079 8.4 8.4 0 0 0-1.198.49 7 7 0 0 0 2.276 1.522zm-1.383-2.964A13.4 13.4 0 0 1 3.508 8.5h-2.49a6.96 6.96 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667m6.728 2.964a7 7 0 0 0 2.275-1.521 8.4 8.4 0 0 0-1.197-.49 9 9 0 0 1-.481 1.078 7 7 0 0 1-.597.933M8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855q.216-.403.395-.872A12.6 12.6 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.96 6.96 0 0 0 14.982 8.5h-2.49a13.4 13.4 0 0 1-.437 3.008M14.982 7.5a6.96 6.96 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008zM11.27 2.461q.266.502.482 1.078a8.4 8.4 0 0 0 1.196-.49 7 7 0 0 0-2.275-1.52c.218.283.418.597.597.932m-.488 1.343a8 8 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"
/>
</Icon>
);
}

View file

@ -4,54 +4,43 @@
* 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 { DataStore, openModal, PluginProps, Toasts, useEffect, UserStore, useState, useStateFromStores } from "..";
import { ColorwayCSS } from "../colorwaysAPI";
import { generateCss, pureGradientBase } from "../css";
import { Colorway } from "../types";
import { colorToHex, stringToHex } from "../utils";
import { Colorway, ModalProps } from "../types";
import { colorToHex, saveFile, 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
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Rename Colorway...
</h2>
<div className="colorwaysModalContent">
<input
type="text"
className="colorwaySelector-search"
value={newName}
error={error}
onChange={setNewName}
onInput={({ currentTarget: { value } }) => {
setNewName(value);
}}
/>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
if (!newName) {
return setError("Error: Please enter a valid name");
@ -64,18 +53,15 @@ function RenameColorwayModal({ modalProps, ogName, onFinish, colorwayList }: { m
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
</button>
<button
className="colorwaysPillButton"
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
</button>
</div>
</div>;
}
export default function ({
@ -94,42 +80,48 @@ export default function ({
"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" }}>
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Colorway: {colorway.name}
</h2>
<div className="colorwaysModalContent">
<div style={{ gap: "8px", width: "100%", display: "flex", flexDirection: "column" }}>
<span className="colorwaysModalSectionHeader">Creator:</span>
<div style={{ gap: ".5rem", display: "flex" }}>
{<img src={`https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.webp?size=32`} width={32} height={32} style={{
borderRadius: "32px"
}} />}
<span className="colorwaysModalSectionHeader" style={{ lineHeight: "32px" }} onClick={() => {
navigator.clipboard.writeText(profile.username);
Toasts.show({
message: "Copied Colorway Author Username Successfully",
type: 1,
id: "copy-colorway-author-username-notify",
});
}}>{colorway.author}</span>
</div>
<span className="colorwaysModalSectionHeader">Colors:</span>
<div style={{ gap: "8px", display: "flex" }}>
{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}
</div>
<span className="colorwaysModalSectionHeader">Actions:</span>
<div style={{ gap: "8px", flexDirection: "column", display: "flex" }}>
<button
className="colorwaysPillButton"
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);
navigator.clipboard.writeText(colorwayID);
Toasts.show({
message: "Copied Colorway ID Successfully",
type: 1,
@ -138,14 +130,12 @@ export default function ({
}}
>
Copy Colorway ID
</Button>
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
style={{ width: "100%" }}
onClick={() => {
Clipboard.copy(colorway["dc-import"]);
navigator.clipboard.writeText(colorway["dc-import"]);
Toasts.show({
message: "Copied CSS to Clipboard",
type: 1,
@ -154,11 +144,9 @@ export default function ({
}}
>
Copy CSS
</Button>
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
style={{ width: "100%" }}
onClick={async () => {
const newColorway = {
@ -169,11 +157,9 @@ export default function ({
}}
>
Update CSS
</Button>
{colorway.sourceType === "offline" && <Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
{colorway.sourceType === "offline" && <button
className="colorwaysPillButton"
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];
@ -199,64 +185,32 @@ export default function ({
}}
>
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}
</button>}
<button
className="colorwaysPillButton"
style={{ width: "100%" }}
onClick={() => {
if (!colorway["dc-import"].includes("@name")) {
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(`/**
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`);
} else {
saveFile(new File([`/**
* @name ${colorway.name || "Colorway"}
* @version ${versionData.creatorVersion}
* @version ${PluginProps.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" }));
}
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}
</button>
<button
className="colorwaysPillButton"
style={{ width: "100%" }}
onClick={() => {
openModal((props: ModalProps) => <ModalRoot className="colorwaysPreview-modal" {...props}>
openModal((props: ModalProps) => <div className={`colorwaysPreview-modal ${props.transitionState === 2 ? "closing" : ""} ${props.transitionState === 4 ? "hidden" : ""}`}>
<style>
{colorway.isGradient ? pureGradientBase + `.colorwaysPreview-modal,.colorwaysPreview-wrapper {--gradient-theme-bg: linear-gradient(${colorway.linearGradient})}` : ""}
</style>
@ -268,15 +222,13 @@ export default function ({
isModal
modalProps={props}
/>
</ModalRoot>);
</div>);
}}
>
Show preview
</Button>
{colorway.sourceType === "offline" && <Button
color={Button.Colors.RED}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
</button>
{colorway.sourceType === "offline" && <button
className="colorwaysPillButton"
style={{ width: "100%" }}
onClick={async () => {
const oldStores = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).filter(source => source.name !== colorway.source);
@ -292,10 +244,9 @@ export default function ({
}}
>
Delete
</Button>}
</Flex>
</Flex>
<div style={{ width: "100%", height: "20px" }} />
</ModalContent>
</ModalRoot>;
</button>}
</div>
</div>
</div>
</div>;
}

View file

@ -4,24 +4,33 @@
* 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 { DataStore, useEffect, useState } from "..";
import { ModalProps } from "../types";
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}
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<div className="colorwaysModalContent">
<span className="colorwaysModalSectionHeader">Colorway ID:</span>
<input
type="text"
className="colorwaySelector-search"
placeholder="Enter Colorway ID"
onInput={({ currentTarget: { value } }) => setColorwayID(value)}
/>
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={() => {
if (!colorwayID) {
throw new Error("Please enter a Colorway ID");
@ -34,16 +43,13 @@ export default function ({ modalProps, onColorwayId }: { modalProps: ModalProps,
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
</button>
</div>
</div>;
}

View file

@ -0,0 +1,114 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { MouseEvent, MouseEventHandler } from "react";
import { DataStore, useEffect, useRef, useState } from "../";
import { ModalProps } from "../types";
import { restartWS, updateRemoteSources, wsOpen } from "../wsClient";
// eslint-disable-next-line no-duplicate-imports
import { boundKey as bk } from "../wsClient";
import Selector from "./Selector";
import SettingsPage from "./SettingsTabs/SettingsPage";
import SourceManager from "./SettingsTabs/SourceManager";
import Store from "./SettingsTabs/Store";
export let changeTheme = (theme: string) => { };
export let updateWSMain: (status: boolean) => void = () => { };
export let updateBoundKeyMain: (boundKey: { [managerKey: string]: string; }) => void = () => { };
export default function ({
modalProps
}: {
modalProps: ModalProps;
}): JSX.Element | any {
const [activeTab, setActiveTab] = useState<"selector" | "settings" | "sources" | "store" | "ws_connection">("selector");
const [theme, setTheme] = useState("discord");
const [pos, setPos] = useState({ x: 0, y: 0 });
const [showMenu, setShowMenu] = useState(false);
const [wsConnected, setWsConnected] = useState(wsOpen);
const [boundKey, setBoundKey] = useState<{ [managerKey: string]: string; }>(bk as { [managerKey: string]: string; });
const menuProps = useRef(null);
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
updateWSMain = status => setWsConnected(status);
changeTheme = (theme: string) => setTheme(theme);
updateBoundKeyMain = bound => setBoundKey(bound);
load();
return () => {
updateWSMain = () => { };
changeTheme = () => { };
updateBoundKeyMain = () => { };
};
}, []);
function SidebarTab({ id, title, icon, bottom }: { id: "selector" | "settings" | "sources" | "store" | "ws_connection", title: string, icon: JSX.Element, bottom?: boolean; }) {
return <div className={"colorwaySelectorSidebar-tab" + (id === activeTab ? " active" : "")} style={bottom ? { marginTop: "auto" } : {}} onClick={!bottom ? ((() => setActiveTab(id)) as unknown as MouseEventHandler<HTMLDivElement>) : rightClickContextMenu}>{icon}</div>;
}
const rightClickContextMenu: MouseEventHandler<HTMLDivElement> = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
window.dispatchEvent(new Event("click"));
setShowMenu(!showMenu);
setPos({
x: e.currentTarget.getBoundingClientRect().x + e.currentTarget.offsetWidth + 8,
y: e.currentTarget.getBoundingClientRect().y + e.currentTarget.offsetHeight - (menuProps.current as unknown as HTMLElement).offsetHeight
});
};
function onPageClick(this: Window, e: globalThis.MouseEvent) {
setShowMenu(false);
}
useEffect(() => {
window.addEventListener("click", onPageClick);
return () => {
window.removeEventListener("click", onPageClick);
};
}, []);
return (
<>
<div className={`colorwaySelectorModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme} {...modalProps}>
<div className="colorwaySelectorSidebar">
<SidebarTab icon={<>&#xF30D;</>} id="selector" title="Change Colorway" />
<SidebarTab icon={<>&#xF3E3;</>} id="settings" title="Settings" />
<SidebarTab icon={<>&#xF2C6;</>} id="sources" title="Sources" />
<SidebarTab icon={<>&#xF543;</>} id="store" title="Store" />
<SidebarTab bottom icon={<>&#xF3EE;</>} id="ws_connection" title="Manager Connection" />
</div>
<div className="colorwayModalContent">
{activeTab === "selector" && <Selector />}
{activeTab === "sources" && <SourceManager />}
{activeTab === "store" && <Store />}
{activeTab === "settings" && <div style={{ padding: "16px" }}><SettingsPage /></div>}
</div>
<div ref={menuProps} className={`colorwaysManagerConnectionMenu ${showMenu ? "visible" : ""}`} style={{
position: "fixed",
top: `${pos.y}px`,
left: `${pos.x}px`
}}>
<span>Manager Connection Status: {wsConnected ? "Connected" : "Disconnected"}</span>
{wsConnected ? <>
<span className="colorwaysManagerConnectionValue">Bound Key: <b>{JSON.stringify(boundKey)}</b></span>
<button className="colorwaysPillButton" style={{
marginTop: "4px"
}} onClick={() => navigator.clipboard.writeText(JSON.stringify(boundKey))}>Copy Bound Key</button>
<button className="colorwaysPillButton" style={{
marginTop: "4px"
}} onClick={restartWS}>Reset Connection</button>
<button className="colorwaysPillButton" style={{
marginTop: "4px"
}} onClick={updateRemoteSources}>Update Remote Sources</button>
</> : <></>}
</div>
</div>
</>
);
}

View file

@ -0,0 +1,35 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore, useEffect, useState } from "..";
import { ModalProps } from "../types";
export default function ({ modalProps }: { modalProps: ModalProps; }) {
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Project Colorway has moved
</h2>
<div className="colorwaysModalContent">
<span style={{ maxWidth: "600px", color: "var(--text-normal)" }}>
In the process of creating a more solid foundation
for Project Colorway, the main Project Colorway repository has been
moved from <a role="link" target="_blank" href="https://github.com/DaBluLite/ProjectColorway">https://github.com/DaBluLite/ProjectColorway</a> to{" "}
<a role="link" target="_blank" href="https://github.com/ProjectColorway/ProjectColorway">https://github.com/ProjectColorway/ProjectColorway</a>
</span>
<br />
<span style={{ textAlign: "center", color: "var(--text-normal)" }}>The default Project Colorway source has been automatically updated/re-added.</span>
<br />
</div>
</div>;
}

View file

@ -0,0 +1,99 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useEffect, useRef, useState } from "..";
export default function ({ onClick, onForceReload }: { onClick: () => void, onForceReload: () => void; }) {
const menuProps = useRef(null);
const [pos, setPos] = useState({ x: 0, y: 0 });
const [showMenu, setShowMenu] = useState(false);
function rightClickContextMenu(e) {
e.stopPropagation();
window.dispatchEvent(new Event("click"));
setShowMenu(!showMenu);
setPos({
x: e.currentTarget.getBoundingClientRect().x,
y: e.currentTarget.getBoundingClientRect().y + e.currentTarget.offsetHeight + 8
});
}
function onPageClick(this: Window, e: globalThis.MouseEvent) {
setShowMenu(false);
}
useEffect(() => {
window.addEventListener("click", onPageClick);
return () => {
window.removeEventListener("click", onPageClick);
};
}, []);
function onForceReload_internal() {
onForceReload();
setShowMenu(false);
}
return <>
{showMenu ? <nav className="colorwaysContextMenu" ref={menuProps} style={{
position: "fixed",
top: `${pos.y}px`,
left: `${pos.x}px`
}}>
<button onClick={onForceReload_internal} className="colorwaysContextMenuItm">
Force Refresh
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="18"
height="18"
style={{ boxSizing: "content-box", marginLeft: "8px" }}
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>
</nav> : null}
<button className="colorwaysPillButton" onContextMenu={rightClickContextMenu} onClick={onClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="14"
height="14"
style={{ 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>
Refresh
</button>
</>;
}

View file

@ -4,67 +4,63 @@
* 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 { DataStore, openModal, useEffect, useState } from "..";
import { Colorway, ModalProps } from "../types";
import { PlusIcon } from "./Icons";
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>
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Save to source:
</h2>
<div className="colorwaysModalContent">
{noStoreError ? <span style={{ color: "var(--text-danger)" }}>Error: No store selected</span> : <></>}
{offlineColorwayStores.map(store => <div
className="discordColorway"
style={{ padding: "10px" }}
aria-checked={storename === store.name}
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>
<span className="colorwayLabel">{store.name}</span>
</div>)}
<div
className="discordColorway"
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} />
<span className="colorwayLabel">Create new store...</span>
</div>
</ModalContent>
<ModalFooter>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND_NEW}
size={Button.Sizes.MEDIUM}
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
setNoStoreError(false);
if (!storename) {
@ -74,19 +70,16 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
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}
openModal(props => <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Duplicate Colorway
</h2>
<div className="colorwaysModalContent">
<span className="colorwaysModalSectionHeader">A colorway with the same name was found in this store, what do you want to do?</span>
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
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]);
@ -98,29 +91,28 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}}
>
Override
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
</button>
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
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}
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Select new name
</h2>
<div className="colorwaysModalContent">
<input
type="text"
className="colorwaySelector-search"
value={newColorwayName}
onInput={({ currentTarget: { value } }) => setNewColorwayName(value)}
placeholder="Enter valid colorway name" />
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton"
onClick={() => {
setErrorMsg("");
if (storeToModify!.colorways.map(colorway => colorway.name).includes(newColorwayName)) {
@ -134,12 +126,9 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
onClick={() => {
if (i + 1 === colorways.length) {
modalProps.onClose();
@ -147,9 +136,9 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
</button>
</div>
</div>;
}
openModal(propss => <NewColorwayNameModal modalProps={propss} onSelected={e => {
const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways, { ...colorway, name: e }] };
@ -163,20 +152,17 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}}
>
Rename
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
onClick={() => {
props.onClose();
}}
>
Select different store
</Button>
</ModalFooter>
</ModalRoot>);
</button>
</div>
</div>);
} else {
const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways, colorway] };
DataStore.set("customColorways", [...oldStores!.filter(source => source.name !== storename), newStore]);
@ -190,18 +176,15 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
onClick={() => {
modalProps.onClose();
}}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
</button>
</div>
</div>;
}

View file

@ -1,24 +0,0 @@
/*
* 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>;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ReactNode } from "../";
export default function ({
children,
divider = false,
disabled = false
}: { children: ReactNode, divider?: boolean, disabled?: boolean; }) {
return <div style={{
display: "flex",
flexDirection: "column",
marginBottom: "20px"
}}>
{disabled ? <div style={{
pointerEvents: "none",
opacity: .5,
cursor: "not-allowed"
}}>{children}</div> : children}
{divider && <div className="colorwaysSettingsDivider" />}
</div>;
}

View file

@ -4,15 +4,27 @@
* 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";
import { DataStore, ReactNode, useCallback, useEffect, useState } from "../../";
import Setting from "../Setting";
import Switch from "../Switch";
export default function () {
export default function ({
hasTheme = false
}: {
hasTheme: boolean;
}) {
const [onDemand, setOnDemand] = useState<boolean>(false);
const [onDemandTinted, setOnDemandTinted] = useState<boolean>(false);
const [onDemandDiscordSat, setOnDemandDiscordSat] = useState<boolean>(false);
const [onDemandOsAccent, setOnDemandOsAccent] = useState<boolean>(false);
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
async function loadUI() {
const [
onDemandWays,
@ -38,47 +50,53 @@ export default function () {
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>;
function Container({ children }: { children: ReactNode; }) {
if (hasTheme) return <div className="colorwaysModalTab" data-theme={theme}>{children}</div>;
else return <div className="colorwaysModalTab">{children}</div>;
}
return <Container>
<Setting divider>
<Switch
label="Enable Colorways On Demand"
id="onDemandWays"
value={onDemand}
onChange={(v: boolean) => {
setOnDemand(v);
DataStore.set("onDemandWays", v);
}} />
<span className="colorwaysNote">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.</span>
</Setting>
<Setting divider disabled={!onDemand}>
<Switch
label="Use tinted text"
id="onDemandWaysTintedText"
value={onDemandTinted}
onChange={(v: boolean) => {
setOnDemandTinted(v);
DataStore.set("onDemandWaysTintedText", v);
}} />
</Setting>
<Setting divider disabled={!onDemand}>
<Switch
label="Use Discord's saturation"
id="onDemandWaysDiscordSaturation"
value={onDemandDiscordSat}
onChange={(v: boolean) => {
setOnDemandDiscordSat(v);
DataStore.set("onDemandWaysDiscordSaturation", v);
}} />
</Setting>
<Setting disabled={!onDemand || !getComputedStyle(document.body).getPropertyValue("--os-accent-color")}>
<Switch
label="Use Operating System's Accent Color"
id="onDemandWaysOsAccentColor"
value={onDemandOsAccent}
onChange={(v: boolean) => {
setOnDemandOsAccent(v);
DataStore.set("onDemandWaysOsAccentColor", v);
}} />
</Setting>
</Container>;
}

View file

@ -4,48 +4,52 @@
* 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 { DataStore, FluxDispatcher, FluxEvents, PluginProps, ReactNode, useEffect, useState } from "../../";
import { defaultColorwaySource, fallbackColorways, nullColorwayObj } from "../../constants";
import { Colorway } from "../../types";
import { connect, updateShouldAutoconnect } from "../../wsClient";
import { changeThemeIDCard } from "../ColorwayID";
import { changeTheme as changeThemeMain } from "../MainModal";
import Setting from "../Setting";
import Switch from "../Switch";
export default function () {
function changeTheme(theme: string) {
changeThemeMain(theme);
changeThemeIDCard(theme);
}
export default function ({
hasTheme = false
}: {
hasTheme?: boolean;
}) {
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);
const [theme, setTheme] = useState("discord");
const [shouldAutoconnect, setShouldAutoconnect] = useState<"1" | "2">("1");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
setShouldAutoconnect(await DataStore.get("colorwaysManagerDoAutoconnect") as "1" | "2");
}
load();
}, []);
useEffect(() => {
(async function () {
const [
customColorways,
colorwaySourceFiles,
showColorwaysButton,
useThinMenuButton,
showLabelsInSelectorGridView
showColorwaysButton
] = await DataStore.getMany([
"customColorways",
"colorwaySourceFiles",
"showColorwaysButton",
"useThinMenuButton",
"showLabelsInSelectorGridView"
"showColorwaysButton"
]);
const responses: Response[] = await Promise.all(
colorwaySourceFiles.map((url: string) =>
colorwaySourceFiles.map(({ url }: { url: string; }) =>
fetch(url)
)
);
@ -57,16 +61,21 @@ export default function () {
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>
function Container({ children }: { children: ReactNode; }) {
if (hasTheme) return <div className="colorwaysModalTab" data-theme={theme}>{children}</div>;
else return <div className="colorwaysModalTab">{children}</div>;
}
return <Container>
<span className="colorwaysModalSectionHeader">Quick Switch</span>
<Setting divider>
<Switch
value={colorsButtonVisibility}
label="Enable Quick Switch"
id="showColorwaysButton"
onChange={(v: boolean) => {
setColorsButtonVisibility(v);
DataStore.set("showColorwaysButton", v);
@ -74,111 +83,198 @@ export default function () {
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)",
}} />
<span className="colorwaysNote">Shows a button on the top of the servers list that opens a colorway selector modal.</span>
</Setting>
<span className="colorwaysModalSectionHeader">Appearance</span>
<Setting divider>
<div style={{
display: "flex",
flexDirection: "row",
width: "100%",
alignItems: "center",
cursor: "pointer"
}}>
<label className="colorwaySwitch-label">Plugin Theme</label>
<select
className="colorwaysPillButton"
style={{ border: "none" }}
onChange={e => {
setTheme(e.currentTarget.value);
DataStore.set("colorwaysPluginTheme", e.currentTarget.value);
changeTheme(e.currentTarget.value);
}}
value={theme}
>
<option value="discord">Discord (Default)</option>
<option value="colorish">Colorish</option>
</select>
</div>
</Setting>
<span className="colorwaysModalSectionHeader">Manager</span>
<Setting>
<div style={{
display: "flex",
flexDirection: "row",
width: "100%",
alignItems: "center",
cursor: "pointer"
}}>
<label className="colorwaySwitch-label">Automatically retry to connect to Manager</label>
<select
className="colorwaysPillButton"
style={{ border: "none" }}
onChange={({ currentTarget: { value } }) => {
if (value === "1") {
DataStore.set("colorwaysManagerDoAutoconnect", true);
updateShouldAutoconnect(true);
} else {
DataStore.set("colorwaysManagerDoAutoconnect", false);
updateShouldAutoconnect(false);
}
}}
value={shouldAutoconnect}
>
<option value="1">On (Default)</option>
<option value="2">Off</option>
</select>
</div>
</Setting>
<Setting divider>
<div style={{
display: "flex",
flexDirection: "row",
width: "100%",
alignItems: "center",
cursor: "pointer"
}}>
<label className="colorwaySwitch-label">Try to connect to Manager manually</label>
<button
className="colorwaysPillButton"
onClick={() => connect()}
value={shouldAutoconnect}
>
Try to connect...
</button>
</div>
</Setting>
<Setting divider>
<div style={{
display: "flex",
flexDirection: "row",
width: "100%",
alignItems: "center",
cursor: "pointer"
}}>
<label className="colorwaySwitch-label">Reset plugin to default settings (CANNOT BE UNDONE)</label>
<button
className="colorwaysPillButton"
onClick={() => {
DataStore.setMany([
["customColorways", []],
["colorwaySourceFiles", [{
name: "Project Colorway",
url: defaultColorwaySource
}]],
["showColorwaysButton", false],
["onDemandWays", false],
["onDemandWaysTintedText", true],
["onDemandWaysDiscordSaturation", false],
["onDemandWaysOsAccentColor", false],
["activeColorwayObject", nullColorwayObj],
["colorwaysPluginTheme", "discord"],
["colorwaysBoundManagers", []],
["colorwaysManagerAutoconnectPeriod", 3000],
["colorwaysManagerDoAutoconnect", true]
]);
}}
>
Reset...
</button>
</div>
<span className="colorwaysNote">Reset the plugin to its default settings. All bound managers, sources, and colorways will be deleted. Please reload Discord after use.</span>
</Setting>
<div style={{ flexDirection: "column", display: "flex" }}>
<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",
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>
backgroundColor: "var(--brand-500)",
padding: "0 4px",
borderRadius: "4px"
}}>Colorways</span>
</h1>
<span
style={{
color: "var(--text-normal)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "12px"
}}
>by Project Colorway</span>
<span className="colorwaysModalSectionHeader">
Plugin Version:
</span>
<span
style={{
color: "var(--text-muted)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "8px"
}}
>
{PluginProps.pluginVersion} ({PluginProps.clientMod})
</span>
<span className="colorwaysModalSectionHeader">
UI Version:
</span>
<span
style={{
color: "var(--text-muted)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "8px"
}}
>
{PluginProps.UIVersion}
</span>
<span className="colorwaysModalSectionHeader">
Creator Version:
</span>
<span
style={{
color: "var(--text-muted)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "8px"
}}
>
{PluginProps.creatorVersion}
</span>
<span className="colorwaysModalSectionHeader">
Loaded Colorways:
</span>
<span
style={{
color: "var(--text-muted)",
fontWeight: 500,
fontSize: "14px",
marginBottom: "8px"
}}
>
{[...colorways, ...customColorways].length}
</span>
<span className="colorwaysModalSectionHeader">
Project Repositories:
</span>
<a role="link" target="_blank" href="https://github.com/DaBluLite/DiscordColorways">DiscordColorways</a>
<a role="link" target="_blank" href="https://github.com/DaBluLite/ProjectColorway">Project Colorway</a>
</div>
</SettingsTab>;
</Container>;
}

View file

@ -4,39 +4,39 @@
* 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 { DataStore, openModal, ReactNode, useEffect, useState } from "../../";
import { defaultColorwaySource } from "../../constants";
import { Colorway } from "../../types";
import { DownloadIcon, ImportIcon } from "../Icons";
import Spinner from "../Spinner";
import { Colorway, ModalProps } from "../../types";
import { chooseFile, saveFile } from "../../utils";
import { updateRemoteSources } from "../../wsClient";
import { CopyIcon, DeleteIcon, DownloadIcon, ImportIcon, PlusIcon } from "../Icons";
import TabBar from "../TabBar";
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
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
{conflicting ? "Duplicate Store Name" : "Give this store a name"}
</h2>
<div className="colorwaysModalContent">
{conflicting ? <span className="colorwaysModalSectionHeader">A store with the same name already exists. Please give a different name to the imported store:</span> : <></>}
<span className="colorwaysModalSectionHeader">Name:</span>
<input type="text" className="colorwaySelector-search" value={newStoreName} onChange={({ currentTarget: { value } }) => setNewStoreName(value)} style={{ marginBottom: "16px" }} />
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
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)) {
@ -47,18 +47,16 @@ export function StoreNameModal({ modalProps, originalName, onFinish, conflicting
}}
>
Finish
</Button>
<Button
</button>
<button
className="colorwaysPillButton"
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
</button>
</div>
</div>;
}
function AddOnlineStoreModal({ modalProps, onFinish }: { modalProps: ModalProps, onFinish: (name: string, url: string) => void; }) {
@ -67,26 +65,35 @@ function AddOnlineStoreModal({ modalProps, onFinish }: { modalProps: ModalProps,
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
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Add a source:
</h2>
<div className="colorwaysModalContent">
<span className="colorwaysModalSectionHeader">Name:</span>
<input
type="text"
className="colorwaySelector-search"
placeholder="Enter a valid Name..."
onChange={setColorwaySourceName}
onInput={e => setColorwaySourceName(e.currentTarget.value)}
value={colorwaySourceName}
error={nameError}
readOnly={nameReadOnly}
disabled={nameReadOnly}
/>
<Forms.FormTitle style={{ marginTop: "8px" }}>URL:</Forms.FormTitle>
<TextInput
<span className="colorwaysModalSectionHeader" style={{ marginTop: "8px" }}>URL:</span>
<input
type="text"
className="colorwaySelector-search"
placeholder="Enter a valid URL..."
onChange={value => {
onChange={({ currentTarget: { value } }) => {
setColorwaySourceURL(value);
if (value === defaultColorwaySource) {
setNameReadOnly(true);
@ -94,16 +101,12 @@ function AddOnlineStoreModal({ modalProps, onFinish }: { modalProps: ModalProps,
}
}}
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}
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
const sourcesArr: { name: string, url: string; }[] = (await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
if (!colorwaySourceName) {
@ -124,197 +127,104 @@ function AddOnlineStoreModal({ modalProps, onFinish }: { modalProps: ModalProps,
}}
>
Finish
</Button>
<Button
style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
</button>
<button
className="colorwaysPillButton"
onClick={() => modalProps.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot>;
</button>
</div>
</div>;
}
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");
export default function ({
hasTheme = false
}: {
hasTheme?: boolean;
}) {
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
function Container({ children }: { children: ReactNode; }) {
if (hasTheme) return <div className="colorwaysModalTab" data-theme={theme}>{children}</div>;
else return <div className="colorwaysModalTab">{children}</div>;
}
return <Container>
<TabBar items={[
{
name: "Online",
component: OnlineTab
},
{
name: "Offline",
component: OfflineTab
}
]} />
</Container >;
}
function OfflineTab() {
const [customColorwayStores, setCustomColorwayStores] = useState<{ name: string, colorways: Colorway[]; }[]>([]);
useEffect(() => {
(async function () {
setColorwaySourceFiles(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
updateRemoteSources();
})();
}, []);
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"
return <div className="colorwaySourceTab">
<div style={{
display: "flex",
gap: "8px"
}}>
<button
className="colorwaysPillButton"
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 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)]);
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[]; }[]);
}
} catch (err) {
new Logger("DiscordColorways").error(err);
updateRemoteSources();
}} />);
} 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[]; }[]);
updateRemoteSources();
}
};
reader.readAsText(file);
}
} catch (err) {
console.error("DiscordColorways: " + err);
}
};
reader.readAsText(file);
updateRemoteSources();
}}
>
<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}
</button>
<button
className="colorwaysPillButton"
style={{ flexShrink: "0" }}
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();
updateRemoteSources();
}} />);
}}>
<svg
@ -330,41 +240,31 @@ export default function () {
/>
</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{" "}
</button>
</div>
<div className="colorwaysSettings-sourceScroller">
{getComputedStyle(document.body).getPropertyValue("--os-accent-color") ? <div className={"colorwaysSettings-colorwaySource"} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<div style={{ alignItems: "center", width: "100%", height: "30px", display: "flex" }}>
<span className="colorwaysSettings-colorwaySourceLabel">OS Accent Color{" "}
<div className="colorways-badge">Built-In</div>
</Text>
</Flex>
</span>
</div>
</div> : <></>}
{customColorwayStores.map(({ name: customColorwaySourceName, colorways: offlineStoreColorways }) => <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<Text className="colorwaysSettings-colorwaySourceLabel">
{customColorwayStores.map(({ name: customColorwaySourceName, colorways: offlineStoreColorways }) => <div className={"colorwaysSettings-colorwaySource"} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<span 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}
</span>
<div style={{ marginLeft: "auto", gap: "8px", display: "flex" }}>
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
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" }));
}
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}
</button>
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
var sourcesArr: { name: string, colorways: Colorway[]; }[] = [];
const customColorwaySources = await DataStore.get("customColorways");
@ -375,13 +275,141 @@ export default function () {
});
DataStore.set("customColorways", sourcesArr);
setCustomColorwayStores(sourcesArr);
updateRemoteSources();
}}
>
<DeleteIcon width={20} height={20} /> Remove
</Button>
</Flex>
</button>
</div>
</div>
)}
</ScrollerThin>
</SettingsTab>;
</div>
</div>;
}
function OnlineTab() {
const [colorwaySourceFiles, setColorwaySourceFiles] = useState<{ name: string, url: string; }[]>([]);
useEffect(() => {
(async function () {
setColorwaySourceFiles(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
updateRemoteSources();
})();
}, []);
return <div className="colorwaySourceTab">
<div style={{
display: "flex",
gap: "8px"
}}>
<button
className="colorwaysPillButton"
style={{ flexShrink: "0" }}
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 }]);
updateRemoteSources();
}} />);
}}>
<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>
</div>
<div className="colorwaysSettings-sourceScroller">
{!colorwaySourceFiles.length && <div className={"colorwaysSettings-colorwaySource"} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }} onClick={async () => {
DataStore.set("colorwaySourceFiles", [{ name: "Project Colorway", url: defaultColorwaySource }, ...(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).filter(i => i.name !== "Project Colorway")]);
setColorwaySourceFiles([{ name: "Project Colorway", url: defaultColorwaySource }, ...(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).filter(i => i.name !== "Project Colorway")]);
}}>
<PlusIcon width={24} height={24} />
<span className="colorwaysSettings-colorwaySourceLabel">
Add Project Colorway Source
</span>
</div>}
{colorwaySourceFiles.map((colorwaySourceFile: { name: string, url: string; }, i: number) => <div className={"colorwaysSettings-colorwaySource"} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<div className="hoverRoll">
<span className="colorwaysSettings-colorwaySourceLabel hoverRoll_normal">
{colorwaySourceFile.name} {colorwaySourceFile.url === defaultColorwaySource && <div className="colorways-badge">Built-In</div>} {colorwaySourceFile.url === "https://raw.githubusercontent.com/DaBluLite/ProjectColorway/master/index.json" && <div className="colorways-badge">Built-In | Outdated</div>}
</span>
<span className="colorwaysSettings-colorwaySourceLabel hoverRoll_hovered">
{colorwaySourceFile.url}
</span>
</div>
<div style={{ marginLeft: "auto", gap: "8px", display: "flex" }}>
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={() => { navigator.clipboard.writeText(colorwaySourceFile.url); }}
>
<CopyIcon width={14} height={14} /> Copy URL
</button>
{colorwaySourceFile.url === "https://raw.githubusercontent.com/DaBluLite/ProjectColorway/master/index.json" && <button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
DataStore.set("colorwaySourceFiles", [{ name: "Project Colorway", url: defaultColorwaySource }, ...(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).filter(i => i.name !== "Project Colorway")]);
setColorwaySourceFiles([{ name: "Project Colorway", url: defaultColorwaySource }, ...(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).filter(i => i.name !== "Project Colorway")]);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="14"
height="14"
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> Update source...
</button>}
{(colorwaySourceFile.url !== defaultColorwaySource && colorwaySourceFile.url !== "https://raw.githubusercontent.com/DaBluLite/ProjectColorway/master/index.json")
&& <>
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
openModal(props => <StoreNameModal conflicting={false} modalProps={props} originalName={colorwaySourceFile.name || ""} onFinish={async e => {
const res = await fetch(colorwaySourceFile.url);
const data = await res.json();
DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: data.colorways || [] }]);
updateRemoteSources();
}} />);
}}
>
<DownloadIcon width={14} height={14} /> Download...
</button>
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
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));
updateRemoteSources();
}}
>
<DeleteIcon width={14} height={14} /> Remove
</button>
</>}
</div>
</div>
)}
</div>
</div>;
}

View file

@ -4,32 +4,27 @@
* 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 { DataStore, openModal, ReactNode, useEffect, useState } from "../../";
import { StoreItem } from "../../types";
import { DownloadIcon, PalleteIcon } from "../Icons";
import { DeleteIcon, 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 () {
export default function ({
hasTheme = false
}: {
hasTheme?: boolean;
}) {
const [storeObject, setStoreObject] = useState<StoreItem[]>([]);
const [colorwaySourceFiles, setColorwaySourceFiles] = useState<{ name: string, url: string; }[]>([]);
const [searchValue, setSearchValue] = useState<string>("");
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
useEffect(() => {
if (!searchValue) {
@ -42,79 +37,76 @@ export default function () {
}
}, []);
const { item: radioBarItem, itemFilled: radioBarItemFilled } = findByProps("radioBar");
function Container({ children }: { children: ReactNode; }) {
if (hasTheme) return <div className="colorwaysModalTab" data-theme={theme}>{children}</div>;
else return <div className="colorwaysModalTab">{children}</div>;
}
return <SettingsTab title="Colorway Store">
<Flex style={{ gap: "0", marginBottom: "8px" }}>
<TextInput
return <Container>
<div style={{ display: "flex", marginBottom: "8px" }}>
<input
type="text"
className="colorwaySelector-search"
placeholder="Search for sources..."
value={searchValue}
onChange={setSearchValue}
onChange={e => setSearchValue(e.currentTarget.value)}
/>
<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; }[]);
}}
<button
className="colorwaysPillButton"
style={{ marginLeft: "8px", marginTop: "auto", marginBottom: "auto" }}
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="14"
height="14"
style={{ boxSizing: "content-box", flexShrink: 0 }}
viewBox="0 0 24 24"
fill="currentColor"
>
<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">
<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>
Refresh
</button>
</div>
<div 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.toLowerCase().includes(searchValue.toLowerCase()) ? <div className={"colorwaysSettings-colorwaySource"} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<div style={{ gap: ".5rem", display: "flex", marginBottom: "8px", flexDirection: "column" }}>
<span className="colorwaysSettings-colorwaySourceLabelHeader">
{item.name}
</Text>
<Text className="colorwaysSettings-colorwaySourceDesc">
</span>
<span className="colorwaysSettings-colorwaySourceDesc">
{item.description}
</Text>
<Text className="colorwaysSettings-colorwaySourceDesc" style={{ opacity: ".8" }}>
</span>
<span 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}
</span>
</div>
<div style={{ gap: "8px", alignItems: "center", width: "100%", display: "flex" }}>
<a role="link" target="_blank" href={"https://github.com/" + item.authorGh}>
<img src="/assets/6a853b4c87fce386cbfef4a2efbacb09.svg" alt="GitHub" />
</a>
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
style={{ marginLeft: "auto" }}
onClick={async () => {
if (colorwaySourceFiles.map(source => source.name).includes(item.name)) {
@ -129,21 +121,26 @@ export default function () {
}}
>
{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}
</button>
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
openModal(props => <Selector modalProps={props} settings={{ selectorType: "preview", previewSource: item.url }} />);
openModal(props => <div className={`colorwaysModal ${props.transitionState === 2 ? "closing" : ""} ${props.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">
Previewing colorways for {item.name}
</h2>
<div className="colorwaysModalContent colorwaysModalContent-sourcePreview">
<Selector settings={{ selectorType: "preview", previewSource: item.url }} />
</div>
</div>);
}}
>
<PalleteIcon width={14} height={14} />{" "}Preview
</Button>
</Flex>
<PalleteIcon width={14} height={14} />
Preview
</button>
</div>
</div> : <></>
)}
</ScrollerThin>
</SettingsTab>;
</div>
</Container>;
}

View file

@ -0,0 +1,61 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useEffect, useRef, useState } from "..";
export default function ({ source, sources, onSourceChange }: { source: { name: string, id: string; }, sources: { name: string, id: string; }[], onSourceChange: (sourceId: string) => void; }) {
const menuProps = useRef(null);
const [pos, setPos] = useState({ x: 0, y: 0 });
const [showMenu, setShowMenu] = useState(false);
const [current, setCurrent] = useState(source);
function rightClickContextMenu(e) {
e.stopPropagation();
window.dispatchEvent(new Event("click"));
setShowMenu(!showMenu);
setPos({
x: e.currentTarget.getBoundingClientRect().x,
y: e.currentTarget.getBoundingClientRect().y + e.currentTarget.offsetHeight + 8
});
}
function onPageClick() {
setShowMenu(false);
}
useEffect(() => {
window.addEventListener("click", onPageClick);
return () => {
window.removeEventListener("click", onPageClick);
};
}, []);
function onSourceChange_internal(newSort: { name: string, id: string; }) {
onSourceChange(newSort.id);
setCurrent(newSort);
setShowMenu(false);
}
return <>
{showMenu ? <nav className="colorwaysContextMenu" ref={menuProps} style={{
position: "fixed",
top: `${pos.y}px`,
left: `${pos.x}px`
}}>
{sources.map(({ name, id }) => {
return <button onClick={() => onSourceChange_internal({ name, id })} className="colorwaysContextMenuItm">
{name}
<svg aria-hidden="true" role="img" width="18" height="18" viewBox="0 0 24 24" style={{
marginLeft: "8px"
}}>
<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" />
{source.id === id ? <circle className="colorwaysRadioSelected" cx="12" cy="12" r="5" /> : null}
</svg>
</button>;
})}
</nav> : null}
<button className="colorwaysPillButton" onClick={rightClickContextMenu}>Source: {current.name}</button>
</>;
}

View file

@ -4,9 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CSSProperties } from "react";
export default function ({ className, style }: { className?: string, style?: CSSProperties; }) {
export default function ({ className, style }: { className?: string, style?: any; }) {
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">

View file

@ -0,0 +1,78 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export default function ({
onChange,
value,
id,
label
}: {
id?: string,
value: boolean,
label?: string,
onChange: (checked: boolean) => void;
}) {
return label ? <div style={{
display: "flex",
flexDirection: "row",
width: "100%",
alignItems: "center",
cursor: "pointer"
}}>
<label className="colorwaySwitch-label" htmlFor={id}>{label}</label>
<div className={`colorwaysSettings-switch ${value ? "checked" : ""}`}>
<svg viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style={{
left: value ? "12px" : "-3px",
transition: ".2s ease",
display: "block",
position: "absolute",
width: "28px",
height: "18px",
margin: "3px"
}}>
<rect className="colorwaysSettings-switchCircle" fill="#000" x="4" y="0" height="20" width="20" rx="10" />
</svg>
<input checked={value} id={id} type="checkbox" style={{
position: "absolute",
opacity: 0,
width: "100%",
height: "100%",
cursor: "pointer",
borderRadius: "14px",
top: 0,
left: 0,
margin: 0
}} tabIndex={0} onChange={e => {
onChange(e.currentTarget.checked);
}} />
</div>
</div> : <div className={`colorwaysSettings-switch ${value ? "checked" : ""}`}>
<svg viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet" aria-hidden="true" style={{
left: value ? "12px" : "-3px",
transition: ".2s ease",
display: "block",
position: "absolute",
width: "28px",
height: "18px",
margin: "3px"
}}>
<rect className="colorwaysSettings-switchCircle" fill="#000" x="4" y="0" height="20" width="20" rx="10" />
</svg>
<input checked={value} id={id} type="checkbox" style={{
position: "absolute",
opacity: 0,
width: "100%",
height: "100%",
cursor: "pointer",
borderRadius: "14px",
top: 0,
left: 0,
margin: 0
}} tabIndex={0} onChange={e => {
onChange(e.currentTarget.checked);
}} />
</div>;
}

View file

@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useState } from "..";
export default function ({
items = []
}: {
items: { name: string, component: () => JSX.Element; }[];
}) {
const [active, setActive] = useState(items[0].name);
return <>
<div className="colorwaysMenuTabs">
{items.map(item => {
return <div className={`colorwaysMenuTab ${active === item.name ? "active" : ""}`} onClick={() => {
setActive(item.name);
}}>{item.name}</div>;
})}
</div>
{items.map(item => {
const Component = item.component;
return active === item.name ? <Component /> : null;
})}
</>;
}

View file

@ -1,12 +1,11 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* 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 { openModal } from "..";
import { ModalProps } from "../types";
import { HexToHSL } from "../utils";
import { CloseIcon } from "./Icons";
@ -48,12 +47,12 @@ export default function ThemePreview({
if (isModal) {
modalProps?.onClose();
} else {
openModal((props: ModalProps) => <ModalRoot className="colorwaysPreview-modal" {...props}>
openModal((props: ModalProps) => <div className={`colorwaysPreview-modal ${props.transitionState === 2 ? "closing" : ""} ${props.transitionState === 4 ? "hidden" : ""}`}>
<style>
{previewCSS}
</style>
<ThemePreview accent={accent} primary={primary} secondary={secondary} tertiary={tertiary} isModal modalProps={props} />
</ModalRoot>);
</div>);
}
}}
>
@ -122,14 +121,12 @@ export default function ThemePreview({
"--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}
>
<span style={{
fontWeight: 700,
color: "var(--text-normal)"
}}>
Preview
</Text>
</span>
</div>
</div>
<div className="colorwayPreview-chat" style={{ background: `var(--dc-overlay-chat, ${primary})` }}>

View file

@ -0,0 +1,63 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useEffect, useRef, useState } from "..";
export default function ({
children,
text,
position = "top"
}: {
children: (props: { onMouseEnter: () => void; onMouseLeave: () => void; onClick: () => void; }) => JSX.Element,
text: JSX.Element,
position?: "top" | "bottom" | "left" | "right";
}) {
const [visible, setVisible] = useState(false);
const [pos, setPos] = useState({ x: 0, y: 0 });
const btn = useRef(null);
function showTooltip() {
setPos({
x: (btn.current as unknown as HTMLElement).children[0].getBoundingClientRect().x + ((btn.current as unknown as HTMLElement).children[0] as HTMLElement).offsetWidth + 8,
y: (btn.current as unknown as HTMLElement).children[0].getBoundingClientRect().y
});
setVisible(true);
}
function onWindowUnfocused(e) {
e = e || window.event;
var from = e.relatedTarget || e.toElement;
if (!from || from.nodeName === "HTML") {
setVisible(false);
}
}
useEffect(() => {
document.addEventListener("mouseout", onWindowUnfocused);
return () => {
document.removeEventListener("mouseout", onWindowUnfocused);
};
}, []);
return <>
<div ref={btn} style={{
display: "contents"
}}>
{children({
onMouseEnter: () => showTooltip(),
onMouseLeave: () => setVisible(false),
onClick: () => setVisible(false)
})}
</div>
<div className={`colorwaysTooltip colorwaysTooltip-${position} ${!visible ? "colorwaysTooltip-hidden" : ""}`} style={{
top: `${pos.y}px`,
left: `${pos.x}px`
}}>
<div className="colorwaysTooltipPointer" />
<div className="colorwaysTooltipContent">{text}</div>
</div>
</>;
}

View file

@ -0,0 +1,57 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore, useEffect, useState } from "..";
import { ModalProps } from "../types";
import { getRepainterTheme } from "../utils";
export default function ({ modalProps, onFinish }: { modalProps: ModalProps, onFinish: ({ id, colors }: { id: string, colors: string[]; }) => void; }) {
const [colorwaySourceURL, setColorwaySourceURL] = useState<string>("");
const [URLError, setURLError] = useState<string>("");
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<h2 className="colorwaysModalHeader">Use Repainter theme</h2>
<div className="colorwaysModalContent">
<span className="colorwaysModalSectionHeader">URL: {URLError ? <span className="colorwaysModalSectionError">{URLError}</span> : <></>}</span>
<input
type="text"
placeholder="Enter a valid URL..."
onInput={e => {
setColorwaySourceURL(e.currentTarget.value);
}}
value={colorwaySourceURL}
className="colorwaySelector-search"
/>
</div>
<div className="colorwaysModalFooter">
<button
className="colorwaysPillButton colorwaysPillButton-onSurface"
onClick={async () => {
getRepainterTheme(colorwaySourceURL).then(data => {
onFinish({ id: data.id as any, colors: data.colors as any });
modalProps.onClose();
}).catch(e => setURLError("Error: " + e));
}}
>
Finish
</button>
<button
className="colorwaysPillButton"
onClick={() => modalProps.onClose()}
>
Cancel
</button>
</div>
</div>;
}

View file

@ -4,7 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const defaultColorwaySource = "https://raw.githubusercontent.com/DaBluLite/ProjectColorway/master/index.json";
import { ColorwayObject } from "./types";
export const defaultColorwaySource = "https://raw.githubusercontent.com/ProjectColorway/ProjectColorway/master/index.json";
export const fallbackColorways = [
{
@ -311,3 +313,5 @@ export const mainColors = [
{ name: "secondary", title: "Secondary", var: "--background-secondary" },
{ name: "tertiary", title: "Tertiary", var: "--background-tertiary" }
];
export const nullColorwayObj: ColorwayObject = { id: null, css: null, sourceType: null, source: null };

View file

@ -4,9 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { UserStore } from "@webpack/common";
import { Plugins } from "Vencord";
import { PluginProps, UserStore } from "./";
import { HexToHSL } from "./utils";
export const colorVariables: string[] = [
@ -398,31 +396,31 @@ export function gradientBase(accentColor?: string, discordSaturation = false) {
--bg-overlay-opacity-home-card: 0.9;
--bg-overlay-opacity-app-frame: var(--bg-overlay-opacity-5);
}
.children_cde9af:after, .form_d8a4a1:before {
.children_fc4f04:after, .form_a7d72e:before {
content: none;
}
.scroller_de945b {
.scroller_fea3ef {
background: var(--bg-overlay-app-frame,var(--background-tertiary));
}
.expandedFolderBackground_b1385f {
.expandedFolderBackground_bc7085 {
background: rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-6));
}
.wrapper__8436d:not(:hover):not(.selected_ae80f7) .childWrapper_a6ce15 {
background: rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-6));
}
.folder__17546:has(.expandedFolderIconWrapper__324c1) {
.folder_bc7085:has(.expandedFolderIconWrapper_bc7085) {
background: var(--bg-overlay-6,var(--background-secondary));
}
.circleIconButton__05cf2:not(.selected_aded59) {
.circleIconButton_db6521:not(.selected_db6521) {
background: rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-6));
}
.auto_a3c0bd::-webkit-scrollbar-thumb,
.thin_b1c063::-webkit-scrollbar-thumb {
.auto_eed6a8::-webkit-scrollbar-thumb,
.thin_eed6a8::-webkit-scrollbar-thumb {
background-size: 200vh;
background-image: -webkit-gradient(linear,left top,left bottom,from(rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-4))),to(rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-4)))),var(--custom-theme-background);
background-image: linear-gradient(rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-4)),rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-4))),var(--custom-theme-background);
}
.auto_a3c0bd::-webkit-scrollbar-track {
.auto_eed6a8::-webkit-scrollbar-track {
background-size: 200vh;
background-image: -webkit-gradient(linear,left top,left bottom,from(rgb(var(--bg-overlay-color)/.4)),to(rgb(var(--bg-overlay-color)/.4))),var(--custom-theme-background);
background-image: linear-gradient(rgb(var(--bg-overlay-color)/.4),rgb(var(--bg-overlay-color)/.4)),var(--custom-theme-background);
@ -470,10 +468,10 @@ export function gradientBase(accentColor?: string, discordSaturation = false) {
}`;
}
export function generateCss(primaryColor: string, secondaryColor: string, tertiaryColor: string, accentColor: string, tintedText: boolean, discordSaturation: boolean, mutedTextBrightness?: number, name?: string) {
export function generateCss(primaryColor: string, secondaryColor: string, tertiaryColor: string, accentColor: string, tintedText: boolean = true, discordSaturation: boolean = true, mutedTextBrightness?: number, name?: string) {
return `/**
* @name ${name}
* @version ${(Plugins.plugins.DiscordColorways as any).creatorVersion}
* @version ${PluginProps.creatorVersion}
* @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id}
@ -529,30 +527,30 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
--primary-160-hsl: ${HexToHSL("#" + secondaryColor)[0]} calc(var(--saturation-factor, 1)*${discordSaturation ? Math.round(((HexToHSL("#" + secondaryColor)[1] / 100) * (100 + PrimarySatDiffs[660])) * 10) / 10 : HexToHSL("#" + secondaryColor)[1]}%) ${Math.min(HexToHSL("#" + secondaryColor)[2] + 76.4, 82.5)}%;
--primary-200-hsl: ${HexToHSL("#" + tertiaryColor)[0]} calc(var(--saturation-factor, 1)*${HexToHSL("#" + tertiaryColor)[1]}%) ${Math.min(HexToHSL("#" + tertiaryColor)[2] + 80, 80)}%;
}
.emptyPage_feb902,
.scrollerContainer_dda72c,
.container__03ec9,
.header__71942 {
.emptyPage_c6b11b,
.scrollerContainer_c6b11b,
.container_f1fd9c,
.header_f1fd9c {
background-color: unset !important;
}
.container__6b2e5,
.container__03ec9,
.header__71942 {
.container_c2efea,
.container_f1fd9c,
.header_f1fd9c {
background: transparent !important;
}${(Math.round(HexToHSL("#" + primaryColor)[2]) > 80) ? `\n\n/*Primary*/
.theme-dark .container_bd15da,
.theme-dark .body__616e6,
.theme-dark .toolbar__62fb5,
.theme-dark .container_e1387b,
.theme-dark .messageContent_abea64,
.theme-dark .attachButtonPlus_fd0021,
.theme-dark .username__0b0e7:not([style]),
.theme-dark .children_cde9af,
.theme-dark .buttonContainer__6de7e,
.theme-dark .listItem__48528,
.theme-dark .body__616e6 .caret__33d19,
.theme-dark .body__616e6 .titleWrapper_d6133e > h1,
.theme-dark .body__616e6 .icon_ae0b42 {
.theme-dark .container_c2739c,
.theme-dark .body_cd82a7,
.theme-dark .toolbar_fc4f04,
.theme-dark .container_f0fccd,
.theme-dark .messageContent_f9f2ca,
.theme-dark .attachButtonPlus_f298d4,
.theme-dark .username_f9f2ca:not([style]),
.theme-dark .children_fc4f04,
.theme-dark .buttonContainer_f9f2ca,
.theme-dark .listItem_c96c45,
.theme-dark .body_cd82a7 .caret_fc4f04,
.theme-dark .body_cd82a7 .titleWrapper_fc4f04 > h1,
.theme-dark .body_cd82a7 .icon_fc4f04 {
--white-500: black !important;
--interactive-normal: black !important;
--text-normal: black !important;
@ -561,23 +559,23 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
--header-secondary: black !important;
}
.theme-dark .contentRegionScroller__9ae20 :not(.mtk1,.mtk2,.mtk3,.mtk4,.mtk5,.mtk6,.mtk7,.mtk8,.mtk9,.monaco-editor .line-numbers) {
.theme-dark .contentRegionScroller_c25c6d :not(.mtk1,.mtk2,.mtk3,.mtk4,.mtk5,.mtk6,.mtk7,.mtk8,.mtk9,.monaco-editor .line-numbers) {
--white-500: black !important;
}
.theme-dark .container__26baa {
.theme-dark .container_fc4f04 {
--channel-icon: black;
}
.theme-dark .callContainer__1477d {
.theme-dark .callContainer_d880dc {
--white-500: ${(HexToHSL("#" + tertiaryColor)[2] > 80) ? "black" : "white"} !important;
}
.theme-dark .channelTextArea_c2094b {
.theme-dark .channelTextArea_a7d72e {
--text-normal: ${(HexToHSL("#" + primaryColor)[2] + 3.6 > 80) ? "black" : "white"};
}
.theme-dark .placeholder_dec8c7 {
.theme-dark .placeholder_a552a6 {
--channel-text-area-placeholder: ${(HexToHSL("#" + primaryColor)[2] + 3.6 > 80) ? "black" : "white"};
opacity: .6;
}
@ -586,16 +584,16 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
background-color: black;
}
.theme-dark .root_a28985 > .header__5e5a6 > h1 {
.theme-dark .root_f9a4c9 > .header_f9a4c9 > h1 {
color: black;
}
/*End Primary*/`: ""}${(HexToHSL("#" + secondaryColor)[2] > 80) ? `\n\n/*Secondary*/
.theme-dark .wrapper__3c6d5 *,
.theme-dark .sidebar_e031be *:not(.hasBanner__04337 *),
.theme-dark .members__573eb *:not([style]),
.theme-dark .sidebarRegionScroller__8113e *,
.theme-dark .header__8e271,
.theme-dark .lookFilled__950dd.colorPrimary_ebe632 {
.theme-dark .wrapper_cd82a7 *,
.theme-dark .sidebar_a4d4d9 *:not(.hasBanner_fd6364 *),
.theme-dark .members_cbd271 *:not([style]),
.theme-dark .sidebarRegionScroller_c25c6d *,
.theme-dark .header_e06857,
.theme-dark .lookFilled_dd4f85.colorPrimary_dd4f85 {
--white-500: black !important;
--channels-default: black !important;
--channel-icon: black !important;
@ -604,36 +602,36 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
--interactive-active: var(--white-500);
}
.theme-dark .channelRow__538ef {
.theme-dark .channelRow_f04d06 {
background-color: var(--background-secondary);
}
.theme-dark .channelRow__538ef * {
.theme-dark .channelRow_f04d06 * {
--channel-icon: black;
}
.theme-dark #app-mount .activity_bafb94 {
.theme-dark #app-mount .activity_a31c43 {
--channels-default: var(--white-500) !important;
}
.theme-dark .nameTag__77ab2 {
.theme-dark .nameTag_b2ca13 {
--header-primary: black !important;
--header-secondary: ${HexToHSL("#" + secondaryColor)[0] === 0 ? "gray" : ((HexToHSL("#" + secondaryColor)[2] < 80) ? "hsl(" + HexToHSL("#" + secondaryColor)[0] + ", calc(var(--saturation-factor, 1)*100%), 90%)" : "hsl(" + HexToHSL("#" + secondaryColor)[0] + ", calc(var(--saturation-factor, 1)*100%), 20%)")} !important;
}
.theme-dark .bannerVisible_ef30fe .headerContent__6fcc7 {
.theme-dark .bannerVisible_fd6364 .headerContent_fd6364 {
color: #fff;
}
.theme-dark .embedFull__14919 {
.theme-dark .embedFull_b0068a {
--text-normal: black;
}
/*End Secondary*/`: ""}${HexToHSL("#" + tertiaryColor)[2] > 80 ? `\n\n/*Tertiary*/
.theme-dark .winButton_f17fb6,
.theme-dark .searchBar__310d8 *,
.theme-dark .wordmarkWindows_ffbc5e,
.theme-dark .searchBar__5a20a *,
.theme-dark .searchBarComponent__8f95f {
.theme-dark .winButton_a934d8,
.theme-dark .searchBar_e0840f *,
.theme-dark .wordmarkWindows_a934d8,
.theme-dark .searchBar_a46bef *,
.theme-dark .searchBarComponent_f0963d {
--white-500: black !important;
}
@ -641,25 +639,25 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
color: ${HexToHSL("#" + secondaryColor)[2] > 80 ? "black" : "white"};
}
.theme-dark .popout__24e32 > * {
.theme-dark .popout_c5b389 > * {
--interactive-normal: black !important;
--header-secondary: black !important;
}
.theme-dark .tooltip__7b090 {
.theme-dark .tooltip_b6c360 {
--text-normal: black !important;
}
.theme-dark .children_cde9af .icon_ae0b42 {
.theme-dark .children_fc4f04 .icon_fc4f04 {
color: var(--interactive-active) !important;
}
/*End Tertiary*/`: ""}${HexToHSL("#" + accentColor)[2] > 80 ? `\n\n/*Accent*/
.selected_aded59 *,
.selected_db6521 *,
.selected_ae80f7 *,
#app-mount .lookFilled__950dd.colorBrand__27d57:not(.buttonColor__7bad9),
.colorDefault_e361cf.focused_dcafb9,
.row__9e25f:hover,
#app-mount .lookFilled_dd4f85.colorBrand_dd4f85:not(.buttonColor_adcaac),
.colorDefault_d90b3d.focused_d90b3d,
.row_c5b389:hover,
.colorwayInfoIcon,
.checkmarkCircle_b1b1cc > circle {
.checkmarkCircle_cb7c27 > circle {
--white-500: black !important;
}
@ -789,10 +787,10 @@ export function getAutoPresets(accentColor?: string) {
--primary-400: hsl(${HexToHSL("#" + accentColor)[0]}, calc(var(--saturation-factor, 1)*12%), 90%);
--primary-360: hsl(${HexToHSL("#" + accentColor)[0]}, calc(var(--saturation-factor, 1)*12%), 90%);
}
.emptyPage_feb902,
.scrollerContainer_dda72c,
.container__03ec9,
.header__71942 {
.emptyPage_c6b11b,
.scrollerContainer_c6b11b,
.container_f1fd9c,
.header_f1fd9c {
background-color: unset !important;
}`;
}
@ -816,7 +814,25 @@ export function getAutoPresets(accentColor?: string) {
} as { [key: string]: { name: string, id: string, preset: () => string; }; };
}
export function getPreset(primaryColor?: string, secondaryColor?: string, tertiaryColor?: string, accentColor?: string): { [preset: string]: { name: string, preset: (...args: any) => string | { full: string, base: string; }, id: string, colors: string[]; }; } {
export function getPreset(
primaryColor?: string,
secondaryColor?: string,
tertiaryColor?: string,
accentColor?: string
): {
[preset: string]: {
name: string,
preset: (...args: any) => string | { full: string, base: string; },
id: string,
colors: string[],
calculated?: {
accent?: string,
primary?: string,
secondary?: string,
tertiary?: string;
};
};
} {
function cyanLegacy(discordSaturation = false) {
return `:root:root {
--cyan-accent-color: #${accentColor};
@ -979,7 +995,12 @@ export function getPreset(primaryColor?: string, secondaryColor?: string, tertia
name: "Hue Rotation",
preset: getAutoPresets(accentColor).hueRotation.preset,
id: "hueRotation",
colors: ["accent"]
colors: ["accent"],
calculated: {
primary: `hsl(${HexToHSL("#" + accentColor)[0]} 11% 21%)`,
secondary: `hsl(${HexToHSL("#" + accentColor)[0]} 11% 18%)`,
tertiary: `hsl(${HexToHSL("#" + accentColor)[0]} 10% 13%)`
}
},
accentSwap: {
name: "Accent Swap",
@ -991,7 +1012,12 @@ export function getPreset(primaryColor?: string, secondaryColor?: string, tertia
name: "Material You",
preset: getAutoPresets(accentColor).materialYou.preset,
id: "materialYou",
colors: ["accent"]
colors: ["accent"],
calculated: {
primary: `hsl(${HexToHSL("#" + accentColor)[0]} 12% 12%)`,
secondary: `hsl(${HexToHSL("#" + accentColor)[0]} 12% 16%)`,
tertiary: `hsl(${HexToHSL("#" + accentColor)[0]} 16% 18%)`
}
}
};
}

View file

@ -0,0 +1,117 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from ".";
import { defaultColorwaySource, nullColorwayObj } from "./constants";
export default async function () {
const [
customColorways,
colorwaySourceFiles,
showColorwaysButton,
onDemandWays,
onDemandWaysTintedText,
onDemandWaysDiscordSaturation,
onDemandWaysOsAccentColor,
activeColorwayObject,
colorwaysPluginTheme,
colorwaysBoundManagers,
colorwaysManagerAutoconnectPeriod,
colorwaysManagerDoAutoconnect
] = await DataStore.getMany([
"customColorways",
"colorwaySourceFiles",
"showColorwaysButton",
"onDemandWays",
"onDemandWaysTintedText",
"onDemandWaysDiscordSaturation",
"onDemandWaysOsAccentColor",
"activeColorwayObject",
"colorwaysPluginTheme",
"colorwaysBoundManagers",
"colorwaysManagerAutoconnectPeriod",
"colorwaysManagerDoAutoconnect"
]);
const defaults = [
{
name: "colorwaysManagerAutoconnectPeriod",
value: colorwaysManagerAutoconnectPeriod,
default: 3000
},
{
name: "colorwaysManagerDoAutoconnect",
value: colorwaysManagerDoAutoconnect,
default: true
},
{
name: "showColorwaysButton",
value: showColorwaysButton,
default: false
},
{
name: "onDemandWays",
value: onDemandWays,
default: false
},
{
name: "onDemandWaysTintedText",
value: onDemandWaysTintedText,
default: true
},
{
name: "onDemandWaysDiscordSaturation",
value: onDemandWaysDiscordSaturation,
default: false
},
{
name: "onDemandWaysOsAccentColor",
value: onDemandWaysOsAccentColor,
default: false
},
{
name: "colorwaysBoundManagers",
value: colorwaysBoundManagers,
default: []
},
{
name: "activeColorwayObject",
value: activeColorwayObject,
default: nullColorwayObj
},
{
name: "colorwaysPluginTheme",
value: colorwaysPluginTheme,
default: "discord"
}
];
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 === "https://raw.githubusercontent.com/DaBluLite/ProjectColorway/master/index.json" ? defaultColorwaySource : sourceURL };
}));
}
} else {
DataStore.set("colorwaySourceFiles", [{
name: "Project Colorway",
url: defaultColorwaySource
}]);
}
}

View file

@ -4,27 +4,30 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
// Plugin Imports
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 { openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import {
Button,
Clipboard,
Forms,
i18n,
SettingsRouter,
Toasts
SettingsRouter
} from "@webpack/common";
import { FluxEvents as $FluxEvents } from "@webpack/types";
// Mod-specific imports
import {
CSSProperties as $CSSProperties,
ReactNode as $ReactNode
} from "react";
import AutoColorwaySelector from "./components/AutoColorwaySelector";
import ColorPickerModal from "./components/ColorPicker";
import { ColorwayCSS } from "./colorwaysAPI";
import ColorwayID from "./components/ColorwayID";
import ColorwaysButton from "./components/ColorwaysButton";
import CreatorModal from "./components/CreatorModal";
import PCSMigrationModal from "./components/PCSMigrationModal";
import Selector from "./components/Selector";
import OnDemandWaysPage from "./components/SettingsTabs/OnDemandPage";
import SettingsPage from "./components/SettingsTabs/SettingsPage";
@ -32,97 +35,45 @@ 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 defaultsLoader from "./defaultsLoader";
import style from "./style.css?managed";
import discordTheme from "./theme.discord.css?managed";
import { ColorPickerProps, ColorwayObject } from "./types";
import { colorToHex, hexToString } from "./utils";
import { connect } from "./wsClient";
export const DataStore = $DataStore;
export type ReactNode = $ReactNode;
export type CSSProperties = $CSSProperties;
export type FluxEvents = $FluxEvents;
export { closeModal, openModal } from "@utils/modal";
export {
Clipboard,
FluxDispatcher,
i18n,
ReactDOM,
SettingsRouter,
Slider,
Toasts,
useCallback,
useEffect,
useReducer,
useRef,
UserStore,
useState,
useStateFromStores
} from "@webpack/common";
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"
]);
defaultsLoader();
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 const PluginProps = {
pluginVersion: "6.1.0",
clientMod: "Vencord User Plugin",
UIVersion: "2.0.0",
creatorVersion: "1.20"
};
export default definePlugin({
@ -130,38 +81,17 @@ export default definePlugin({
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,
pluginVersion: PluginProps.pluginVersion,
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,
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
replace: "$&,_:$1",
predicate: () => true
}
@ -170,8 +100,8 @@ export default definePlugin({
{
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: {
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
replace: "$&(async ()=>$2)(),"
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
replace: "(async ()=>$2)(),"
},
predicate: () => true
},
@ -208,6 +138,57 @@ export default definePlugin({
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>) {
return [
{
section: SectionTypes.HEADER,
label: "Discord Colorways",
className: "vc-settings-header"
},
{
section: "ColorwaysSelector",
label: "Colorways",
element: () => <Selector hasTheme />,
className: "dc-colorway-selector"
},
{
section: "ColorwaysSettings",
label: "Settings",
element: () => <SettingsPage hasTheme />,
className: "dc-colorway-settings"
},
{
section: "ColorwaysSourceManager",
label: "Sources",
element: () => <SourceManager hasTheme />,
className: "dc-colorway-sources-manager"
},
{
section: "ColorwaysOnDemand",
label: "On-Demand",
element: () => <OnDemandWaysPage hasTheme />,
className: "dc-colorway-ondemand"
},
{
section: "ColorwaysStore",
label: "Store",
element: () => <Store hasTheme />,
className: "dc-colorway-store"
},
{
section: SectionTypes.DIVIDER
}
].filter(Boolean);
},
ColorwaysButton: () => <ColorwaysButton />,
async start() {
@ -220,31 +201,31 @@ export default definePlugin({
const ColorwaysSelector = () => ({
section: "ColorwaysSelector",
label: "Colorways Selector",
element: () => <Selector isSettings modalProps={{ onClose: () => new Promise(() => true), transitionState: 1 }} />,
element: () => <Selector hasTheme />,
className: "dc-colorway-selector"
});
const ColorwaysSettings = () => ({
section: "ColorwaysSettings",
label: "Colorways Settings",
element: SettingsPage,
element: () => <SettingsPage hasTheme />,
className: "dc-colorway-settings"
});
const ColorwaysSourceManager = () => ({
section: "ColorwaysSourceManager",
label: "Colorways Sources",
element: SourceManager,
element: () => <SourceManager hasTheme />,
className: "dc-colorway-sources-manager"
});
const ColorwaysOnDemand = () => ({
section: "ColorwaysOnDemand",
label: "Colorways On-Demand",
element: OnDemandWaysPage,
element: () => <OnDemandWaysPage hasTheme />,
className: "dc-colorway-ondemand"
});
const ColorwaysStore = () => ({
section: "ColorwaysStore",
label: "Colorways Store",
element: Store,
element: () => <Store hasTheme />,
className: "dc-colorway-store"
});
@ -252,109 +233,25 @@ export default definePlugin({
addServerListElement(ServerListRenderPosition.Above, this.ColorwaysButton);
connect();
enableStyle(style);
enableStyle(discordTheme);
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;
}
});
if ((await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).map(i => i.url).includes("https://raw.githubusercontent.com/DaBluLite/ProjectColorway/master/index.json") || (!(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).map(i => i.url).includes("https://raw.githubusercontent.com/DaBluLite/ProjectColorway/master/index.json") && !(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).map(i => i.url).includes("https://raw.githubusercontent.com/ProjectColorway/ProjectColorway/master/index.json"))) {
DataStore.set("colorwaySourceFiles", [{ name: "Project Colorway", url: defaultColorwaySource }, ...(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).filter(i => i.name !== "Project Colorway")]);
openModal(props => <PCSMigrationModal modalProps={props} />);
}
addAccessory("colorway-id-card", props => <ColorwayID props={props} />);
},
stop() {
removeServerListElement(ServerListRenderPosition.In, this.ColorwaysButton);
removeServerListElement(ServerListRenderPosition.Above, this.ColorwaysButton);
disableStyle(style);
disableStyle(discordTheme);
ColorwayCSS.remove();
removeAccessory("colorways-btn");
removeAccessory("colorway-id-card");
const customSettingsSections = (
Vencord.Plugins.plugins.Settings as any as {
customSections: ((ID: Record<string, unknown>) => any)[];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,333 @@
/* stylelint-disable color-function-notation */
.colorwaySelectorModal[data-theme="discord"],
.colorwayModal[data-theme="discord"] {
border: none;
box-shadow: var(--legacy-elevation-border), var(--legacy-elevation-high);
background-color: var(--modal-background);
}
[data-theme="discord"] .colorwaysSettingsDivider {
border-color: var(--background-modifier-accent);
}
[data-theme="discord"] .colorwaySwitch-label,
[data-theme="discord"] .colorwaysNote {
color: var(--header-primary);
}
[data-theme="discord"] .colorwaysSettings-switchCircle {
fill: #fff !important;
}
[data-theme="discord"] .colorwaysSettings-switch {
background-color: rgb(128, 132, 142);
}
[data-theme="discord"] .colorwaysSettings-switch.checked {
background-color: #23a55a;
}
[data-theme="discord"] > .colorwaySelectorSidebar > .colorwaySelectorSidebar-tab {
transition: none;
border-radius: 4px;
border: none;
}
[data-theme="discord"] > .colorwaySelectorSidebar > .colorwaySelectorSidebar-tab.active {
background-color: var(--background-modifier-selected);
}
[data-theme="discord"] > .colorwaySelectorSidebar > .colorwaySelectorSidebar-tab:hover {
background-color: var(--background-modifier-hover);
}
[data-theme="discord"] .colorwaysPillButton {
color: var(--white-500);
background-color: var(--button-secondary-background);
height: var(--custom-button-button-sm-height);
min-width: var(--custom-button-button-sm-width);
min-height: var(--custom-button-button-sm-height);
width: auto;
transition:
background-color var(--custom-button-transition-duration) ease,
color var(--custom-button-transition-duration) ease;
position: relative;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: none;
border-radius: 3px;
font-size: 14px;
font-weight: 500;
line-height: 16px;
padding: 2px 16px;
user-select: none;
}
[data-theme="discord"] .colorwaysPillButton:hover {
background-color: var(--button-secondary-background-hover);
}
[data-theme="discord"] > .colorwaySelectorSidebar {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
background-color: var(--modal-footer-background);
box-shadow: inset 0 1px 0 hsl(var(--primary-630-hsl) / 60%);
padding: 12px;
}
[data-theme="discord"] .colorwaySelector-search {
border-radius: 3px;
color: var(--text-normal);
background-color: var(--input-background) !important;
height: 40px;
padding: 10px;
transition: none;
font-size: 16px;
border: none;
}
[data-theme="discord"] .colorwaysSettings-colorwaySource {
border-radius: 4px;
color: var(--interactive-normal);
background-color: var(--background-secondary);
}
[data-theme="discord"] .colorwaysSettings-colorwaySource:hover {
color: var(--interactive-active);
background-color: var(--background-modifier-hover);
}
[data-theme="discord"] .discordColorway {
border-radius: 4px;
transition: none;
background-color: var(--background-secondary);
border: none;
}
[data-theme="discord"] .discordColorway:hover {
filter: none;
background-color: var(--background-modifier-hover);
}
[data-theme="discord"] .discordColorway[aria-checked="true"] {
background-color: var(--background-modifier-selected);
}
[data-theme="discord"] .colorwaysSettings-colorwaySourceLabelHeader,
[data-theme="discord"] .colorwaysSettings-colorwaySourceDesc {
color: var(--header-primary);
}
[data-theme="discord"] .colorways-badge {
height: 16px;
padding: 0 4px;
border-radius: 4px;
margin-left: 4px;
flex: 0 0 auto;
background: var(--bg-brand);
color: var(--white);
text-transform: uppercase;
vertical-align: top;
display: inline-flex;
align-items: center;
text-indent: 0;
font-weight: 600;
font-size: 12px;
line-height: 16px;
}
.colorwaysModal[data-theme="discord"] {
box-shadow: var(--legacy-elevation-border), var(--legacy-elevation-high);
background-color: var(--modal-background);
border-radius: 4px;
display: flex;
flex-direction: column;
margin: 0 auto;
pointer-events: all;
position: relative;
}
[data-theme="discord"] .colorwaysMenuTabs {
padding-bottom: 16px;
}
[data-theme="discord"] .colorwaysMenuTab {
padding: 0;
padding-bottom: 16px;
margin-right: 32px;
margin-bottom: -2px;
border-bottom: 2px solid transparent;
transition: none;
border-radius: 0;
background-color: transparent;
font-size: 16px;
line-height: 20px;
cursor: pointer;
font-weight: 500;
}
[data-theme="discord"] .colorwaysMenuTab:hover {
color: var(--interactive-hover);
border-bottom-color: var(--brand-500);
}
[data-theme="discord"] .colorwaysMenuTab.active {
cursor: default;
color: var(--interactive-active);
border-bottom-color: var(--control-brand-foreground);
}
[data-theme="discord"] .colorwaysModalFooter {
border-radius: 0 0 5px 5px;
background-color: var(--modal-footer-background);
padding: 16px;
box-shadow: inset 0 1px 0 hsl(var(--primary-630-hsl) / 60%);
gap: 0;
width: unset;
}
[data-theme="discord"] .colorwaysModalFooter > .colorwaysPillButton {
width: auto;
height: var(--custom-button-button-md-height);
min-width: var(--custom-button-button-md-width);
min-height: var(--custom-button-button-md-height);
transition:
color var(--custom-button-transition-duration) ease,
background-color var(--custom-button-transition-duration) ease,
border-color var(--custom-button-transition-duration) ease;
border: 1px solid var(--button-outline-primary-border);
color: var(--button-outline-primary-text);
margin-left: 8px;
background-color: transparent;
}
[data-theme="discord"] .colorwaysModalFooter > .colorwaysPillButton:hover {
background-color: var(--button-outline-primary-background-hover);
border-color: var(--button-outline-primary-border-hover);
color: var(--button-outline-primary-text-hover);
}
[data-theme="discord"] .colorwaysModalFooter > .colorwaysPillButton:active {
background-color: var(--button-outline-primary-background-active);
border-color: var(--button-outline-primary-border-active);
color: var(--button-outline-primary-text-active);
}
[data-theme="discord"] .colorwaysModalFooter > .colorwaysPillButton.colorwaysPillButton-onSurface {
color: var(--white-500);
background-color: var(--brand-500);
border: none;
}
[data-theme="discord"] .colorwaysModalFooter > .colorwaysPillButton.colorwaysPillButton-onSurface:hover {
background-color: var(--brand-560);
}
[data-theme="discord"] .colorwaysModalFooter > .colorwaysPillButton.colorwaysPillButton-onSurface:active {
background-color: var(--brand-600);
}
[data-theme="discord"] .colorwaysModalHeader {
box-shadow:
0 1px 0 0 hsl(var(--primary-800-hsl) / 30%),
0 1px 2px 0 hsl(var(--primary-800-hsl) / 30%);
border-radius: 4px 4px 0 0;
transition: box-shadow 0.1s ease-out;
word-wrap: break-word;
}
[data-theme="discord"] .colorwaysModalSectionHeader,
[data-theme="discord"] .colorwaysSettings-colorwaySourceLabel,
[data-theme="discord"] .colorwaysSettings-colorwaySourceLabelHeader,
[data-theme="discord"] .colorwaysSettings-colorwaySourceDesc {
color: var(--header-primary);
}
[data-theme="discord"] .colorwaysCreator-setting,
[data-theme="discord"] .colorwaysCreator-settingCat {
border-radius: 4px;
background-color: var(--background-secondary);
}
[data-theme="discord"] .colorwaysCreator-setting:hover {
background-color: var(--background-modifier-hover);
}
[data-theme="discord"] .colorwaysContextMenu {
background: var(--background-floating);
box-shadow: var(--shadow-high);
border-radius: 4px;
padding: 6px 8px;
border: none;
gap: 0;
min-width: 188px;
max-width: 320px;
box-sizing: border-box;
}
[data-theme="discord"] .colorwaysContextMenuItm {
border: none;
transition: none;
margin: 2px 0;
border-radius: 2px;
font-size: 14px;
font-weight: 500;
line-height: 18px;
color: var(--interactive-normal);
background-color: transparent;
}
[data-theme="discord"] .colorwaysContextMenuItm:hover {
background-color: var(--menu-item-default-hover-bg);
color: var(--white);
}
[data-theme="discord"] .colorwaysContextMenuItm:active {
background-color: var(--menu-item-default-active-bg);
color: var(--white);
}
[data-theme="discord"] .colorwaysRadioSelected {
fill: var(--control-brand-foreground-new);
}
[data-theme="discord"] .colorwaysConflictingColors-warning {
color: var(--text-normal);
}
[data-theme="discord"] .colorwaysManagerConnectionMenu {
transition:
transform 0.1s ease,
opacity 0.1s ease;
transform: scale(0.95);
transform-origin: 0% 50%;
background-color: var(--background-floating);
box-shadow: var(--shadow-high);
color: var(--text-normal);
border: none;
border-radius: 5px;
}
.colorwayIDCard[data-theme="discord"] > .colorwayMessage {
border-radius: 5px;
border: none;
background-color: var(--background-secondary);
}
.theme-dark .colorwayIDCard[data-theme="discord"] .colorwayMessage {
background: hsl(var(--primary-630-hsl) / 60%);
}
.theme-light .colorwayIDCard[data-theme="discord"] .colorwayMessage {
background: hsl(var(--primary-100-hsl) / 60%);
}
[data-theme="discord"] .colorwaysManagerConnectionValue {
color: var(--text-muted);
}
[data-theme="discord"] .colorwaysManagerConnectionValue > b {
color: var(--text-normal);
}

View file

@ -21,7 +21,8 @@ export interface Colorway {
source?: string,
linearGradient?: string,
preset?: string,
creatorVersion: string;
creatorVersion: string,
colorObj?: { accent?: string, primary?: string, secondary?: string, tertiary?: string; };
}
export interface ColorPickerProps {
@ -34,9 +35,15 @@ export interface ColorPickerProps {
export interface ColorwayObject {
id: string | null,
css: string | null,
css?: string | null,
sourceType: "online" | "offline" | "temporary" | null,
source: string | null | undefined;
source: string | null | undefined,
colors?: {
accent?: string | undefined,
primary?: string | undefined,
secondary?: string | undefined,
tertiary?: string | undefined;
} | undefined;
}
export interface SourceObject {
@ -63,3 +70,8 @@ export interface StoreItem {
url: string,
authorGh: string;
}
export interface ModalProps {
transitionState: 0 | 1 | 2 | 3 | 4;
onClose(): void;
}

View file

@ -148,3 +148,84 @@ export function colorToHex(color: string) {
}
return color.replace("#", "");
}
export const parseClr = (clr: number) => (clr & 0x00ffffff).toString(16).padStart(6, "0");
export async function getRepainterTheme(link: string): Promise<{ status: "success" | "fail", id?: string, colors?: string[], errorCode?: number, errorMsg?: string; }> {
const linkCheck: string | undefined = link.match(/https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)/g)!.filter(x => x.startsWith("https://repainter.app/themes/"))[0];
if (!linkCheck) return { status: "fail", errorCode: 0, errorMsg: "Invalid URL" };
// const res = await (
// await fetch(
// `https://repainter.app/_next/data/Z0BCpVYZyrdkss0k0zqLC/themes/${link.match(/themes\/([a-z0-9]+)/i)?.[1] ?? ""
// }.json`,
// {
// "headers": {
// "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
// "accept-language": "en-US,en;q=0.9",
// "if-none-match": "W/\"4b2-Wsw1gFTK1l04ijqMn5s6ZUnH6hM\"",
// "priority": "u=0, i",
// "sec-ch-ua": "\"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
// "sec-ch-ua-mobile": "?0",
// "sec-ch-ua-platform": "\"Linux\"",
// "sec-fetch-dest": "document",
// "sec-fetch-mode": "navigate",
// "sec-fetch-site": "none",
// "sec-fetch-user": "?1",
// "upgrade-insecure-requests": "1"
// },
// "referrerPolicy": "strict-origin-when-cross-origin",
// "body": null,
// "method": "GET",
// "mode": "cors",
// "credentials": "omit",
// "cache": "no-store"
// },
// )
// );
const { pageProps: { fallback: { a: { name, colors } } } } = { "pageProps": { "initialId": "01G5PMR5G9H76H1R2RET4A0ZHY", "fallback": { a: { "id": "01G5PMR5G9H76H1R2RET4A0ZHY", "name": "Midwinter Fire", "description": "Very red", "createdAt": "2022-06-16T16:15:11.881Z", "updatedAt": "2022-07-12T08:37:13.141Z", "settingsLines": ["Colorful", "Bright", "Vibrant style"], "voteCount": 309, "colors": [-1426063361, 4294901760, 4294901760, -1426071591, -1426080078, -1426089335, 4294901760, -1426119398, -1428615936, -1431629312, -1434644480, 4294901760, 4294901760, 4294901760, 4294901760, -1426067223, -1426071086, -1426079070, -1426088082, 4294901760, -1428201216, -1430761216, -1433255936, 4294901760, 4294901760, 4294901760, 4294901760, 4294901760, 4294901760, -1426070330, 4294901760, -1426086346, 4294901760, -1430030080, 4294901760, -1434431744, 4294901760, 4294901760, 4294901760, 4294901760, -1426064133, 4294901760, -1426071591, 4294901760, -1426874223, 4294901760, -1430359452, 4294901760, -1433845194, 4294901760, -1437922816, 4294901760, 4294901760, 4294901760, 4294901760, -1426071591, -1426080078, -1426089335, -1427799438, -1429640356, 4294901760, -1433191891, 4294901760, 4294901760, 4294901760] } } }, "__N_SSP": true } as any;
return { status: "success", id: name, colors: colors.filter(c => c !== 4294901760).map(c => "#" + parseClr(c)) };
}
/**
* Prompts the user to choose a file from their system
* @param mimeTypes A comma separated list of mime types to accept, see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers
* @returns A promise that resolves to the chosen file or null if the user cancels
*/
export function chooseFile(mimeTypes: string) {
return new Promise<File | null>(resolve => {
const input = document.createElement("input");
input.type = "file";
input.style.display = "none";
input.accept = mimeTypes;
input.onchange = async () => {
resolve(input.files?.[0] ?? null);
};
document.body.appendChild(input);
input.click();
setImmediate(() => document.body.removeChild(input));
});
}
/**
* Prompts the user to save a file to their system
* @param file The file to save
*/
export function saveFile(file: File) {
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
a.download = file.name;
document.body.appendChild(a);
a.click();
setImmediate(() => {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
});
}
export function classes(...classes: Array<string | null | undefined | false>) {
return classes.filter(Boolean).join(" ");
}

View file

@ -0,0 +1,229 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from ".";
import { ColorwayCSS } from "./colorwaysAPI";
import { updateBoundKeyMain, updateWSMain } from "./components/MainModal";
import { updateActiveColorway, updateManagerRole, updateWS as updateWSSelector } from "./components/Selector";
import { nullColorwayObj } from "./constants";
import { generateCss } from "./css";
import { ColorwayObject } from "./types";
import { colorToHex } from "./utils";
export let wsOpen = false;
export let boundKey: { [managerKey: string]: string; } | null = null;
export let hasManagerRole: boolean = false;
export let sendColorway: (obj: ColorwayObject) => void = () => { };
export let requestManagerRole: () => void = () => { };
export let updateRemoteSources: () => void = () => { };
export let closeWS: () => void = () => { };
export let restartWS: () => void = () => connect();
export let updateShouldAutoconnect: (shouldAutoconnect: boolean) => void = () => connect();
function updateWS(status: boolean) {
updateWSSelector(status);
updateWSMain(status);
}
function updateBoundKey(bound: { [managerKey: string]: string; }) {
updateBoundKeyMain(bound);
}
export function connect() {
var ws: WebSocket | null = new WebSocket("ws://localhost:6124");
updateShouldAutoconnect = shouldAutoconnect => {
if (shouldAutoconnect && ws?.readyState === ws?.CLOSED) connect();
};
ws.onopen = function () {
wsOpen = true;
hasManagerRole = false;
updateWS(true);
};
restartWS = () => {
ws?.close();
connect();
};
closeWS = () => ws?.close();
ws.onmessage = function (e) {
const data: {
type: "change-colorway" | "remove-colorway" | "manager-connection-established" | "complication:remote-sources:received" | "complication:remote-sources:update-request" | "complication:manager-role:granted" | "complication:manager-role:revoked",
[key: string]: any;
} = JSON.parse(e.data);
function typeSwitch(type) {
switch (type) {
case "change-colorway":
if (data.active.id == null) {
DataStore.set("activeColorwayObject", nullColorwayObj);
ColorwayCSS.remove();
updateActiveColorway(nullColorwayObj);
} else {
const demandedColorway = generateCss(
colorToHex("#" + data.active.colors.primary || "#313338").replace("#", ""),
colorToHex("#" + data.active.colors.secondary || "#2b2d31").replace("#", ""),
colorToHex("#" + data.active.colors.tertiary || "#1e1f22").replace("#", ""),
colorToHex("#" + data.active.colors.accent || "#5865f2").replace("#", "")
);
ColorwayCSS.set(demandedColorway);
DataStore.set("activeColorwayObject", { ...data.active, css: demandedColorway });
updateActiveColorway({ ...data.active, css: demandedColorway });
}
return;
case "remove-colorway":
DataStore.set("activeColorwayObject", nullColorwayObj);
ColorwayCSS.remove();
updateActiveColorway(nullColorwayObj);
return;
case "manager-connection-established":
DataStore.get("colorwaysBoundManagers").then((boundManagers: { [managerKey: string]: string; }[]) => {
if (data.MID) {
const boundSearch = boundManagers.filter(boundManager => {
if (Object.keys(boundManager)[0] === data.MID) return boundManager;
});
if (boundSearch.length) {
boundKey = boundSearch[0];
} else {
const id = { [data.MID]: `vencord.${Math.random().toString(16).slice(2)}.${new Date().getUTCMilliseconds()}` };
DataStore.set("colorwaysBoundManagers", [...boundManagers, id]);
boundKey = id;
}
updateBoundKey(typeof boundKey === "string" ? JSON.parse(boundKey) : boundKey);
ws?.send(JSON.stringify({
type: "client-sync-established",
boundKey,
complications: [
"remote-sources",
"manager-role"
]
}));
DataStore.getMany([
"colorwaySourceFiles",
"customColorways"
]).then(([
colorwaySourceFiles,
customColorways
]) => {
ws?.send(JSON.stringify({
type: "complication:remote-sources:init",
boundKey,
online: colorwaySourceFiles,
offline: customColorways
}));
});
sendColorway = obj => ws?.send(JSON.stringify({
type: "complication:manager-role:send-colorway",
active: obj,
boundKey
}));
requestManagerRole = () => ws?.send(JSON.stringify({
type: "complication:manager-role:request",
boundKey
}));
updateRemoteSources = () => DataStore.getMany([
"colorwaySourceFiles",
"customColorways"
]).then(([
colorwaySourceFiles,
customColorways
]) => {
ws?.send(JSON.stringify({
type: "complication:remote-sources:init",
boundKey,
online: colorwaySourceFiles,
offline: customColorways
}));
});
}
});
return;
case "complication:manager-role:granted":
hasManagerRole = true;
updateManagerRole(true);
return;
case "complication:manager-role:revoked":
hasManagerRole = false;
updateManagerRole(false);
return;
case "complication:remote-sources:update-request":
DataStore.getMany([
"colorwaySourceFiles",
"customColorways"
]).then(([
colorwaySourceFiles,
customColorways
]) => {
ws?.send(JSON.stringify({
type: "complication:remote-sources:init",
boundKey,
online: colorwaySourceFiles,
offline: customColorways
}));
});
return;
}
}
typeSwitch(data.type);
};
ws.onclose = function (e) {
boundKey = null;
hasManagerRole = false;
sendColorway = () => { };
requestManagerRole = () => { };
updateRemoteSources = () => { };
restartWS = () => connect();
closeWS = () => { };
try {
ws?.close();
} catch (e) {
return;
}
ws = null;
wsOpen = false;
updateWS(false);
DataStore.getMany([
"colorwaysManagerAutoconnectPeriod",
"colorwaysManagerDoAutoconnect"
]).then(([
colorwaysManagerAutoconnectPeriod,
colorwaysManagerDoAutoconnect
]) => {
// eslint-disable-next-line no-constant-condition
if (colorwaysManagerDoAutoconnect || true) setTimeout(() => connect(), colorwaysManagerAutoconnectPeriod || 3000);
});
};
ws.onerror = function (e) {
e.preventDefault();
boundKey = null;
sendColorway = () => { };
requestManagerRole = () => { };
updateRemoteSources = () => { };
restartWS = () => connect();
closeWS = () => { };
hasManagerRole = false;
ws?.close();
ws = null;
wsOpen = false;
updateWS(false);
DataStore.getMany([
"colorwaysManagerAutoconnectPeriod",
"colorwaysManagerDoAutoconnect"
]).then(([
colorwaysManagerAutoconnectPeriod,
colorwaysManagerDoAutoconnect
]) => {
// eslint-disable-next-line no-constant-condition
if (colorwaysManagerDoAutoconnect || true) setTimeout(() => connect(), colorwaysManagerAutoconnectPeriod || 3000);
});
};
}

View file

@ -47,13 +47,13 @@ export default definePlugin({
RUNNING_GAMES_CHANGE(event) {
const status = PresenceStore.getStatus(UserStore.getCurrentUser().id);
if (event.games.length > 0) {
if (savedStatus !== "" && savedStatus !== settings.store.statusToSet)
updateAsync(savedStatus);
} else {
if (status !== settings.store.statusToSet) {
savedStatus = status;
updateAsync(settings.store.statusToSet);
}
} else {
if (savedStatus !== "" && savedStatus !== settings.store.statusToSet)
updateAsync(savedStatus);
}
},
}