IconViewer: use IconModule (#662)

* IconViewer: use IconModule

* t
This commit is contained in:
prism 2026-01-19 22:00:25 +01:00 • committed by GitHub
parent 019c04c332
commit 83357c5dfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 41 additions and 215 deletions

View file

@ -17,9 +17,8 @@ import { useIntersection } from "@utils/react";
import { Icon } from "@vencord/discord-types";
import { Clickable, TextInput, useCallback, useEffect, useMemo, useState } from "@webpack/common";
import { getNameByIcon, IconsFinds } from "../names";
import { IconsDef } from "../types";
import { findAllIcons } from "../utils";
import { getIconsModule } from "../utils";
import { openIconModal } from "./Modals";
let cachedIcons: IconsDef | null = null;
@ -27,10 +26,13 @@ let cachedIcons: IconsDef | null = null;
function getIcons(): IconsDef {
if (cachedIcons) return cachedIcons;
const allIcons = Array.from(new Set(findAllIcons()));
const iconsModule = getIconsModule();
cachedIcons = Object.fromEntries(
allIcons.map((icon, idx) => [getNameByIcon(icon, String(idx)), icon])
Object.entries(iconsModule).filter(([name, fn]) =>
typeof fn === "function" && name.endsWith("Icon")
)
);
return cachedIcons;
}
@ -47,11 +49,11 @@ function searchMatch(search: string, name: string, Icon: Icon, searchByFunction:
}
function IconItem({ iconName, Icon }: { iconName: string; Icon: Icon; }) {
const fill = iconName === "CircleShield" ? "var(--background-base-low)" : "var(--interactive-icon-default)";
const fill = iconName === "CircleShieldIcon" ? "var(--background-base-low)" : "var(--interactive-icon-default)";
return (
<div className="vc-icon-box">
<Clickable onClick={() => openIconModal(iconName, Icon, IconsFinds[iconName])}>
<Clickable onClick={() => openIconModal(iconName, Icon)}>
<div className="vc-icon-container">
<Icon className="vc-icon-icon" size="lg" width={32} height={32} color="var(--interactive-icon-default)" fill={fill} />
</div>
@ -107,7 +109,7 @@ function IconsTab() {
return (
<SettingsTab>
<div className={classes(Margins.top16, "vc-icon-tab-search-bar-grid")}>
<TextInput autoFocus value={searchInput} placeholder="Search for an icon..." onChange={onSearch} />
<TextInput autoFocus value={searchInput} placeholder={`Search ${Object.keys(icons).length} icons...`} onChange={onSearch} />
<TooltipContainer text="Search by function context">
<Button
size="small"

View file

@ -6,6 +6,7 @@
import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button";
import { CodeBlock } from "@components/CodeBlock";
import { Flex } from "@components/Flex";
import { HeadingSecondary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
@ -25,6 +26,7 @@ import { saveFile } from "@utils/web";
import { Icon } from "@vencord/discord-types";
import { findComponentByCodeLazy } from "@webpack";
import {
Clickable,
ContextMenuApi,
createRoot,
FluxDispatcher,
@ -164,7 +166,7 @@ function saveIcon(iconName: string, icon: Element | string, color: number, size:
img.src = `data:image/svg+xml;base64,${btoa(icon.outerHTML)}`;
}
function OtherContextMenu({ iconName, Icon, color, findCode }: { iconName: string; Icon: Icon; color: number; findCode: string | null; }) {
function OtherContextMenu({ iconName, Icon, color }: { iconName: string; Icon: Icon; color: number; }) {
const colorData = cssColors[color];
const handleSave = (type: string) => {
@ -224,15 +226,13 @@ function OtherContextMenu({ iconName, Icon, color, findCode }: { iconName: strin
);
}
function IconModal({ iconName, Icon, findPattern, onClose, transitionState }: { iconName: string; Icon: Icon; findPattern?: string; } & ModalProps) {
function IconModal({ iconName, Icon, onClose, transitionState }: { iconName: string; Icon: Icon; } & ModalProps) {
const [color, setColor] = useColorNavigation(209);
const colorData = cssColors[color];
const colorKeys = useMemo(() => getCssColorKeys(), []);
const fill = iconName === "CircleShield" ? "var(--background-base-low)" : colorData?.css;
const findCode = findPattern
? `const ${iconName}Icon = findComponentByCode(${JSON.stringify(findPattern)})`
: null;
const fill = iconName === "CircleShieldIcon" ? "var(--background-base-low)" : colorData?.css;
const findCode = `const ${iconName} = findExportedComponentLazy("${iconName}")`;
const openColorMenu = (e: React.MouseEvent) => {
ContextMenuApi.openContextMenu(e, () => <ColorContextMenu colorKeys={colorKeys} />);
@ -249,7 +249,7 @@ function IconModal({ iconName, Icon, findPattern, onClose, transitionState }: {
const openOtherMenu = (e: React.MouseEvent) => {
ContextMenuApi.openContextMenu(e, () => (
<OtherContextMenu iconName={iconName} Icon={Icon} color={color} findCode={findCode} />
<OtherContextMenu iconName={iconName} Icon={Icon} color={color} />
));
};
@ -288,12 +288,17 @@ function IconModal({ iconName, Icon, findPattern, onClose, transitionState }: {
</TooltipContainer>
</Flex>
</Flex>
<div className="vc-ic-use-as">
<BaseText size="md" weight="semibold">Usage</BaseText>
<BaseText size="sm" color="text-muted">Click to copy</BaseText>
</div>
{/* for some reason i cant make this shit codeblock full width, FF 15 */}
<Clickable className="vc-ic-codeblock-wrapper" onClick={() => copyWithToast(findCode, "Copied!")}>
<CodeBlock content={findCode} lang="ts" />
</Clickable>
</ModalContent>
<ModalFooter className="vc-ic-modal-footer">
<Button variant="primary" onClick={() => copyWithToast(findCode ?? String(Icon), findCode ? "Find code copied!" : "Raw function copied!")}>
{findCode ? "Copy" : "Copy Raw"}
</Button>
<Button variant="secondary" onClick={openOtherMenu}>
<Button variant="primary" onClick={openOtherMenu}>
Actions
</Button>
</ModalFooter>
@ -301,8 +306,8 @@ function IconModal({ iconName, Icon, findPattern, onClose, transitionState }: {
);
}
export function openIconModal(iconName: string, Icon: Icon, findPattern?: string) {
openModal(props => <IconModal iconName={iconName} Icon={Icon} findPattern={findPattern} {...props} />);
export function openIconModal(iconName: string, Icon: Icon) {
openModal(props => <IconModal iconName={iconName} Icon={Icon} {...props} />);
}
export function SettingsAbout() {

View file

@ -1,151 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Icon } from "@vencord/discord-types";
// name: pattern
export const IconsFinds: Record<string, string> = {
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",
CircleShield: "8 10.9999V17.9999C8"
};
// 13l4.91-8.05a1.8
const namePatterns = new Map(Object.entries(IconsFinds));
export function getNameByIcon(Icon: Icon, defaultName: string) {
for (const [name, pattern] of namePatterns) {
if (String(Icon).includes(pattern)) {
namePatterns.delete(name);
return name;
}
}
return defaultName;
}

View file

@ -1,3 +1,4 @@
/* Icon grid */
.vc-icons-tab-grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
@ -13,7 +14,6 @@
.vc-icon-container {
padding: 12px;
border-radius: 8px;
border: 3px solid transparent;
transition: background-color 0.15s ease;
}
@ -22,7 +22,6 @@
}
.vc-icon-icon {
color: var(--interactive-icon-default);
transition: transform 0.15s ease, filter 0.15s ease;
}
@ -35,7 +34,6 @@
font-size: 0.75rem;
margin-top: 4px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 64px;
@ -44,11 +42,10 @@
.vc-icon-tab-search-bar-grid {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 16px;
}
.vc-icon-tab-search-bar-grid > div:first-child {
.vc-icon-tab-search-bar-grid>div:first-child {
flex: 1;
}
@ -56,32 +53,16 @@
height: 32px;
}
/* Modal */
.vc-ic-modal-header {
align-items: flex-start;
position: relative;
padding: 16px 16px 16px 24px;
z-index: 1;
}
.vc-ic-modal-header-content {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}
.vc-ic-modal-header-trailing {
position: absolute;
top: 8px;
right: 8px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.vc-ic-modal-content {
padding: 16px;
}
.vc-ic-modal-main {
@ -95,11 +76,7 @@
display: flex;
align-items: center;
justify-content: center;
background-image: repeating-linear-gradient(45deg,
#ffffff1a 0,
#ffffff1a 10px,
#0000001a 10px,
#0000001a 20px);
background-image: repeating-linear-gradient(45deg, #ffffff1a 0, #ffffff1a 10px, #0000001a 10px, #0000001a 20px);
border-radius: 8px;
cursor: context-menu;
}
@ -123,18 +100,15 @@
}
.vc-ic-modal-footer {
display: flex;
flex-direction: row-reverse;
justify-content: flex-start;
gap: 8px;
}
.vc-ic-use-as {
margin-top: 16px;
margin-bottom: 8px;
}
.vc-ic-unordered-list li {
margin-left: 16px;
list-style: disc;
}
.vc-ic-modal-title {
user-select: none;
padding-right: 32px;
}

View file

@ -4,13 +4,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findAll, findByPropsLazy, waitFor } from "@webpack";
import { Icon } from "@vencord/discord-types";
import { findByPropsLazy, waitFor } from "@webpack";
import { CssColorData, IconSize } from "./types";
let colorKeys: string[] = [];
const Colors = findByPropsLazy("colors", "layout");
const IconsModule = findByPropsLazy("AngleBracketsIcon", "StaffBadgeIcon") as Record<string, Icon>;
export const iconSizesInPx: Record<string, number> = findByPropsLazy("md", "lg", "xxs");
export const iconSizes: IconSize[] = ["xxs", "xs", "sm", "md", "lg"];
@ -35,14 +37,8 @@ export const cssColors = new Proxy({} as Record<number, CssColorData>, {
}
});
export function findAllIcons() {
return findAll(m => {
if (typeof m !== "function") return false;
const str = m.toString?.() ?? "";
if (str.includes("direction:")) return false;
if (str.includes('viewBox:"0 0 272 143"')) return false;
return str.includes("viewBox:") && str.includes("color:") && (str.includes("foreground:") || str.includes("colorClass:"));
});
export function getIconsModule(): Record<string, Icon> {
return IconsModule;
}
waitFor(["colors", "layout"], m => {