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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { DataStore } from "@api/index"; import { DataStore, useEffect, useState } from "../";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { findByProps } from "@webpack";
import { Button, Forms, Text, useState } from "@webpack/common";
import { getAutoPresets } from "../css"; import { getAutoPresets } from "../css";
import { ModalProps } from "../types";
export default function ({ modalProps, onChange, autoColorwayId = "" }: { modalProps: ModalProps, onChange: (autoPresetId: string) => void, autoColorwayId: string; }) { export default function ({ modalProps, onChange, autoColorwayId = "" }: { modalProps: ModalProps, onChange: (autoPresetId: string) => void, autoColorwayId: string; }) {
const [autoId, setAutoId] = useState(autoColorwayId); const [autoId, setAutoId] = useState(autoColorwayId);
const { radioBar, item: radioBarItem, itemFilled: radioBarItemFilled, radioPositionLeft } = findByProps("radioBar"); const [theme, setTheme] = useState("discord");
return <ModalRoot {...modalProps}>
<ModalHeader> useEffect(() => {
<Text variant="heading-lg/semibold" tag="h1"> async function load() {
Auto Preset Settings setTheme(await DataStore.get("colorwaysPluginTheme") as string);
</Text> }
</ModalHeader> load();
<ModalContent> }, []);
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" }}> <div className="dc-info-card" style={{ marginTop: "1em" }}>
<strong>About the Auto Colorway</strong> <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> <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>
<div style={{ marginBottom: "20px" }}> <div style={{ marginBottom: "20px" }}>
<Forms.FormTitle>Presets:</Forms.FormTitle> <span className="colorwaysModalSectionHeader">Presets:</span>
{Object.values(getAutoPresets()).map(autoPreset => { {Object.values(getAutoPresets()).map(autoPreset => <div
return <div className={`${radioBarItem} ${radioBarItemFilled}`} aria-checked={autoId === autoPreset.id}> className="discordColorway"
<div aria-checked={autoId === autoPreset.id}
className={`${radioBar} ${radioPositionLeft}`} style={{ padding: "10px", marginBottom: "8px" }}
style={{ padding: "10px" }} onClick={() => {
onClick={() => { setAutoId(autoPreset.id);
setAutoId(autoPreset.id); }}>
}}> <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<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" />
<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" />}
{autoId === autoPreset.id && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />} </svg>
</svg> <span className="colorwayLabel">{autoPreset.name}</span>
<Text variant="eyebrow" tag="h5">{autoPreset.name}</Text> </div>)}
</div>
</div>;
})}
</div> </div>
</ModalContent> </div>
<ModalFooter> <div className="colorwaysModalFooter">
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton colorwaysPillButton-onSurface"
color={Button.Colors.BRAND_NEW}
size={Button.Sizes.MEDIUM}
onClick={() => { onClick={() => {
DataStore.set("activeAutoPreset", autoId); DataStore.set("activeAutoPreset", autoId);
onChange(autoId); onChange(autoId);
@ -57,18 +54,15 @@ export default function ({ modalProps, onChange, autoColorwayId = "" }: { modalP
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => { onClick={() => {
modalProps.onClose(); modalProps.onClose();
}} }}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot>; </div>;
} }

View file

@ -4,28 +4,25 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Flex } from "@components/Flex"; import { DataStore, Toasts, useEffect, useState } from "..";
import { CopyIcon } from "@components/Icons";
import {
ModalProps,
ModalRoot,
} from "@utils/modal";
import {
Button,
Clipboard,
ScrollerThin,
TextInput,
Toasts,
useState,
} from "@webpack/common";
import { mainColors } from "../constants"; import { mainColors } from "../constants";
import { colorVariables } from "../css"; import { colorVariables } from "../css";
import { ModalProps } from "../types";
import { getHex } from "../utils"; import { getHex } from "../utils";
import { CopyIcon } from "./Icons";
export default function ({ modalProps }: { modalProps: ModalProps; }) { export default function ({ modalProps }: { modalProps: ModalProps; }) {
const [ColorVars, setColorVars] = useState<string[]>(colorVariables); const [ColorVars, setColorVars] = useState<string[]>(colorVariables);
const [collapsedSettings, setCollapsedSettings] = useState<boolean>(true); 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[]; let results: string[];
function searchToolboxItems(e: string) { function searchToolboxItems(e: string) {
results = []; results = [];
@ -37,55 +34,53 @@ export default function ({ modalProps }: { modalProps: ModalProps; }) {
setColorVars(results); setColorVars(results);
} }
return <ModalRoot {...modalProps} className="colorwayColorpicker"> return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<Flex style={{ gap: "8px", marginBottom: "8px" }}> <div style={{ gap: "8px", marginBottom: "8px", display: "flex" }}>
<TextInput <input
className="colorwaysColorpicker-search" type="text"
className="colorwaySelector-search"
placeholder="Search for a color:" placeholder="Search for a color:"
onChange={e => { onChange={({ currentTarget: { value } }) => {
searchToolboxItems(e); searchToolboxItems(value);
if (e) { if (value) {
setCollapsedSettings(false); setCollapsedSettings(false);
} else { } else {
setCollapsedSettings(true); setCollapsedSettings(true);
} }
}} }}
/> />
<Button <button
innerClassName="colorwaysSettings-iconButtonInner" className="colorwaysPillButton"
size={Button.Sizes.ICON}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={() => setCollapsedSettings(!collapsedSettings)} onClick={() => setCollapsedSettings(!collapsedSettings)}
> >
<svg width="32" height="24" viewBox="0 0 24 24" aria-hidden="true" role="img"> <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" /> <path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M7 10L12 15 17 10" aria-hidden="true" />
</svg> </svg>
</Button> </button>
</Flex> </div>
<ScrollerThin style={{ color: "var(--text-normal)" }} orientation="vertical" className={collapsedSettings ? " colorwaysColorpicker-collapsed" : ""} paddingFix> <div style={{ color: "var(--text-normal)", overflow: "hidden auto", scrollbarWidth: "none" }} className={collapsedSettings ? " colorwaysColorpicker-collapsed" : ""}>
{ColorVars.map((colorVariable: string) => <div {ColorVars.map((colorVariable: string) => <div
id={`colorways-colorstealer-item_${colorVariable}`} id={`colorways-colorstealer-item_${colorVariable}`}
className="colorwaysCreator-settingItm colorwaysCreator-toolboxItm" className="colorwaysCreator-settingItm colorwaysCreator-toolboxItm"
onClick={() => { 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 }); Toasts.show({ message: "Color " + colorVariable + " copied to clipboard", id: "toolbox-color-var-copied", type: 1 });
}} style={{ "--brand-experiment": `var(--${colorVariable})` } as React.CSSProperties}> }} style={{ "--brand-experiment": `var(--${colorVariable})` } as React.CSSProperties}>
{`Copy ${colorVariable}`} {`Copy ${colorVariable}`}
</div>)} </div>)}
</ScrollerThin> </div>
<Flex style={{ justifyContent: "space-between", marginTop: "8px" }} wrap="wrap" className={collapsedSettings ? "" : " colorwaysColorpicker-collapsed"}> <div style={{ justifyContent: "space-between", marginTop: "8px", flexWrap: "wrap", gap: "1em" }} className={collapsedSettings ? "" : " colorwaysColorpicker-collapsed"}>
{mainColors.map(mainColor => <div {mainColors.map(mainColor => <div
id={`colorways-toolbox_copy-${mainColor.name}`} id={`colorways-toolbox_copy-${mainColor.name}`}
className="colorwayToolbox-listItem" className="colorwayToolbox-listItem"
> >
<CopyIcon onClick={() => { <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 }); Toasts.show({ message: `${mainColor.title} color copied to clipboard`, id: `toolbox-${mainColor.name}-color-copied`, type: 1 });
}} width={20} height={20} className="colorwayToolbox-listItemSVG" /> }} width={20} height={20} className="colorwayToolbox-listItemSVG" />
<span className="colorwaysToolbox-label">{`Copy ${mainColor.title} Color`}</span> <span className="colorwaysToolbox-label">{`Copy ${mainColor.title} Color`}</span>
</div> </div>
)} )}
</Flex> </div>
</ModalRoot>; </div>;
} }

View file

@ -4,67 +4,70 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { DataStore, useEffect, useState } from "..";
import { findByProps } from "@webpack";
import { Button, Forms, ScrollerThin, Switch, Text, useState } from "@webpack/common";
import { getPreset } from "../css"; 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; }) { 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 [tintedText, setTintedText] = useState<boolean>(hasTintedText);
const [discordSaturation, setDiscordSaturation] = useState<boolean>(hasDiscordSaturation); const [discordSaturation, setDiscordSaturation] = useState<boolean>(hasDiscordSaturation);
const [preset, setPreset] = useState<string>(presetId); const [preset, setPreset] = useState<string>(presetId);
const { radioBar, item: radioBarItem, itemFilled: radioBarItemFilled, radioPositionLeft } = findByProps("radioBar"); const [theme, setTheme] = useState("discord");
return <ModalRoot {...modalProps} className="colorwaysPresetPicker">
<ModalHeader><Text variant="heading-lg/semibold" tag="h1">Creator Settings</Text></ModalHeader> useEffect(() => {
<ModalContent className="colorwaysPresetPicker-content"> async function load() {
<Forms.FormTitle> 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: Presets:
</Forms.FormTitle> </span>
<ScrollerThin orientation="vertical" paddingFix style={{ paddingRight: "2px", marginBottom: "20px", maxHeight: "250px" }}> <div className="colorwaysScroller" style={{ paddingRight: "2px", marginBottom: "20px", maxHeight: "250px" }}>
{Object.values(getPreset()).map(pre => { {Object.values(getPreset()).map(pre => <div
return <div className={`${radioBarItem} ${radioBarItemFilled}`} aria-checked={preset === pre.id}> aria-checked={preset === pre.id}
<div className="discordColorway"
className={`${radioBar} ${radioPositionLeft}`} style={{ padding: "10px", marginBottom: "8px" }}
style={{ padding: "10px" }} onClick={() => {
onClick={() => { setPreset(pre.id);
setPreset(pre.id); }}>
}}> <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<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" />
<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" />}
{preset === pre.id && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />} </svg>
</svg> <span className="colorwayLabel">{pre.name}</span>
<Text variant="eyebrow" tag="h5">{pre.name}</Text> </div>)}
</div> </div>
</div>; <Setting divider>
})} <Switch value={tintedText} onChange={setTintedText} label="Use colored text" />
</ScrollerThin> </Setting>
<Switch value={tintedText} onChange={setTintedText}>Use colored text</Switch> <Switch value={discordSaturation} onChange={setDiscordSaturation} label="Use Discord's saturation" />
<Switch value={discordSaturation} onChange={setDiscordSaturation} hideBorder style={{ marginBottom: "0" }}>Use Discord's saturation</Switch> </div>
</ModalContent> <div className="colorwaysModalFooter">
<ModalFooter> <button
<Button className="colorwaysPillButton colorwaysPillButton-onSurface"
style={{ marginLeft: 8 }}
color={Button.Colors.BRAND_NEW}
size={Button.Sizes.MEDIUM}
onClick={() => { onClick={() => {
onSettings({ presetId: preset, discordSaturation: discordSaturation, tintedText: tintedText }); onSettings({ presetId: preset, discordSaturation: discordSaturation, tintedText: tintedText });
modalProps.onClose(); modalProps.onClose();
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => { onClick={() => {
modalProps.onClose(); modalProps.onClose();
}} }}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot>; </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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import * as DataStore from "@api/DataStore"; import { DataStore, FluxDispatcher, FluxEvents, openModal, useEffect, useState } from "..";
import { openModal } from "@utils/modal";
import { FluxDispatcher, Text, Tooltip, useEffect, useState } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
import { getAutoPresets } from "../css"; import { getAutoPresets } from "../css";
import { ColorwayObject } from "../types"; import { ColorwayObject } from "../types";
import { PalleteIcon } from "./Icons"; import { PalleteIcon } from "./Icons";
import Selector from "./Selector"; import Selector from "./MainModal";
import Tooltip from "./Tooltip";
export default function () { export default function () {
const [activeColorway, setActiveColorway] = useState<string>("None"); const [activeColorway, setActiveColorway] = useState<string>("None");
@ -39,11 +36,11 @@ export default function () {
<> <>
{!isThin ? <> {!isThin ? <>
<span>Colorways</span> <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>} </> : <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"> {({ onMouseEnter, onMouseLeave, onClick }) => visibility ? <div className="ColorwaySelectorBtnContainer">
<div <div
@ -58,7 +55,7 @@ export default function () {
onClick(); onClick();
openModal((props: any) => <Selector modalProps={props} />); 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> : <></>} </div> : <></>}
</Tooltip>; </Tooltip>;
} }

View file

@ -4,10 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { DataStore, useEffect, useState } from "..";
import { Button, Forms, ScrollerThin, Text, useState } from "@webpack/common";
import { knownThemeVars } from "../constants"; import { knownThemeVars } from "../constants";
import { ModalProps } from "../types";
import { getFontOnBg, getHex } from "../utils"; import { getFontOnBg, getHex } from "../utils";
export default function ({ export default function ({
@ -37,15 +36,22 @@ export default function ({
document.body document.body
).getPropertyValue("--background-tertiary") ).getPropertyValue("--background-tertiary")
)); ));
return <ModalRoot {...modalProps} className="colorwayCreator-modal"> const [theme, setTheme] = useState("discord");
<ModalHeader>
<Text variant="heading-lg/semibold" tag="h1"> useEffect(() => {
Conflicting Colors Found async function load() {
</Text> setTheme(await DataStore.get("colorwaysPluginTheme") as string);
</ModalHeader> }
<ModalContent className="colorwayCreator-menuWrapper"> load();
<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>
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-colorPreviews">
<div className="colorwayCreator-colorPreview" style={{ backgroundColor: primaryColor, color: getFontOnBg(primaryColor) }} >Primary</div> <div className="colorwayCreator-colorPreview" style={{ backgroundColor: primaryColor, color: getFontOnBg(primaryColor) }} >Primary</div>
<div className="colorwayCreator-colorPreview" style={{ backgroundColor: secondaryColor, color: getFontOnBg(secondaryColor) }} >Secondary</div> <div className="colorwayCreator-colorPreview" style={{ backgroundColor: 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 className="colorwayCreator-colorPreview" style={{ backgroundColor: accentColor, color: getFontOnBg(accentColor) }} >Accent</div>
</div> </div>
<div className="colorwaysCreator-settingCat"> <div className="colorwaysCreator-settingCat">
<ScrollerThin orientation="vertical" className="colorwaysCreator-settingsList" paddingFix> <div className="colorwaysCreator-settingsList">
<div <div
id="colorways-colorstealer-item_Default" id="colorways-colorstealer-item_Default"
className="colorwaysCreator-settingItm colorwaysCreator-colorPreviewItm" className="colorwaysCreator-settingItm colorwaysCreator-colorPreviewItm"
> >
<Forms.FormTitle>Discord</Forms.FormTitle> <span className="colorwaysModalSectionHeader">Discord</span>
<div className="colorwayCreator-colorPreviews"> <div className="colorwayCreator-colorPreviews">
<div <div
className="colorwayCreator-colorPreview" style={{ className="colorwayCreator-colorPreview" style={{
@ -164,7 +170,7 @@ export default function ({
} }
className="colorwaysCreator-settingItm colorwaysCreator-colorPreviewItm" 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"> <div className="colorwayCreator-colorPreviews">
{theme.primary && getComputedStyle(document.body).getPropertyValue(theme.primary).match(/^\d.*%$/) {theme.primary && getComputedStyle(document.body).getPropertyValue(theme.primary).match(/^\d.*%$/)
? <div ? <div
@ -294,15 +300,12 @@ export default function ({
); );
} }
})} })}
</ScrollerThin> </div>
</div> </div>
</ModalContent> </div>
<ModalFooter> <div className="colorwaysModalFooter">
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton colorwaysPillButton-onSurface"
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => { onClick={() => {
onFinished({ onFinished({
accent: accentColor, accent: accentColor,
@ -312,7 +315,7 @@ export default function ({
}); });
modalProps.onClose(); modalProps.onClose();
}} }}
>Finish</Button> >Finish</button>
</ModalFooter> </div>
</ModalRoot >; </div >;
} }

View file

@ -4,30 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { import { ColorPicker, DataStore, openModal, PluginProps, Slider, useEffect, useReducer, UserStore, useState } from "..";
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
openModal,
} from "@utils/modal";
import {
Button,
Forms,
Slider,
Text,
TextInput,
useEffect,
UserStore,
useState,
} from "@webpack/common";
import { ColorPicker, versionData } from "..";
import { knownThemeVars } from "../constants"; import { knownThemeVars } from "../constants";
import { generateCss, getPreset, gradientPresetIds, PrimarySatDiffs, pureGradientBase } from "../css"; 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 { colorToHex, getHex, HexToHSL, hexToString } from "../utils";
import { updateRemoteSources } from "../wsClient";
import ColorwayCreatorSettingsModal from "./ColorwayCreatorSettingsModal"; import ColorwayCreatorSettingsModal from "./ColorwayCreatorSettingsModal";
import ConflictingColorsModal from "./ConflictingColorsModal"; import ConflictingColorsModal from "./ConflictingColorsModal";
import InputColorwayIdModal from "./InputColorwayIdModal"; import InputColorwayIdModal from "./InputColorwayIdModal";
@ -35,61 +17,98 @@ import SaveColorwayModal from "./SaveColorwayModal";
import ThemePreviewCategory from "./ThemePreview"; import ThemePreviewCategory from "./ThemePreview";
export default function ({ export default function ({
modalProps, modalProps,
loadUIProps, loadUIProps = () => new Promise(() => { }),
colorwayID colorwayID
}: { }: {
modalProps: ModalProps; modalProps: ModalProps;
loadUIProps?: () => Promise<void>; loadUIProps?: () => Promise<void>;
colorwayID?: string; colorwayID?: string;
}) { }) {
const [accentColor, setAccentColor] = useState<string>("5865f2"); const [colors, updateColors] = useReducer((colors: {
const [primaryColor, setPrimaryColor] = useState<string>("313338"); accent: string,
const [secondaryColor, setSecondaryColor] = useState<string>("2b2d31"); primary: string,
const [tertiaryColor, setTertiaryColor] = useState<string>("1e1f22"); 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 [colorwayName, setColorwayName] = useState<string>("");
const [tintedText, setTintedText] = useState<boolean>(true); const [tintedText, setTintedText] = useState<boolean>(true);
const [discordSaturation, setDiscordSaturation] = useState<boolean>(true); const [discordSaturation, setDiscordSaturation] = useState<boolean>(true);
const [preset, setPreset] = useState<string>("default"); const [preset, setPreset] = useState<string>("default");
const [presetColorArray, setPresetColorArray] = useState<string[]>(["accent", "primary", "secondary", "tertiary"]); 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 = { useEffect(() => {
accent: { async function load() {
get: accentColor, setTheme(await DataStore.get("colorwaysPluginTheme") as string);
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"
} }
}; 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(() => { useEffect(() => {
if (colorwayID) { if (colorwayID) {
if (!colorwayID.includes(",")) { if (!colorwayID.includes(",")) {
throw new Error("Invalid Colorway ID"); throw new Error("Invalid Colorway ID");
} else { } else {
const setColor = [
setAccentColor,
setPrimaryColor,
setSecondaryColor,
setTertiaryColor
];
colorwayID.split("|").forEach((prop: string) => { colorwayID.split("|").forEach((prop: string) => {
if (prop.includes(",#")) { 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:")) { if (prop.includes("n:")) {
setColorwayName(prop.split("n:")[1]); setColorwayName(prop.split("n:")[1]);
@ -115,43 +134,54 @@ export default function ({
}; };
return ( return (
<ModalRoot {...modalProps} className="colorwayCreator-modal"> <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<ModalHeader> <h2 className="colorwaysModalHeader">Create a Colorway</h2>
<Text variant="heading-lg/semibold" tag="h1"> <div className="colorwaysModalContent" style={{ minWidth: 500 }}>
Create Colorway <span className="colorwaysModalSectionHeader">Name:</span>
</Text> <input
</ModalHeader> type="text"
<ModalContent className="colorwayCreator-menuWrapper"> className="colorwaySelector-search"
<Forms.FormTitle style={{ marginBottom: 0 }}>
Name:
</Forms.FormTitle>
<TextInput
placeholder="Give your Colorway a name" placeholder="Give your Colorway a name"
value={colorwayName} value={colorwayName}
onChange={setColorwayName} onInput={e => setColorwayName(e.currentTarget.value)}
/> />
<div className="colorwaysCreator-settingCat"> <div className="colorwaysCreator-settingCat">
<Forms.FormTitle style={{ marginBottom: "0" }}> <span className="colorwaysModalSectionHeader">Colors & Values:</span>
Colors & Values:
</Forms.FormTitle>
<div className="colorwayCreator-colorPreviews"> <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 return <ColorPicker
label={<Text className="colorwaysPicker-colorLabel">{colorProps[presetColor].name}</Text>} label={<span className="colorwaysPicker-colorLabel">{Object.keys(getPreset()[preset].calculated! || {}).includes(presetColor.id) ? (presetColor.name + " (Calculated)") : presetColor.name}</span>}
color={parseInt(colorProps[presetColor].get, 16)} 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) => { onChange={(color: number) => {
let hexColor = color.toString(16); if (!Object.keys(getPreset()[preset].calculated! || {}).includes(presetColor.id)) {
while (hexColor.length < 6) { let hexColor = color.toString(16);
hexColor = "0" + hexColor; while (hexColor.length < 6) {
hexColor = "0" + hexColor;
}
updateColors({ task: presetColor.id as "accent" | "primary" | "secondary" | "tertiary", color: hexColor });
} }
colorProps[presetColor].set(hexColor);
}} }}
{...colorPickerProps} {...colorPickerProps}
/>; />;
})} })}
</div> </div>
<Forms.FormDivider style={{ margin: "10px 0" }} /> <div className="colorwaysSettingsDivider" style={{ margin: "10px 0" }} />
<Forms.FormTitle>Muted Text Brightness:</Forms.FormTitle> <span className="colorwaysModalSectionHeader">Muted Text Brightness:</span>
<Slider <Slider
minValue={0} minValue={0}
maxValue={100} maxValue={100}
@ -172,41 +202,38 @@ export default function ({
setDiscordSaturation(discordSaturation); setDiscordSaturation(discordSaturation);
setTintedText(tintedText); 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" }}> <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" /> <path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M7 10L12 15 17 10" aria-hidden="true" />
</svg> </svg>
</div> </div>
<ThemePreviewCategory <ThemePreviewCategory
accent={"#" + accentColor} accent={"#" + colors.accent}
primary={"#" + primaryColor} primary={"#" + colors.primary}
secondary={"#" + secondaryColor} secondary={"#" + colors.secondary}
tertiary={"#" + tertiaryColor} tertiary={"#" + colors.tertiary}
previewCSS={gradientPresetIds.includes(getPreset()[preset].id) ? pureGradientBase + `.colorwaysPreview-modal,.colorwaysPreview-wrapper {--gradient-theme-bg: linear-gradient(${(getPreset( previewCSS={gradientPresetIds.includes(getPreset()[preset].id) ? pureGradientBase + `.colorwaysPreview-modal,.colorwaysPreview-wrapper {--gradient-theme-bg: linear-gradient(${(getPreset(
primaryColor, colors.primary,
secondaryColor, colors.secondary,
tertiaryColor, colors.tertiary,
accentColor colors.accent
)[preset].preset(discordSaturation) as { full: string, base: string; }).base})}` : (tintedText ? `.colorwaysPreview-modal,.colorwaysPreview-wrapper { )[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-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("#" + secondaryColor)[0]} calc(var(--saturation-factor, 1)*${discordSaturation ? Math.round(((HexToHSL("#" + primaryColor)[1] / 100) * (100 + PrimarySatDiffs[360])) * 10) / 10 : HexToHSL("#" + primaryColor)[1]}%) 90%); --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> </div>
<ModalFooter> <div className="colorwaysModalFooter">
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton colorwaysPillButton-onSurface"
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={async () => { onClick={async () => {
var customColorwayCSS: string = ""; var customColorwayCSS: string = "";
if (preset === "default") { if (preset === "default") {
customColorwayCSS = generateCss( customColorwayCSS = generateCss(
primaryColor, colors.primary,
secondaryColor, colors.secondary,
tertiaryColor, colors.tertiary,
accentColor, colors.accent,
tintedText, tintedText,
discordSaturation, discordSaturation,
mutedTextBrightness, mutedTextBrightness,
@ -216,61 +243,64 @@ export default function ({
gradientPresetIds.includes(getPreset()[preset].id) ? gradientPresetIds.includes(getPreset()[preset].id) ?
customColorwayCSS = `/** customColorwayCSS = `/**
* @name ${colorwayName || "Colorway"} * @name ${colorwayName || "Colorway"}
* @version ${versionData.creatorVersion} * @version ${PluginProps.creatorVersion}
* @description Automatically generated Colorway. * @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username} * @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id} * @authorId ${UserStore.getCurrentUser().id}
* @preset Gradient * @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"} * @name ${colorwayName || "Colorway"}
* @version ${versionData.creatorVersion} * @version ${PluginProps.creatorVersion}
* @description Automatically generated Colorway. * @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username} * @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id} * @authorId ${UserStore.getCurrentUser().id}
* @preset ${getPreset()[preset].name} * @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 = { const customColorway: Colorway = {
name: (colorwayName || "Colorway"), name: (colorwayName || "Colorway"),
"dc-import": customColorwayCSS, "dc-import": customColorwayCSS,
accent: "#" + accentColor, accent: "#" + colors.accent,
primary: "#" + primaryColor, primary: "#" + colors.primary,
secondary: "#" + secondaryColor, secondary: "#" + colors.secondary,
tertiary: "#" + tertiaryColor, tertiary: "#" + colors.tertiary,
colors: presetColorArray, colors: presetColorArray,
author: UserStore.getCurrentUser().username, author: UserStore.getCurrentUser().username,
authorID: UserStore.getCurrentUser().id, authorID: UserStore.getCurrentUser().id,
isGradient: gradientPresetIds.includes(getPreset()[preset].id), isGradient: gradientPresetIds.includes(getPreset()[preset].id),
linearGradient: gradientPresetIds.includes(getPreset()[preset].id) ? (getPreset( linearGradient: gradientPresetIds.includes(getPreset()[preset].id) ? (getPreset(
primaryColor, colors.primary,
secondaryColor, colors.secondary,
tertiaryColor, colors.tertiary,
accentColor colors.accent
)[preset].preset(discordSaturation) as { base: string; }).base : "", )[preset].preset(discordSaturation) as { base: string; }).base : "",
preset: getPreset()[preset].id, preset: getPreset()[preset].id,
creatorVersion: versionData.creatorVersion creatorVersion: PluginProps.creatorVersion
}; };
openModal(props => <SaveColorwayModal modalProps={props} colorways={[customColorway]} onFinish={() => { openModal(props => <SaveColorwayModal modalProps={props} colorways={[customColorway]} onFinish={() => {
modalProps.onClose(); modalProps.onClose();
loadUIProps!(); loadUIProps();
updateRemoteSources();
}} />); }} />);
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => { onClick={() => {
function setAllColors({ accent, primary, secondary, tertiary }: { accent: string, primary: string, secondary: string, tertiary: string; }) { function setAllColors({ accent, primary, secondary, tertiary }: { accent: string, primary: string, secondary: string, tertiary: string; }) {
setAccentColor(accent.split("#")[1]); updateColors({
setPrimaryColor(primary.split("#")[1]); task: "all",
setSecondaryColor(secondary.split("#")[1]); colorObj: {
setTertiaryColor(tertiary.split("#")[1]); accent: accent.split("#")[1],
primary: primary.split("#")[1],
secondary: secondary.split("#")[1],
tertiary: tertiary.split("#")[1]
}
});
} }
var copiedThemes = ["Discord"]; var copiedThemes = ["Discord"];
Object.values(knownThemeVars).map((theme: { variable: string; variableType?: string; }, i: number) => { Object.values(knownThemeVars).map((theme: { variable: string; variableType?: string; }, i: number) => {
@ -281,69 +311,53 @@ export default function ({
if (copiedThemes.length > 1) { if (copiedThemes.length > 1) {
openModal(props => <ConflictingColorsModal modalProps={props} onFinished={setAllColors} />); openModal(props => <ConflictingColorsModal modalProps={props} onFinished={setAllColors} />);
} else { } else {
setPrimaryColor( updateColors({
getHex( task: "all", colorObj: {
getComputedStyle( primary: getHex(
document.body getComputedStyle(
).getPropertyValue("--background-primary") document.body
).split("#")[1] ).getPropertyValue("--primary-600")
); ).split("#")[1],
setSecondaryColor( secondary: getHex(
getHex( getComputedStyle(
getComputedStyle( document.body
document.body ).getPropertyValue("--primary-630")
).getPropertyValue("--background-secondary") ).split("#")[1],
).split("#")[1] tertiary: getHex(
); getComputedStyle(
setTertiaryColor( document.body
getHex( ).getPropertyValue("--primary-700")
getComputedStyle( ).split("#")[1],
document.body accent: getHex(
).getPropertyValue("--background-tertiary") getComputedStyle(
).split("#")[1] document.body
); ).getPropertyValue("--brand-experiment")
setAccentColor( ).split("#")[1]
getHex( }
getComputedStyle( });
document.body
).getPropertyValue("--brand-experiment")
).split("#")[1]
);
} }
}} }}
> >
Copy Current Colors Copy Current Colors
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => openModal((props: any) => <InputColorwayIdModal modalProps={props} onColorwayId={colorwayID => { onClick={() => openModal((props: any) => <InputColorwayIdModal modalProps={props} onColorwayId={colorwayID => {
const setColor = [ hexToString(colorwayID).split(/,#/).forEach((color: string, i: number) => updateColors({ task: setColor[i], color: colorToHex(color) }));
setAccentColor,
setPrimaryColor,
setSecondaryColor,
setTertiaryColor
];
hexToString(colorwayID).split(/,#/).forEach((color: string, i: number) => setColor[i](colorToHex(color)));
}} />)} }} />)}
> >
Enter Colorway ID Enter Colorway ID
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => { onClick={() => {
modalProps.onClose(); modalProps.onClose();
}} }}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot> </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 * Vencord, a modification for Discord's desktop app
* Copyright (c) 2024 Vendicated and contributors * Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later *
*/ * 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 } from "react";
import type { PropsWithChildren, SVGProps } from "react";
import { classes } from "../utils";
interface BaseIconProps extends IconProps { interface BaseIconProps extends IconProps {
viewBox: string; viewBox: string;
} }
interface IconProps extends SVGProps<SVGSVGElement> { type IconProps = JSX.IntrinsicElements["svg"];
className?: string;
height?: string | number;
width?: string | number;
}
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) { function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
return ( return (
<svg <svg
className={classes(className, "vc-icon")} className={classes(className, "dc-icon")}
role="img" role="img"
width={width} width={width}
height={height} 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 ( return (
<Icon <Icon
{...props} {...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" viewBox="0 0 24 24"
> >
<path <path
fill="currentColor" 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> </Icon>
); );
@ -51,7 +265,7 @@ export function CloseIcon(props: IconProps) {
return ( return (
<Icon <Icon
{...props} {...props}
className={classes(props.className, "vc-close-icon")} className={classes(props.className, "dc-close-icon")}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <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) { export function DownloadIcon(props: IconProps) {
return ( return (
<Icon <Icon
{...props} {...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" viewBox="0 0 24 24"
> >
<path <path
@ -81,7 +360,7 @@ export function ImportIcon(props: IconProps) {
return ( return (
<Icon <Icon
{...props} {...props}
className={classes(props.className, "vc-import-icon")} className={classes(props.className, "dc-import-icon")}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path
@ -96,7 +375,22 @@ export function IDIcon(props: IconProps) {
return ( return (
<Icon <Icon
{...props} {...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" viewBox="0 0 24 24"
> >
<path <path
@ -117,7 +411,7 @@ export function CodeIcon(props: IconProps) {
return ( return (
<Icon <Icon
{...props} {...props}
className={classes(props.className, "vc-code-icon")} className={classes(props.className, "dc-code-icon")}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path
@ -132,7 +426,7 @@ export function MoreIcon(props: IconProps) {
return ( return (
<Icon <Icon
{...props} {...props}
className={classes(props.className, "vc-more-icon")} className={classes(props.className, "dc-more-icon")}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path
@ -144,3 +438,112 @@ export function MoreIcon(props: IconProps) {
</Icon> </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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import * as DataStore from "@api/DataStore"; import { DataStore, openModal, PluginProps, Toasts, useEffect, UserStore, useState, useStateFromStores } from "..";
import { CodeBlock } from "@components/CodeBlock"; import { ColorwayCSS } from "../colorwaysAPI";
import { Flex } from "@components/Flex";
import {
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
openModal,
} from "@utils/modal";
import { saveFile } from "@utils/web";
import { findComponentByCodeLazy } from "@webpack";
import { Button, Clipboard, Forms, Text, TextInput, Toasts, UserStore, useState, useStateFromStores } from "@webpack/common";
import { ColorwayCSS, versionData } from "..";
import { generateCss, pureGradientBase } from "../css"; import { generateCss, pureGradientBase } from "../css";
import { Colorway } from "../types"; import { Colorway, ModalProps } from "../types";
import { colorToHex, stringToHex } from "../utils"; import { colorToHex, saveFile, stringToHex } from "../utils";
import SaveColorwayModal from "./SaveColorwayModal"; import SaveColorwayModal from "./SaveColorwayModal";
import ThemePreview from "./ThemePreview"; 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[]; }) { function RenameColorwayModal({ modalProps, ogName, onFinish, colorwayList }: { modalProps: ModalProps, ogName: string, onFinish: (name: string) => void, colorwayList: Colorway[]; }) {
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [newName, setNewName] = useState<string>(ogName); const [newName, setNewName] = useState<string>(ogName);
return <ModalRoot {...modalProps}> const [theme, setTheme] = useState("discord");
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1" style={{ marginRight: "auto" }}> useEffect(() => {
Rename Colorway... async function load() {
</Text> setTheme(await DataStore.get("colorwaysPluginTheme") as string);
<ModalCloseButton onClick={() => modalProps.onClose()} /> }
</ModalHeader> load();
<ModalContent> }, []);
<TextInput
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} value={newName}
error={error} onInput={({ currentTarget: { value } }) => {
onChange={setNewName} setNewName(value);
}}
/> />
</ModalContent> </div>
<ModalFooter> <div className="colorwaysModalFooter">
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton colorwaysPillButton-onSurface"
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={async () => { onClick={async () => {
if (!newName) { if (!newName) {
return setError("Error: Please enter a valid name"); return setError("Error: Please enter a valid name");
@ -64,18 +53,15 @@ function RenameColorwayModal({ modalProps, ogName, onFinish, colorwayList }: { m
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => modalProps.onClose()} onClick={() => modalProps.onClose()}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot>; </div>;
} }
export default function ({ export default function ({
@ -94,42 +80,48 @@ export default function ({
"tertiary", "tertiary",
]; ];
const profile = useStateFromStores([UserStore], () => UserStore.getUser(colorway.authorID)); const profile = useStateFromStores([UserStore], () => UserStore.getUser(colorway.authorID));
return <ModalRoot {...modalProps}> const [theme, setTheme] = useState("discord");
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1" style={{ marginRight: "auto" }}> useEffect(() => {
Colorway: {colorway.name} async function load() {
</Text> setTheme(await DataStore.get("colorwaysPluginTheme") as string);
<ModalCloseButton onClick={() => modalProps.onClose()} /> }
</ModalHeader> load();
<ModalContent> }, []);
<Flex style={{ gap: "8px", width: "100%" }} flexDirection="column">
<Forms.FormTitle style={{ marginBottom: 0, width: "100%" }}>Creator:</Forms.FormTitle> return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<Flex style={{ gap: ".5rem" }}> <h2 className="colorwaysModalHeader">
<UserSummaryItem Colorway: {colorway.name}
users={[profile]} </h2>
guildId={undefined} <div className="colorwaysModalContent">
renderIcon={false} <div style={{ gap: "8px", width: "100%", display: "flex", flexDirection: "column" }}>
showDefaultAvatarsForNullUsers <span className="colorwaysModalSectionHeader">Creator:</span>
size={32} <div style={{ gap: ".5rem", display: "flex" }}>
showUserPopout {<img src={`https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.webp?size=32`} width={32} height={32} style={{
/> borderRadius: "32px"
<Text style={{ lineHeight: "32px" }}>{colorway.author}</Text> }} />}
</Flex> <span className="colorwaysModalSectionHeader" style={{ lineHeight: "32px" }} onClick={() => {
<Forms.FormTitle style={{ marginBottom: 0, width: "100%" }}>Colors:</Forms.FormTitle> navigator.clipboard.writeText(profile.username);
<Flex style={{ gap: "8px" }}> 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] }} />)} {colors.map(color => <div className="colorwayInfo-colorSwatch" style={{ backgroundColor: colorway[color] }} />)}
</Flex> </div>
<Forms.FormTitle style={{ marginBottom: 0, width: "100%" }}>Actions:</Forms.FormTitle> <span className="colorwaysModalSectionHeader">Actions:</span>
<Flex style={{ gap: "8px" }} flexDirection="column"> <div style={{ gap: "8px", flexDirection: "column", display: "flex" }}>
<Button <button
color={Button.Colors.PRIMARY} className="colorwaysPillButton"
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }} style={{ width: "100%" }}
onClick={() => { onClick={() => {
const colorwayIDArray = `${colorway.accent},${colorway.primary},${colorway.secondary},${colorway.tertiary}|n:${colorway.name}${colorway.preset ? `|p:${colorway.preset}` : ""}`; const colorwayIDArray = `${colorway.accent},${colorway.primary},${colorway.secondary},${colorway.tertiary}|n:${colorway.name}${colorway.preset ? `|p:${colorway.preset}` : ""}`;
const colorwayID = stringToHex(colorwayIDArray); const colorwayID = stringToHex(colorwayIDArray);
Clipboard.copy(colorwayID); navigator.clipboard.writeText(colorwayID);
Toasts.show({ Toasts.show({
message: "Copied Colorway ID Successfully", message: "Copied Colorway ID Successfully",
type: 1, type: 1,
@ -138,14 +130,12 @@ export default function ({
}} }}
> >
Copy Colorway ID Copy Colorway ID
</Button> </button>
<Button <button
color={Button.Colors.PRIMARY} className="colorwaysPillButton"
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }} style={{ width: "100%" }}
onClick={() => { onClick={() => {
Clipboard.copy(colorway["dc-import"]); navigator.clipboard.writeText(colorway["dc-import"]);
Toasts.show({ Toasts.show({
message: "Copied CSS to Clipboard", message: "Copied CSS to Clipboard",
type: 1, type: 1,
@ -154,11 +144,9 @@ export default function ({
}} }}
> >
Copy CSS Copy CSS
</Button> </button>
<Button <button
color={Button.Colors.PRIMARY} className="colorwaysPillButton"
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }} style={{ width: "100%" }}
onClick={async () => { onClick={async () => {
const newColorway = { const newColorway = {
@ -169,11 +157,9 @@ export default function ({
}} }}
> >
Update CSS Update CSS
</Button> </button>
{colorway.sourceType === "offline" && <Button {colorway.sourceType === "offline" && <button
color={Button.Colors.PRIMARY} className="colorwaysPillButton"
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }} style={{ width: "100%" }}
onClick={async () => { 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]; 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 Rename
</Button>} </button>}
<Button <button
color={Button.Colors.PRIMARY} className="colorwaysPillButton"
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }}
onClick={() => {
openModal(props => <ModalRoot {...props} className="colorwayInfo-cssModal">
<ModalContent><CodeBlock lang="css" content={colorway["dc-import"]} /></ModalContent>
</ModalRoot>);
}}
>
Show CSS
</Button>
<Button
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }} style={{ width: "100%" }}
onClick={() => { onClick={() => {
if (!colorway["dc-import"].includes("@name")) { if (!colorway["dc-import"].includes("@name")) {
if (IS_DISCORD_DESKTOP) { saveFile(new File([`/**
DiscordNative.fileManager.saveWithDialog(`/**
* @name ${colorway.name || "Colorway"} * @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`);
} else {
saveFile(new File([`/**
* @name ${colorway.name || "Colorway"}
* @version ${versionData.creatorVersion}
* @description Automatically generated Colorway. * @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username} * @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id} * @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" })); ${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 { } else {
if (IS_DISCORD_DESKTOP) { saveFile(new File([colorway["dc-import"]], `${colorway.name.replaceAll(" ", "-").toLowerCase()}.theme.css`, { type: "text/plain" }));
DiscordNative.fileManager.saveWithDialog(colorway["dc-import"], `${colorway.name.replaceAll(" ", "-").toLowerCase()}.theme.css`);
} else {
saveFile(new File([colorway["dc-import"]], `${colorway.name.replaceAll(" ", "-").toLowerCase()}.theme.css`, { type: "text/plain" }));
}
} }
}} }}
> >
Download CSS Download CSS
</Button> </button>
<Button <button
color={Button.Colors.PRIMARY} className="colorwaysPillButton"
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
style={{ width: "100%" }} style={{ width: "100%" }}
onClick={() => { 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> <style>
{colorway.isGradient ? pureGradientBase + `.colorwaysPreview-modal,.colorwaysPreview-wrapper {--gradient-theme-bg: linear-gradient(${colorway.linearGradient})}` : ""} {colorway.isGradient ? pureGradientBase + `.colorwaysPreview-modal,.colorwaysPreview-wrapper {--gradient-theme-bg: linear-gradient(${colorway.linearGradient})}` : ""}
</style> </style>
@ -268,15 +222,13 @@ export default function ({
isModal isModal
modalProps={props} modalProps={props}
/> />
</ModalRoot>); </div>);
}} }}
> >
Show preview Show preview
</Button> </button>
{colorway.sourceType === "offline" && <Button {colorway.sourceType === "offline" && <button
color={Button.Colors.RED} className="colorwaysPillButton"
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
style={{ width: "100%" }} style={{ width: "100%" }}
onClick={async () => { onClick={async () => {
const oldStores = (await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]).filter(source => source.name !== colorway.source); 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 Delete
</Button>} </button>}
</Flex> </div>
</Flex> </div>
<div style={{ width: "100%", height: "20px" }} /> </div>
</ModalContent> </div>;
</ModalRoot>;
} }

View file

@ -4,24 +4,33 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { ModalContent, ModalFooter, ModalProps, ModalRoot } from "@utils/modal"; import { DataStore, useEffect, useState } from "..";
import { Button, Forms, TextInput, useState } from "@webpack/common"; import { ModalProps } from "../types";
import { hexToString } from "../utils"; import { hexToString } from "../utils";
export default function ({ modalProps, onColorwayId }: { modalProps: ModalProps, onColorwayId: (colorwayID: string) => void; }) { export default function ({ modalProps, onColorwayId }: { modalProps: ModalProps, onColorwayId: (colorwayID: string) => void; }) {
const [colorwayID, setColorwayID] = useState<string>(""); const [colorwayID, setColorwayID] = useState<string>("");
return <ModalRoot {...modalProps} className="colorwaysCreator-noMinHeight"> const [theme, setTheme] = useState("discord");
<ModalContent className="colorwaysCreator-noHeader colorwaysCreator-noMinHeight">
<Forms.FormTitle>Colorway ID:</Forms.FormTitle> useEffect(() => {
<TextInput placeholder="Enter Colorway ID" onInput={e => setColorwayID(e.currentTarget.value)} /> async function load() {
</ModalContent> setTheme(await DataStore.get("colorwaysPluginTheme") as string);
<ModalFooter> }
<Button load();
style={{ marginLeft: 8 }} }, []);
color={Button.Colors.BRAND} return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
size={Button.Sizes.MEDIUM} <div className="colorwaysModalContent">
look={Button.Looks.FILLED} <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={() => { onClick={() => {
if (!colorwayID) { if (!colorwayID) {
throw new Error("Please enter a Colorway ID"); throw new Error("Please enter a Colorway ID");
@ -34,16 +43,13 @@ export default function ({ modalProps, onColorwayId }: { modalProps: ModalProps,
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => modalProps.onClose()} onClick={() => modalProps.onClose()}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot>; </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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { DataStore } from "@api/index"; import { DataStore, openModal, useEffect, useState } from "..";
import { PlusIcon } from "@components/Icons"; import { Colorway, ModalProps } from "../types";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import { PlusIcon } from "./Icons";
import { findByProps } from "@webpack";
import { Button, Text, TextInput, useEffect, useState } from "@webpack/common";
import { Colorway } from "../types";
import { StoreNameModal } from "./SettingsTabs/SourceManager"; import { StoreNameModal } from "./SettingsTabs/SourceManager";
export default function ({ modalProps, colorways, onFinish }: { modalProps: ModalProps, colorways: Colorway[], onFinish: () => void; }) { export default function ({ modalProps, colorways, onFinish }: { modalProps: ModalProps, colorways: Colorway[], onFinish: () => void; }) {
const [offlineColorwayStores, setOfflineColorwayStores] = useState<{ name: string, colorways: Colorway[], id?: string; }[]>([]); const [offlineColorwayStores, setOfflineColorwayStores] = useState<{ name: string, colorways: Colorway[], id?: string; }[]>([]);
const [storename, setStorename] = useState<string>(); const [storename, setStorename] = useState<string>();
const [noStoreError, setNoStoreError] = useState<boolean>(false); const [noStoreError, setNoStoreError] = useState<boolean>(false);
const { radioBar, item: radioBarItem, itemFilled: radioBarItemFilled, radioPositionLeft } = findByProps("radioBar");
useEffect(() => { useEffect(() => {
(async () => { (async () => {
setOfflineColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]); setOfflineColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[], id?: string; }[]);
})(); })();
}); });
return <ModalRoot {...modalProps}> const [theme, setTheme] = useState("discord");
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1">Select Offline Colorway Source</Text> useEffect(() => {
</ModalHeader> async function load() {
<ModalContent> setTheme(await DataStore.get("colorwaysPluginTheme") as string);
{noStoreError ? <Text variant="text-xs/normal" style={{ color: "var(--text-danger)" }}>Error: No store selected</Text> : <></>} }
{offlineColorwayStores.map(store => { load();
return <div className={`${radioBarItem} ${radioBarItemFilled}`} aria-checked={storename === store.name}> }, []);
<div return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
className={`${radioBar} ${radioPositionLeft}`} <h2 className="colorwaysModalHeader">
style={{ padding: "10px" }} Save to source:
onClick={() => { </h2>
setStorename(store.name); <div className="colorwaysModalContent">
}}> {noStoreError ? <span style={{ color: "var(--text-danger)" }}>Error: No store selected</span> : <></>}
<svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24"> {offlineColorwayStores.map(store => <div
<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" /> className="discordColorway"
{storename === store.name && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />} style={{ padding: "10px" }}
</svg> aria-checked={storename === store.name}
<Text variant="eyebrow" tag="h5">{store.name}</Text> onClick={() => {
</div> setStorename(store.name);
</div>; }}>
})} <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24">
<div className={`${radioBarItem} ${radioBarItemFilled}`}> <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" />
<div {storename === store.name && <circle cx="12" cy="12" r="5" className="radioIconForeground-3wH3aU" fill="currentColor" />}
className={`${radioBar} ${radioPositionLeft}`} </svg>
style={{ padding: "10px" }} <span className="colorwayLabel">{store.name}</span>
onClick={() => { </div>)}
openModal(props => <StoreNameModal modalProps={props} conflicting={false} originalName="" onFinish={async e => { <div
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: [] }]); className="discordColorway"
setOfflineColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]); style={{ padding: "10px" }}
}} />); onClick={() => {
}}> openModal(props => <StoreNameModal modalProps={props} conflicting={false} originalName="" onFinish={async e => {
<PlusIcon width={24} height={24} /> await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: [] }]);
<Text variant="eyebrow" tag="h5">Create new store...</Text> setOfflineColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
</div> }} />);
}}>
<PlusIcon width={24} height={24} />
<span className="colorwayLabel">Create new store...</span>
</div> </div>
</ModalContent> </div>
<ModalFooter> <div className="colorwaysModalFooter">
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton colorwaysPillButton-onSurface"
color={Button.Colors.BRAND_NEW}
size={Button.Sizes.MEDIUM}
onClick={async () => { onClick={async () => {
setNoStoreError(false); setNoStoreError(false);
if (!storename) { 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]; 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) => { colorways.map((colorway, i) => {
if (storeToModify.colorways.map(colorway => colorway.name).includes(colorway.name)) { if (storeToModify.colorways.map(colorway => colorway.name).includes(colorway.name)) {
openModal(props => <ModalRoot {...props}> openModal(props => <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<ModalHeader separator={false}> <h2 className="colorwaysModalHeader">
<Text variant="heading-lg/semibold" tag="h1">Duplicate Colorway</Text> Duplicate Colorway
</ModalHeader> </h2>
<ModalContent> <div className="colorwaysModalContent">
<Text>A colorway with the same name was found in this store, what do you want to do?</Text> <span className="colorwaysModalSectionHeader">A colorway with the same name was found in this store, what do you want to do?</span>
</ModalContent> </div>
<ModalFooter> <div className="colorwaysModalFooter">
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton colorwaysPillButton-onSurface"
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => { onClick={() => {
const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways.filter(colorwayy => colorwayy.name !== colorway.name), colorway] }; 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]); DataStore.set("customColorways", [...oldStores!.filter(source => source.name !== storename), newStore]);
@ -98,29 +91,28 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}} }}
> >
Override Override
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton colorwaysPillButton-onSurface"
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={() => { onClick={() => {
function NewColorwayNameModal({ modalProps, onSelected }: { modalProps: ModalProps, onSelected: (e: string) => void; }) { function NewColorwayNameModal({ modalProps, onSelected }: { modalProps: ModalProps, onSelected: (e: string) => void; }) {
const [errorMsg, setErrorMsg] = useState<string>(); const [errorMsg, setErrorMsg] = useState<string>();
const [newColorwayName, setNewColorwayName] = useState(""); const [newColorwayName, setNewColorwayName] = useState("");
return <ModalRoot {...modalProps}> return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<ModalHeader separator={false}> <h2 className="colorwaysModalHeader">
<Text variant="heading-lg/semibold" tag="h1">Select new name</Text> Select new name
</ModalHeader> </h2>
<ModalContent> <div className="colorwaysModalContent">
<TextInput error={errorMsg} value={newColorwayName} onChange={e => setNewColorwayName(e)} placeholder="Enter valid colorway name" /> <input
</ModalContent> type="text"
<ModalFooter> className="colorwaySelector-search"
<Button value={newColorwayName}
style={{ marginLeft: 8 }} onInput={({ currentTarget: { value } }) => setNewColorwayName(value)}
color={Button.Colors.PRIMARY} placeholder="Enter valid colorway name" />
size={Button.Sizes.MEDIUM} </div>
look={Button.Looks.OUTLINED} <div className="colorwaysModalFooter">
<button
className="colorwaysPillButton"
onClick={() => { onClick={() => {
setErrorMsg(""); setErrorMsg("");
if (storeToModify!.colorways.map(colorway => colorway.name).includes(newColorwayName)) { if (storeToModify!.colorways.map(colorway => colorway.name).includes(newColorwayName)) {
@ -134,12 +126,9 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => { onClick={() => {
if (i + 1 === colorways.length) { if (i + 1 === colorways.length) {
modalProps.onClose(); modalProps.onClose();
@ -147,9 +136,9 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}} }}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot>; </div>;
} }
openModal(propss => <NewColorwayNameModal modalProps={propss} onSelected={e => { openModal(propss => <NewColorwayNameModal modalProps={propss} onSelected={e => {
const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways, { ...colorway, name: 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 Rename
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => { onClick={() => {
props.onClose(); props.onClose();
}} }}
> >
Select different store Select different store
</Button> </button>
</ModalFooter> </div>
</ModalRoot>); </div>);
} else { } else {
const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways, colorway] }; const newStore = { name: storeToModify.name, colorways: [...storeToModify.colorways, colorway] };
DataStore.set("customColorways", [...oldStores!.filter(source => source.name !== storename), newStore]); DataStore.set("customColorways", [...oldStores!.filter(source => source.name !== storename), newStore]);
@ -190,18 +176,15 @@ export default function ({ modalProps, colorways, onFinish }: { modalProps: Moda
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => { onClick={() => {
modalProps.onClose(); modalProps.onClose();
}} }}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot>; </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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { DataStore } from "@api/index"; import { DataStore, ReactNode, useCallback, useEffect, useState } from "../../";
import { SettingsTab } from "@components/VencordSettings/shared"; import Setting from "../Setting";
import { Switch, useCallback, useEffect, useState } from "@webpack/common"; import Switch from "../Switch";
export default function () { export default function ({
hasTheme = false
}: {
hasTheme: boolean;
}) {
const [onDemand, setOnDemand] = useState<boolean>(false); const [onDemand, setOnDemand] = useState<boolean>(false);
const [onDemandTinted, setOnDemandTinted] = useState<boolean>(false); const [onDemandTinted, setOnDemandTinted] = useState<boolean>(false);
const [onDemandDiscordSat, setOnDemandDiscordSat] = useState<boolean>(false); const [onDemandDiscordSat, setOnDemandDiscordSat] = useState<boolean>(false);
const [onDemandOsAccent, setOnDemandOsAccent] = 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() { async function loadUI() {
const [ const [
onDemandWays, onDemandWays,
@ -38,47 +50,53 @@ export default function () {
useEffect(() => { useEffect(() => {
cached_loadUI(); cached_loadUI();
}, []); }, []);
return <SettingsTab title="On-Demand">
<Switch function Container({ children }: { children: ReactNode; }) {
value={onDemand} if (hasTheme) return <div className="colorwaysModalTab" data-theme={theme}>{children}</div>;
onChange={(v: boolean) => { else return <div className="colorwaysModalTab">{children}</div>;
setOnDemand(v); }
DataStore.set("onDemandWays", v);
}} return <Container>
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." <Setting divider>
> <Switch
Enable Colorways On Demand label="Enable Colorways On Demand"
</Switch> id="onDemandWays"
<Switch value={onDemand}
value={onDemandTinted} onChange={(v: boolean) => {
onChange={(v: boolean) => { setOnDemand(v);
setOnDemandTinted(v); DataStore.set("onDemandWays", v);
DataStore.set("onDemandWaysTintedText", 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>
disabled={!onDemand} </Setting>
> <Setting divider disabled={!onDemand}>
Use tinted text <Switch
</Switch> label="Use tinted text"
<Switch id="onDemandWaysTintedText"
value={onDemandDiscordSat} value={onDemandTinted}
onChange={(v: boolean) => { onChange={(v: boolean) => {
setOnDemandDiscordSat(v); setOnDemandTinted(v);
DataStore.set("onDemandWaysDiscordSaturation", v); DataStore.set("onDemandWaysTintedText", v);
}} }} />
disabled={!onDemand} </Setting>
> <Setting divider disabled={!onDemand}>
Use Discord's saturation <Switch
</Switch> label="Use Discord's saturation"
<Switch id="onDemandWaysDiscordSaturation"
hideBorder value={onDemandDiscordSat}
value={onDemandOsAccent} onChange={(v: boolean) => {
onChange={(v: boolean) => { setOnDemandDiscordSat(v);
setOnDemandOsAccent(v); DataStore.set("onDemandWaysDiscordSaturation", v);
DataStore.set("onDemandWaysOsAccentColor", v); }} />
}} </Setting>
disabled={!onDemand || !getComputedStyle(document.body).getPropertyValue("--os-accent-color")} <Setting disabled={!onDemand || !getComputedStyle(document.body).getPropertyValue("--os-accent-color")}>
> <Switch
Use Operating System's Accent Color label="Use Operating System's Accent Color"
</Switch> id="onDemandWaysOsAccentColor"
</SettingsTab>; 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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { DataStore } from "@api/index"; import { DataStore, FluxDispatcher, FluxEvents, PluginProps, ReactNode, useEffect, useState } from "../../";
import { Flex } from "@components/Flex"; import { defaultColorwaySource, fallbackColorways, nullColorwayObj } from "../../constants";
import { Link } from "@components/Link";
import { SettingsTab } from "@components/VencordSettings/shared";
import {
FluxDispatcher,
Forms,
Switch,
Text,
useEffect,
useState
} from "@webpack/common";
import { FluxEvents } from "@webpack/types";
import { versionData } from "../../.";
import { fallbackColorways } from "../../constants";
import { Colorway } from "../../types"; 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 [colorways, setColorways] = useState<Colorway[]>([]);
const [customColorways, setCustomColorways] = useState<Colorway[]>([]); const [customColorways, setCustomColorways] = useState<Colorway[]>([]);
const [colorsButtonVisibility, setColorsButtonVisibility] = useState<boolean>(false); const [colorsButtonVisibility, setColorsButtonVisibility] = useState<boolean>(false);
const [isButtonThin, setIsButtonThin] = useState<boolean>(false); const [theme, setTheme] = useState("discord");
const [showLabelsInSelectorGridView, setShowLabelsInSelectorGridView] = useState<boolean>(false); 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(() => { useEffect(() => {
(async function () { (async function () {
const [ const [
customColorways, customColorways,
colorwaySourceFiles, colorwaySourceFiles,
showColorwaysButton, showColorwaysButton
useThinMenuButton,
showLabelsInSelectorGridView
] = await DataStore.getMany([ ] = await DataStore.getMany([
"customColorways", "customColorways",
"colorwaySourceFiles", "colorwaySourceFiles",
"showColorwaysButton", "showColorwaysButton"
"useThinMenuButton",
"showLabelsInSelectorGridView"
]); ]);
const responses: Response[] = await Promise.all( const responses: Response[] = await Promise.all(
colorwaySourceFiles.map((url: string) => colorwaySourceFiles.map(({ url }: { url: string; }) =>
fetch(url) fetch(url)
) )
); );
@ -57,16 +61,21 @@ export default function () {
setColorways(colorways || fallbackColorways); setColorways(colorways || fallbackColorways);
setCustomColorways(customColorways.map(source => source.colorways).flat(2)); setCustomColorways(customColorways.map(source => source.colorways).flat(2));
setColorsButtonVisibility(showColorwaysButton); setColorsButtonVisibility(showColorwaysButton);
setIsButtonThin(useThinMenuButton);
setShowLabelsInSelectorGridView(showLabelsInSelectorGridView);
})(); })();
}, []); }, []);
return <SettingsTab title="Settings"> function Container({ children }: { children: ReactNode; }) {
<div className="colorwaysSettingsPage-wrapper"> if (hasTheme) return <div className="colorwaysModalTab" data-theme={theme}>{children}</div>;
<Forms.FormTitle tag="h5">Quick Switch</Forms.FormTitle> else return <div className="colorwaysModalTab">{children}</div>;
}
return <Container>
<span className="colorwaysModalSectionHeader">Quick Switch</span>
<Setting divider>
<Switch <Switch
value={colorsButtonVisibility} value={colorsButtonVisibility}
label="Enable Quick Switch"
id="showColorwaysButton"
onChange={(v: boolean) => { onChange={(v: boolean) => {
setColorsButtonVisibility(v); setColorsButtonVisibility(v);
DataStore.set("showColorwaysButton", v); DataStore.set("showColorwaysButton", v);
@ -74,111 +83,198 @@ export default function () {
type: "COLORWAYS_UPDATE_BUTTON_VISIBILITY" as FluxEvents, type: "COLORWAYS_UPDATE_BUTTON_VISIBILITY" as FluxEvents,
isVisible: v isVisible: v
}); });
}} }} />
note="Shows a button on the top of the servers list that opens a colorway selector modal." <span className="colorwaysNote">Shows a button on the top of the servers list that opens a colorway selector modal.</span>
> </Setting>
Enable Quick Switch <span className="colorwaysModalSectionHeader">Appearance</span>
</Switch> <Setting divider>
<Switch <div style={{
value={isButtonThin} display: "flex",
onChange={(v: boolean) => { flexDirection: "row",
setIsButtonThin(v); width: "100%",
DataStore.set("useThinMenuButton", v); alignItems: "center",
FluxDispatcher.dispatch({ cursor: "pointer"
type: "COLORWAYS_UPDATE_BUTTON_HEIGHT" as FluxEvents, }}>
isTall: v <label className="colorwaySwitch-label">Plugin Theme</label>
}); <select
}} className="colorwaysPillButton"
note="Replaces the icon on the colorways launcher button with text, making it more compact." style={{ border: "none" }}
> onChange={e => {
Use thin Quick Switch button setTheme(e.currentTarget.value);
</Switch> DataStore.set("colorwaysPluginTheme", e.currentTarget.value);
<Forms.FormTitle tag="h5">Selector</Forms.FormTitle> changeTheme(e.currentTarget.value);
<Switch }}
value={showLabelsInSelectorGridView} value={theme}
onChange={(v: boolean) => { >
setShowLabelsInSelectorGridView(v); <option value="discord">Discord (Default)</option>
DataStore.set("showLabelsInSelectorGridView", v); <option value="colorish">Colorish</option>
}} </select>
> </div>
Show labels in Grid View </Setting>
</Switch> <span className="colorwaysModalSectionHeader">Manager</span>
<Flex flexDirection="column" style={{ gap: 0 }}> <Setting>
<h1 style={{ <div style={{
fontFamily: "var(--font-headline)", 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", fontSize: "24px",
color: "var(--header-primary)", backgroundColor: "var(--brand-500)",
lineHeight: "31px", padding: "0 4px",
marginBottom: "0" borderRadius: "4px"
}}> }}>Colorways</span>
Discord <span style={{ </h1>
fontFamily: "var(--font-display)", <span
fontSize: "24px", style={{
backgroundColor: "var(--brand-500)", color: "var(--text-normal)",
padding: "0 4px", fontWeight: 500,
borderRadius: "4px" fontSize: "14px",
}}>Colorways</span> marginBottom: "12px"
</h1> }}
<Text >by Project Colorway</span>
variant="text-xs/normal" <span className="colorwaysModalSectionHeader">
style={{ Plugin Version:
color: "var(--text-normal)", </span>
fontWeight: 500, <span
fontSize: "14px", style={{
marginBottom: "12px" color: "var(--text-muted)",
}} fontWeight: 500,
>by Project Colorway</Text> fontSize: "14px",
<Forms.FormTitle style={{ marginBottom: 0 }}> marginBottom: "8px"
Plugin Version: }}
</Forms.FormTitle> >
<Text {PluginProps.pluginVersion} ({PluginProps.clientMod})
variant="text-xs/normal" </span>
style={{ <span className="colorwaysModalSectionHeader">
color: "var(--text-muted)", UI Version:
fontWeight: 500, </span>
fontSize: "14px", <span
marginBottom: "8px" style={{
}} color: "var(--text-muted)",
> fontWeight: 500,
{versionData.pluginVersion} fontSize: "14px",
</Text> marginBottom: "8px"
<Forms.FormTitle style={{ marginBottom: 0 }}> }}
Creator Version: >
</Forms.FormTitle> {PluginProps.UIVersion}
<Text </span>
variant="text-xs/normal" <span className="colorwaysModalSectionHeader">
style={{ Creator Version:
color: "var(--text-muted)", </span>
fontWeight: 500, <span
fontSize: "14px", style={{
marginBottom: "8px" color: "var(--text-muted)",
}} fontWeight: 500,
> fontSize: "14px",
{versionData.creatorVersion}{" (Stable)"} marginBottom: "8px"
</Text> }}
<Forms.FormTitle style={{ marginBottom: 0 }}> >
Loaded Colorways: {PluginProps.creatorVersion}
</Forms.FormTitle> </span>
<Text <span className="colorwaysModalSectionHeader">
variant="text-xs/normal" Loaded Colorways:
style={{ </span>
color: "var(--text-muted)", <span
fontWeight: 500, style={{
fontSize: "14px", color: "var(--text-muted)",
marginBottom: "8px" fontWeight: 500,
}} fontSize: "14px",
> marginBottom: "8px"
{[...colorways, ...customColorways].length + 1} }}
</Text> >
<Forms.FormTitle style={{ marginBottom: 0 }}> {[...colorways, ...customColorways].length}
Project Repositories: </span>
</Forms.FormTitle> <span className="colorwaysModalSectionHeader">
<Forms.FormText style={{ marginBottom: "8px" }}> Project Repositories:
<Link href="https://github.com/DaBluLite/DiscordColorways">DiscordColorways</Link> </span>
<br /> <a role="link" target="_blank" href="https://github.com/DaBluLite/DiscordColorways">DiscordColorways</a>
<Link href="https://github.com/DaBluLite/ProjectColorway">Project Colorway</Link> <a role="link" target="_blank" href="https://github.com/DaBluLite/ProjectColorway">Project Colorway</a>
</Forms.FormText>
</Flex>
</div> </div>
</SettingsTab>; </Container>;
} }

View file

@ -4,39 +4,39 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { DataStore } from "@api/index"; import { DataStore, openModal, ReactNode, useEffect, useState } from "../../";
import { Flex } from "@components/Flex";
import { CopyIcon, DeleteIcon, PlusIcon } from "@components/Icons";
import { SettingsTab } from "@components/VencordSettings/shared";
import { Logger } from "@utils/Logger";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { chooseFile, saveFile } from "@utils/web";
import { findByProps } from "@webpack";
import { Button, Clipboard, Forms, ScrollerThin, Text, TextInput, useEffect, useState } from "@webpack/common";
import { defaultColorwaySource } from "../../constants"; import { defaultColorwaySource } from "../../constants";
import { Colorway } from "../../types"; import { Colorway, ModalProps } from "../../types";
import { DownloadIcon, ImportIcon } from "../Icons"; import { chooseFile, saveFile } from "../../utils";
import Spinner from "../Spinner"; 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; }) { export function StoreNameModal({ modalProps, originalName, onFinish, conflicting }: { modalProps: ModalProps, originalName: string, onFinish: (newName: string) => Promise<void>, conflicting: boolean; }) {
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [newStoreName, setNewStoreName] = useState<string>(originalName); const [newStoreName, setNewStoreName] = useState<string>(originalName);
return <ModalRoot {...modalProps}> const [theme, setTheme] = useState("discord");
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1">{conflicting ? "Duplicate Store Name" : "Give this store a name"}</Text> useEffect(() => {
</ModalHeader> async function load() {
<ModalContent> setTheme(await DataStore.get("colorwaysPluginTheme") as string);
{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> load();
<TextInput error={error} value={newStoreName} onChange={e => setNewStoreName(e)} style={{ marginBottom: "16px" }} /> }, []);
</ModalContent>
<ModalFooter> return <div className={`colorwaysModal ${modalProps.transitionState === 2 ? "closing" : ""} ${modalProps.transitionState === 4 ? "hidden" : ""}`} data-theme={theme}>
<Button <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 }} style={{ marginLeft: 8 }}
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={async () => { onClick={async () => {
setError(""); setError("");
if ((await DataStore.get("customColorways")).map(store => store.name).includes(newStoreName)) { if ((await DataStore.get("customColorways")).map(store => store.name).includes(newStoreName)) {
@ -47,18 +47,16 @@ export function StoreNameModal({ modalProps, originalName, onFinish, conflicting
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
className="colorwaysPillButton"
style={{ marginLeft: 8 }} style={{ marginLeft: 8 }}
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => modalProps.onClose()} onClick={() => modalProps.onClose()}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot>; </div>;
} }
function AddOnlineStoreModal({ modalProps, onFinish }: { modalProps: ModalProps, onFinish: (name: string, url: string) => void; }) { 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 [nameError, setNameError] = useState<string>("");
const [URLError, setURLError] = useState<string>(""); const [URLError, setURLError] = useState<string>("");
const [nameReadOnly, setNameReadOnly] = useState<boolean>(false); const [nameReadOnly, setNameReadOnly] = useState<boolean>(false);
return <ModalRoot {...modalProps}> const [theme, setTheme] = useState("discord");
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" tag="h1"> useEffect(() => {
Add a source: async function load() {
</Text> setTheme(await DataStore.get("colorwaysPluginTheme") as string);
</ModalHeader> }
<ModalContent> load();
<Forms.FormTitle>Name:</Forms.FormTitle> }, []);
<TextInput 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..." placeholder="Enter a valid Name..."
onChange={setColorwaySourceName} onInput={e => setColorwaySourceName(e.currentTarget.value)}
value={colorwaySourceName} value={colorwaySourceName}
error={nameError}
readOnly={nameReadOnly} readOnly={nameReadOnly}
disabled={nameReadOnly} disabled={nameReadOnly}
/> />
<Forms.FormTitle style={{ marginTop: "8px" }}>URL:</Forms.FormTitle> <span className="colorwaysModalSectionHeader" style={{ marginTop: "8px" }}>URL:</span>
<TextInput <input
type="text"
className="colorwaySelector-search"
placeholder="Enter a valid URL..." placeholder="Enter a valid URL..."
onChange={value => { onChange={({ currentTarget: { value } }) => {
setColorwaySourceURL(value); setColorwaySourceURL(value);
if (value === defaultColorwaySource) { if (value === defaultColorwaySource) {
setNameReadOnly(true); setNameReadOnly(true);
@ -94,16 +101,12 @@ function AddOnlineStoreModal({ modalProps, onFinish }: { modalProps: ModalProps,
} }
}} }}
value={colorwaySourceURL} value={colorwaySourceURL}
error={URLError}
style={{ marginBottom: "16px" }} style={{ marginBottom: "16px" }}
/> />
</ModalContent> </div>
<ModalFooter> <div className="colorwaysModalFooter">
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton colorwaysPillButton-onSurface"
color={Button.Colors.BRAND}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onClick={async () => { onClick={async () => {
const sourcesArr: { name: string, url: string; }[] = (await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]); const sourcesArr: { name: string, url: string; }[] = (await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
if (!colorwaySourceName) { if (!colorwaySourceName) {
@ -124,197 +127,104 @@ function AddOnlineStoreModal({ modalProps, onFinish }: { modalProps: ModalProps,
}} }}
> >
Finish Finish
</Button> </button>
<Button <button
style={{ marginLeft: 8 }} className="colorwaysPillButton"
color={Button.Colors.PRIMARY}
size={Button.Sizes.MEDIUM}
look={Button.Looks.OUTLINED}
onClick={() => modalProps.onClose()} onClick={() => modalProps.onClose()}
> >
Cancel Cancel
</Button> </button>
</ModalFooter> </div>
</ModalRoot>; </div>;
} }
export default function () { export default function ({
const [colorwaySourceFiles, setColorwaySourceFiles] = useState<{ name: string, url: string; }[]>([]); hasTheme = false
const [customColorwayStores, setCustomColorwayStores] = useState<{ name: string, colorways: Colorway[]; }[]>([]); }: {
hasTheme?: boolean;
const { item: radioBarItem, itemFilled: radioBarItemFilled } = findByProps("radioBar"); }) {
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(() => { useEffect(() => {
(async function () { (async function () {
setColorwaySourceFiles(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]); setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
updateRemoteSources();
})(); })();
}, []); }, []);
return <SettingsTab title="Sources"> return <div className="colorwaySourceTab">
<Flex style={{ gap: "0", marginBottom: "8px", alignItems: "center" }}> <div style={{
<Forms.FormTitle tag="h5" style={{ marginBottom: 0, flexGrow: 1 }}>Online</Forms.FormTitle> display: "flex",
<Button gap: "8px"
className="colorwaysSettings-colorwaySourceAction" }}>
innerClassName="colorwaysSettings-iconButtonInner" <button
className="colorwaysPillButton"
style={{ flexShrink: "0" }} 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 () => { onClick={async () => {
if (IS_DISCORD_DESKTOP) { const file = await chooseFile("application/json");
const [file] = await DiscordNative.fileManager.openFiles({ if (!file) return;
filters: [
{ name: "DiscordColorways Offline Store", extensions: ["json"] },
{ name: "all", extensions: ["*"] }
]
});
if (file) {
try {
if ((await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]).map(store => store.name).includes(JSON.parse(new TextDecoder().decode(file.data)).name)) {
openModal(props => <StoreNameModal conflicting modalProps={props} originalName={JSON.parse(new TextDecoder().decode(file.data)).name} onFinish={async e => {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: JSON.parse(new TextDecoder().decode(file.data)).colorways }]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
}} />);
} else {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), JSON.parse(new TextDecoder().decode(file.data))]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
}
} catch (err) {
new Logger("DiscordColorways").error(err);
}
}
} else {
const file = await chooseFile("application/json");
if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async () => { reader.onload = async () => {
try { try {
if ((await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]).map(store => store.name).includes(JSON.parse(reader.result as string).name)) { 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 => { 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 }]); await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: JSON.parse(reader.result as string).colorways }]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
}} />);
} else {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), JSON.parse(reader.result as string)]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]); setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
} updateRemoteSources();
} catch (err) { }} />);
new Logger("DiscordColorways").error(err); } 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();
} }
}; } catch (err) {
reader.readAsText(file); console.error("DiscordColorways: " + err);
} }
};
reader.readAsText(file);
updateRemoteSources();
}} }}
> >
<ImportIcon width={14} height={14} /> <ImportIcon width={14} height={14} />
Import... Import...
</Button> </button>
<Button <button
className="colorwaysSettings-colorwaySourceAction" className="colorwaysPillButton"
innerClassName="colorwaysSettings-iconButtonInner" style={{ flexShrink: "0" }}
style={{ flexShrink: "0", marginLeft: "8px" }}
size={Button.Sizes.SMALL}
color={Button.Colors.TRANSPARENT}
onClick={() => { onClick={() => {
openModal(props => <StoreNameModal conflicting={false} modalProps={props} originalName="" onFinish={async e => { openModal(props => <StoreNameModal conflicting={false} modalProps={props} originalName="" onFinish={async e => {
await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: [] }]); await DataStore.set("customColorways", [...await DataStore.get("customColorways"), { name: e, colorways: [] }]);
setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]); setCustomColorwayStores(await DataStore.get("customColorways") as { name: string, colorways: Colorway[]; }[]);
props.onClose(); props.onClose();
updateRemoteSources();
}} />); }} />);
}}> }}>
<svg <svg
@ -330,41 +240,31 @@ export default function () {
/> />
</svg> </svg>
New... New...
</Button> </button>
</Flex> </div>
<ScrollerThin orientation="vertical" style={{ maxHeight: "50%" }} className="colorwaysSettings-sourceScroller"> <div className="colorwaysSettings-sourceScroller">
{getComputedStyle(document.body).getPropertyValue("--os-accent-color") ? <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}> {getComputedStyle(document.body).getPropertyValue("--os-accent-color") ? <div className={"colorwaysSettings-colorwaySource"} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<Flex style={{ gap: 0, alignItems: "center", width: "100%", height: "30px" }}> <div style={{ alignItems: "center", width: "100%", height: "30px", display: "flex" }}>
<Text className="colorwaysSettings-colorwaySourceLabel">OS Accent Color{" "} <span className="colorwaysSettings-colorwaySourceLabel">OS Accent Color{" "}
<div className="colorways-badge">Built-In</div> <div className="colorways-badge">Built-In</div>
</Text> </span>
</Flex> </div>
</div> : <></>} </div> : <></>}
{customColorwayStores.map(({ name: customColorwaySourceName, colorways: offlineStoreColorways }) => <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}> {customColorwayStores.map(({ name: customColorwaySourceName, colorways: offlineStoreColorways }) => <div className={"colorwaysSettings-colorwaySource"} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<Text className="colorwaysSettings-colorwaySourceLabel"> <span className="colorwaysSettings-colorwaySourceLabel">
{customColorwaySourceName} {customColorwaySourceName}
</Text> </span>
<Flex style={{ marginLeft: "auto", gap: "8px" }}> <div style={{ marginLeft: "auto", gap: "8px", display: "flex" }}>
<Button <button
innerClassName="colorwaysSettings-iconButtonInner" className="colorwaysPillButton colorwaysPillButton-onSurface"
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={async () => { onClick={async () => {
if (IS_DISCORD_DESKTOP) { saveFile(new File([JSON.stringify({ "name": customColorwaySourceName, "colorways": [...offlineStoreColorways] })], `${customColorwaySourceName.replaceAll(" ", "-").toLowerCase()}.colorways.json`, { type: "application/json" }));
DiscordNative.fileManager.saveWithDialog(JSON.stringify({ "name": customColorwaySourceName, "colorways": [...offlineStoreColorways] }), `${customColorwaySourceName.replaceAll(" ", "-").toLowerCase()}.colorways.json`);
} else {
saveFile(new File([JSON.stringify({ "name": customColorwaySourceName, "colorways": [...offlineStoreColorways] })], `${customColorwaySourceName.replaceAll(" ", "-").toLowerCase()}.colorways.json`, { type: "application/json" }));
}
}} }}
> >
<DownloadIcon width={14} height={14} /> Export as... <DownloadIcon width={14} height={14} /> Export as...
</Button> </button>
<Button <button
innerClassName="colorwaysSettings-iconButtonInner" className="colorwaysPillButton colorwaysPillButton-onSurface"
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
look={Button.Looks.OUTLINED}
onClick={async () => { onClick={async () => {
var sourcesArr: { name: string, colorways: Colorway[]; }[] = []; var sourcesArr: { name: string, colorways: Colorway[]; }[] = [];
const customColorwaySources = await DataStore.get("customColorways"); const customColorwaySources = await DataStore.get("customColorways");
@ -375,13 +275,141 @@ export default function () {
}); });
DataStore.set("customColorways", sourcesArr); DataStore.set("customColorways", sourcesArr);
setCustomColorwayStores(sourcesArr); setCustomColorwayStores(sourcesArr);
updateRemoteSources();
}} }}
> >
<DeleteIcon width={20} height={20} /> Remove <DeleteIcon width={20} height={20} /> Remove
</Button> </button>
</Flex> </div>
</div> </div>
)} )}
</ScrollerThin> </div>
</SettingsTab>; </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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { DataStore } from "@api/index"; import { DataStore, openModal, ReactNode, useEffect, useState } from "../../";
import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { SettingsTab } from "@components/VencordSettings/shared";
import { getTheme, Theme } from "@utils/discord";
import { openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { Button, ScrollerThin, Text, TextInput, Tooltip, useEffect, useState } from "@webpack/common";
import { StoreItem } from "../../types"; import { StoreItem } from "../../types";
import { DownloadIcon, PalleteIcon } from "../Icons"; import { DeleteIcon, DownloadIcon, PalleteIcon } from "../Icons";
import Selector from "../Selector"; import Selector from "../Selector";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg"; export default function ({
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg"; hasTheme = false
}: {
function GithubIcon() { hasTheme?: boolean;
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark; }) {
return <img src={src} alt="GitHub" />;
}
export default function () {
const [storeObject, setStoreObject] = useState<StoreItem[]>([]); const [storeObject, setStoreObject] = useState<StoreItem[]>([]);
const [colorwaySourceFiles, setColorwaySourceFiles] = useState<{ name: string, url: string; }[]>([]); const [colorwaySourceFiles, setColorwaySourceFiles] = useState<{ name: string, url: string; }[]>([]);
const [searchValue, setSearchValue] = useState<string>(""); const [searchValue, setSearchValue] = useState<string>("");
const [theme, setTheme] = useState("discord");
useEffect(() => {
async function load() {
setTheme(await DataStore.get("colorwaysPluginTheme") as string);
}
load();
}, []);
useEffect(() => { useEffect(() => {
if (!searchValue) { 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"> return <Container>
<Flex style={{ gap: "0", marginBottom: "8px" }}> <div style={{ display: "flex", marginBottom: "8px" }}>
<TextInput <input
type="text"
className="colorwaySelector-search" className="colorwaySelector-search"
placeholder="Search for sources..." placeholder="Search for sources..."
value={searchValue} value={searchValue}
onChange={setSearchValue} onChange={e => setSearchValue(e.currentTarget.value)}
/> />
<Tooltip text="Refresh..."> <button
{({ onMouseEnter, onMouseLeave }) => <Button className="colorwaysPillButton"
innerClassName="colorwaysSettings-iconButtonInner" style={{ marginLeft: "8px", marginTop: "auto", marginBottom: "auto" }}
size={Button.Sizes.ICON} onClick={async function () {
color={Button.Colors.PRIMARY} const res: Response = await fetch("https://dablulite.vercel.app/");
look={Button.Looks.OUTLINED} const data = await res.json();
style={{ marginLeft: "8px" }} setStoreObject(data.sources);
onMouseEnter={onMouseEnter} setColorwaySourceFiles(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]);
onMouseLeave={onMouseLeave} }}
onClick={async function () { >
const res: Response = await fetch("https://dablulite.vercel.app/"); <svg
const data = await res.json(); xmlns="http://www.w3.org/2000/svg"
setStoreObject(data.sources); x="0px"
setColorwaySourceFiles(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]); y="0px"
}} width="14"
height="14"
style={{ boxSizing: "content-box", flexShrink: 0 }}
viewBox="0 0 24 24"
fill="currentColor"
> >
<svg <rect
xmlns="http://www.w3.org/2000/svg" y="0"
x="0px" fill="none"
y="0px" width="24"
width="20" height="24"
height="20" />
style={{ padding: "6px", boxSizing: "content-box" }} <path
viewBox="0 0 24 24" 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"
fill="currentColor" />
> <path
<rect 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"
y="0" />
fill="none" </svg>
width="24" Refresh
height="24" </button>
/> </div>
<path <div className="colorwaysSettings-sourceScroller">
d="M6.351,6.351C7.824,4.871,9.828,4,12,4c4.411,0,8,3.589,8,8h2c0-5.515-4.486-10-10-10 C9.285,2,6.779,3.089,4.938,4.938L3,3v6h6L6.351,6.351z"
/>
<path
d="M17.649,17.649C16.176,19.129,14.173,20,12,20c-4.411,0-8-3.589-8-8H2c0,5.515,4.486,10,10,10 c2.716,0,5.221-1.089,7.062-2.938L21,21v-6h-6L17.649,17.649z"
/>
</svg>
</Button>}
</Tooltip>
</Flex>
<ScrollerThin orientation="vertical" className="colorwaysSettings-sourceScroller">
{storeObject.map((item: StoreItem) => {storeObject.map((item: StoreItem) =>
item.name.toLowerCase().includes(searchValue.toLowerCase()) ? <div className={`${radioBarItem} ${radioBarItemFilled} colorwaysSettings-colorwaySource`} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}> item.name.toLowerCase().includes(searchValue.toLowerCase()) ? <div className={"colorwaysSettings-colorwaySource"} style={{ flexDirection: "column", padding: "16px", alignItems: "start" }}>
<Flex flexDirection="column" style={{ gap: ".5rem", marginBottom: "8px" }}> <div style={{ gap: ".5rem", display: "flex", marginBottom: "8px", flexDirection: "column" }}>
<Text className="colorwaysSettings-colorwaySourceLabelHeader"> <span className="colorwaysSettings-colorwaySourceLabelHeader">
{item.name} {item.name}
</Text> </span>
<Text className="colorwaysSettings-colorwaySourceDesc"> <span className="colorwaysSettings-colorwaySourceDesc">
{item.description} {item.description}
</Text> </span>
<Text className="colorwaysSettings-colorwaySourceDesc" style={{ opacity: ".8" }}> <span className="colorwaysSettings-colorwaySourceDesc" style={{ opacity: ".8" }}>
by {item.authorGh} by {item.authorGh}
</Text> </span>
</Flex> </div>
<Flex style={{ gap: "8px", alignItems: "center", width: "100%" }}> <div style={{ gap: "8px", alignItems: "center", width: "100%", display: "flex" }}>
<Link href={"https://github.com/" + item.authorGh}><GithubIcon /></Link> <a role="link" target="_blank" href={"https://github.com/" + item.authorGh}>
<Button <img src="/assets/6a853b4c87fce386cbfef4a2efbacb09.svg" alt="GitHub" />
innerClassName="colorwaysSettings-iconButtonInner" </a>
size={Button.Sizes.SMALL} <button
color={colorwaySourceFiles.map(source => source.name).includes(item.name) ? Button.Colors.RED : Button.Colors.PRIMARY} className="colorwaysPillButton colorwaysPillButton-onSurface"
look={Button.Looks.OUTLINED}
style={{ marginLeft: "auto" }} style={{ marginLeft: "auto" }}
onClick={async () => { onClick={async () => {
if (colorwaySourceFiles.map(source => source.name).includes(item.name)) { 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</>} {colorwaySourceFiles.map(source => source.name).includes(item.name) ? <><DeleteIcon width={14} height={14} /> Remove</> : <><DownloadIcon width={14} height={14} /> Add to Sources</>}
</Button> </button>
<Button <button
innerClassName="colorwaysSettings-iconButtonInner" className="colorwaysPillButton colorwaysPillButton-onSurface"
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
onClick={async () => { 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 <PalleteIcon width={14} height={14} />
</Button> Preview
</Flex> </button>
</div>
</div> : <></> </div> : <></>
)} )}
</ScrollerThin> </div>
</SettingsTab>; </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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { CSSProperties } from "react"; export default function ({ className, style }: { className?: string, style?: any; }) {
export default function ({ className, style }: { className?: string, style?: CSSProperties; }) {
return <div className={"colorwaysBtn-spinner" + (className ? ` ${className}` : "")} role="img" aria-label="Loading" style={style}> return <div className={"colorwaysBtn-spinner" + (className ? ` ${className}` : "")} role="img" aria-label="Loading" style={style}>
<div className="colorwaysBtn-spinnerInner"> <div className="colorwaysBtn-spinnerInner">
<svg className="colorwaysBtn-spinnerCircular" viewBox="25 25 50 50" fill="currentColor"> <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 * 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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { ModalProps, ModalRoot, openModal } from "@utils/modal"; import { openModal } from "..";
import { Text } from "@webpack/common"; import { ModalProps } from "../types";
import { HexToHSL } from "../utils"; import { HexToHSL } from "../utils";
import { CloseIcon } from "./Icons"; import { CloseIcon } from "./Icons";
@ -48,12 +47,12 @@ export default function ThemePreview({
if (isModal) { if (isModal) {
modalProps?.onClose(); modalProps?.onClose();
} else { } 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> <style>
{previewCSS} {previewCSS}
</style> </style>
<ThemePreview accent={accent} primary={primary} secondary={secondary} tertiary={tertiary} isModal modalProps={props} /> <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)}%` "--primary-500-hsl": `${HexToHSL(primary)[0]} ${HexToHSL(primary)[1]}% ${Math.min(HexToHSL(primary)[2] + (3.6 * 3), 100)}%`
} as React.CSSProperties} } as React.CSSProperties}
> >
<Text <span style={{
tag="div" fontWeight: 700,
variant="text-md/semibold" color: "var(--text-normal)"
lineClamp={1} }}>
selectable={false}
>
Preview Preview
</Text> </span>
</div> </div>
</div> </div>
<div className="colorwayPreview-chat" style={{ background: `var(--dc-overlay-chat, ${primary})` }}> <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 * 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 = [ export const fallbackColorways = [
{ {
@ -311,3 +313,5 @@ export const mainColors = [
{ name: "secondary", title: "Secondary", var: "--background-secondary" }, { name: "secondary", title: "Secondary", var: "--background-secondary" },
{ name: "tertiary", title: "Tertiary", var: "--background-tertiary" } { 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 * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { UserStore } from "@webpack/common"; import { PluginProps, UserStore } from "./";
import { Plugins } from "Vencord";
import { HexToHSL } from "./utils"; import { HexToHSL } from "./utils";
export const colorVariables: string[] = [ 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-home-card: 0.9;
--bg-overlay-opacity-app-frame: var(--bg-overlay-opacity-5); --bg-overlay-opacity-app-frame: var(--bg-overlay-opacity-5);
} }
.children_cde9af:after, .form_d8a4a1:before { .children_fc4f04:after, .form_a7d72e:before {
content: none; content: none;
} }
.scroller_de945b { .scroller_fea3ef {
background: var(--bg-overlay-app-frame,var(--background-tertiary)); 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)); background: rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-6));
} }
.wrapper__8436d:not(:hover):not(.selected_ae80f7) .childWrapper_a6ce15 { .wrapper__8436d:not(:hover):not(.selected_ae80f7) .childWrapper_a6ce15 {
background: rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-6)); 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)); 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)); background: rgb(var(--bg-overlay-color-inverse)/var(--bg-overlay-opacity-6));
} }
.auto_a3c0bd::-webkit-scrollbar-thumb, .auto_eed6a8::-webkit-scrollbar-thumb,
.thin_b1c063::-webkit-scrollbar-thumb { .thin_eed6a8::-webkit-scrollbar-thumb {
background-size: 200vh; 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: -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); 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-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: -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); 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 `/** return `/**
* @name ${name} * @name ${name}
* @version ${(Plugins.plugins.DiscordColorways as any).creatorVersion} * @version ${PluginProps.creatorVersion}
* @description Automatically generated Colorway. * @description Automatically generated Colorway.
* @author ${UserStore.getCurrentUser().username} * @author ${UserStore.getCurrentUser().username}
* @authorId ${UserStore.getCurrentUser().id} * @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-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)}%; --primary-200-hsl: ${HexToHSL("#" + tertiaryColor)[0]} calc(var(--saturation-factor, 1)*${HexToHSL("#" + tertiaryColor)[1]}%) ${Math.min(HexToHSL("#" + tertiaryColor)[2] + 80, 80)}%;
} }
.emptyPage_feb902, .emptyPage_c6b11b,
.scrollerContainer_dda72c, .scrollerContainer_c6b11b,
.container__03ec9, .container_f1fd9c,
.header__71942 { .header_f1fd9c {
background-color: unset !important; background-color: unset !important;
} }
.container__6b2e5, .container_c2efea,
.container__03ec9, .container_f1fd9c,
.header__71942 { .header_f1fd9c {
background: transparent !important; background: transparent !important;
}${(Math.round(HexToHSL("#" + primaryColor)[2]) > 80) ? `\n\n/*Primary*/ }${(Math.round(HexToHSL("#" + primaryColor)[2]) > 80) ? `\n\n/*Primary*/
.theme-dark .container_bd15da, .theme-dark .container_c2739c,
.theme-dark .body__616e6, .theme-dark .body_cd82a7,
.theme-dark .toolbar__62fb5, .theme-dark .toolbar_fc4f04,
.theme-dark .container_e1387b, .theme-dark .container_f0fccd,
.theme-dark .messageContent_abea64, .theme-dark .messageContent_f9f2ca,
.theme-dark .attachButtonPlus_fd0021, .theme-dark .attachButtonPlus_f298d4,
.theme-dark .username__0b0e7:not([style]), .theme-dark .username_f9f2ca:not([style]),
.theme-dark .children_cde9af, .theme-dark .children_fc4f04,
.theme-dark .buttonContainer__6de7e, .theme-dark .buttonContainer_f9f2ca,
.theme-dark .listItem__48528, .theme-dark .listItem_c96c45,
.theme-dark .body__616e6 .caret__33d19, .theme-dark .body_cd82a7 .caret_fc4f04,
.theme-dark .body__616e6 .titleWrapper_d6133e > h1, .theme-dark .body_cd82a7 .titleWrapper_fc4f04 > h1,
.theme-dark .body__616e6 .icon_ae0b42 { .theme-dark .body_cd82a7 .icon_fc4f04 {
--white-500: black !important; --white-500: black !important;
--interactive-normal: black !important; --interactive-normal: black !important;
--text-normal: black !important; --text-normal: black !important;
@ -561,23 +559,23 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
--header-secondary: black !important; --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; --white-500: black !important;
} }
.theme-dark .container__26baa { .theme-dark .container_fc4f04 {
--channel-icon: black; --channel-icon: black;
} }
.theme-dark .callContainer__1477d { .theme-dark .callContainer_d880dc {
--white-500: ${(HexToHSL("#" + tertiaryColor)[2] > 80) ? "black" : "white"} !important; --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"}; --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"}; --channel-text-area-placeholder: ${(HexToHSL("#" + primaryColor)[2] + 3.6 > 80) ? "black" : "white"};
opacity: .6; opacity: .6;
} }
@ -586,16 +584,16 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
background-color: black; background-color: black;
} }
.theme-dark .root_a28985 > .header__5e5a6 > h1 { .theme-dark .root_f9a4c9 > .header_f9a4c9 > h1 {
color: black; color: black;
} }
/*End Primary*/`: ""}${(HexToHSL("#" + secondaryColor)[2] > 80) ? `\n\n/*Secondary*/ /*End Primary*/`: ""}${(HexToHSL("#" + secondaryColor)[2] > 80) ? `\n\n/*Secondary*/
.theme-dark .wrapper__3c6d5 *, .theme-dark .wrapper_cd82a7 *,
.theme-dark .sidebar_e031be *:not(.hasBanner__04337 *), .theme-dark .sidebar_a4d4d9 *:not(.hasBanner_fd6364 *),
.theme-dark .members__573eb *:not([style]), .theme-dark .members_cbd271 *:not([style]),
.theme-dark .sidebarRegionScroller__8113e *, .theme-dark .sidebarRegionScroller_c25c6d *,
.theme-dark .header__8e271, .theme-dark .header_e06857,
.theme-dark .lookFilled__950dd.colorPrimary_ebe632 { .theme-dark .lookFilled_dd4f85.colorPrimary_dd4f85 {
--white-500: black !important; --white-500: black !important;
--channels-default: black !important; --channels-default: black !important;
--channel-icon: black !important; --channel-icon: black !important;
@ -604,36 +602,36 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
--interactive-active: var(--white-500); --interactive-active: var(--white-500);
} }
.theme-dark .channelRow__538ef { .theme-dark .channelRow_f04d06 {
background-color: var(--background-secondary); background-color: var(--background-secondary);
} }
.theme-dark .channelRow__538ef * { .theme-dark .channelRow_f04d06 * {
--channel-icon: black; --channel-icon: black;
} }
.theme-dark #app-mount .activity_bafb94 { .theme-dark #app-mount .activity_a31c43 {
--channels-default: var(--white-500) !important; --channels-default: var(--white-500) !important;
} }
.theme-dark .nameTag__77ab2 { .theme-dark .nameTag_b2ca13 {
--header-primary: black !important; --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; --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; color: #fff;
} }
.theme-dark .embedFull__14919 { .theme-dark .embedFull_b0068a {
--text-normal: black; --text-normal: black;
} }
/*End Secondary*/`: ""}${HexToHSL("#" + tertiaryColor)[2] > 80 ? `\n\n/*Tertiary*/ /*End Secondary*/`: ""}${HexToHSL("#" + tertiaryColor)[2] > 80 ? `\n\n/*Tertiary*/
.theme-dark .winButton_f17fb6, .theme-dark .winButton_a934d8,
.theme-dark .searchBar__310d8 *, .theme-dark .searchBar_e0840f *,
.theme-dark .wordmarkWindows_ffbc5e, .theme-dark .wordmarkWindows_a934d8,
.theme-dark .searchBar__5a20a *, .theme-dark .searchBar_a46bef *,
.theme-dark .searchBarComponent__8f95f { .theme-dark .searchBarComponent_f0963d {
--white-500: black !important; --white-500: black !important;
} }
@ -641,25 +639,25 @@ export function generateCss(primaryColor: string, secondaryColor: string, tertia
color: ${HexToHSL("#" + secondaryColor)[2] > 80 ? "black" : "white"}; color: ${HexToHSL("#" + secondaryColor)[2] > 80 ? "black" : "white"};
} }
.theme-dark .popout__24e32 > * { .theme-dark .popout_c5b389 > * {
--interactive-normal: black !important; --interactive-normal: black !important;
--header-secondary: black !important; --header-secondary: black !important;
} }
.theme-dark .tooltip__7b090 { .theme-dark .tooltip_b6c360 {
--text-normal: black !important; --text-normal: black !important;
} }
.theme-dark .children_cde9af .icon_ae0b42 { .theme-dark .children_fc4f04 .icon_fc4f04 {
color: var(--interactive-active) !important; color: var(--interactive-active) !important;
} }
/*End Tertiary*/`: ""}${HexToHSL("#" + accentColor)[2] > 80 ? `\n\n/*Accent*/ /*End Tertiary*/`: ""}${HexToHSL("#" + accentColor)[2] > 80 ? `\n\n/*Accent*/
.selected_aded59 *, .selected_db6521 *,
.selected_ae80f7 *, .selected_ae80f7 *,
#app-mount .lookFilled__950dd.colorBrand__27d57:not(.buttonColor__7bad9), #app-mount .lookFilled_dd4f85.colorBrand_dd4f85:not(.buttonColor_adcaac),
.colorDefault_e361cf.focused_dcafb9, .colorDefault_d90b3d.focused_d90b3d,
.row__9e25f:hover, .row_c5b389:hover,
.colorwayInfoIcon, .colorwayInfoIcon,
.checkmarkCircle_b1b1cc > circle { .checkmarkCircle_cb7c27 > circle {
--white-500: black !important; --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-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%); --primary-360: hsl(${HexToHSL("#" + accentColor)[0]}, calc(var(--saturation-factor, 1)*12%), 90%);
} }
.emptyPage_feb902, .emptyPage_c6b11b,
.scrollerContainer_dda72c, .scrollerContainer_c6b11b,
.container__03ec9, .container_f1fd9c,
.header__71942 { .header_f1fd9c {
background-color: unset !important; background-color: unset !important;
}`; }`;
} }
@ -816,7 +814,25 @@ export function getAutoPresets(accentColor?: string) {
} as { [key: string]: { name: string, id: string, preset: () => 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) { function cyanLegacy(discordSaturation = false) {
return `:root:root { return `:root:root {
--cyan-accent-color: #${accentColor}; --cyan-accent-color: #${accentColor};
@ -979,7 +995,12 @@ export function getPreset(primaryColor?: string, secondaryColor?: string, tertia
name: "Hue Rotation", name: "Hue Rotation",
preset: getAutoPresets(accentColor).hueRotation.preset, preset: getAutoPresets(accentColor).hueRotation.preset,
id: "hueRotation", 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: { accentSwap: {
name: "Accent Swap", name: "Accent Swap",
@ -991,7 +1012,12 @@ export function getPreset(primaryColor?: string, secondaryColor?: string, tertia
name: "Material You", name: "Material You",
preset: getAutoPresets(accentColor).materialYou.preset, preset: getAutoPresets(accentColor).materialYou.preset,
id: "materialYou", 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 * 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 { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
import { disableStyle, enableStyle } from "@api/Styles"; import { disableStyle, enableStyle } from "@api/Styles";
import { Flex } from "@components/Flex";
import { Devs, EquicordDevs } from "@utils/constants"; import { Devs, EquicordDevs } from "@utils/constants";
import { ModalProps, openModal } from "@utils/modal"; import { openModal } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { import {
Button,
Clipboard,
Forms,
i18n, i18n,
SettingsRouter, SettingsRouter
Toasts
} from "@webpack/common"; } 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 { ColorwayCSS } from "./colorwaysAPI";
import ColorPickerModal from "./components/ColorPicker"; import ColorwayID from "./components/ColorwayID";
import ColorwaysButton from "./components/ColorwaysButton"; import ColorwaysButton from "./components/ColorwaysButton";
import CreatorModal from "./components/CreatorModal"; import CreatorModal from "./components/CreatorModal";
import PCSMigrationModal from "./components/PCSMigrationModal";
import Selector from "./components/Selector"; import Selector from "./components/Selector";
import OnDemandWaysPage from "./components/SettingsTabs/OnDemandPage"; import OnDemandWaysPage from "./components/SettingsTabs/OnDemandPage";
import SettingsPage from "./components/SettingsTabs/SettingsPage"; import SettingsPage from "./components/SettingsTabs/SettingsPage";
@ -32,97 +35,45 @@ import SourceManager from "./components/SettingsTabs/SourceManager";
import Store from "./components/SettingsTabs/Store"; import Store from "./components/SettingsTabs/Store";
import Spinner from "./components/Spinner"; import Spinner from "./components/Spinner";
import { defaultColorwaySource } from "./constants"; import { defaultColorwaySource } from "./constants";
import { generateCss, getAutoPresets } from "./css"; import defaultsLoader from "./defaultsLoader";
import style from "./style.css?managed"; import style from "./style.css?managed";
import discordTheme from "./theme.discord.css?managed";
import { ColorPickerProps, ColorwayObject } from "./types"; 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> = () => { export let ColorPicker: React.FunctionComponent<ColorPickerProps> = () => {
return <Spinner className="colorways-creator-module-warning" />; return <Spinner className="colorways-creator-module-warning" />;
}; };
(async function () { defaultsLoader();
const [
customColorways,
colorwaySourceFiles,
showColorwaysButton,
onDemandWays,
onDemandWaysTintedText,
useThinMenuButton,
onDemandWaysDiscordSaturation,
onDemandWaysOsAccentColor,
activeColorwayObject,
selectorViewMode,
showLabelsInSelectorGridView
] = await DataStore.getMany([
"customColorways",
"colorwaySourceFiles",
"showColorwaysButton",
"onDemandWays",
"onDemandWaysTintedText",
"useThinMenuButton",
"onDemandWaysDiscordSaturation",
"onDemandWaysOsAccentColor",
"activeColorwayObject",
"selectorViewMode",
"showLabelsInSelectorGridView"
]);
const defaults = [ export const PluginProps = {
{ name: "showColorwaysButton", value: showColorwaysButton, default: false }, pluginVersion: "6.1.0",
{ name: "onDemandWays", value: onDemandWays, default: false }, clientMod: "Vencord User Plugin",
{ name: "onDemandWaysTintedText", value: onDemandWaysTintedText, default: true }, UIVersion: "2.0.0",
{ name: "useThinMenuButton", value: useThinMenuButton, default: false }, creatorVersion: "1.20"
{ name: "onDemandWaysDiscordSaturation", value: onDemandWaysDiscordSaturation, default: false },
{ name: "onDemandWaysOsAccentColor", value: onDemandWaysOsAccentColor, default: false },
{ name: "activeColorwayObject", value: activeColorwayObject, default: { id: null, css: null, sourceType: null, source: null } },
{ name: "selectorViewMode", value: selectorViewMode, default: "grid" },
{ name: "showLabelsInSelectorGridView", value: showLabelsInSelectorGridView, default: false }
];
defaults.forEach(({ name, value, default: def }) => {
if (!value) DataStore.set(name, def);
});
if (customColorways) {
if (!customColorways[0]?.colorways) {
DataStore.set("customColorways", [{ name: "Custom", colorways: customColorways }]);
}
} else {
DataStore.set("customColorways", []);
}
if (colorwaySourceFiles) {
if (typeof colorwaySourceFiles[0] === "string") {
DataStore.set("colorwaySourceFiles", colorwaySourceFiles.map((sourceURL: string, i: number) => {
return { name: sourceURL === defaultColorwaySource ? "Project Colorway" : `Source #${i}`, url: sourceURL };
}));
}
} else {
DataStore.set("colorwaySourceFiles", [{
name: "Project Colorway",
url: defaultColorwaySource
}]);
}
})();
export const ColorwayCSS = {
get: () => document.getElementById("activeColorwayCSS")!.textContent || "",
set: (e: string) => {
if (!document.getElementById("activeColorwayCSS")) {
document.head.append(Object.assign(document.createElement("style"), {
id: "activeColorwayCSS",
textContent: e
}));
} else document.getElementById("activeColorwayCSS")!.textContent = e;
},
remove: () => document.getElementById("activeColorwayCSS")!.remove(),
};
export const versionData = {
pluginVersion: "5.7.1",
creatorVersion: "1.20",
}; };
export default definePlugin({ 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", description: "A plugin that offers easy access to simple color schemes/themes for Discord, also known as Colorways",
authors: [EquicordDevs.DaBluLite, Devs.ImLvna], authors: [EquicordDevs.DaBluLite, Devs.ImLvna],
dependencies: ["ServerListAPI", "MessageAccessoriesAPI"], dependencies: ["ServerListAPI", "MessageAccessoriesAPI"],
pluginVersion: versionData.pluginVersion, pluginVersion: PluginProps.pluginVersion,
creatorVersion: versionData.creatorVersion,
toolboxActions: { toolboxActions: {
"Change Colorway": () => openModal(props => <Selector modalProps={props} />),
"Open Colorway Creator": () => openModal(props => <CreatorModal modalProps={props} />), "Open Colorway Creator": () => openModal(props => <CreatorModal modalProps={props} />),
"Open Color Stealer": () => openModal(props => <ColorPickerModal modalProps={props} />),
"Open Settings": () => SettingsRouter.open("ColorwaysSettings"), "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: [ patches: [
// Credits to Kyuuhachi for the BetterSettings plugin patches // Credits to Kyuuhachi for the BetterSettings plugin patches
{ {
find: "this.renderArtisanalHack()", find: "this.renderArtisanalHack()",
replacement: { replacement: {
match: /createPromise:\(\)=>([^:}]*?),webpackId:"?\d+"?,name:(?!="CollectiblesShop")"[^"]+"/g, match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
replace: "$&,_:$1", replace: "$&,_:$1",
predicate: () => true predicate: () => true
} }
@ -170,8 +100,8 @@ export default definePlugin({
{ {
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format", find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: { replacement: {
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/, match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
replace: "$&(async ()=>$2)()," replace: "(async ()=>$2)(),"
}, },
predicate: () => true predicate: () => true
}, },
@ -208,6 +138,57 @@ export default definePlugin({
patchedSettings: new WeakSet(), 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 />, ColorwaysButton: () => <ColorwaysButton />,
async start() { async start() {
@ -220,31 +201,31 @@ export default definePlugin({
const ColorwaysSelector = () => ({ const ColorwaysSelector = () => ({
section: "ColorwaysSelector", section: "ColorwaysSelector",
label: "Colorways Selector", label: "Colorways Selector",
element: () => <Selector isSettings modalProps={{ onClose: () => new Promise(() => true), transitionState: 1 }} />, element: () => <Selector hasTheme />,
className: "dc-colorway-selector" className: "dc-colorway-selector"
}); });
const ColorwaysSettings = () => ({ const ColorwaysSettings = () => ({
section: "ColorwaysSettings", section: "ColorwaysSettings",
label: "Colorways Settings", label: "Colorways Settings",
element: SettingsPage, element: () => <SettingsPage hasTheme />,
className: "dc-colorway-settings" className: "dc-colorway-settings"
}); });
const ColorwaysSourceManager = () => ({ const ColorwaysSourceManager = () => ({
section: "ColorwaysSourceManager", section: "ColorwaysSourceManager",
label: "Colorways Sources", label: "Colorways Sources",
element: SourceManager, element: () => <SourceManager hasTheme />,
className: "dc-colorway-sources-manager" className: "dc-colorway-sources-manager"
}); });
const ColorwaysOnDemand = () => ({ const ColorwaysOnDemand = () => ({
section: "ColorwaysOnDemand", section: "ColorwaysOnDemand",
label: "Colorways On-Demand", label: "Colorways On-Demand",
element: OnDemandWaysPage, element: () => <OnDemandWaysPage hasTheme />,
className: "dc-colorway-ondemand" className: "dc-colorway-ondemand"
}); });
const ColorwaysStore = () => ({ const ColorwaysStore = () => ({
section: "ColorwaysStore", section: "ColorwaysStore",
label: "Colorways Store", label: "Colorways Store",
element: Store, element: () => <Store hasTheme />,
className: "dc-colorway-store" className: "dc-colorway-store"
}); });
@ -252,109 +233,25 @@ export default definePlugin({
addServerListElement(ServerListRenderPosition.Above, this.ColorwaysButton); addServerListElement(ServerListRenderPosition.Above, this.ColorwaysButton);
connect();
enableStyle(style); enableStyle(style);
enableStyle(discordTheme);
ColorwayCSS.set((await DataStore.get("activeColorwayObject") as ColorwayObject).css || ""); ColorwayCSS.set((await DataStore.get("activeColorwayObject") as ColorwayObject).css || "");
addAccessory("colorways-btn", props => { 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"))) {
if (String(props.message.content).match(/colorway:[0-9a-f]{0,100}/)) { DataStore.set("colorwaySourceFiles", [{ name: "Project Colorway", url: defaultColorwaySource }, ...(await DataStore.get("colorwaySourceFiles") as { name: string, url: string; }[]).filter(i => i.name !== "Project Colorway")]);
return <Flex flexDirection="column"> openModal(props => <PCSMigrationModal modalProps={props} />);
{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"> addAccessory("colorway-id-card", props => <ColorwayID props={props} />);
<div className="discordColorwayPreviewColorContainer" style={{ width: "56px", height: "56px", marginRight: "16px" }}>
{(() => {
if (colorID) {
if (!colorID.includes(",")) {
throw new Error("Invalid Colorway ID");
} else {
return colorID.split("|").filter(string => string.includes(",#"))[0].split(/,#/).map((color: string) => <div className="discordColorwayPreviewColor" style={{ backgroundColor: `#${colorToHex(color)}` }} />);
}
} else return null;
})()}
</div>
<div className="colorwayMessage-contents">
<Forms.FormTitle>Colorway{/n:([A-Za-z0-9]+( [A-Za-z0-9]+)+)/i.exec(colorID) ? `: ${/n:([A-Za-z0-9]+( [A-Za-z0-9]+)+)/i.exec(colorID)![1]}` : ""}</Forms.FormTitle>
<Flex>
<Button
onClick={() => openModal(modalProps => <CreatorModal
modalProps={modalProps}
colorwayID={colorID}
/>)}
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.FILLED}
>
Add this Colorway...
</Button>
<Button
onClick={() => {
Clipboard.copy(colorID);
Toasts.show({
message: "Copied Colorway ID Successfully",
type: 1,
id: "copy-colorway-id-notify",
});
}}
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.FILLED}
>
Copy Colorway ID
</Button>
<Button
onClick={() => {
if (!colorID.includes(",")) {
throw new Error("Invalid Colorway ID");
} else {
colorID.split("|").forEach((prop: string) => {
if (prop.includes(",#")) {
DataStore.set("activeColorwayObject", {
id: "Temporary Colorway", css: generateCss(
colorToHex(prop.split(/,#/)[1]),
colorToHex(prop.split(/,#/)[2]),
colorToHex(prop.split(/,#/)[3]),
colorToHex(prop.split(/,#/)[0]),
true,
true,
32,
"Temporary Colorway"
), sourceType: "temporary", source: null
});
ColorwayCSS.set(generateCss(
colorToHex(prop.split(/,#/)[1]),
colorToHex(prop.split(/,#/)[2]),
colorToHex(prop.split(/,#/)[3]),
colorToHex(prop.split(/,#/)[0]),
true,
true,
32,
"Temporary Colorway"
));
}
});
}
}}
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
look={Button.Looks.FILLED}
>
Apply temporarily
</Button>
</Flex>
</div>
</div>;
})}
</Flex>;
} else {
return null;
}
});
}, },
stop() { stop() {
removeServerListElement(ServerListRenderPosition.In, this.ColorwaysButton); removeServerListElement(ServerListRenderPosition.Above, this.ColorwaysButton);
disableStyle(style); disableStyle(style);
disableStyle(discordTheme);
ColorwayCSS.remove(); ColorwayCSS.remove();
removeAccessory("colorways-btn"); removeAccessory("colorway-id-card");
const customSettingsSections = ( const customSettingsSections = (
Vencord.Plugins.plugins.Settings as any as { Vencord.Plugins.plugins.Settings as any as {
customSections: ((ID: Record<string, unknown>) => any)[]; 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, source?: string,
linearGradient?: string, linearGradient?: string,
preset?: string, preset?: string,
creatorVersion: string; creatorVersion: string,
colorObj?: { accent?: string, primary?: string, secondary?: string, tertiary?: string; };
} }
export interface ColorPickerProps { export interface ColorPickerProps {
@ -34,9 +35,15 @@ export interface ColorPickerProps {
export interface ColorwayObject { export interface ColorwayObject {
id: string | null, id: string | null,
css: string | null, css?: string | null,
sourceType: "online" | "offline" | "temporary" | 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 { export interface SourceObject {
@ -63,3 +70,8 @@ export interface StoreItem {
url: string, url: string,
authorGh: 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("#", ""); 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) { RUNNING_GAMES_CHANGE(event) {
const status = PresenceStore.getStatus(UserStore.getCurrentUser().id); const status = PresenceStore.getStatus(UserStore.getCurrentUser().id);
if (event.games.length > 0) { if (event.games.length > 0) {
if (savedStatus !== "" && savedStatus !== settings.store.statusToSet)
updateAsync(savedStatus);
} else {
if (status !== settings.store.statusToSet) { if (status !== settings.store.statusToSet) {
savedStatus = status; savedStatus = status;
updateAsync(settings.store.statusToSet); updateAsync(settings.store.statusToSet);
} }
} else {
if (savedStatus !== "" && savedStatus !== settings.store.statusToSet)
updateAsync(savedStatus);
} }
}, },
} }