mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-27 15:34:26 -04:00
Add 4 Plugins
Added: FriendCodes by HypedDomi IconViewer by iamme SpotifyLyrics by Joona PanelSettings by nin0dev
This commit is contained in:
parent
c5230c94b0
commit
220b44b4ed
30 changed files with 3130 additions and 1 deletions
112
src/equicordplugins/iconViewer/IconModal.tsx
Normal file
112
src/equicordplugins/iconViewer/IconModal.tsx
Normal 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} />);
|
||||
}
|
||||
|
117
src/equicordplugins/iconViewer/IconsTab.css
Normal file
117
src/equicordplugins/iconViewer/IconsTab.css
Normal 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;
|
||||
}
|
88
src/equicordplugins/iconViewer/IconsTab.tsx
Normal file
88
src/equicordplugins/iconViewer/IconsTab.tsx
Normal 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");
|
54
src/equicordplugins/iconViewer/index.tsx
Normal file
54
src/equicordplugins/iconViewer/index.tsx
Normal 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);
|
||||
},
|
||||
});
|
150
src/equicordplugins/iconViewer/names.ts
Normal file
150
src/equicordplugins/iconViewer/names.ts
Normal 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;
|
||||
}
|
70
src/equicordplugins/iconViewer/rawModal.tsx
Normal file
70
src/equicordplugins/iconViewer/rawModal.tsx
Normal 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} />);
|
||||
}
|
||||
|
153
src/equicordplugins/iconViewer/saveModal.tsx
Normal file
153
src/equicordplugins/iconViewer/saveModal.tsx
Normal 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} />);
|
||||
}
|
||||
|
118
src/equicordplugins/iconViewer/subComponents.tsx
Normal file
118
src/equicordplugins/iconViewer/subComponents.tsx
Normal 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>
|
||||
</>;
|
||||
}
|
||||
|
100
src/equicordplugins/iconViewer/utils.tsx
Normal file
100
src/equicordplugins/iconViewer/utils.tsx
Normal 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;
|
||||
});
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue