diff --git a/components/icon/demo/module b/components/icon/demo/module index 0f841539dc..4f7259b88a 100644 --- a/components/icon/demo/module +++ b/components/icon/demo/module @@ -2,5 +2,28 @@ import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzRadioModule } from 'ng-zorro-antd/radio'; import { NzInputModule } from 'ng-zorro-antd/input'; import { NzBadgeModule } from 'ng-zorro-antd/badge'; +import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzMessageModule } from 'ng-zorro-antd/message'; +import { NzModalModule } from 'ng-zorro-antd/modal'; +import { NzPopoverModule } from 'ng-zorro-antd/popover'; +import { NzProgressModule } from 'ng-zorro-antd/progress'; +import { NzResultModule } from 'ng-zorro-antd/result'; +import { NzSpinModule } from 'ng-zorro-antd/spin'; +import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; +import { NzUploadModule } from 'ng-zorro-antd/upload'; -export const moduleList = [ NzIconModule, NzRadioModule, NzInputModule, NzBadgeModule ]; +export const moduleList = [ + NzIconModule, + NzRadioModule, + NzInputModule, + NzBadgeModule, + NzButtonModule, + NzMessageModule, + NzModalModule, + NzPopoverModule, + NzProgressModule, + NzResultModule, + NzSpinModule, + NzToolTipModule, + NzUploadModule, +]; diff --git a/components/icon/page/en-US.txt b/components/icon/page/en-US.txt index e47cf2ad5e..9c946e9b84 100644 --- a/components/icon/page/en-US.txt +++ b/components/icon/page/en-US.txt @@ -6,5 +6,15 @@ data: 'Data Icons', other: 'Application Icons', logo: 'Brand and Logos', - search: 'Search icon here. Click icon to copy code.' + search: 'Search icon here. Click icon to copy code.', + picSearcherIntro: 'AI Search by image is online, you are welcome to use it! 🎉', + picSearcherMatching: 'Matching...', + picSearcherModelLoading: 'Model is loading...', + picSearcherResultTip: 'Matched the following icons for you:', + picSearcherServerError: 'Predict service is temporarily unavailable', + picSearcherThIcon: 'Icon', + picSearcherThScore: 'Probability', + picSearcherTitle: 'Search by image', + picSearcherUploadHint: 'We will find the best matching icon based on the image provided', + picSearcherUploadText: 'Click, drag, or paste file to this area to upload' } diff --git a/components/icon/page/index.ts b/components/icon/page/index.ts index 55d2664483..9ffd70547f 100644 --- a/components/icon/page/index.ts +++ b/components/icon/page/index.ts @@ -4,13 +4,27 @@ */ import { DOCUMENT } from '@angular/common'; -import { Component, Inject, OnInit } from '@angular/core'; +import { + Component, + Inject, + OnDestroy, + OnInit, + ComponentFactoryResolver, + Input, + ViewChild, + TemplateRef, + ViewContainerRef +} from '@angular/core'; +import { of, Subscription } from 'rxjs'; import { manifest, ThemeType } from '@ant-design/icons-angular'; import { AccountBookFill } from '@ant-design/icons-angular/icons'; +import { PREFIX } from 'ng-zorro-antd/core/logger'; import { NzSafeAny } from 'ng-zorro-antd/core/types'; import { NzIconService } from 'ng-zorro-antd/icon'; +import { NzMessageService } from 'ng-zorro-antd/message'; +import { NzUploadChangeParam, NzUploadFile, NzUploadXHRArgs } from 'ng-zorro-antd/upload'; export interface Categories { direction: string[]; @@ -21,6 +35,27 @@ export interface Categories { other?: string[]; } +declare global { + interface Window { + antdIconClassifier: AntdIconClassifier; + } +} + +interface AntdIconClassifier { + load: Function; + predict: Function; +} + +interface Result { + className: string; + score: number; +} + +interface Icon { + type: string; + score: number; +} + const direction = [ 'StepBackward', 'StepForward', @@ -282,7 +317,7 @@ declare const locale: NzSafeAny; Two Tone - + - - + +
+ + + + + +
+
+ + @@ -310,6 +366,76 @@ declare const locale: NzSafeAny; + + + + +

+ +

+

{{ localeObj.picSearcherUploadText }}

+

{{ localeObj.picSearcherUploadHint }}

+
+ +
+ +
+ {{ localeObj.picSearcherResultTip }} +
+ + + + + + + + + + + + + +
+ {{ localeObj.picSearcherThIcon }} + {{ localeObj.picSearcherThScore }}
+ + + + +
+
+ + +
+
+
+ + +
+
+
+
+
`, styles: [ ` @@ -336,13 +462,21 @@ declare const locale: NzSafeAny; ` ] }) -export class NzPageDemoIconComponent implements OnInit { +export class NzPageDemoIconComponent implements OnInit, OnDestroy { displayedNames: Array<{ name: string; icons: string[] }> = []; categoryNames: string[] = []; currentTheme: ThemeType = 'outline'; localeObj: { [key: string]: string } = locale; searchingString = ''; + error = false; + loading = false; + modelLoaded = false; + modalVisible = false; + popoverVisible = false; + fileList: NzUploadFile[] = []; + icons: Icon[] = []; + trackByFn = (_index: number, item: string): string => `${item}-${this.currentTheme}`; kebabCase = (str: string): string => kebabCase(str); @@ -358,6 +492,9 @@ export class NzPageDemoIconComponent implements OnInit { target.classList.remove('copied'); }, 1000); }); + + const content = this.getCopiedStringTemplateRef(copiedString); + this.message.success(content); } private _copy(value: string): Promise { @@ -418,17 +555,156 @@ export class NzPageDemoIconComponent implements OnInit { this.prepareIcons(); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(@Inject(DOCUMENT) private dom: any, private _iconService: NzIconService) { + private getCopiedStringTemplateRef(copiedString: string): TemplateRef { + this.viewContainerRef.clear(); + const factory = this.componentFactoryResolver.resolveComponentFactory(NzPageDemoIconCopiedCodeComponent); + const componentRef = this.viewContainerRef.createComponent(factory); + componentRef.instance.copiedCode = copiedString; + + return componentRef.instance.templateRef; + } + + private loadModel(): void { + if (window.antdIconClassifier) { + this.onLoad(); + return; + } + + const script = this.dom.createElement('script'); + const source = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js'; + script.type = 'text/javascript'; + script.src = source; + script.onload = async () => { + await window.antdIconClassifier.load(); + this.onLoad(); + }; + script.onerror = () => { + throw new Error(`${PREFIX} cannot load assets of antd icon classifier from source "${source}".`); + }; + + this.dom.head.appendChild(script); + } + + private onLoad(): void { + this.modelLoaded = true; + this.dom.addEventListener('paste', this.onPaste as EventListener); + } + + private onPaste = (event: ClipboardEvent): void => { + const items = event.clipboardData && event.clipboardData.items; + let file = null; + if (items && items.length) { + for (let i = 0; i < items.length; i += 1) { + if (items[i].type.indexOf('image') !== -1) { + file = items[i].getAsFile(); + break; + } + } + } + if (file) this.uploadFile(file); + }; + + // We don't need to upload it, so just fake api here + customRequestUploadFile = (o: NzUploadXHRArgs): Subscription => { + return of(true).subscribe(() => { + this.uploadFile(o.file); + }); + }; + + private uploadFile = (file: File | NzUploadFile): void => { + this.loading = true; + const reader: FileReader = new FileReader(); + reader.onload = () => { + this.toImage(reader.result as string).then(this.predict); + this.fileList = [{ uid: '1', name: file.name, status: 'done', url: reader.result as string }]; + }; + reader.readAsDataURL(file as File); + }; + + private toImage(url: string): Promise { + return new Promise(resolve => { + const img = new Image(); + img.setAttribute('crossOrigin', 'anonymous'); + img.src = url; + img.onload = function onload() { + resolve(img); + }; + }); + } + + private predict = (imgEl: HTMLImageElement): void => { + try { + const results = window.antdIconClassifier.predict(imgEl); + this.icons = results.map((r: Result) => ({ + score: Math.ceil(r.score * 100), + type: r.className.replace(/\s/g, '-') + })); + this.loading = false; + this.error = false; + } catch (err) { + this.loading = false; + this.error = true; + } + }; + + toggleModal(): void { + this.modalVisible = !this.modalVisible; + this.popoverVisible = false; + this.fileList = []; + this.icons = []; + + if (!localStorage.getItem('disableIconTip')) { + localStorage.setItem('disableIconTip', 'true'); + } + } + + handleChange({ file, fileList }: NzUploadChangeParam): void { + if (file.status === 'uploading' || file.status === 'error') { + this.fileList = fileList; + } else if (file.status === 'done') { + this.uploadFile(file.originFileObj!); + } + } + + constructor( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + @Inject(DOCUMENT) private dom: any, + private _iconService: NzIconService, + private componentFactoryResolver: ComponentFactoryResolver, + private message: NzMessageService, + private viewContainerRef: ViewContainerRef + ) { // This is to test that tree shake works! this._iconService.addIcon(AccountBookFill); } ngOnInit(): void { this.setIconsShouldBeDisplayed('outline'); + this.loadModel(); + this.popoverVisible = !localStorage.getItem('disableIconTip'); + } + + ngOnDestroy(): void { + this.dom.removeEventListener('paste', this.onPaste as EventListener); + this.viewContainerRef.clear(); } } +@Component({ + selector: 'nz-page-demo-icon-copied-code', + template: ` + + + {{ copiedCode }} copied 🎉 + + + ` +}) +export class NzPageDemoIconCopiedCodeComponent { + @Input() copiedCode!: string; + @ViewChild('templateRef', { static: true }) templateRef!: TemplateRef; +} + function camelCase(value: string): string { return value.replace(/-\w/g, (_r, i) => value.charAt(i + 1).toUpperCase()); } diff --git a/components/icon/page/zh-CN.txt b/components/icon/page/zh-CN.txt index 6c82758656..b60d785f36 100644 --- a/components/icon/page/zh-CN.txt +++ b/components/icon/page/zh-CN.txt @@ -6,5 +6,15 @@ data: '数据类图标', other: '网站通用图标', logo: '品牌和标识', - search: '在此搜索图标,点击图标可复制代码' + search: '在此搜索图标,点击图标可复制代码', + picSearcherIntro: 'AI 截图搜索上线了,快来体验吧!🎉', + picSearcherMatching: '匹配中...', + picSearcherModelLoading: '神经网络模型加载中...', + picSearcherResultTip: '为您匹配到以下图标:', + picSearcherServerError: '识别服务暂不可用', + picSearcherThIcon: '图标', + picSearcherThScore: '匹配度', + picSearcherTitle: '上传图片搜索图标', + picSearcherUploadHint: '我们会通过上传的图片进行匹配,得到最相似的图标', + picSearcherUploadText: '点击/拖拽/粘贴上传图片' }