Add 4 Plugins

Added:
FriendCodes by HypedDomi
IconViewer by iamme
SpotifyLyrics by Joona
PanelSettings by nin0dev
This commit is contained in:
thororen1234 2025-03-10 14:07:29 -04:00
parent c5230c94b0
commit 220b44b4ed
No known key found for this signature in database
30 changed files with 3130 additions and 1 deletions

View file

@ -0,0 +1,112 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CodeBlock } from "@components/CodeBlock";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import {
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { Button, FluxDispatcher, TooltipContainer, useCallback, useEffect, useState } from "@webpack/common";
import * as t from "@webpack/types";
import { IconsFinds } from "./names";
import { openRawModal } from "./rawModal";
import { openSaveModal } from "./saveModal";
import { ModalHeaderTitle } from "./subComponents";
import { _cssColors, cssColors, iconSizes } from "./utils";
const defaultColor = 209;
function ModalComponent(props: { iconName: string; Icon: t.Icon; } & ModalProps) {
const [color, SetColor] = useState(defaultColor);
const onKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
e.preventDefault();
if (e.key === "ArrowLeft") {
SetColor(color + -1);
} else if (e.key === "ArrowRight") {
SetColor(color + 1);
}
}
}, [color]);
const onColorChange = useCallback((e: { type: string; color: string; }) => {
SetColor(_cssColors.indexOf(e.color));
}, [color]);
useEffect(() => {
document.addEventListener("keydown", onKeyDown);
// @ts-ignore
FluxDispatcher.subscribe("ICONVIEWER_COLOR_CHANGE", onColorChange);
return () => {
document.removeEventListener("keydown", onKeyDown);
// @ts-ignore
FluxDispatcher.unsubscribe("ICONVIEWER_COLOR_CHANGE", onColorChange);
};
}, [onKeyDown]);
if (color < 0 || color >= cssColors.length) {
SetColor(0);
}
const { iconName, Icon } = props;
return (<ModalRoot {...props} size={ModalSize.DYNAMIC} className="vc-ic-modals-root vc-ic-icon-modal-root">
<ModalHeader>
<ModalHeaderTitle iconName={iconName} color={color} name="icon" />
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
{IconsFinds[iconName] ?
<div className="vc-icon-modal-codeblock">
<CodeBlock lang="ts" content={`const ${iconName + "Icon"} = findComponentByCode(${JSON.stringify(IconsFinds[iconName])})`} />
</div>
: null
}
<div className="vc-icon-modal-main-container">
<div className="vc-icon-display-box" aria-label={cssColors[color]?.name}>
<Icon className="vc-icon-modal-icon" color={cssColors[color]?.css} />
</div>
<div className="vc-icon-other-icon-sizes">
{iconSizes.map((size, idx) =>
<TooltipContainer text={`${size} size`} key={`vc-iv-size-${size}-${idx}`}>
<Icon className="vc-icon-modal-size-ex-icon" size={size} color={cssColors[color]?.css} style={{
marginLeft: "25px"
}} />
</TooltipContainer>
)}
</div>
</div>
</ModalContent>
<ModalFooter className="vc-ic-modals-footer">
<Button
color={Button.Colors.BRAND}
onClick={() => openSaveModal(iconName, Icon, color)}
>
Save as
</Button>
<Button
color={Button.Colors.YELLOW}
className={classes(Margins.right8, "vc-iv-raw-modal-button")}
onClick={() => openRawModal(iconName, Icon, color)}
>
Raw
</Button>
</ModalFooter>
</ModalRoot>);
}
export function openIconModal(iconName: string, Icon: t.Icon) {
openModal(props => <ModalComponent iconName={iconName} Icon={Icon} {...props} />);
}

View file

@ -0,0 +1,117 @@
.vc-icon-modal-codeblock {
margin-left: 10%;
margin-top: 30px;
}
.vc-icon-icon {
margin-left: 5%;
}
.vc-icon-modal-main-container {
display: flex;
}
.vc-ic-unordered-list li {
margin-left: 5%;
list-style: disc;
}
.vc-icon-other-icon-sizes {
height: 32px;
display: flex;
margin-top: 15%;
margin-left: 5%;
}
.vc-ic-icon-modal-root {
height: 450px;
width: 700px;
}
.vc-icons-tab-grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
gap: 8px;
}
.vc-icon-modal-size-ex-icon {
margin-right: 5%;
}
.vc-icon-modal-icon {
height: 164px;
width: 164px;
}
.vc-icon-tab-search-bar-grid {
display: grid;
height: 50px;
gap: 10px;
grid-template-columns: 1fr 10px;
}
.vc-icon-display-box {
height: 164px;
width: 164px;
margin-top: 5%;
margin-left: 15%;
background-image: repeating-linear-gradient(
45deg,
#ffffff1a 0,
#ffffff1a 10px,
#0000001a 10px,
#0000001a 20px
);
border-radius: 10px;
}
.vc-icon-container {
margin-top: 5px;
padding: 15px;
border-radius: 5px;
border: 3px solid transparent;
box-sizing: border-box;
}
.vc-icon-container:hover {
border-radius: 5px;
border: 3px solid var(--background-tertiary);
box-sizing: border-box;
}
.vc-icon-title {
font-size: 0.8em;
margin-top: 0;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.vc-icon-modal-color-tooltip:hover {
background-color: var(--info-help-background);
}
.vc-save-modal {
margin-top: 10%;
display: grid;
grid-template-columns: auto 1fr;
gap: 10px;
}
.vc-save-select-option-1 {
margin-bottom: 5%;
}
.vc-save-select-option-2 {
margin-top: 5%;
}
.vc-ic-modals-root {
border-radius: 25px;
}
.vc-ic-modals-footer {
border-bottom-left-radius: 25px;
border-bottom-right-radius: 25px;
}

View file

@ -0,0 +1,88 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./IconsTab.css";
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { Button, Clickable, Forms, React, TextInput, TooltipContainer } from "@webpack/common";
import * as t from "@webpack/types";
import { openIconModal } from "./IconModal";
import { getNameByIcon } from "./names";
import { findAllByCode, IconsDef } from "./utils";
export let Icons: IconsDef | null = null;
function searchMatch(search: string, name: string, Icon: t.Icon, searchbyFunction: boolean): boolean {
if (search === "") return true;
if (searchbyFunction) {
return String(Icon).includes(search);
}
const words = name.replace(/([A-Z]([a-z]+)?)/g, " $1").toLowerCase().split(" ");
const searchKeywords = search.toLowerCase().split(" ");
return searchKeywords.every(keyword => words.includes(keyword)) || words.every(keyword => searchKeywords.includes(keyword)) || name.toLowerCase().includes(search.toLowerCase());
}
function RenderIcons({ search, searchbyFunction }: { search: string; searchbyFunction: boolean; }) {
if (Icons === null) {
const OrgIcons = Array.from(new Set(findAllByCode("[\"size\",\"width\",\"height\",\"color\",\"colorClass\"]")));
Icons = Object.fromEntries(Object.keys(OrgIcons).map(k => [String(getNameByIcon(OrgIcons[k], k)), OrgIcons[k]])) as IconsDef;
}
return <div className="vc-icons-tab-grid-container">
{Object.entries(Icons).map(([iconName, Icon], index) =>
searchMatch(search, iconName, Icon, searchbyFunction) && <React.Fragment key={`iv-${iconName}`}>
<div className="vc-icon-box">
<Clickable onClick={() => openIconModal(iconName, Icon)}>
<div className="vc-icon-container">
<Icon className="vc-icon-icon" size="xxl" />
</div>
</Clickable>
<Forms.FormTitle className="vc-icon-title" tag="h3">{iconName}</Forms.FormTitle>
</div>
</React.Fragment>
)}</div>;
}
function IconsTab() {
const [search, setSearch] = React.useState<string>("");
const [searchByFunction, setSearchByFunction] = React.useState<boolean>(false);
const MemoRenderIcons = React.memo(RenderIcons);
return (
<SettingsTab title="Icons">
<div className={classes(Margins.top16, "vc-icon-tab-search-bar-grid")}>
<TextInput autoFocus value={search} placeholder="Search for an icon..." onChange={setSearch} />
<TooltipContainer text="Search by function context">
<Button
size={Button.Sizes.SMALL}
aria-label="Search by function context"
style={{ marginTop: "50%" }}
color={searchByFunction ? Button.Colors.GREEN : Button.Colors.PRIMARY}
onClick={() => setSearchByFunction(!searchByFunction)}
>Func</Button>
</TooltipContainer>
</div>
<MemoRenderIcons search={search} searchbyFunction={searchByFunction} />
</SettingsTab>
);
}
export default wrapTab(IconsTab, "IconsTab");

View file

@ -0,0 +1,54 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
import { SettingsRouter } from "@webpack/common";
import IconsTab from "./IconsTab";
import { SettingsAbout } from "./subComponents";
export default definePlugin({
name: "IconViewer",
description: "Adds a new tab to settings, to preview all icons",
authors: [EquicordDevs.iamme],
dependencies: ["Settings"],
startAt: StartAt.WebpackReady,
toolboxActions: {
"Open Icons Tab"() {
SettingsRouter.open("VencordDiscordIcons");
},
},
settingsAboutComponent: SettingsAbout,
start() {
const customSettingsSections = (
Vencord.Plugins.plugins.Settings as any as {
customSections: ((ID: Record<string, unknown>) => any)[];
}
).customSections;
const IconViewerSection = () => ({
section: "VencordDiscordIcons",
label: "Icons",
element: IconsTab,
className: "vc-discord-icons",
id: "IconViewer"
});
customSettingsSections.push(IconViewerSection);
},
stop() {
const customSettingsSections = (
Vencord.Plugins.plugins.Settings as any as {
customSections: ((ID: Record<string, unknown>) => any)[];
}
).customSections;
const i = customSettingsSections.findIndex(section => section({}).id === "IconViewer");
if (i !== -1) customSettingsSections.splice(i, 1);
},
});

View file

@ -0,0 +1,150 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as t from "@webpack/types";
// name: pattern
export const IconsFinds = {
Discord: "1.6 5.64-2.87",
XboxNeutral: "8.68-.62c.89-.81 1.5",
PlaystationNeutral: "2.04Zm-9.35",
TwitterNeutral: "M13.86 10.47", // even discord calls it twitter lmao
InstagramNeutral: "4.12.07Zm.1 2c-",
YoutubeNeutral: "11.5s0 3.95.5 5.85a3.",
FacebookNeutral: "2.8V12h2.8V9",
NintendoSwitchNeutral: "14V2.32c0",
Pencil: "0-2.82 0l-1.38 1.38a1",
AngleBrackets: "0-.4.8v1.98c0",
NitroWheel: "2h3a1 1 0 1 1 0 2H5.5",
Bill: "75.34.75.75C6 7.99 5 9",
Chat: "2.2 22H12Z\",",
ChatVoice: "22H12Zm2-5.26c0",
ChatX: ".23.46-.48.47L12 22H2.",
ChatSmile: "04-.61A10 10 0 1 1 22 ",
ChatRetry: "14.07.3.09.44.04a7",
ChatPlus: "1.24-.37V12a10 10 0 1 ",
Bug: "1.1.27.1.37 0a6.66 6.6",
B: "9V2.9c0-.5.4-.9.9-.9h7",
Eye: "19.1 21 12 21c-7.11 0-",
EyeSlash: "2.63-2.63c",
EyePlus: "3.32ZM19 14a1 ",
Heart: "0C9.43 20.48 1 15.09 1",
Star: ".73-2.25h6.12l1.9-5.83Z",
StarOutline: "3.1h5.26l1.62",
StarShooting: "1.35l2.95 2.14",
QrCode: "0v3ZM20",
Friends: "12h1a8",
PlusSmall: "0v-5h5a1",
CircleQuestion: "10.58l-3.3-3.3a1",
Pin: "1-.06-.63L6.16",
PinUpright: "5H8v4.35l-3.39",
PinUprightSlash: "1.56ZM11.08 ",
ArrowsLeftRight: "18.58V3a1",
XSmall: "13.42l5.3",
XLarge: "13.42l7.3 7.3Z",
XSmallBold: "12l4.94-4.94a1.5",
XLargeBold: "12l6.94-6.94a1.5",
Lock: "3Zm9-3v3H9V6a3",
LockUnlocked: "1-1.33-1.5ZM14",
Video: "1.45-.9V7.62a1",
VideoSlash: "1.4l20-20ZM9.2",
VideoLock: "1.32-.5V7.62a1",
Fire: "14Zm9.26-.84a.57.57",
Warning: "3.15H3.29c-1.74",
Download: "1.42l3.3 3.3V3a1",
Upload: "0ZM3 20a1 1",
// QuestionMark: "0ZM5.5 7a1.5" Unknown name
Quest: "10.47a.76.76",
Play: "4.96v14.08c0",
Emoji: " 0 0 0 0 22ZM6.5",
Gif: "3H5Zm2.18 13.8",
Trash: "2.81h8.36a3",
Bell: "9.5v2.09c0",
Screen: "0-3-3H5ZM13.5",
ScreenArrow: "3V5Zm16",
ScreenStream: " 2-2h3a2",
ScreenSystemRequirements: "3V5Zm3", // a guess
ScreenSlash: "5.8ZM17.15",
ScreenX: "1-3-3V5Zm6.3.3a1",
Plus: "0v8H3a1 1 0 1 0 0 2h8v8a1",
Id: "15h2.04V7.34H6V17Zm4",
Tv: "0-3-3H4ZM6 20a1",
Crown: "1.18l.82.82-3.61",
React: "04-4ZM16.96 4.08c",
Camera: "1.34 1.71 1.34H20a3",
Sticker: "1-.58.82l-4.24",
StageX: "13.07-1.38ZM16.7",
StageLock: "7.14-3.85ZM18.98",
Stage: "20.03c-.25.72.12",
ConnectionFine: "1 0 1 1-2 0A17 17 ",
ConnectionAverage: "\"M3 7a1 1 0 0",
ConnectionBad: "\"M2 13a1 1 0 0",
ConnectionUnknown: "15.86-.6.9-.2.02",
ChatWarning: ".54.5H2.2a1",
ChatCheck: "22H12c.22",
Hammer: "1.42ZM7.76",
StickerSmall: "1-.5.5H7a4",
StickerSad: "1.66-1.12 5.5",
StickerDeny: "\"M21.76 14.83a", // a guess
MagnifyingGlassPlus: "M11 7a1 1 0",
MagnifyingGlassMinus: "3v12H5.5a1.5 1.5",
// MagnifyingGlass: "???", // not quite possible
ChatArrowRight: "2.43l.06",
Bookmark: "1-1.67.74l",
ChannelList: "1-1-1ZM2 8a1",
ChannelListMagnifyingGlass: "2h18a1 1 0 1 0 0-2H3ZM2",
Activities: "1h3a3 3 0 0 0 3-3Z\"",
ActivitiesPlus: "14.35v1.29a",
AnnouncementsLock: "1-2.46-1.28 3.86",
AnnouncementsWarning: "1-2.46-1.28 3.85",
Announcements: ".42.27.79.62",
ShieldLock: "2.83v2.67a.5.5",
ShieldUser: "9.77V6.75c0-.57.17",
ShieldAt: "14.42-.35.75",
Shield: "M4.27 5.22A2.66", // a guess
Slash: "1-.43-.76L15.78",
SlashBox: "0-3-3H5Zm12.79",
Apps: "2.95H20a2 2 0",
CheckmarkLarge: "1.4l-12 12a1",
CheckmarkLargeBold: "2.12-2.12L9",
CheckmarkSmallBold: "13.88l6.94-6.94a1.5",
CheckmarkSmall: "1-1.4 0l-4-4a1",
DoubleCheckmark: "1.4l4.5 4.5a1",
NewUser: "0-.92h-.03a2", // a guess
UserCheck: "0l1.8-1.8c.17",
User: "2.9.06.24.26.",
UserMinus: "3-3h5.02c.38",
UserPlus: "2.07ZM12",
UserPlay: "0-3.61-.71h-.94Z",
UserBox: "0-3-3H5Zm10 6a3", // a guess
Settings: "0ZM16 12a4",
SettingsInfo: "10Zm1-4a1",
Hashtag: "8 4.84a1", // a guess
HashtagLocked: "2.02.31.03", // a guess
HashtagWarning: "8h1.26Z", // a guess
HashtagPlay: "52.88H9.85l", // a guess
Flag: "5.85v7.3a2",
Language: "5.43h3.85l",
Lightbulb: "8.5ZM15.1 19c.5",
Key: "23-.24ZM10 16a2",
InBox: "3H5ZM4 5.5C4",
BookmarkOutline: "0-1-1ZM7 2a3",
Food: "7.58V8a1 1"
};
// 13l4.91-8.05a1.8
export const namePatterns = new Map(Object.entries(IconsFinds).map(([name, pattern]) => [name, pattern]));
export function getNameByIcon(Icon: t.Icon, defaultName: any) {
for (const [name, pattern] of namePatterns) {
if (String(Icon).includes(pattern)) {
namePatterns.delete(name); // remove pattern from map after being found prevent overshadowing
return name;
}
}
return defaultName;
}

View file

@ -0,0 +1,70 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CodeBlock } from "@components/CodeBlock";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import {
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { Button, Toasts } from "@webpack/common";
import * as t from "@webpack/types";
import { ModalHeaderTitle } from "./subComponents";
function ModalComponent(props: { func: Function; iconName: string; color: number; } & ModalProps) {
const { func, iconName, color } = props;
return (<ModalRoot {...props} size={ModalSize.LARGE} className="vc-ic-modals-root vc-ic-raw-modal-root">
<ModalHeader>
<ModalHeaderTitle iconName={iconName} color={color} name="raw" />
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<div className="vc-iv-raw-modal">
<CodeBlock content={String(func)} lang="js" />
</div>
</ModalContent>
<ModalFooter className="vc-ic-modals-footer">
<Button
color={Button.Colors.PRIMARY}
className={"vc-iv-raw-modal-copy-button"}
onClick={() => {
// silly typescript
// @ts-ignore
Clipboard.copy(String(func));
Toasts.show({
id: Toasts.genId(),
message: `Copied raw \`${iconName}\` to clipboard`,
type: Toasts.Type.SUCCESS
});
}}
>
Copy
</Button>
<Button
color={Button.Colors.YELLOW}
className={classes(Margins.right8, "vc-iv-log-to-console-button")}
onClick={() => { console.log(func); }}
>
log to console
</Button>
</ModalFooter>
</ModalRoot>);
}
export function openRawModal(iconName: string, Icon: t.Icon, colorIndex: number) {
openModal(props => <ModalComponent iconName={iconName} func={Icon} color={colorIndex} {...props} />);
}

View file

@ -0,0 +1,153 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import {
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalProps,
ModalRoot,
ModalSize,
openModal
} from "@utils/modal";
import { Button, Forms, Select, TextInput, useCallback, useEffect, useState } from "@webpack/common";
import * as t from "@webpack/types";
import { ModalHeaderTitle } from "./subComponents";
import { convertComponentToHtml, cssColors, iconSizesInPx, saveIcon } from "./utils";
type IDivElement = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export function NumericComponent({ onChange, value, className, style }: { onChange: (value: number) => void, value: number; className?: string; style?: React.CSSProperties; }) {
const handleChange = (value: string) => {
const newValue = Number(value);
if (isNaN(newValue)) return;
onChange(newValue);
};
return (
<div className={className} style={style}>
<TextInput
type="number"
pattern="-?[0-9]+"
value={value}
placeholder="Enter a number"
onChange={handleChange}
/>
</div>
);
}
export function SelectComponent({ option, onChange, onError, className }: IDivElement & { option: any, onChange: (value: any) => void, onError: (msg: string | null) => void; className?: string; }) {
const [state, setState] = useState(option.options?.find(o => o.default)?.value ?? null);
const [error, setError] = useState<string | null>(null);
useEffect(() => onError(error), [error]);
const handleChange = (newValue: any) => {
const isValid = option.isValid?.call({}, newValue) ?? true;
if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}
};
return (<div className={className}>
<Select
options={option.options}
placeholder={"Select an option"}
maxVisibleItems={5}
closeOnSelect={true}
select={handleChange}
isSelected={v => v === state}
serialize={v => String(v)}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</div>);
}
function ModalComponent(props: { iconName: string, Icon: t.Icon; color: number; } & ModalProps) {
const [color, SetColor] = useState((props.color ?? 187));
const [iconSize, SetIconSize] = useState("lg");
const [saveType, SetSaveType] = useState("png");
const [customSize, SetCustomSize] = useState(32);
const onKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
e.preventDefault();
if (e.key === "ArrowLeft") {
SetColor(color + -1);
} else if (e.key === "ArrowRight") {
SetColor(color + 1);
}
}
}, [color]);
useEffect(() => {
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [onKeyDown]);
const { iconName, Icon } = props;
return (<ModalRoot {...props} size={ModalSize.MEDIUM} className="vc-ic-modals-root vc-ic-save-modal-root">
<ModalHeader>
<ModalHeaderTitle iconName={iconName} color={color} name="save" />
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<div className="vc-save-modal">
<div className="vc-icon-display-box vc-save-modal-icon-display-box" aria-label={cssColors[color]?.name} style={{ marginLeft: "0", marginTop: "0" }}>
<Icon className="vc-icon-modal-icon" color={cssColors[color].css} />
</div>
<div className="vc-save-options" style={{ marginTop: "0", marginLeft: "0" }}>
<SelectComponent className="vc-save-select-option-1"
option={{
options: [
{ "label": "large", "value": "lg", "default": true },
{ "label": "medium", "value": "md" },
{ "label": "small", "value": "sm" },
{ "label": "extra small", "value": "xs" },
{ "label": "extra extra small", "value": "xxs" },
{ "label": "custom", "value": "custom" }
]
}} onChange={newValue => SetIconSize(newValue)} onError={() => { }} />
<NumericComponent style={{ visibility: iconSize === "custom" ? "visible" : "hidden" }} value={customSize} onChange={(value: number) => SetCustomSize(value)} />
<SelectComponent className="vc-save-select-option-2"
option={{
options: [
{ "label": "png", "value": "image/png", "default": true },
{ "label": "jpeg", "value": "image/jpeg" },
{ "label": "gif", "value": "image/gif" },
{ "label": "avif", "value": "image/avif" },
{ "label": "webp", "value": "image/webp" },
{ "label": "svg", "value": "image/svg+xml" },
]
}} onChange={newValue => SetSaveType(newValue)} onError={() => { }} />
</div>
</div>
</ModalContent>
<ModalFooter className="vc-ic-modals-footer">
<Button
color={Button.Colors.BRAND}
onClick={() => saveIcon(iconName,
saveType === "image/svg+xml" || document.querySelector(".vc-icon-modal-icon") == null ?
convertComponentToHtml(<Icon className="vc-icon-modal-icon" color={cssColors[color].css} />) :
document.querySelector(".vc-icon-modal-icon") as Element,
color, iconSizesInPx[iconSize] ?? customSize, saveType)}
>
Save
</Button>
</ModalFooter>
</ModalRoot>);
}
export function openSaveModal(iconName: string, Icon: t.Icon, colorIndex: number) {
openModal(props => <ModalComponent iconName={iconName} Icon={Icon} color={colorIndex} {...props} />);
}

View file

@ -0,0 +1,118 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { Clickable, ContextMenuApi, FluxDispatcher, Forms, Menu, Text, TooltipContainer, useState } from "@webpack/common";
import type { ComponentPropsWithRef, PropsWithChildren } from "react";
import { _cssColors, cssColors } from "./utils";
function searchMatch(search: string, name: string): boolean {
if (search === "") return true;
const words = name.toLowerCase().split("_");
const searchKeywords = search.toLowerCase().split(" ").filter(keyword => keyword !== "");
return searchKeywords.every(keyword => words.includes(keyword)) || words.every(keyword => searchKeywords.includes(keyword)) || name.toLowerCase().includes(search.toLowerCase());
}
export type ClickableProps<T extends "a" | "div" | "span" | "li" = "div"> = PropsWithChildren<ComponentPropsWithRef<T>> & {
tag?: T;
};
export function IconTooltip({ children, copy, className, ...props }: ClickableProps & { children: string; copy: string; }) {
return <TooltipContainer text={"Click to copy"} className={className}>
<Clickable onClick={() => {
// @ts-ignore
Clipboard.copy(copy);
}} {...props}>{children}</Clickable>
</TooltipContainer>;
}
export const ModalHeaderTitle = ({ iconName, color, name }: { iconName: string; color: number; name: string; }) => {
return <Text variant="heading-lg/semibold"
style={{ flexGrow: 1, display: "flex" }}
className={classes("vc-ic-modal-header-title", `vc-ic-${name}-modal-header-title`)}>
<IconTooltip copy={iconName} className={classes(Margins.right8, "vc-icon-modal-color-tooltip")}>
{iconName}
</IconTooltip>
{" - "}
<IconTooltip copy={cssColors[color]?.css} className={classes(Margins.left8, "vc-icon-modal-color-tooltip")}
onContextMenu={e => {
ContextMenuApi.openContextMenu(e, () => {
const [query, setQuery] = useState("");
return (<Menu.Menu
navId="vc-ic-colors-header-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
color="danger"
aria-label="Icon Viewer Colors"
>
<Menu.MenuControlItem
id="vc-ic-colors-search"
control={(props, ref) => (
<Menu.MenuSearchControl
{...props}
query={query}
onChange={setQuery}
ref={ref}
placeholder={getIntlMessage("SEARCH")}
autoFocus
/>
)}
/>
{!!_cssColors.length && <Menu.MenuSeparator />}
{_cssColors.map(p => (
searchMatch(query, p) && <Menu.MenuItem
key={p}
id={p}
label={p}
action={() => {
// @ts-ignore
FluxDispatcher.dispatch({ type: "ICONVIEWER_COLOR_CHANGE", color: p });
}}
/>
))}
</Menu.Menu>);
});
}}>
{cssColors[color]?.name}
</IconTooltip>
</Text >;
};
export function SettingsAbout() {
return <>
<Forms.FormTitle tag="h3">Features</Forms.FormTitle>
<Forms.FormText>
<Text variant="heading-sm/normal">
<ul className="vc-ic-unordered-list">
<li>Preview icons</li>
<li>Copy icon names and CSS variables</li>
<li>Ability to download icons in different formats (SVG, PNG, GIF, etc.)</li>
<li>Copy pre-made icon finds for your plugins (Only some icons have this, submit finds either in a server or DMs)</li>
<li>Find icons by function context (helpful when creating finds)</li>
<li>Search for colors by right-clicking the color name in the modal title</li>
</ul>
</Text>
</Forms.FormText>
<Forms.FormTitle tag="h3">Special thanks</Forms.FormTitle>
<Forms.FormText>
<Text variant="heading-sm/normal" className="vc-ic-unordered-list">
<ul>
<li>krystalskullofficial._.</li>
<li>davr1</li>
<li>suffocate</li>
</ul>
</Text>
</Forms.FormText>
</>;
}

View file

@ -0,0 +1,100 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { saveFile } from "@utils/web";
import { filters, findAll, findByPropsLazy, waitFor } from "@webpack";
import { React, ReactDOM } from "@webpack/common";
import * as t from "@webpack/types";
export let _cssColors: string[] = [];
export type IconsDef = { [k: string]: t.Icon; };
export const iconSizesInPx = findByPropsLazy("md", "lg", "xxs");
export const Colors = findByPropsLazy("colors", "layout");
export const cssColors = new Proxy(
{
},
{
get: (target, key) => {
const colorKey = _cssColors[key];
return key in target
? target[key]
: Colors.colors[colorKey]?.css != null ? (target[key] = { name: colorKey.split("_").map((x: string) => x[0].toUpperCase() + x.toLowerCase().slice(1)).join(" "), css: Colors.colors[colorKey].css, key: colorKey }) : undefined;
},
set: (target, key, value) => {
target[key] = value;
return true;
}
}
) as unknown as Array<{ name: string; css: string; key: string; }>;
export const iconSizes = ["xxs", "xs", "sm", "md", "lg"];
const CrosspendingTypes: Record<string, string> = {
"image/png": "png",
"image/jpeg": "jpeg",
"image/gif": "gif",
"image/bmp": "bmp",
"image/tiff": "tiff",
"image/webp": "webp",
"image/svg+xml": "svg",
"image/avif": "avif"
};
export function saveIcon(iconName: string, icon: EventTarget & SVGSVGElement | Element | string, color: number, size: number, type = "image/png") {
const filename = `${iconName}-${cssColors[color]?.name ?? "unknown"}-${size}px.${CrosspendingTypes[type] ?? "png"}`;
if (typeof icon === "string") {
const file = new File([icon], filename, { type: "text/plain" });
saveFile(file);
return;
}
const innerElements = icon.children;
for (const el of innerElements) {
const fill = el.getAttribute("fill");
if (fill && fill.startsWith("var(")) {
el.setAttribute("fill", getComputedStyle(icon).getPropertyValue(fill.replace("var(", "").replace(")", "")));
}
}
// save svg as the given type
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, size, size);
const link = document.createElement("a");
link.download = filename;
link.href = canvas.toDataURL(type);
link.click();
};
img.src = `data:image/svg+xml;base64,${btoa(icon.outerHTML)}`;
}
export function convertComponentToHtml(component?: React.ReactElement): string {
const container = document.createElement("div");
const root = ReactDOM.createRoot(container);
ReactDOM.flushSync(() => root.render(component));
const content = container.innerHTML;
root.unmount();
return content;
}
export const findAllByCode = (code: string) => findAll(filters.byCode(code));
waitFor(["colors", "layout"], m => {
_cssColors = Object.keys(m.colors);
cssColors.length = _cssColors.length;
});