diff --git a/src/equicordplugins/iconViewer/IconModal.tsx b/src/equicordplugins/iconViewer/IconModal.tsx
deleted file mode 100644
index 98556ab44..000000000
--- a/src/equicordplugins/iconViewer/IconModal.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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 * as t from "@vencord/discord-types";
-import { Button, FluxDispatcher, TooltipContainer, useCallback, useEffect, useState } from "@webpack/common";
-
-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 (
-
-
-
-
-
- {IconsFinds[iconName] ?
-
-
-
- : null
- }
-
-
-
-
-
- {iconSizes.map((size, idx) =>
-
-
-
- )}
-
-
-
-
-
-
-
- );
-}
-
-export function openIconModal(iconName: string, Icon: t.Icon) {
- openModal(props => );
-}
-
diff --git a/src/equicordplugins/iconViewer/IconsTab.css b/src/equicordplugins/iconViewer/IconsTab.css
deleted file mode 100644
index 38daa42ec..000000000
--- a/src/equicordplugins/iconViewer/IconsTab.css
+++ /dev/null
@@ -1,122 +0,0 @@
-.vc-icon-modal-codeblock {
- margin-left: 10%;
- margin-top: 30px;
-}
-
-.vc-icon-icon {
- margin-left: 5%;
- color: var(--interactive-icon-default);
- transition: transform 0.15s ease, color 0.15s ease;
-}
-
-.vc-icon-container:hover .vc-icon-icon {
- transform: scale(1.020);
- filter: brightness(1.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-base-low);
- 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(--background-feedback-info);
-}
-
-.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;
-}
diff --git a/src/equicordplugins/iconViewer/IconsTab.tsx b/src/equicordplugins/iconViewer/IconsTab.tsx
deleted file mode 100644
index a67bde976..000000000
--- a/src/equicordplugins/iconViewer/IconsTab.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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 .
-*/
-
-import "./IconsTab.css";
-
-import { Heading } from "@components/Heading";
-import { SettingsTab, wrapTab } from "@components/settings";
-import { Margins } from "@utils/margins";
-import { classes } from "@utils/misc";
-import * as t from "@vencord/discord-types";
-import { Button, Clickable, React, TextInput, TooltipContainer } from "@webpack/common";
-
-import { openIconModal } from "./IconModal";
-import { getNameByIcon } from "./names";
-import { findAllIcons, 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(findAllIcons()));
- Icons = Object.fromEntries(Object.keys(OrgIcons).map(k => [String(getNameByIcon(OrgIcons[k], k)), OrgIcons[k]])) as IconsDef;
- }
- return
- {Object.entries(Icons).map(([iconName, Icon], index) =>
- searchMatch(search, iconName, Icon, searchbyFunction) &&
-
-
openIconModal(iconName, Icon)}>
-
-
-
-
-
{iconName}
-
-
- )}
;
-}
-
-function IconsTab() {
- const [search, setSearch] = React.useState("");
- const [searchByFunction, setSearchByFunction] = React.useState(false);
- const MemoRenderIcons = React.memo(RenderIcons);
-
- return (
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default wrapTab(IconsTab, "IconsTab");
diff --git a/src/equicordplugins/iconViewer/components/IconsTab.tsx b/src/equicordplugins/iconViewer/components/IconsTab.tsx
new file mode 100644
index 000000000..6f846dabc
--- /dev/null
+++ b/src/equicordplugins/iconViewer/components/IconsTab.tsx
@@ -0,0 +1,132 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "../styles.css";
+
+import { Button } from "@components/Button";
+import { Heading } from "@components/Heading";
+import { SettingsTab, wrapTab } from "@components/settings";
+import { debounce } from "@shared/debounce";
+import { Margins } from "@utils/margins";
+import { classes } from "@utils/misc";
+import { useIntersection } from "@utils/react";
+import { Icon } from "@vencord/discord-types";
+import { Clickable, TextInput, TooltipContainer, useCallback, useEffect, useMemo, useState } from "@webpack/common";
+
+import { getNameByIcon, IconsFinds } from "../names";
+import { IconsDef } from "../types";
+import { findAllIcons } from "../utils";
+import { openIconModal } from "./Modals";
+
+let cachedIcons: IconsDef | null = null;
+
+function getIcons(): IconsDef {
+ if (cachedIcons) return cachedIcons;
+
+ const allIcons = Array.from(new Set(findAllIcons()));
+ cachedIcons = Object.fromEntries(
+ allIcons.map((icon, idx) => [getNameByIcon(icon, String(idx)), icon])
+ );
+ return cachedIcons;
+}
+
+function searchMatch(search: string, name: string, Icon: 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 keywords = search.toLowerCase().split(" ");
+ return keywords.every(k => words.includes(k)) ||
+ words.every(w => keywords.includes(w)) ||
+ name.toLowerCase().includes(search.toLowerCase());
+}
+
+function IconItem({ iconName, Icon }: { iconName: string; Icon: Icon; }) {
+ return (
+
+
openIconModal(iconName, Icon, IconsFinds[iconName])}>
+
+
+
+
+
{iconName}
+
+ );
+}
+
+function IconsTab() {
+ const [searchInput, setSearchInput] = useState("");
+ const [search, setSearch] = useState("");
+ const [searchByFunction, setSearchByFunction] = useState(false);
+
+ const icons = useMemo(() => getIcons(), []);
+
+ const debouncedSetSearch = useMemo(
+ () => debounce((query: string) => setSearch(query), 150),
+ []
+ );
+
+ const onSearch = useCallback((query: string) => {
+ setSearchInput(query);
+ debouncedSetSearch(query);
+ }, [debouncedSetSearch]);
+
+ const filteredIcons = useMemo(() =>
+ Object.entries(icons).filter(([name, Icon]) => searchMatch(search, name, Icon, searchByFunction)),
+ [icons, search, searchByFunction]
+ );
+
+ const iconsToLoad = 120;
+ const [visibleCount, setVisibleCount] = useState(iconsToLoad);
+
+ useEffect(() => {
+ setVisibleCount(iconsToLoad);
+ }, [search, searchByFunction]);
+
+ const loadMore = useCallback(() => {
+ setVisibleCount(v => Math.min(v + iconsToLoad, filteredIcons.length));
+ }, [filteredIcons.length]);
+
+ const [sentinelRef, isSentinelVisible] = useIntersection();
+
+ useEffect(() => {
+ if (isSentinelVisible && visibleCount < filteredIcons.length) {
+ loadMore();
+ }
+ }, [isSentinelVisible, visibleCount, filteredIcons.length, loadMore]);
+
+ const visibleIcons = filteredIcons.slice(0, visibleCount);
+
+ return (
+
+
+
+
+
+
+
+
+ {visibleIcons.map(([iconName, Icon]) => (
+
+ ))}
+
+ {visibleCount < filteredIcons.length && (
+
+ )}
+
+ );
+}
+
+export default wrapTab(IconsTab, "IconsTab");
diff --git a/src/equicordplugins/iconViewer/components/Modals.tsx b/src/equicordplugins/iconViewer/components/Modals.tsx
new file mode 100644
index 000000000..10ae4547e
--- /dev/null
+++ b/src/equicordplugins/iconViewer/components/Modals.tsx
@@ -0,0 +1,331 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { BaseText } from "@components/BaseText";
+import { Button } from "@components/Button";
+import { Flex } from "@components/Flex";
+import { HeadingSecondary } from "@components/Heading";
+import { Paragraph } from "@components/Paragraph";
+import { copyWithToast, getIntlMessage } from "@utils/discord";
+import { Logger } from "@utils/Logger";
+import {
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalProps,
+ ModalRoot,
+ ModalSize,
+ openModal
+} from "@utils/modal";
+import { saveFile } from "@utils/web";
+import { Icon } from "@vencord/discord-types";
+import { findComponentByCodeLazy } from "@webpack";
+import {
+ ContextMenuApi,
+ createRoot,
+ FluxDispatcher,
+ Menu,
+ ReactDOM,
+ TooltipContainer,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState
+} from "@webpack/common";
+
+import { cssColors, getCssColorKeys, iconSizes, iconSizesInPx } from "../utils";
+
+const logger = new Logger("IconViewer");
+const CloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL");
+const BugIcon = findComponentByCodeLazy("1.1.27.1.37 0a6.66 6.6");
+
+const FORMAT_EXTENSIONS: Record = {
+ "image/png": "png",
+ "image/jpeg": "jpeg",
+ "image/gif": "gif",
+ "image/webp": "webp",
+ "image/svg+xml": "svg",
+ "image/avif": "avif"
+};
+
+function useColorNavigation(initialColor: number) {
+ const [color, setColor] = useState(initialColor);
+
+ const onKeyDown = useCallback((e: KeyboardEvent) => {
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
+ e.preventDefault();
+ setColor(c => {
+ const next = c + (e.key === "ArrowLeft" ? -1 : 1);
+ const max = getCssColorKeys().length;
+ return next < 0 ? max - 1 : next >= max ? 0 : next;
+ });
+ }
+ }, []);
+
+ const onColorChange = useCallback((e: { color: string; }) => {
+ const keys = getCssColorKeys();
+ const idx = keys.indexOf(e.color);
+ if (idx !== -1) setColor(idx);
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener("keydown", onKeyDown);
+ FluxDispatcher.subscribe("ICONVIEWER_COLOR_CHANGE", onColorChange);
+ return () => {
+ document.removeEventListener("keydown", onKeyDown);
+ FluxDispatcher.unsubscribe("ICONVIEWER_COLOR_CHANGE", onColorChange);
+ };
+ }, [onKeyDown, onColorChange]);
+
+ return [color, setColor] as const;
+}
+
+function ColorContextMenu({ colorKeys }: { colorKeys: string[]; }) {
+ const [query, setQuery] = useState("");
+ const filtered = colorKeys.filter(k =>
+ !query || k.toLowerCase().includes(query.toLowerCase())
+ );
+
+ return (
+ FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
+ aria-label="Icon Viewer Colors"
+ >
+ (
+
+ )}
+ />
+
+ {filtered.map(colorKey => (
+ FluxDispatcher.dispatch({ type: "ICONVIEWER_COLOR_CHANGE", color: colorKey })}
+ />
+ ))}
+
+ );
+}
+
+function convertToHtml(component: React.ReactElement): string {
+ const container = document.createElement("div");
+ const root = createRoot(container);
+ ReactDOM.flushSync(() => root.render(component));
+ const content = container.innerHTML;
+ root.unmount();
+ return content;
+}
+
+function saveIcon(iconName: string, icon: Element | string, color: number, size: number, type: string) {
+ const colorName = cssColors[color]?.name ?? "unknown";
+ const ext = FORMAT_EXTENSIONS[type] ?? "png";
+ const filename = `${iconName}-${colorName}-${size}px.${ext}`;
+
+ if (typeof icon === "string") {
+ saveFile(new File([icon], filename, { type: "text/plain" }));
+ return;
+ }
+
+ for (const el of icon.children) {
+ const fill = el.getAttribute("fill");
+ if (fill?.startsWith("var(")) {
+ el.setAttribute("fill", getComputedStyle(icon).getPropertyValue(fill.slice(4, -1)));
+ }
+ }
+
+ 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)}`;
+}
+
+function OtherContextMenu({ iconName, Icon, color, findCode }: { iconName: string; Icon: Icon; color: number; findCode: string | null; }) {
+ const colorData = cssColors[color];
+
+ const handleSave = (type: string) => {
+ const size = iconSizesInPx.lg;
+ const iconEl = type === "image/svg+xml"
+ ? convertToHtml()
+ : document.querySelector(".vc-ic-icon-preview .vc-ic-icon-large") as Element | null;
+
+ if (iconEl) saveIcon(iconName, iconEl, color, size, type);
+ };
+
+ return (
+ FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
+ aria-label="Icon Options"
+ >
+ logger.info(Icon)}
+ />
+
+ handleSave("image/png")}
+ />
+ handleSave("image/svg+xml")}
+ />
+ handleSave("image/jpeg")}
+ />
+ handleSave("image/webp")}
+ />
+ handleSave("image/gif")}
+ />
+ handleSave("image/avif")}
+ />
+
+
+ );
+}
+
+function IconModal({ iconName, Icon, findPattern, onClose, transitionState }: { iconName: string; Icon: Icon; findPattern?: string; } & ModalProps) {
+ const [color, setColor] = useColorNavigation(209);
+ const colorData = cssColors[color];
+ const colorKeys = useMemo(() => getCssColorKeys(), []);
+
+ const findCode = findPattern
+ ? `const ${iconName}Icon = findComponentByCode(${JSON.stringify(findPattern)})`
+ : null;
+
+ const openColorMenu = (e: React.MouseEvent) => {
+ ContextMenuApi.openContextMenu(e, () => );
+ };
+
+ const onWheel = useCallback((e: React.WheelEvent) => {
+ e.preventDefault();
+ const max = colorKeys.length;
+ setColor(c => {
+ const next = c + (e.deltaY > 0 ? 1 : -1);
+ return next < 0 ? max - 1 : next >= max ? 0 : next;
+ });
+ }, [colorKeys.length, setColor]);
+
+ const openOtherMenu = (e: React.MouseEvent) => {
+ ContextMenuApi.openContextMenu(e, () => (
+
+ ));
+ };
+
+ return (
+
+
+
+ {iconName}
+
+
+
+
+
+
+
+
+
+
+
+
+ {iconSizes.map(size => (
+
+
+
+ ))}
+
+
+
+ {colorData?.name}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function openIconModal(iconName: string, Icon: Icon, findPattern?: string) {
+ openModal(props => );
+}
+
+export function SettingsAbout() {
+ return (
+ <>
+ Features
+
+
+ - Preview icons
+ - Copy icon names and CSS variables
+ - Download icons in different formats (SVG, PNG, GIF, etc.)
+ - Copy pre-made icon finds for your plugins
+ - Find icons by function context
+ - Search for colors by right-clicking the color name
+
+
+ Special thanks
+
+
+ - krystalskullofficial._.
+ - davr1
+ - suffocate
+
+
+ >
+ );
+}
diff --git a/src/equicordplugins/iconViewer/index.tsx b/src/equicordplugins/iconViewer/index.tsx
index 738d9c248..f79651cfa 100644
--- a/src/equicordplugins/iconViewer/index.tsx
+++ b/src/equicordplugins/iconViewer/index.tsx
@@ -1,6 +1,6 @@
/*
* Vencord, a Discord client mod
- * Copyright (c) 2024 Vendicated and contributors
+ * Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -10,13 +10,12 @@ import { EquicordDevs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
import { openUserSettingsPanel } from "@webpack/common";
-import IconsTab from "./IconsTab";
-import { SettingsAbout } from "./subComponents";
-
+import IconsTab from "./components/IconsTab";
+import { SettingsAbout } from "./components/Modals";
export default definePlugin({
name: "IconViewer",
- description: "Adds a new tab to settings, to preview all icons",
+ description: "Adds a new tab to settings to preview all icons.",
authors: [EquicordDevs.iamme],
dependencies: ["Settings"],
startAt: StartAt.WebpackReady,
@@ -46,9 +45,9 @@ export default definePlugin({
},
stop() {
const { customEntries, customSections } = SettingsPlugin;
- const entry = customEntries.findIndex(entry => entry.key === "equicord_icon_viewer");
- const section = customSections.findIndex(section => section({} as any).id === "IconViewer");
- if (entry !== -1) customEntries.splice(entry, 1);
- if (section !== -1) customSections.splice(section, 1);
+ const entryIdx = customEntries.findIndex(e => e.key === "equicord_icon_viewer");
+ const sectionIdx = customSections.findIndex(s => s({} as any).id === "IconViewer");
+ if (entryIdx !== -1) customEntries.splice(entryIdx, 1);
+ if (sectionIdx !== -1) customSections.splice(sectionIdx, 1);
},
});
diff --git a/src/equicordplugins/iconViewer/names.ts b/src/equicordplugins/iconViewer/names.ts
index 70cddc34f..d1ef744ed 100644
--- a/src/equicordplugins/iconViewer/names.ts
+++ b/src/equicordplugins/iconViewer/names.ts
@@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import * as t from "@vencord/discord-types";
+import { Icon } from "@vencord/discord-types";
// name: pattern
-export const IconsFinds = {
+export const IconsFinds: Record = {
Discord: "1.6 5.64-2.87",
XboxNeutral: "8.68-.62c.89-.81 1.5",
PlaystationNeutral: "2.04Zm-9.35",
@@ -137,12 +137,12 @@ export const IconsFinds = {
// 13l4.91-8.05a1.8
-export const namePatterns = new Map(Object.entries(IconsFinds).map(([name, pattern]) => [name, pattern]));
+const namePatterns = new Map(Object.entries(IconsFinds));
-export function getNameByIcon(Icon: t.Icon, defaultName: any) {
+export function getNameByIcon(Icon: Icon, defaultName: string) {
for (const [name, pattern] of namePatterns) {
if (String(Icon).includes(pattern)) {
- namePatterns.delete(name); // remove pattern from map after being found prevent overshadowing
+ namePatterns.delete(name);
return name;
}
}
diff --git a/src/equicordplugins/iconViewer/rawModal.tsx b/src/equicordplugins/iconViewer/rawModal.tsx
deleted file mode 100644
index 70edebbcb..000000000
--- a/src/equicordplugins/iconViewer/rawModal.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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 { copyToClipboard } from "@utils/clipboard";
-import { Margins } from "@utils/margins";
-import { classes } from "@utils/misc";
-import {
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalProps,
- ModalRoot,
- ModalSize,
- openModal
-} from "@utils/modal";
-import * as t from "@vencord/discord-types";
-import { Button, Toasts } from "@webpack/common";
-
-import { ModalHeaderTitle } from "./subComponents";
-
-
-
-function ModalComponent(props: { func: Function; iconName: string; color: number; } & ModalProps) {
- const { func, iconName, color } = props;
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export function openRawModal(iconName: string, Icon: t.Icon, colorIndex: number) {
- openModal(props => );
-}
-
diff --git a/src/equicordplugins/iconViewer/saveModal.tsx b/src/equicordplugins/iconViewer/saveModal.tsx
deleted file mode 100644
index 24064e3de..000000000
--- a/src/equicordplugins/iconViewer/saveModal.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Vencord, a Discord client mod
- * Copyright (c) 2024 Vendicated and contributors
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-import { Paragraph } from "@components/Paragraph";
-import {
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalProps,
- ModalRoot,
- ModalSize,
- openModal
-} from "@utils/modal";
-import * as t from "@vencord/discord-types";
-import { Button, Select, TextInput, useCallback, useEffect, useState } from "@webpack/common";
-
-import { ModalHeaderTitle } from "./subComponents";
-import { convertComponentToHtml, cssColors, iconSizesInPx, saveIcon } from "./utils";
-
-type IDivElement = React.DetailedHTMLProps, 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 (
-
-
-
- );
-}
-
-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(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 (
-
);
-}
-
-
-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 (
-
-
-
-
-
-
-
-
-
-
- SetIconSize(newValue)} onError={() => { }} />
- SetCustomSize(value)} />
- SetSaveType(newValue)} onError={() => { }} />
-
-
-
-
-
-
- );
-}
-
-export function openSaveModal(iconName: string, Icon: t.Icon, colorIndex: number) {
- openModal(props => );
-}
-
diff --git a/src/equicordplugins/iconViewer/styles.css b/src/equicordplugins/iconViewer/styles.css
new file mode 100644
index 000000000..229877b14
--- /dev/null
+++ b/src/equicordplugins/iconViewer/styles.css
@@ -0,0 +1,140 @@
+.vc-icons-tab-grid-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
+ gap: 8px;
+}
+
+.vc-icon-box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.vc-icon-container {
+ padding: 12px;
+ border-radius: 8px;
+ border: 3px solid transparent;
+ transition: background-color 0.15s ease;
+}
+
+.vc-icon-container:hover {
+ background-color: var(--background-modifier-hover);
+}
+
+.vc-icon-icon {
+ color: var(--interactive-icon-default);
+ transition: transform 0.15s ease, filter 0.15s ease;
+}
+
+.vc-icon-container:hover .vc-icon-icon {
+ transform: scale(1.1);
+ filter: brightness(1.5);
+}
+
+.vc-icon-title {
+ font-size: 0.75rem;
+ margin-top: 4px;
+ text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 64px;
+}
+
+.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 {
+ flex: 1;
+}
+
+.vc-icon-sentinel {
+ height: 32px;
+}
+
+.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 {
+ gap: 24px;
+ align-items: center;
+}
+
+.vc-ic-icon-preview {
+ width: 128px;
+ height: 128px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-image: repeating-linear-gradient(45deg,
+ #ffffff1a 0,
+ #ffffff1a 10px,
+ #0000001a 10px,
+ #0000001a 20px);
+ border-radius: 8px;
+ cursor: context-menu;
+}
+
+.vc-ic-icon-large {
+ width: 96px;
+ height: 96px;
+}
+
+.vc-ic-icon-info {
+ gap: 12px;
+}
+
+.vc-ic-icon-sizes {
+ gap: 16px;
+ align-items: flex-end;
+}
+
+.vc-ic-color-label {
+ cursor: help;
+}
+
+.vc-ic-modal-footer {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-start;
+ gap: 8px;
+}
+
+.vc-ic-unordered-list li {
+ margin-left: 16px;
+ list-style: disc;
+}
+
+.vc-ic-modal-title {
+ user-select: none;
+ padding-right: 32px;
+}
diff --git a/src/equicordplugins/iconViewer/subComponents.tsx b/src/equicordplugins/iconViewer/subComponents.tsx
deleted file mode 100644
index 166057b33..000000000
--- a/src/equicordplugins/iconViewer/subComponents.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Vencord, a Discord client mod
- * Copyright (c) 2025 Vendicated and contributors
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-import { BaseText } from "@components/BaseText";
-import { HeadingSecondary } from "@components/Heading";
-import { Paragraph } from "@components/Paragraph";
-import { copyToClipboard } from "@utils/clipboard";
-import { getIntlMessage } from "@utils/discord";
-import { Margins } from "@utils/margins";
-import { classes } from "@utils/misc";
-import { Clickable, ContextMenuApi, FluxDispatcher, Menu, 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 = PropsWithChildren> & {
- tag?: T;
-};
-
-export function IconTooltip({ children, copy, className, ...props }: ClickableProps & { children: string; copy: string; }) {
- return
- {
- copyToClipboard(copy);
- }} {...props}>{children}
- ;
-}
-
-export const ModalHeaderTitle = ({ iconName, color, name }: { iconName: string; color: number; name: string; }) => {
- return
-
- {iconName}
-
- {" - "}
- {
- ContextMenuApi.openContextMenu(e, () => {
- const [query, setQuery] = useState("");
- return ( FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
- color="danger"
- aria-label="Icon Viewer Colors"
- >
-
- (
-
- )}
- />
-
- {!!_cssColors.length && }
-
- {_cssColors.map(p => (
- searchMatch(query, p) && {
- // @ts-ignore
- FluxDispatcher.dispatch({ type: "ICONVIEWER_COLOR_CHANGE", color: p });
- }}
- />
- ))}
-
- );
- });
- }}>
- {cssColors[color]?.name}
-
- ;
-};
-
-export function SettingsAbout() {
- return <>
- Features
-
-
-
- - Preview icons
- - Copy icon names and CSS variables
- - Ability to download icons in different formats (SVG, PNG, GIF, etc.)
- - Copy pre-made icon finds for your plugins (Only some icons have this, submit finds either in a server or DMs)
- - Find icons by function context (helpful when creating finds)
- - Search for colors by right-clicking the color name in the modal title
-
-
-
- Special thanks
-
-
-
- - krystalskullofficial._.
- - davr1
- - suffocate
-
-
-
- >;
-}
-
diff --git a/src/equicordplugins/iconViewer/types.ts b/src/equicordplugins/iconViewer/types.ts
new file mode 100644
index 000000000..61114c3d3
--- /dev/null
+++ b/src/equicordplugins/iconViewer/types.ts
@@ -0,0 +1,17 @@
+/*
+ * 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";
+
+export type IconsDef = Record;
+
+export interface CssColorData {
+ name: string;
+ css: string;
+ key: string;
+}
+
+export type IconSize = "xxs" | "xs" | "sm" | "md" | "lg";
diff --git a/src/equicordplugins/iconViewer/utils.ts b/src/equicordplugins/iconViewer/utils.ts
new file mode 100644
index 000000000..944bed202
--- /dev/null
+++ b/src/equicordplugins/iconViewer/utils.ts
@@ -0,0 +1,49 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { findAll, findByPropsLazy, waitFor } from "@webpack";
+
+import { CssColorData, IconSize } from "./types";
+
+let colorKeys: string[] = [];
+
+const Colors = findByPropsLazy("colors", "layout");
+
+export const iconSizesInPx: Record = findByPropsLazy("md", "lg", "xxs");
+export const iconSizes: IconSize[] = ["xxs", "xs", "sm", "md", "lg"];
+
+export function getCssColorKeys(): string[] {
+ return colorKeys;
+}
+
+export const cssColors = new Proxy({} as Record, {
+ get: (target, key) => {
+ const idx = Number(key);
+ if (isNaN(idx)) return undefined;
+
+ if (target[idx]) return target[idx];
+
+ const colorKey = colorKeys[idx];
+ if (!colorKey || !Colors.colors[colorKey]?.css) return undefined;
+
+ const name = colorKey.split("_").map(x => x[0].toUpperCase() + x.toLowerCase().slice(1)).join(" ");
+ target[idx] = { name, css: Colors.colors[colorKey].css, key: colorKey };
+ return target[idx];
+ }
+});
+
+export function findAllIcons() {
+ return findAll(m => {
+ if (typeof m !== "function") return false;
+ const str = m.toString?.() ?? "";
+ if (str.includes("direction:")) return false;
+ return str.includes("viewBox:") && str.includes("color:") && (str.includes("foreground:") || str.includes("colorClass:"));
+ });
+}
+
+waitFor(["colors", "layout"], m => {
+ colorKeys = Object.keys(m.colors);
+});
diff --git a/src/equicordplugins/iconViewer/utils.tsx b/src/equicordplugins/iconViewer/utils.tsx
deleted file mode 100644
index be44b67e0..000000000
--- a/src/equicordplugins/iconViewer/utils.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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 * as t from "@vencord/discord-types";
-import { findAll, findByPropsLazy, waitFor } from "@webpack";
-import { createRoot, ReactDOM } from "@webpack/common";
-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 = {
- "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 = createRoot(container);
-
- ReactDOM.flushSync(() => root.render(component));
- const content = container.innerHTML;
- root.unmount();
-
- return content;
-}
-
-export function findAllIcons() {
- return findAll(m => {
- if (typeof m !== "function") return false;
- const str = m.toString?.() || "";
- if (str.includes("direction:")) return false;
- return str.includes("viewBox:") && str.includes("color:") && (str.includes("foreground:") || str.includes("colorClass:"));
- });
-}
-
-waitFor(["colors", "layout"], m => {
- _cssColors = Object.keys(m.colors);
- cssColors.length = _cssColors.length;
-});