parent
bc769692dc
commit
6ad57d76c1
14 changed files with 682 additions and 787 deletions
|
|
@ -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 (<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.LINK}
|
||||
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} />);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 <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="lg" width={32} height={32} color="var(--interactive-icon-default)" />
|
||||
</div>
|
||||
</Clickable>
|
||||
<Heading className="vc-icon-title" tag="h3">{iconName}</Heading>
|
||||
</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>
|
||||
<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");
|
||||
132
src/equicordplugins/iconViewer/components/IconsTab.tsx
Normal file
132
src/equicordplugins/iconViewer/components/IconsTab.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="vc-icon-box">
|
||||
<Clickable onClick={() => openIconModal(iconName, Icon, IconsFinds[iconName])}>
|
||||
<div className="vc-icon-container">
|
||||
<Icon className="vc-icon-icon" size="lg" width={32} height={32} color="var(--interactive-icon-default)" />
|
||||
</div>
|
||||
</Clickable>
|
||||
<Heading className="vc-icon-title" tag="h3">{iconName}</Heading>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<SettingsTab>
|
||||
<div className={classes(Margins.top16, "vc-icon-tab-search-bar-grid")}>
|
||||
<TextInput autoFocus value={searchInput} placeholder="Search for an icon..." onChange={onSearch} />
|
||||
<TooltipContainer text="Search by function context">
|
||||
<Button
|
||||
size="small"
|
||||
aria-label="Search by function context"
|
||||
className="vc-icon-search-func-btn"
|
||||
variant={searchByFunction ? "positive" : "primary"}
|
||||
onClick={() => setSearchByFunction(!searchByFunction)}
|
||||
>
|
||||
Func
|
||||
</Button>
|
||||
</TooltipContainer>
|
||||
</div>
|
||||
<div className="vc-icons-tab-grid-container">
|
||||
{visibleIcons.map(([iconName, Icon]) => (
|
||||
<IconItem key={iconName} iconName={iconName} Icon={Icon} />
|
||||
))}
|
||||
</div>
|
||||
{visibleCount < filteredIcons.length && (
|
||||
<div ref={sentinelRef} className="vc-icon-sentinel" />
|
||||
)}
|
||||
</SettingsTab>
|
||||
);
|
||||
}
|
||||
|
||||
export default wrapTab(IconsTab, "IconsTab");
|
||||
331
src/equicordplugins/iconViewer/components/Modals.tsx
Normal file
331
src/equicordplugins/iconViewer/components/Modals.tsx
Normal file
|
|
@ -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<string, string> = {
|
||||
"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 (
|
||||
<Menu.Menu
|
||||
navId="vc-ic-colors-menu"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
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
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
{filtered.map(colorKey => (
|
||||
<Menu.MenuItem
|
||||
key={colorKey}
|
||||
id={colorKey}
|
||||
label={colorKey}
|
||||
action={() => FluxDispatcher.dispatch({ type: "ICONVIEWER_COLOR_CHANGE", color: colorKey })}
|
||||
/>
|
||||
))}
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
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(<Icon className="vc-ic-save-icon" color={colorData?.css} />)
|
||||
: document.querySelector(".vc-ic-icon-preview .vc-ic-icon-large") as Element | null;
|
||||
|
||||
if (iconEl) saveIcon(iconName, iconEl, color, size, type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId="vc-ic-other-menu"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Icon Options"
|
||||
>
|
||||
<Menu.MenuItem
|
||||
id="log-console"
|
||||
label="Log to Console"
|
||||
icon={BugIcon}
|
||||
action={() => logger.info(Icon)}
|
||||
/>
|
||||
<Menu.MenuItem id="save" label="Save As...">
|
||||
<Menu.MenuItem
|
||||
id="save-png"
|
||||
label="PNG"
|
||||
action={() => handleSave("image/png")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="save-svg"
|
||||
label="SVG"
|
||||
action={() => handleSave("image/svg+xml")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="save-jpeg"
|
||||
label="JPEG"
|
||||
action={() => handleSave("image/jpeg")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="save-webp"
|
||||
label="WEBP"
|
||||
action={() => handleSave("image/webp")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="save-gif"
|
||||
label="GIF"
|
||||
action={() => handleSave("image/gif")}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="save-avif"
|
||||
label="AVIF"
|
||||
action={() => handleSave("image/avif")}
|
||||
/>
|
||||
</Menu.MenuItem>
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
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, () => <ColorContextMenu colorKeys={colorKeys} />);
|
||||
};
|
||||
|
||||
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, () => (
|
||||
<OtherContextMenu iconName={iconName} Icon={Icon} color={color} findCode={findCode} />
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader separator={false} className="vc-ic-modal-header">
|
||||
<div className="vc-ic-modal-header-content">
|
||||
<BaseText size="lg" weight="semibold" className="vc-ic-modal-title">{iconName}</BaseText>
|
||||
</div>
|
||||
<div className="vc-ic-modal-header-trailing">
|
||||
<CloseButton onClick={onClose} />
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalContent className="vc-ic-modal-content">
|
||||
<Flex className="vc-ic-modal-main">
|
||||
<div
|
||||
className="vc-ic-icon-preview"
|
||||
aria-label={colorData?.name}
|
||||
onContextMenu={openColorMenu}
|
||||
onWheel={onWheel}
|
||||
>
|
||||
<Icon className="vc-ic-icon-large" color={colorData?.css} />
|
||||
</div>
|
||||
<Flex flexDirection="column" className="vc-ic-icon-info">
|
||||
<Flex className="vc-ic-icon-sizes">
|
||||
{iconSizes.map(size => (
|
||||
<TooltipContainer text={size} key={size}>
|
||||
<Icon size={size} color={colorData?.css} />
|
||||
</TooltipContainer>
|
||||
))}
|
||||
</Flex>
|
||||
<TooltipContainer text="Right-click icon to change">
|
||||
<BaseText size="sm" color="text-muted" className="vc-ic-color-label">
|
||||
{colorData?.name}
|
||||
</BaseText>
|
||||
</TooltipContainer>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</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}>
|
||||
Actions
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export function openIconModal(iconName: string, Icon: Icon, findPattern?: string) {
|
||||
openModal(props => <IconModal iconName={iconName} Icon={Icon} findPattern={findPattern} {...props} />);
|
||||
}
|
||||
|
||||
export function SettingsAbout() {
|
||||
return (
|
||||
<>
|
||||
<HeadingSecondary>Features</HeadingSecondary>
|
||||
<Paragraph>
|
||||
<ul className="vc-ic-unordered-list">
|
||||
<li>Preview icons</li>
|
||||
<li>Copy icon names and CSS variables</li>
|
||||
<li>Download icons in different formats (SVG, PNG, GIF, etc.)</li>
|
||||
<li>Copy pre-made icon finds for your plugins</li>
|
||||
<li>Find icons by function context</li>
|
||||
<li>Search for colors by right-clicking the color name</li>
|
||||
</ul>
|
||||
</Paragraph>
|
||||
<HeadingSecondary>Special thanks</HeadingSecondary>
|
||||
<Paragraph>
|
||||
<ul className="vc-ic-unordered-list">
|
||||
<li>krystalskullofficial._.</li>
|
||||
<li>davr1</li>
|
||||
<li>suffocate</li>
|
||||
</ul>
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (<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={() => {
|
||||
copyToClipboard(String(func));
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: `Copied raw \`${iconName}\` to clipboard`,
|
||||
type: Toasts.Type.SUCCESS
|
||||
});
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.LINK}
|
||||
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} />);
|
||||
}
|
||||
|
||||
|
|
@ -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<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 && <Paragraph style={{ color: "var(--text-feedback-critical)" }}>{error}</Paragraph>}
|
||||
</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} />);
|
||||
}
|
||||
|
||||
140
src/equicordplugins/iconViewer/styles.css
Normal file
140
src/equicordplugins/iconViewer/styles.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<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={() => {
|
||||
copyToClipboard(copy);
|
||||
}} {...props}>{children}</Clickable>
|
||||
</TooltipContainer>;
|
||||
}
|
||||
|
||||
export const ModalHeaderTitle = ({ iconName, color, name }: { iconName: string; color: number; name: string; }) => {
|
||||
return <BaseText size="lg" weight="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>
|
||||
</BaseText >;
|
||||
};
|
||||
|
||||
export function SettingsAbout() {
|
||||
return <>
|
||||
<HeadingSecondary>Features</HeadingSecondary>
|
||||
<Paragraph>
|
||||
<BaseText size="sm">
|
||||
<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>
|
||||
</BaseText>
|
||||
</Paragraph>
|
||||
<HeadingSecondary>Special thanks</HeadingSecondary>
|
||||
<Paragraph>
|
||||
<BaseText size="sm" className="vc-ic-unordered-list">
|
||||
<ul>
|
||||
<li>krystalskullofficial._.</li>
|
||||
<li>davr1</li>
|
||||
<li>suffocate</li>
|
||||
</ul>
|
||||
</BaseText>
|
||||
</Paragraph>
|
||||
</>;
|
||||
}
|
||||
|
||||
17
src/equicordplugins/iconViewer/types.ts
Normal file
17
src/equicordplugins/iconViewer/types.ts
Normal file
|
|
@ -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<string, Icon>;
|
||||
|
||||
export interface CssColorData {
|
||||
name: string;
|
||||
css: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type IconSize = "xxs" | "xs" | "sm" | "md" | "lg";
|
||||
49
src/equicordplugins/iconViewer/utils.ts
Normal file
49
src/equicordplugins/iconViewer/utils.ts
Normal file
|
|
@ -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<string, number> = 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<number, CssColorData>, {
|
||||
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);
|
||||
});
|
||||
|
|
@ -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<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 = 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;
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue