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 (
-