mirror of https://github.com/vizality/vizality
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
364 lines
12 KiB
364 lines
12 KiB
/* eslint-disable no-unused-vars */
|
|
import parseHTML, { attributesToProps, domToReact } from 'html-react-parser';
|
|
import * as FontAwesomeIcons from '@fortawesome/free-solid-svg-icons';
|
|
import { toKebabCase, toTitleCase } from '@vizality/util/string';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
|
import { Events, Directories } from '@vizality/constants';
|
|
import { excludeProperties } from '@vizality/util/object';
|
|
import { joinClassNames } from '@vizality/util/dom';
|
|
import { getModules } from '@vizality/webpack';
|
|
import { readdirSync, readFileSync } from 'fs';
|
|
import { error } from '@vizality/util/logger';
|
|
import React, { memo, useState } from 'react';
|
|
import { sleep } from '@vizality/util/time';
|
|
import { Messages } from '@vizality/i18n';
|
|
import { join, parse } from 'path';
|
|
|
|
import { Clickable, Flex, Tooltip as TooltipContainer } from '.';
|
|
|
|
const iconList = Object
|
|
.keys(FontAwesomeIcons)
|
|
.filter(key => key !== 'fas' && key !== 'prefix')
|
|
.map(icon => FontAwesomeIcons[icon]);
|
|
|
|
library.add(...iconList);
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
const _labels = [ 'Component', 'Icon' ];
|
|
const _error = (...message) => error({ labels: _labels, message });
|
|
|
|
export const Icons = {};
|
|
|
|
/*
|
|
* We're going to process our assets folder SVGs now and turn them into React components.
|
|
*/
|
|
(async () => {
|
|
try {
|
|
const dirs = [ 'svgs', 'logos' ];
|
|
for (const dirName of dirs) {
|
|
const icons = readdirSync(join(Directories.ASSETS, dirName)).map(item => parse(item).name);
|
|
for (const name of icons) {
|
|
const icon = readFileSync(join(Directories.ASSETS, dirName, `${name}.svg`), { encoding: 'utf8' });
|
|
Icons[toKebabCase(name)] = memo(props => parseHTML(icon, {
|
|
replace: domNode => {
|
|
if (domNode?.attribs && domNode?.name === 'svg') {
|
|
const attrs = attributesToProps(domNode.attribs);
|
|
return (
|
|
<svg {...attrs} {...props}>
|
|
{domToReact(domNode?.children)}
|
|
</svg>
|
|
);
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the Font Awesome icons to our collection. These will override any of the existing
|
|
* icons with the same name.
|
|
*/
|
|
Object.keys(FontAwesomeIcons)
|
|
.filter(key => key !== 'fas' && key !== 'prefix')
|
|
.forEach(icon => Icons[FontAwesomeIcons[icon].iconName] = memo(props => (
|
|
<FontAwesomeIcon icon={FontAwesomeIcons[icon].iconName} {...props} />
|
|
)));
|
|
|
|
/*
|
|
* @note The following is a sort of automated warning system to let us know when Discord
|
|
* has added an icon to their batch, so we can be made aware of and add it. This will
|
|
* initiate after Vizality's settings are ready so that only Vizality developers get
|
|
* alerted about this.
|
|
*/
|
|
vizality.once(Events.VIZALITY_SETTINGS_READY, async () => {
|
|
if (vizality.settings.get('developer')) {
|
|
/*
|
|
* These are Discord's icons that will crash the appl if attempted to render as a normal icon.
|
|
*/
|
|
const blacklist = [
|
|
'ApplicationPlaceholder',
|
|
'DiscordNitro',
|
|
'DiscordWordmark',
|
|
'Nitro',
|
|
'NitroClassic',
|
|
'NitroStackedIcon',
|
|
'NitroClassicHorizontal',
|
|
'NowPlayingMemberMenuItem',
|
|
'PremiumGuildSubscriptionLogoCentered',
|
|
'Arrow',
|
|
'PremiumGuildTier1Simple',
|
|
'PremiumGuildTier2Simple',
|
|
'PremiumGuildTier3Simple'
|
|
];
|
|
|
|
/*
|
|
* These are Discord's inherent icons I have purposely altered or removed for whatever reason.
|
|
*/
|
|
const knownAlterations = [
|
|
'ChannelTextNSFW',
|
|
'CopyID',
|
|
'EarlyAccess',
|
|
'EmojiActivityCategory',
|
|
'ExpandIcon',
|
|
'FlowerStarIcon',
|
|
'Grid',
|
|
'GridSmall',
|
|
'HelpButton',
|
|
'InvertedGIFLabel',
|
|
'LeftCaret',
|
|
'MegaphoneNSFW',
|
|
'MultipleChoice',
|
|
'NitroWheel2',
|
|
'NitroStackedLeftAlignedIcon',
|
|
'NSFWAnnouncementThreadIcon',
|
|
'NSFWThreadIcon',
|
|
'PlatformSpotify',
|
|
'PlatformSteam',
|
|
'PlatformTwitch',
|
|
'PlatformXbox',
|
|
'PlatformBlizzard',
|
|
'PlayIcon',
|
|
'RightCaret',
|
|
'StarBadge',
|
|
'Synced',
|
|
'TemplateIcon',
|
|
'TitleBarClose',
|
|
'TitleBarCloseMac',
|
|
'TitleBarMaximize',
|
|
'TitleBarMaximizeMac',
|
|
'TitleBarMinimize',
|
|
'TitleBarMinimizeMac',
|
|
'TrendingArrow',
|
|
'Unsynced',
|
|
'UpdateAvailable',
|
|
'Upload2',
|
|
'SwitchAccountsIcon',
|
|
'WalletIcon',
|
|
'ThickArrowUp',
|
|
'ImagePlaceholderWithPlusIcon',
|
|
'HandPointIcon',
|
|
'PrivacyAndSafetyShield',
|
|
'Home',
|
|
'PremiumChannelIcon',
|
|
'BoostedGuildTier1Simple',
|
|
'BoostedGuildTier2Simple',
|
|
'BoostedGuildTier3Simple',
|
|
'BoostedGuildTier1',
|
|
'HubBadge',
|
|
'GuildBoostingLogoCentered',
|
|
'DiscordLogoLockup',
|
|
'ForumPostLockedIcon',
|
|
'ForumPostIcon',
|
|
'AllChannelsIcon',
|
|
'DoubleStarIcon',
|
|
'ForumPostNSFWIcon'
|
|
];
|
|
|
|
const registry = getModules(m => typeof m === 'function' && m.toString()?.indexOf('"currentColor"') !== -1);
|
|
const Names = Object.keys(Icons).map(name => toTitleCase(name).replaceAll(' ', ''));
|
|
const DiscordIcons = registry?.map(m => m?.displayName);
|
|
const missing = DiscordIcons?.filter(icon => !Names?.includes(icon) && !blacklist?.includes(icon) && !knownAlterations?.includes(icon));
|
|
const SVG = memo(({ icon }) => {
|
|
const Icon = registry.find(m => m?.displayName === icon);
|
|
const [ tooltipColor, setTooltipColor ] = useState('black');
|
|
const [ tooltipText, setTooltipText ] = useState(icon);
|
|
const [ tooltipShow, setTooltipShow ] = useState(false);
|
|
const handleCodeCopy = () => {
|
|
try {
|
|
// Prevent clicking when it's still showing copied
|
|
if (tooltipText === Messages?.COPIED) return;
|
|
setTooltipText(Messages?.COPIED);
|
|
setTooltipColor('green');
|
|
setTooltipShow(true);
|
|
setTimeout(() => {
|
|
setTooltipText(icon);
|
|
setTooltipColor('black');
|
|
setTooltipShow(false);
|
|
}, 1500);
|
|
// Make it easy to copy the markup for the icon with just a click
|
|
const copy = document.querySelector(`.vz-missing-icon-${icon.toLowerCase()}`)?.outerHTML;
|
|
DiscordNative?.clipboard?.copy(copy);
|
|
} catch (err) {
|
|
return _error(err);
|
|
}
|
|
};
|
|
return (
|
|
<div style={{ margin: 5 }}>
|
|
<TooltipContainer
|
|
className='vz-icon-wrapper'
|
|
text={tooltipText}
|
|
color={tooltipColor}
|
|
forceOpen={tooltipShow}
|
|
>
|
|
<Icon
|
|
className={joinClassNames('vz-icon', `vz-missing-icon-${icon.toLowerCase()}`)}
|
|
onClick={handleCodeCopy}
|
|
/>
|
|
</TooltipContainer>
|
|
</div>
|
|
);
|
|
});
|
|
if (missing?.length) {
|
|
while (!vizality.manager.builtins.get('notifications')) await sleep(100);
|
|
vizality.manager.builtins.get('notifications') && vizality.api.notifications.sendToast({
|
|
header: `Found ${missing.length} Missing Icon ${missing.length === 1 ? 'Asset' : 'Assets '}`,
|
|
icon: 'uwu',
|
|
timeout: false,
|
|
content:
|
|
<Flex wrap={Flex.Wrap.WRAP} style={ { gap: 5 } }>
|
|
{missing.map(icon => <SVG icon={icon} />)}
|
|
</Flex>
|
|
});
|
|
}
|
|
}
|
|
});
|
|
} catch (err) {
|
|
return _error(err);
|
|
}
|
|
})();
|
|
|
|
export default memo(props => {
|
|
let {
|
|
name,
|
|
icon,
|
|
width = '24',
|
|
height = '24',
|
|
size,
|
|
className,
|
|
iconClassName,
|
|
color = 'currentColor',
|
|
tooltip,
|
|
tooltipColor = 'primary',
|
|
tooltipPosition = 'top',
|
|
onClick,
|
|
onContextMenu,
|
|
svgOnly = false
|
|
} = props;
|
|
|
|
try {
|
|
if (!name) {
|
|
throw new Error('You must specify a valid name property!');
|
|
}
|
|
|
|
const SVG = icon ? icon : Icons[name] ? Icons[name] : null;
|
|
|
|
if (!SVG && !icon) {
|
|
throw new Error(`"${name}" is not a valid name property.`);
|
|
}
|
|
|
|
if (size) {
|
|
width = size;
|
|
height = size;
|
|
}
|
|
|
|
const isClickable = Boolean(onClick || onContextMenu);
|
|
const exposeProps = excludeProperties(props, 'name', 'icon', 'size', 'width', 'height', 'className', 'iconClassName', 'color', 'tooltip', 'tooltipColor', 'tooltipPosition', 'onClick', 'onContextMenu', 'svgOnly');
|
|
|
|
const renderIcon = () => {
|
|
// !svgOnly
|
|
if (!svgOnly) {
|
|
// !svgOnly and tooltip
|
|
if (tooltip) {
|
|
// !svgOnly and tooltip and clickable
|
|
if (isClickable) {
|
|
return (
|
|
<TooltipContainer
|
|
text={tooltip}
|
|
color={tooltipColor}
|
|
position={tooltipPosition}
|
|
>
|
|
<Clickable
|
|
className={joinClassNames(className, 'vz-icon-wrapper')}
|
|
onClick={onClick}
|
|
onContextMenu={onContextMenu}
|
|
>
|
|
<SVG
|
|
vz-icon={name}
|
|
className={joinClassNames(iconClassName, 'vz-icon')}
|
|
fill={color}
|
|
color={color}
|
|
width={width}
|
|
height={height}
|
|
{...exposeProps}
|
|
/>
|
|
</Clickable>
|
|
</TooltipContainer>
|
|
);
|
|
}
|
|
// !svgOnly and tooltip and !clickable
|
|
return (
|
|
<TooltipContainer
|
|
className={joinClassNames(className, 'vz-icon-wrapper')}
|
|
text={tooltip}
|
|
color={tooltipColor}
|
|
position={tooltipPosition}
|
|
>
|
|
<SVG
|
|
vz-icon={name}
|
|
className={joinClassNames(iconClassName, 'vz-icon')}
|
|
fill={color}
|
|
color={color}
|
|
width={width}
|
|
height={height}
|
|
{...exposeProps}
|
|
/>
|
|
</TooltipContainer>
|
|
);
|
|
}
|
|
// !svgOnly and !tooltip and clickable
|
|
if (isClickable) {
|
|
return (
|
|
<Clickable
|
|
className={joinClassNames(className, 'vz-icon-wrapper')}
|
|
onClick={onClick}
|
|
onContextMenu={onContextMenu}
|
|
>
|
|
<SVG
|
|
vz-icon={name}
|
|
className={joinClassNames(iconClassName, 'vz-icon')}
|
|
fill={color}
|
|
color={color}
|
|
width={width}
|
|
height={height}
|
|
{...exposeProps}
|
|
/>
|
|
</Clickable>
|
|
);
|
|
}
|
|
// !svgOnly and !tooltip and !clickable
|
|
return (
|
|
<div className={joinClassNames(className, 'vz-icon-wrapper')}>
|
|
<SVG
|
|
vz-icon={name}
|
|
className={joinClassNames(iconClassName, 'vz-icon')}
|
|
fill={color}
|
|
color={color}
|
|
width={width}
|
|
height={height}
|
|
{...exposeProps}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
// svgOnly
|
|
return (
|
|
<SVG
|
|
vz-icon={name}
|
|
className={joinClassNames(className, 'vz-icon')}
|
|
fill={color}
|
|
color={color}
|
|
width={width}
|
|
height={height}
|
|
{...exposeProps}
|
|
/>
|
|
);
|
|
};
|
|
return renderIcon();
|
|
} catch (err) {
|
|
return _error(err);
|
|
}
|
|
});
|