afc163
2 years ago
committed by
GitHub
4 changed files with 0 additions and 261 deletions
@ -1,239 +0,0 @@ |
|||
import React, { useCallback, useEffect, useState } from 'react'; |
|||
import { Upload, Tooltip, Popover, Modal, Progress, message, Spin, Result } from 'antd'; |
|||
import CopyToClipboard from 'react-copy-to-clipboard'; |
|||
import { useIntl } from 'dumi'; |
|||
import * as AntdIcons from '@ant-design/icons'; |
|||
|
|||
const allIcons: { [key: string]: any } = AntdIcons; |
|||
|
|||
const { Dragger } = Upload; |
|||
interface AntdIconClassifier { |
|||
load: Function; |
|||
predict: Function; |
|||
} |
|||
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 [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): Promise<HTMLImageElement> => |
|||
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'); |
|||
} |
|||
}, []); |
|||
// eslint-disable-next-line class-methods-use-this
|
|||
const onCopied = useCallback((text: string) => { |
|||
message.success( |
|||
<span> |
|||
<code className="copied-code">{text}</code> copied 🎉 |
|||
</span>, |
|||
); |
|||
}, []); |
|||
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="icon-pic-searcher"> |
|||
<Popover |
|||
content={intl.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` })} |
|||
open={state.modalOpen} |
|||
onCancel={toggleModal} |
|||
footer={null} |
|||
> |
|||
{state.modelLoaded || ( |
|||
<Spin |
|||
spinning={!state.modelLoaded} |
|||
tip={intl.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"> |
|||
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-text' })} |
|||
</p> |
|||
<p className="ant-upload-hint"> |
|||
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-hint' })} |
|||
</p> |
|||
</Dragger> |
|||
)} |
|||
<Spin |
|||
spinning={state.loading} |
|||
tip={intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.matching' })} |
|||
> |
|||
<div className="icon-pic-search-result"> |
|||
{state.icons.length > 0 && ( |
|||
<div className="result-tip"> |
|||
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.result-tip' })} |
|||
</div> |
|||
)} |
|||
<table> |
|||
{state.icons.length > 0 && ( |
|||
<thead> |
|||
<tr> |
|||
<th className="col-icon"> |
|||
{intl.formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-icon' })} |
|||
</th> |
|||
<th> |
|||
{intl.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"> |
|||
<CopyToClipboard text={`<${iconName} />`} onCopy={onCopied}> |
|||
<Tooltip title={icon.type} placement="right"> |
|||
{React.createElement(allIcons[iconName])} |
|||
</Tooltip> |
|||
</CopyToClipboard> |
|||
</td> |
|||
<td> |
|||
<Progress percent={Math.ceil(icon.score * 100)} /> |
|||
</td> |
|||
</tr> |
|||
); |
|||
})} |
|||
</tbody> |
|||
</table> |
|||
{state.error && ( |
|||
<Result |
|||
status="500" |
|||
title="503" |
|||
subTitle={intl.formatMessage({ |
|||
id: 'app.docs.components.icon.pic-searcher.server-error', |
|||
})} |
|||
/> |
|||
)} |
|||
</div> |
|||
</Spin> |
|||
</Modal> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default PicSearcher; |
Loading…
Reference in new issue