| import React, { useCallback, useEffect, useState } from 'react'; |
| import { Upload, Tooltip, Popover, Modal, Progress, Spin, Result } from 'antd'; |
| import * as AntdIcons from '@ant-design/icons'; |
| import { useIntl } from '@umijs/max'; |
| import './style.less'; |
| |
| const allIcons: { [key: string]: any } = AntdIcons; |
| |
| const { Dragger } = Upload; |
| interface AntdIconClassifier { |
| load: () => void; |
| predict: (imgEl: HTMLImageElement) => void; |
| } |
| declare global { |
| interface Window { |
| antdIconClassifier: AntdIconClassifier; |
| } |
| } |
| |
| interface PicSearcherState { |
| loading: boolean; |
| modalOpen: boolean; |
| popoverVisible: boolean; |
| icons: iconObject[]; |
| fileList: any[]; |
| error: boolean; |
| modelLoaded: boolean; |
| } |
| |
| interface iconObject { |
| type: string; |
| score: number; |
| } |
| |
| const PicSearcher: React.FC = () => { |
| const intl = useIntl(); |
| const {formatMessage} = intl; |
| const [state, setState] = useState<PicSearcherState>({ |
| loading: false, |
| modalOpen: false, |
| popoverVisible: false, |
| icons: [], |
| fileList: [], |
| error: false, |
| modelLoaded: false, |
| }); |
| const predict = (imgEl: HTMLImageElement) => { |
| try { |
| let icons: any[] = window.antdIconClassifier.predict(imgEl); |
| if (gtag && icons.length) { |
| gtag('event', 'icon', { |
| event_category: 'search-by-image', |
| event_label: icons[0].className, |
| }); |
| } |
| icons = icons.map(i => ({ score: i.score, type: i.className.replace(/\s/g, '-') })); |
| setState(prev => ({ ...prev, loading: false, error: false, icons })); |
| } catch { |
| setState(prev => ({ ...prev, loading: false, error: true })); |
| } |
| }; |
| // eslint-disable-next-line class-methods-use-this |
| const toImage = (url: string) => |
| new Promise(resolve => { |
| const img = new Image(); |
| img.setAttribute('crossOrigin', 'anonymous'); |
| img.src = url; |
| img.onload = () => { |
| resolve(img); |
| }; |
| }); |
| |
| const uploadFile = useCallback((file: File) => { |
| setState(prev => ({ ...prev, loading: true })); |
| const reader = new FileReader(); |
| reader.onload = () => { |
| toImage(reader.result as string).then(predict); |
| setState(prev => ({ |
| ...prev, |
| fileList: [{ uid: 1, name: file.name, status: 'done', url: reader.result }], |
| })); |
| }; |
| reader.readAsDataURL(file); |
| }, []); |
| |
| const onPaste = useCallback((event: ClipboardEvent) => { |
| const items = event.clipboardData && event.clipboardData.items; |
| let file = null; |
| if (items && items.length) { |
| for (let i = 0; i < items.length; i++) { |
| if (items[i].type.includes('image')) { |
| file = items[i].getAsFile(); |
| break; |
| } |
| } |
| } |
| if (file) { |
| uploadFile(file); |
| } |
| }, []); |
| const toggleModal = useCallback(() => { |
| setState(prev => ({ |
| ...prev, |
| modalOpen: !prev.modalOpen, |
| popoverVisible: false, |
| fileList: [], |
| icons: [], |
| })); |
| if (!localStorage.getItem('disableIconTip')) { |
| localStorage.setItem('disableIconTip', 'true'); |
| } |
| }, []); |
| |
| useEffect(() => { |
| const script = document.createElement('script'); |
| script.onload = async () => { |
| await window.antdIconClassifier.load(); |
| setState(prev => ({ ...prev, modelLoaded: true })); |
| document.addEventListener('paste', onPaste); |
| }; |
| script.src = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js'; |
| document.head.appendChild(script); |
| setState(prev => ({ ...prev, popoverVisible: !localStorage.getItem('disableIconTip') })); |
| return () => { |
| document.removeEventListener('paste', onPaste); |
| }; |
| }, []); |
| |
| return ( |
| <div className="iconPicSearcher"> |
| <Popover |
| content={formatMessage({id: 'app.docs.components.icon.pic-searcher.intro'})} |
| open={state.popoverVisible} |
| > |
| <AntdIcons.CameraOutlined className="icon-pic-btn" onClick={toggleModal} /> |
| </Popover> |
| <Modal |
| title={intl.formatMessage({ |
| id: 'app.docs.components.icon.pic-searcher.title', |
| defaultMessage: '信息', |
| })} |
| open={state.modalOpen} |
| onCancel={toggleModal} |
| footer={null} |
| > |
| {state.modelLoaded || ( |
| <Spin |
| spinning={!state.modelLoaded} |
| tip={formatMessage({ |
| id: 'app.docs.components.icon.pic-searcher.modelloading', |
| |
| })} |
| > |
| <div style={{ height: 100 }} /> |
| </Spin> |
| )} |
| {state.modelLoaded && ( |
| <Dragger |
| accept="image/jpeg, image/png" |
| listType="picture" |
| customRequest={o => uploadFile(o.file as File)} |
| fileList={state.fileList} |
| showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }} |
| > |
| <p className="ant-upload-drag-icon"> |
| <AntdIcons.InboxOutlined /> |
| </p> |
| <p className="ant-upload-text"> |
| {formatMessage({id: 'app.docs.components.icon.pic-searcher.upload-text'})} |
| </p> |
| <p className="ant-upload-hint"> |
| {formatMessage({id: 'app.docs.components.icon.pic-searcher.upload-hint'})} |
| </p> |
| </Dragger> |
| )} |
| <Spin |
| spinning={state.loading} |
| tip={formatMessage({id: 'app.docs.components.icon.pic-searcher.matching'})} |
| > |
| <div className="icon-pic-search-result"> |
| {state.icons.length > 0 && ( |
| <div className="result-tip"> |
| {formatMessage({id: 'app.docs.components.icon.pic-searcher.result-tip'})} |
| </div> |
| )} |
| <table> |
| {state.icons.length > 0 && ( |
| <thead> |
| <tr> |
| <th className="col-icon"> |
| {formatMessage({id: 'app.docs.components.icon.pic-searcher.th-icon'})} |
| </th> |
| <th>{formatMessage({id: 'app.docs.components.icon.pic-searcher.th-score'})}</th> |
| </tr> |
| </thead> |
| )} |
| <tbody> |
| {state.icons.map(icon => { |
| const { type } = icon; |
| const iconName = `${type |
| .split('-') |
| .map(str => `${str[0].toUpperCase()}${str.slice(1)}`) |
| .join('')}Outlined`; |
| return ( |
| <tr key={iconName}> |
| <td className="col-icon"> |
| <Tooltip title={icon.type} placement="right"> |
| {React.createElement(allIcons[iconName])} |
| </Tooltip> |
| </td> |
| <td> |
| <Progress percent={Math.ceil(icon.score * 100)} /> |
| </td> |
| </tr> |
| ); |
| })} |
| </tbody> |
| </table> |
| {state.error && ( |
| <Result |
| status="500" |
| title="503" |
| subTitle={formatMessage({id: 'app.docs.components.icon.pic-searcher.server-error'})} |
| /> |
| )} |
| </div> |
| </Spin> |
| </Modal> |
| </div> |
| ); |
| }; |
| |
| export default PicSearcher; |