SPFx 1.11 version of smart profile photo editor
This commit is contained in:
parent
d78f91462b
commit
58d62e958f
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.11.0",
|
||||
"libraryName": "smart-profile-photo-editor",
|
||||
"libraryId": "82fe5f5d-8152-449d-b94d-45ba84066443",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
# Smart Profile Photo Editor
|
||||
|
||||
## Summary
|
||||
|
||||
Uses [Azure Cognitive Services](https://azure.microsoft.com/en-us/services/cognitive-services/) to analyze and approve or reject user-submitted photos.
|
||||
|
||||
![picture of the web part in action](./assets/WebPartPreview.gif)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![1.11.0](https://img.shields.io/badge/version-1.11.0-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](https:/dev.office.com/sharepoint)
|
||||
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This sample requires an [**Azure Cognitive Services**](https://azure.microsoft.com/en-us/services/cognitive-services/) resource instance in order to analyze submitted photographs.
|
||||
|
||||
To configure your key and endpoint, use the following steps:
|
||||
|
||||
1. If you don't already have an Azure Cognitive Services key, [create a cognitive service resource](https://azure.microsoft.com/en-us/try/cognitive-services/) and select **Get API Key** by the **Computer Vision**.
|
||||
2. Create a **Computer Vision** resource
|
||||
3. Make note of the **Key** and **Endpoint**.
|
||||
4. Edit the web part's properties and update the **Key** and **Endpoint** settings
|
||||
|
||||
### Using SharePoint Online Tenant Properties
|
||||
|
||||
If you do not wish to reveal your Azure Cognitive Service API key (or prompt users to enter it), you can pre-configure values using [SharePoint Online Tenant Properties](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/tenant-properties?tabs=sprest).
|
||||
|
||||
To do so, follow these steps:
|
||||
1. If you don't already have an Azure Cognitive Services key, [create a cognitive service resource](https://azure.microsoft.com/en-us/try/cognitive-services/) and select **Get API Key** by the **Computer Vision**.
|
||||
2. Create a **Computer Vision** resource
|
||||
3. Make note of the **Key** and **Endpoint**.
|
||||
4. Using [Office365 CLI](https://pnp.github.io/office365-cli?utm_source=msft_docs&utm_medium=page&utm_campaign=Use+SharePoint+Online+tenant+properties), set the storage entity by using the following commands:
|
||||
|
||||
```PowerShell
|
||||
spo storageentity set --appCatalogUrl <appCatalogUrl> --key azurekey --value <value of the key>
|
||||
spo storageentity set --appCatalogUrl <appCatalogUrl> --key azureendpoint --value <value of the endpoint>
|
||||
```
|
||||
|
||||
5. If you want to verify that your key and endpoint are stored, use the following command to list all your tenant properties:
|
||||
|
||||
```PowerShell
|
||||
spo storageentity list --appCatalogUrl <appCatalogUrl>
|
||||
```
|
||||
|
||||
6. Edit the `ProfilePhotoEditorWebPart.manifest.json` and set the `useStorageEntity` property to `true`. This will cause the web part to hide the Azure Cognitive Services property pane configuration group and use the tenant properties.
|
||||
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-smart-profile-photo-editor | Hugo Bernier ([Tahoe Ninjas](http://tahoeninjas.blog), @bernierh)
|
||||
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|October 15, 2019|Initial release
|
||||
1.1|August 12, 2020| Upgraded to SPFx 1.11; Added placeholder, markdown control, and property pane controls for API key and endpoint
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
* Clone this repository
|
||||
* in the command line run:
|
||||
* `npm install`
|
||||
* `gulp serve`
|
||||
|
||||
## Features
|
||||
|
||||
This web part demonstrates the following concepts:
|
||||
|
||||
* Uploading images
|
||||
* Creating a drag and drop target for uploading images
|
||||
* Using a web cam to capture images
|
||||
* Retrieving settings from the SharePoint Online tenant properties
|
||||
* Using Azure Cognitive Services
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-smart-profile-photo-editor" />
|
Binary file not shown.
After Width: | Height: | Size: 12 MiB |
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"avatar-editor-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/profilePhotoEditor/ProfilePhotoEditorWebPart.js",
|
||||
"manifest": "./src/webparts/profilePhotoEditor/ProfilePhotoEditorWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"ProfilePhotoEditorWebPartStrings": "lib/webparts/profilePhotoEditor/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
|
||||
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "smart-profile-photo-editor",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "smart-profile-photo-editor",
|
||||
"id": "82fe5f5d-8152-449d-b94d-45ba84066443",
|
||||
"version": "1.1.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"isDomainIsolated": false,
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "User.ReadWrite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/smart-profile-photo-editor.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
build.initialize(gulp);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "smart-profile-photo-editor",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/cognitiveservices-computervision": "^6.0.0",
|
||||
"@azure/ms-rest-js": "^2.0.5",
|
||||
"@microsoft/sp-core-library": "1.11.0",
|
||||
"@microsoft/sp-dialog": "1.11.0",
|
||||
"@microsoft/sp-lodash-subset": "1.11.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
|
||||
"@microsoft/sp-property-pane": "1.11.0",
|
||||
"@microsoft/sp-webpart-base": "1.11.0",
|
||||
"@pnp/common": "^1.3.8",
|
||||
"@pnp/graph": "^1.3.8",
|
||||
"@pnp/logging": "^1.3.8",
|
||||
"@pnp/odata": "^1.3.8",
|
||||
"@pnp/sp": "^1.3.8",
|
||||
"@pnp/spfx-controls-react": "^1.19.0",
|
||||
"@pnp/spfx-property-controls": "^1.20.0-beta.1472053",
|
||||
"cropperjs": "^1.5.6",
|
||||
"ms-rest-azure": "^3.0.0",
|
||||
"office-ui-fabric-react": "6.214.0",
|
||||
"react": "16.8.5",
|
||||
"react-butterfiles": "^1.3.3",
|
||||
"react-cropper": "^1.3.0",
|
||||
"react-cropperjs": "^1.2.5",
|
||||
"react-dom": "16.8.5",
|
||||
"react-webcam": "^3.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
|
||||
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
|
||||
"@microsoft/sp-build-web": "1.11.0",
|
||||
"@microsoft/sp-module-interfaces": "1.11.0",
|
||||
"@microsoft/sp-tslint-rules": "1.11.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.11.0",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,38 @@
|
|||
import { IAnalysisService } from "./IAnalysisService";
|
||||
import { AnalyzeImageInStreamResponse } from '@azure/cognitiveservices-computervision/esm/models';
|
||||
import { ComputerVisionClient } from '@azure/cognitiveservices-computervision';
|
||||
import { ApiKeyCredentials } from '@azure/ms-rest-js';
|
||||
|
||||
|
||||
export class AnalysisService implements IAnalysisService {
|
||||
private readonly key: string = undefined;
|
||||
private readonly endpoint: string = undefined;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(apiKey: string, endpoint: string) {
|
||||
this.key = apiKey;
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public async AnalyzeImage(dataUrl: string): Promise<AnalyzeImageInStreamResponse> {
|
||||
let base64data: string = dataUrl.replace(/^data:image\/png;base64,|^data:image\/jpeg;base64,|^data:image\/jpg;base64,|^data:image\/jpeg;base64,/, '');
|
||||
var buf = new Buffer(base64data, 'base64');
|
||||
let computerVisionClient = new ComputerVisionClient(
|
||||
new ApiKeyCredentials({ inHeader: { 'Ocp-Apim-Subscription-Key': this.key } }), this.endpoint);
|
||||
|
||||
var analysis: AnalyzeImageInStreamResponse = (await computerVisionClient.analyzeImageInStream(buf, {
|
||||
visualFeatures: ["Categories",
|
||||
"Adult",
|
||||
"Tags",
|
||||
"Description",
|
||||
"Faces",
|
||||
"Color",
|
||||
"ImageType",
|
||||
"Objects"]
|
||||
}));
|
||||
return analysis;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { AnalyzeImageInStreamResponse } from '@azure/cognitiveservices-computervision/esm/models';
|
||||
|
||||
export interface IAnalysisService {
|
||||
AnalyzeImage(dataUrl: string): Promise<AnalyzeImageInStreamResponse>;
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import { IAnalysisService } from ".";
|
||||
import { AnalyzeImageInStreamResponse } from '@azure/cognitiveservices-computervision/esm/models';
|
||||
|
||||
const FAKE_DELAY: number = 5000;
|
||||
|
||||
const GOOD_REPONSE: any = {
|
||||
categories: [
|
||||
{
|
||||
name: "people_portrait",
|
||||
score: 0.953125,
|
||||
detail: {
|
||||
celebrities: []
|
||||
}
|
||||
}],
|
||||
adult:
|
||||
{
|
||||
isAdultContent: false,
|
||||
isRacyContent: false,
|
||||
isGoryContent: false,
|
||||
adultScore: 0.05552997067570686,
|
||||
racyScore: 0.05687160789966583,
|
||||
goreScore: 0.003745682304725051
|
||||
},
|
||||
color: {
|
||||
dominantColorForeground: "Black",
|
||||
dominantColorBackground: "Black",
|
||||
dominantColors: ["Black"],
|
||||
accentColor: "C5064F",
|
||||
isBWImg: false, "isBwImg": false
|
||||
},
|
||||
imageType: {
|
||||
clipArtType: 0,
|
||||
lineDrawingType: 0
|
||||
},
|
||||
tags: [{ name: "person", confidence: 0.9995626211166382 },
|
||||
{ name: "man", confidence: 0.9974520206451416 },
|
||||
{ name: "necktie", confidence: 0.9936947226524353 },
|
||||
{ name: "human face", confidence: 0.9810442328453064 },
|
||||
{ name: "suit", confidence: 0.9699360132217407 },
|
||||
{ name: "portrait", confidence: 0.9696626663208008 },
|
||||
{ name: "tie", confidence: 0.9663352966308594 },
|
||||
{ name: "wearing", confidence: 0.927700400352478 },
|
||||
{ name: "smile", confidence: 0.9273260831832886 },
|
||||
{ name: "clothing", confidence: 0.9267553091049194 },
|
||||
{ name: "indoor", confidence: 0.9261177778244019 },
|
||||
{ name: "headshot", confidence: 0.8910813331604004 },
|
||||
{ name: "dark", confidence: 0.7800303101539612 },
|
||||
{ name: "shirt", confidence: 0.7269779443740845 },
|
||||
{ name: "beard", confidence: 0.7061726450920105 },
|
||||
{ name: "posing", confidence: 0.6241326928138733 },
|
||||
{ name: "face", confidence: 0.6114757061004639 },
|
||||
{ name: "jacket", confidence: 0.5651172995567322 },
|
||||
{ name: "human beard", confidence: 0.5122537016868591 },
|
||||
{ name: "dressed", confidence: 0.4890264570713043 }],
|
||||
description: {
|
||||
tags: [
|
||||
"person",
|
||||
"man",
|
||||
"necktie",
|
||||
"suit",
|
||||
"wearing",
|
||||
"indoor",
|
||||
"clothing",
|
||||
"dark",
|
||||
"posing",
|
||||
"jacket",
|
||||
"looking",
|
||||
"dressed",
|
||||
"camera",
|
||||
"smiling",
|
||||
"photo",
|
||||
"glasses",
|
||||
"shirt",
|
||||
"standing",
|
||||
"red",
|
||||
"black",
|
||||
"holding",
|
||||
"young",
|
||||
"purple",
|
||||
"pink",
|
||||
"white"],
|
||||
captions: [
|
||||
{
|
||||
text: "a man wearing a suit and tie smiling at the camera",
|
||||
confidence: 0.9828590384248658
|
||||
}]
|
||||
},
|
||||
faces: [
|
||||
{
|
||||
age: 45,
|
||||
gender: "Male",
|
||||
faceRectangle: {
|
||||
left: 63,
|
||||
top: 54,
|
||||
width: 82,
|
||||
height: 82
|
||||
}
|
||||
}],
|
||||
objects: [
|
||||
{
|
||||
rectangle: {
|
||||
x: 110,
|
||||
y: 159,
|
||||
w: 42,
|
||||
h: 41
|
||||
},
|
||||
object: "tie",
|
||||
confidence: 0.723
|
||||
},
|
||||
{
|
||||
rectangle:
|
||||
{
|
||||
x: 2,
|
||||
y: 127,
|
||||
w: 196,
|
||||
h: 73
|
||||
},
|
||||
object: "suit",
|
||||
confidence: 0.545
|
||||
},
|
||||
{
|
||||
rectangle:
|
||||
{
|
||||
x: 3,
|
||||
y: 23,
|
||||
w: 194,
|
||||
h: 177
|
||||
},
|
||||
object: "person",
|
||||
confidence: 0.897
|
||||
}],
|
||||
requestId: "d6556475-6e77-484e-97b2-7dcb5c02f74c",
|
||||
metadata: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
format: "Png"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export class MockAnalysisService implements IAnalysisService {
|
||||
/**
|
||||
* Constructor doesn't do anything, it just mimics the real one
|
||||
*/
|
||||
constructor(_apiKey: string, _endpoint: string) {
|
||||
|
||||
}
|
||||
|
||||
public AnalyzeImage(_dataUrl: string): Promise<AnalyzeImageInStreamResponse> {
|
||||
return new Promise<AnalyzeImageInStreamResponse>((resolve) => {
|
||||
// pretend we're getting the data from Azure
|
||||
setTimeout(() => {
|
||||
resolve(GOOD_REPONSE);
|
||||
}, FAKE_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './IAnalysisService';
|
||||
export * from './AnalysisService';
|
||||
export * from './MockAnalysisService';
|
|
@ -0,0 +1,3 @@
|
|||
export interface IStorageEntityService {
|
||||
GetStorageEntity(storageKey: string): Promise<string>;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { IStorageEntityService } from ".";
|
||||
|
||||
const FAKE_DELAY: number = 200;
|
||||
|
||||
/**
|
||||
* Mocks a storage entity provider for testing purposes
|
||||
*/
|
||||
export class MockStorageEntityService implements IStorageEntityService {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(_context: WebPartContext) {
|
||||
}
|
||||
|
||||
public GetStorageEntity = async (storageKey: string): Promise<string> => {
|
||||
return new Promise<string>((resolve) => {
|
||||
// pretend we're getting the data from a service
|
||||
setTimeout(() => {
|
||||
resolve(storageKey);
|
||||
}, FAKE_DELAY);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { SPHttpClientResponse, SPHttpClient } from "@microsoft/sp-http";
|
||||
import { IStorageEntityService } from ".";
|
||||
|
||||
export class StorageEntityService implements IStorageEntityService {
|
||||
private context: WebPartContext = undefined;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(context: WebPartContext) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public GetStorageEntity = async (storageKey: string): Promise<string> => {
|
||||
// Get the page context
|
||||
const { absoluteUrl } = this.context.pageContext.web;
|
||||
|
||||
// Construct the storage key URL
|
||||
const apiUrl: string = `${absoluteUrl}/_api/web/GetStorageEntity('${storageKey}')`;
|
||||
|
||||
// Get the response
|
||||
const response: SPHttpClientResponse = await this.context.spHttpClient.get(apiUrl, SPHttpClient.configurations.v1);
|
||||
|
||||
// Read the value from the JSON
|
||||
const json: any = await response.json();
|
||||
|
||||
// Return the value
|
||||
return json.Value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './StorageEntityService';
|
||||
export * from './IStorageEntityService';
|
||||
export * from './MockStorageEntityService';
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "8a6108f9-4648-403d-b3bb-75e1a451fffd",
|
||||
"alias": "ProfilePhotoEditorWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart"],
|
||||
"preconfiguredEntries": [
|
||||
{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "Profile Photo Editor" },
|
||||
"description": { "default": "Upload your own profile photo" },
|
||||
"iconImageUrl": "data:image/svg+xml,%3C?xml version='1.0' encoding='iso-8859-1'?%3E%3C!--Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)--%3E%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 60'%3E%3Cpath d='M52.179 40.5l-5.596 8.04-3.949-3.241c-.426-.352-1.057-.287-1.407.138-.351.427-.289 1.058.139 1.407l4.786 3.929c.179.148.404.227.634.227.045 0 .091-.003.137-.01.276-.038.524-.19.684-.419l6.214-8.929c.315-.453.204-1.076-.25-1.392-.454-.317-1.076-.204-1.392.25z'/%3E%3Cpath d='M54.164 35.163c.545-2.185.836-4.421.836-6.663C55 13.337 42.664 1 27.5 1S0 13.337 0 28.5c0 8.01 3.444 15.229 8.927 20.259l-.026.023.891.751c.056.047.117.086.173.133.477.396.972.772 1.476 1.136.159.115.318.23.479.341.535.369 1.085.719 1.646 1.051.122.071.244.141.366.211.613.349 1.239.678 1.881.981.047.022.094.042.141.064 2.089.971 4.319 1.684 6.65 2.105.062.011.123.022.185.033.723.125 1.455.225 2.197.292.09.008.181.013.271.021.741.06 1.487.099 2.243.099 3.262 0 6.454-.577 9.503-1.702C39.389 57.168 42.984 59 47 59c7.168 0 13-5.832 13-13 0-4.522-2.323-8.508-5.836-10.837zM2 28.5C2 14.439 13.439 3 27.5 3S53 14.439 53 28.5c0 1.903-.214 3.804-.639 5.666-.017-.008-.036-.013-.053-.021-.376-.169-.762-.32-1.156-.453-.034-.011-.067-.026-.101-.037-.411-.135-.83-.251-1.258-.345-.02-.005-.04-.011-.06-.016-.417-.09-.841-.158-1.271-.207-.03-.004-.06-.01-.09-.014C47.921 33.027 47.464 33 47 33c-5.923 0-10.923 3.986-12.485 9.413-.438-.413-.697-.988-.697-1.613v-2.957c.198-.243.405-.518.617-.817 1.096-1.547 1.975-3.269 2.616-5.123 1.266-.602 2.085-1.864 2.085-3.289v-3.545c0-.866-.318-1.708-.886-2.369v-4.667c.052-.52.236-3.448-1.883-5.864C34.524 10.065 31.541 9 27.5 9s-7.024 1.065-8.867 3.168c-2.119 2.416-1.935 5.346-1.883 5.864v4.667c-.568.661-.886 1.503-.886 2.369v3.545c0 1.101.494 2.128 1.339 2.821.81 3.173 2.476 5.575 3.092 6.389v2.894c0 .816-.445 1.566-1.162 1.958l-7.907 4.313c-.253.138-.502.298-.752.477C5.276 42.792 2 36.022 2 28.5zm23.605 25.422c-.109-.008-.218-.015-.326-.025-.634-.056-1.266-.131-1.893-.234-.026-.004-.052-.01-.077-.014-1.327-.222-2.632-.548-3.903-.974-.034-.011-.068-.023-.102-.035-1.237-.42-2.44-.939-3.601-1.544-.067-.035-.135-.068-.201-.103-.515-.275-1.019-.573-1.515-.883-.143-.09-.284-.181-.425-.273-.456-.298-.905-.608-1.343-.936-.045-.034-.088-.07-.133-.104.032-.018.064-.036.097-.054l7.907-4.313c1.359-.742 2.204-2.165 2.204-3.714v-3.603l-.233-.278c-.021-.025-2.176-2.634-2.999-6.215l-.091-.396-.341-.221c-.481-.311-.769-.831-.769-1.392v-3.545c0-.465.198-.898.557-1.223l.33-.298v-5.57l-.009-.131c-.003-.024-.298-2.429 1.396-4.36C21.583 11.837 24.06 11 27.5 11c3.425 0 5.897.83 7.346 2.466 1.692 1.911 1.415 4.361 1.413 4.381l-.009 5.701.33.298c.359.324.557.758.557 1.223v3.545c0 .724-.475 1.356-1.181 1.574l-.498.154-.16.496c-.589 1.833-1.429 3.525-2.496 5.032-.259.367-.514.695-.736.948l-.248.283V40.8c0 1.587.868 3.015 2.268 3.746-.053.478-.086.962-.086 1.454 0 .292.01.583.029.873.007.103.021.205.031.307.009.096.018.191.029.287.01.09.015.181.027.27.023.17.056.338.086.507.02.115.035.231.058.345l.003.012c.006.03.015.058.021.088.031.146.068.291.104.436.025.101.045.202.072.302.024.088.055.173.081.26.034.116.07.231.108.345.024.072.042.145.067.216.07.201.15.399.23.596.001.003.002.005.003.008.061.15.125.297.191.444.049.109.1.218.152.326.034.071.064.143.099.213.023.046.05.09.074.136.084.163.173.322.264.48.027.047.051.096.078.143C33.119 53.527 30.33 54 27.5 54c-.634 0-1.266-.031-1.895-.078zM47 57c-3.661 0-6.901-1.805-8.902-4.564-.083-.114-.158-.231-.236-.347-.054-.08-.111-.158-.162-.239-.043-.069-.085-.138-.127-.208-.045-.074-.085-.15-.128-.225-.069-.122-.143-.241-.207-.365-.047-.091-.089-.185-.134-.278-.012-.025-.023-.051-.035-.076-.075-.157-.153-.312-.221-.472-.036-.085-.063-.173-.097-.258-.077-.199-.155-.398-.221-.602-.031-.095-.055-.193-.083-.289-.009-.03-.017-.059-.025-.088-.056-.199-.11-.399-.155-.603-.014-.063-.025-.127-.038-.191-.012-.06-.025-.121-.036-.181-.041-.222-.075-.446-.103-.672-.003-.026-.006-.052-.009-.079-.009-.082-.022-.164-.029-.246C36.021 46.681 36 46.343 36 46c0-.315.021-.626.047-.934.028-.295.067-.599.122-.919l.074-.433C37.298 38.742 41.719 35 47 35c.446 0 .89.034 1.332.089.101.012.199.031.299.046.365.055.728.127 1.086.219.075.019.15.037.225.058.882.247 1.735.601 2.537 1.063C55.773 38.377 58 41.93 58 46c0 6.065-4.935 11-11 11z'/%3E%3C/svg%3E",
|
||||
"properties": {
|
||||
"instructions": "To begin, upload or drag a passport-style photo of yourself.",
|
||||
"requirePortrait": true,
|
||||
"allowClipart": false,
|
||||
"allowLinedrawing": false,
|
||||
"allowRacy": false,
|
||||
"allowAdult": false,
|
||||
"allowGory": false,
|
||||
"useStorageEntity": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
|
||||
import { IPropertyPaneConfiguration } from "@microsoft/sp-property-pane";
|
||||
import {
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneToggle,
|
||||
PropertyPaneLabel
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import * as strings from 'ProfilePhotoEditorWebPartStrings';
|
||||
import ProfilePhotoEditor from './components/ProfilePhotoEditor';
|
||||
import { IProfilePhotoEditorProps } from './components/IProfilePhotoEditorProps';
|
||||
|
||||
// Used for password fields
|
||||
import { PropertyFieldPassword } from '@pnp/spfx-property-controls/lib/PropertyFieldPassword';
|
||||
|
||||
// Used to provide guidance for users
|
||||
import { PropertyPaneMarkdownContent } from '@pnp/spfx-property-controls/lib/PropertyPaneMarkdownContent';
|
||||
|
||||
// Used to retrieve storage key
|
||||
import { StorageEntityService, IStorageEntityService, MockStorageEntityService } from '../../services/StorageEntityService';
|
||||
|
||||
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
|
||||
|
||||
export interface IProfilePhotoEditorWebPartProps {
|
||||
instructions: string;
|
||||
requirePortrait: boolean;
|
||||
allowClipart: boolean;
|
||||
allowLinedrawing: boolean;
|
||||
allowRacy: boolean;
|
||||
allowAdult: boolean;
|
||||
allowGory: boolean;
|
||||
forbiddenKeywords: string;
|
||||
azureKey: string;
|
||||
azureEndPoint: string;
|
||||
useStorageEntity: boolean;
|
||||
}
|
||||
|
||||
export default class ProfilePhotoEditorWebPart extends BaseClientSideWebPart<IProfilePhotoEditorWebPartProps> {
|
||||
// These are used if we're configured to use storage entity service
|
||||
private _storageAzureKey: string = undefined;
|
||||
private _storageAzureEndPoint: string = undefined;
|
||||
|
||||
/***
|
||||
* If the web part is configured to use storage entity, will retrieve the
|
||||
* Azure Endpoint and the Azure Key
|
||||
*/
|
||||
protected async onInit() {
|
||||
if (this.properties.useStorageEntity === undefined) {
|
||||
this.properties.useStorageEntity = false;
|
||||
}
|
||||
|
||||
if (this.properties.useStorageEntity) {
|
||||
// Get an instance of the entity storage service
|
||||
let storageService: IStorageEntityService = undefined;
|
||||
if (Environment.type === EnvironmentType.Local || Environment.type === EnvironmentType.Test) {
|
||||
//Running on Unit test environment or local workbench
|
||||
storageService = new MockStorageEntityService(this.context);
|
||||
} else if (Environment.type === EnvironmentType.SharePoint) {
|
||||
//Modern SharePoint page
|
||||
storageService = new StorageEntityService(this.context);
|
||||
}
|
||||
|
||||
// Attempt to retrieve the Azure key from tenant storage if it isn't specified
|
||||
this._storageAzureKey = await storageService.GetStorageEntity("azurekey");
|
||||
this._storageAzureEndPoint = await storageService.GetStorageEntity("azureendpoint");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the web part component
|
||||
*/
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IProfilePhotoEditorProps > = React.createElement(
|
||||
ProfilePhotoEditor,
|
||||
{
|
||||
//instructions: this.properties.instructions,
|
||||
azureVisionKey: this.properties.azureKey,
|
||||
azureVisionEndpoint: this.properties.azureEndPoint,
|
||||
context: this.context,
|
||||
displayMode: this.displayMode,
|
||||
... this.properties
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
var config: IPropertyPaneConfiguration = {
|
||||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.AzureGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneMarkdownContent({
|
||||
markdown: strings.AzureGroupMarkdown,
|
||||
key: 'azureInstructions'
|
||||
}),
|
||||
PropertyFieldPassword("azureKey", {
|
||||
key: "azureKey",
|
||||
label: strings.AzureKey,
|
||||
value: this.properties.azureKey
|
||||
}),
|
||||
PropertyPaneTextField('azureEndPoint', {
|
||||
label: strings.AzureEndPoint
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneLabel('instructions', {
|
||||
text: strings.BasicInstructions,
|
||||
}),
|
||||
PropertyPaneTextField('instructions', {
|
||||
label: strings.InstructionsFieldLabel,
|
||||
multiline: true
|
||||
}),
|
||||
PropertyPaneToggle('requirePortrait', {
|
||||
label: strings.RequirePortraitFieldLabel
|
||||
}),
|
||||
PropertyPaneToggle('allowClipart', {
|
||||
label: strings.AllowClipartFieldLabel
|
||||
}),
|
||||
PropertyPaneToggle('allowLinedrawing', {
|
||||
label: strings.AllowLineDrawingFieldLabel
|
||||
}),
|
||||
PropertyPaneToggle('allowRacy', {
|
||||
label: strings.AllowRacyFieldLabel
|
||||
}),
|
||||
PropertyPaneToggle('allowAdult', {
|
||||
label: strings.AllowAdultImagesFieldLabel
|
||||
}),
|
||||
PropertyPaneToggle('allowGory', {
|
||||
label: strings.AllowGoryFieldLabel
|
||||
}),
|
||||
PropertyPaneTextField('forbiddenKeywords', {
|
||||
label: strings.ForbiddenTagsFieldLabel,
|
||||
multiline: true,
|
||||
description: strings.ForbiddenTagsFieldDescription
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Hide the Azure cognitive services settings if the web part is configured to use storage entity
|
||||
if (this.properties.useStorageEntity) {
|
||||
// Remove the first group from the property pane
|
||||
config.pages[0].groups.splice(0,1);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.iconGood {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.iconBad {
|
||||
color: red;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import styles from './AnalysisChecklist.module.scss';
|
||||
import { IAnalysisChecklistProps } from '.';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
|
||||
export default class AnalysisChecklist extends React.Component<IAnalysisChecklistProps, {}> {
|
||||
public render(): React.ReactElement<IAnalysisChecklistProps> {
|
||||
return (
|
||||
<li><Icon iconName={this.props.isValid ? 'StatusCircleCheckmark': 'StatusCircleErrorX'} className={this.props.isValid ?styles.iconGood: styles.iconBad} /> <strong>{this.props.title}:</strong> {this.props.value}</li>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface IAnalysisChecklistProps {
|
||||
isValid: boolean;
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './AnalysisChecklist';
|
||||
export * from './AnalysisChecklist.types';
|
|
@ -0,0 +1,64 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog';
|
||||
|
||||
import { IPhotoRequirements } from './IPhotoRequirements';
|
||||
import { AnalysisDialogContent } from './AnalysisDialogContent';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
|
||||
/**
|
||||
* Analysis Panel Dialog
|
||||
*/
|
||||
export class AnalysisPanelDialog extends BaseDialog {
|
||||
private readonly imageUrl: string = undefined;
|
||||
private readonly azureKey: string = undefined;
|
||||
private readonly azureEndpoint: string = undefined;
|
||||
private readonly photoRequirements: IPhotoRequirements = undefined;
|
||||
private readonly context: WebPartContext = undefined;
|
||||
private readonly blob: Blob = undefined;
|
||||
|
||||
constructor(imageUrl: string, azureKey: string, azureEndpoint: string, photoRequirements: IPhotoRequirements, context: WebPartContext, blob: Blob) {
|
||||
super();
|
||||
|
||||
this.imageUrl = imageUrl;
|
||||
this.azureEndpoint = azureEndpoint;
|
||||
this.azureKey = azureKey;
|
||||
this.photoRequirements = photoRequirements;
|
||||
this.context = context;
|
||||
this.blob = blob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a non-blocking dialog
|
||||
*/
|
||||
public getConfig(): IDialogConfiguration {
|
||||
return {
|
||||
isBlocking: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the icon finder panel
|
||||
*/
|
||||
public render(): void {
|
||||
ReactDOM.render(<AnalysisDialogContent
|
||||
domElement={document.activeElement.parentElement}
|
||||
onDismiss={this.onDismiss.bind(this)}
|
||||
imageUrl={this.imageUrl}
|
||||
azureEndpoint={this.azureEndpoint}
|
||||
azureKey={this.azureKey}
|
||||
photoRequirements={this.photoRequirements}
|
||||
context={this.context}
|
||||
blob={this.blob}
|
||||
/>, this.domElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog when dismissed
|
||||
*/
|
||||
private onDismiss() {
|
||||
this.close();
|
||||
ReactDOM.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
@import "~office-ui-fabric-react/dist/sass/References.scss";
|
||||
|
||||
// We have to hide this style to prevent this ugly white square from showing
|
||||
:global {
|
||||
.ms-Dialog-main {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnailImg {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.laser {
|
||||
width: 150%;
|
||||
margin-left: -25%;
|
||||
background-color: rgb(0, 255, 0);
|
||||
height: 3px;
|
||||
position: absolute;
|
||||
top: 145px;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 4px rgb(217, 255, 47);
|
||||
animation: scanning 4s infinite;
|
||||
}
|
||||
|
||||
.diode {
|
||||
animation: beam 0.01s infinite;
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
font-size: 21px;
|
||||
font-weight: 100;
|
||||
color: $ms-color-neutralDark;
|
||||
padding: 32px 0 14px;
|
||||
}
|
||||
|
||||
.analysisOutcome {
|
||||
background-color: $ms-color-neutralLighterAlt;
|
||||
padding-left:12px;
|
||||
padding-right:12px;
|
||||
padding-top:0px;
|
||||
padding-bottom:12px;
|
||||
}
|
||||
|
||||
.analysisDialog {
|
||||
//margin-top:-50px;
|
||||
color: inherit;
|
||||
}
|
||||
.iconContainer {
|
||||
margin-bottom: 16px;
|
||||
font-size: 21px;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.icon
|
||||
{
|
||||
line-height: 44px;
|
||||
|
||||
border-radius: 50%;
|
||||
width:44px;
|
||||
height:44px;
|
||||
color: #ffffff;
|
||||
animation: popup .45s;
|
||||
}
|
||||
|
||||
.iconGood {
|
||||
background-color: #599b00;
|
||||
}
|
||||
|
||||
.iconBad {
|
||||
background-color: red;
|
||||
}
|
||||
.analysisChecklist {
|
||||
list-style-type: none;
|
||||
padding-inline-start: 0px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@keyframes beam {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scanning {
|
||||
50% {
|
||||
transform: translateY(260px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popup {
|
||||
from {
|
||||
transform: scale(0.0);
|
||||
}
|
||||
to {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
to {
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { IAnalysisDialogContentProps } from './IAnalysisDialogContentProps';
|
||||
import { IAnalysisDialogContentState } from './IAnalysisDialogContentState';
|
||||
|
||||
import { Panel } from 'office-ui-fabric-react/lib/Panel';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer';
|
||||
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||
|
||||
import styles from './AnalysisDialogContent.module.scss';
|
||||
|
||||
// Used for localized text
|
||||
import * as strings from 'ProfilePhotoEditorWebPartStrings';
|
||||
|
||||
// Used to determine if we should be making real calls to APIs or just mock calls
|
||||
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
|
||||
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { css } from "@uifabric/utilities/lib/css";
|
||||
import {
|
||||
MessageBar,
|
||||
MessageBarType
|
||||
} from 'office-ui-fabric-react';
|
||||
|
||||
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
|
||||
import { IAnalysisService, AnalysisService, MockAnalysisService } from '../../../../services/AnalysisServices';
|
||||
import { AnalyzeImageInStreamResponse, ImageTag } from '@azure/cognitiveservices-computervision/esm/models';
|
||||
import AnalysisChecklist from '../AnalysisChecklist/AnalysisChecklist';
|
||||
|
||||
import { sp } from "@pnp/sp";
|
||||
import { MSGraphClient, SPHttpClient } from '@microsoft/sp-http';
|
||||
|
||||
export class AnalysisDialogContent extends
|
||||
React.Component<IAnalysisDialogContentProps, IAnalysisDialogContentState> {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(props: IAnalysisDialogContentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isAnalyzing: true,
|
||||
analysis: undefined,
|
||||
isValid: false,
|
||||
isSubmitted: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* When the dialog is loaded, perform the analysis
|
||||
*/
|
||||
public async componentDidMount() {
|
||||
if (this.state.isAnalyzing) {
|
||||
const { azureKey, azureEndpoint, photoRequirements } = this.props;
|
||||
|
||||
// Get the analysis service
|
||||
let service: IAnalysisService = undefined;
|
||||
|
||||
if (Environment.type === EnvironmentType.Local || Environment.type === EnvironmentType.Test) {
|
||||
//Running on Unit test environment or local workbench
|
||||
service = new MockAnalysisService(azureKey, azureEndpoint);
|
||||
} else if (Environment.type === EnvironmentType.SharePoint) {
|
||||
//Modern SharePoint page
|
||||
service = new AnalysisService(azureKey, azureEndpoint);
|
||||
}
|
||||
|
||||
// Perform the analysis
|
||||
const analysis: AnalyzeImageInStreamResponse = await service.AnalyzeImage(this.props.imageUrl);
|
||||
|
||||
// Evaluate analysis against requirements
|
||||
const isPortrait: boolean = analysis && analysis.categories && analysis.categories.filter(c => c.name === "people_portrait").length > 0;
|
||||
const isPortraitValid: boolean = photoRequirements.requirePortrait ? isPortrait : true;
|
||||
const onlyOnePersonValid: boolean = analysis.faces.length === 1;
|
||||
const isClipartValid: boolean = photoRequirements.allowClipart ? true : analysis.imageType.clipArtType === 0;
|
||||
const isLinedrawingValid: boolean = photoRequirements.allowLinedrawing ? true : analysis.imageType.lineDrawingType === 0;
|
||||
const isAdultValid: boolean = photoRequirements.allowAdult ? true : !analysis.adult.isAdultContent;
|
||||
const isRacyValid: boolean = photoRequirements.allowRacy ? true : !analysis.adult.isRacyContent;
|
||||
const isGoryValid: boolean = photoRequirements.allowGory ? true : !analysis.adult.isGoryContent;
|
||||
|
||||
// Verify against all forbidden keywords
|
||||
let invalidKeywords: string[] = [];
|
||||
if (photoRequirements.forbiddenKeywords && photoRequirements.forbiddenKeywords.length > 0) {
|
||||
photoRequirements.forbiddenKeywords.forEach((keyword: string) => {
|
||||
if (analysis.tags.filter((tag: ImageTag) => {
|
||||
return keyword.toLowerCase() === tag.name;
|
||||
}).length > 0) {
|
||||
invalidKeywords.push(keyword);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const keywordsValid: boolean = invalidKeywords.length < 1;
|
||||
|
||||
console.log("Invalid keywords", invalidKeywords);
|
||||
|
||||
// Photo is valid if it meets all requirements
|
||||
const isValid: boolean = isPortraitValid
|
||||
&& onlyOnePersonValid
|
||||
&& isClipartValid
|
||||
&& isLinedrawingValid
|
||||
&& isAdultValid
|
||||
&& isRacyValid
|
||||
&& isGoryValid
|
||||
&& keywordsValid;
|
||||
|
||||
this.setState({
|
||||
isAnalyzing: false,
|
||||
analysis,
|
||||
isValid,
|
||||
isPortrait,
|
||||
isPortraitValid,
|
||||
onlyOnePersonValid,
|
||||
isClipartValid,
|
||||
isLinedrawingValid,
|
||||
isAdultValid,
|
||||
isRacyValid,
|
||||
isGoryValid,
|
||||
keywordsValid,
|
||||
invalidKeywords
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
|
||||
const { analysis,
|
||||
isAnalyzing,
|
||||
isValid,
|
||||
isPortrait,
|
||||
isPortraitValid,
|
||||
isAdultValid,
|
||||
isRacyValid,
|
||||
isGoryValid,
|
||||
isClipartValid,
|
||||
isLinedrawingValid,
|
||||
onlyOnePersonValid,
|
||||
invalidKeywords,
|
||||
keywordsValid } = this.state;
|
||||
|
||||
if (analysis !== undefined) {
|
||||
console.log("Analysis", analysis);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className={styles.analysisDialog}
|
||||
isOpen={true}
|
||||
onDismiss={(ev?: React.SyntheticEvent<HTMLElement, Event>) => this.onDismiss(ev)}
|
||||
isLightDismiss={true}
|
||||
onRenderFooterContent={this.onRenderFooterContent}
|
||||
>
|
||||
<h1 className={styles.panelHeader}>{strings.PanelTitle}</h1>
|
||||
<Image
|
||||
className={styles.thumbnailImg}
|
||||
imageFit={ImageFit.centerContain}
|
||||
src={this.props.imageUrl}
|
||||
/>
|
||||
{isAnalyzing &&
|
||||
<div className={styles.diode}>
|
||||
<div className={styles.laser}></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{isAnalyzing &&
|
||||
<ProgressIndicator label={strings.AnalyzingLabel} />
|
||||
}
|
||||
|
||||
<div className={styles.analysisOutcome}>
|
||||
<div className={styles.section}>
|
||||
<Label><strong>{strings.DescriptionLabel}</strong></Label>
|
||||
{isAnalyzing ? <Shimmer /> : analysis.description && analysis.description.captions && analysis.description.captions.length > 0 && <span>{analysis.description.captions[0].text}</span>}
|
||||
</div>
|
||||
|
||||
<div className={styles.section}><Label><strong>{strings.EstimatedAgeLabel}</strong></Label>
|
||||
{isAnalyzing ? <Shimmer width="10%" /> : analysis.faces.length > 0 && <span>{analysis.faces[0].age}</span>}
|
||||
</div>
|
||||
|
||||
<div className={styles.section}><Label><strong>{strings.GenderLabel}</strong></Label>
|
||||
{isAnalyzing ? <Shimmer width="20%" /> : analysis.faces.length > 0 && <span>{analysis.faces[0].gender}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isAnalyzing &&
|
||||
<div className={styles.iconContainer} ><Icon iconName={isValid ? "CheckMark" : "StatusCircleErrorX"} className={css(styles.icon, isValid ? styles.iconGood : styles.iconBad)} /></div>
|
||||
}
|
||||
|
||||
{!isAnalyzing && isValid &&
|
||||
<div>{strings.AnalysisGoodLabel}</div>
|
||||
}
|
||||
|
||||
{!isAnalyzing && !isValid &&
|
||||
<div>{strings.AnalysisBadLabel}</div>
|
||||
}
|
||||
|
||||
{!isAnalyzing &&
|
||||
<div className={styles.section}>
|
||||
<ul className={styles.analysisChecklist}>
|
||||
<AnalysisChecklist title={strings.PortraitLabel} value={isPortrait ? strings.YesLabel : strings.NoLabel} isValid={isPortraitValid} />
|
||||
<AnalysisChecklist title={strings.NumberOfFacesDetectedLabel} value={`${analysis.faces.length}`} isValid={onlyOnePersonValid} />
|
||||
<AnalysisChecklist title={strings.ClipartLabel} value={analysis.imageType.clipArtType > 0 ? strings.YesLabel : strings.NoLabel} isValid={isClipartValid} />
|
||||
<AnalysisChecklist title={strings.LineDrawingLabel} value={analysis.imageType.lineDrawingType > 0 ? strings.YesLabel : strings.NoLabel} isValid={isLinedrawingValid} />
|
||||
<AnalysisChecklist title={strings.RacyLabel} value={analysis.adult.isRacyContent ? strings.YesLabel : strings.NoLabel} isValid={isRacyValid} />
|
||||
<AnalysisChecklist title={strings.AdultLabel} value={analysis.adult.isAdultContent ? strings.YesLabel : strings.NoLabel} isValid={isAdultValid} />
|
||||
<AnalysisChecklist title={strings.GoryLabel} value={analysis.adult.isGoryContent ? strings.YesLabel : strings.NoLabel} isValid={isGoryValid} />
|
||||
<AnalysisChecklist title={strings.ForbiddenKeywordsLabel} value={keywordsValid ? strings.NoKeywords : invalidKeywords.join(', ')} isValid={keywordsValid} />
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
{!isAnalyzing && isValid &&
|
||||
<div>{strings.PanelInstructionsLabel}<strong>{strings.UpdateButtonLabel}</strong>.</div>
|
||||
}
|
||||
|
||||
{this.state.isSubmitted &&
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.success}
|
||||
isMultiline={false}
|
||||
>
|
||||
{strings.SuccessMessage}
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private onRenderFooterContent = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
<PrimaryButton onClick={(_ev) => this.onUpdateProfilePhoto()} style={{ marginRight: '8px' }} disabled={this.state.isValid !== true}>
|
||||
{strings.UpdateButtonLabel}
|
||||
</PrimaryButton>
|
||||
<DefaultButton onClick={(_ev) => this.onDismiss()}>{strings.CancelButtonLabel}</DefaultButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onUpdateProfilePhoto = async (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
|
||||
console.log("Submitting photo");
|
||||
const profileBlob: Blob = this.props.blob;
|
||||
|
||||
// Get image array buffer
|
||||
this.updateProfilePic(profileBlob);
|
||||
}
|
||||
|
||||
private async updateProfilePic(buffer) {
|
||||
console.log("Update profile pic", buffer);
|
||||
|
||||
this.props.context.msGraphClientFactory
|
||||
.getClient().then((client: MSGraphClient) => {
|
||||
client
|
||||
.api("me/photo/$value")
|
||||
.version("v1.0").header("Content-Type", buffer.type).put(buffer, (error, res) => {
|
||||
if (error) {
|
||||
console.log("Error updating profile", error);
|
||||
} else {
|
||||
console.log("Profile property Updated");
|
||||
this.setState({
|
||||
isSubmitted: true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private onDismiss = (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
|
||||
this.props.onDismiss();
|
||||
}
|
||||
|
||||
// private dataURLtoBlob = (dataurl: string): Blob => {
|
||||
// var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
|
||||
// bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
|
||||
// while (n--) {
|
||||
// u8arr[n] = bstr.charCodeAt(n);
|
||||
// }
|
||||
// return new Blob([u8arr], { type: mime });
|
||||
// }
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { AnalyzeImageInStreamResponse } from '@azure/cognitiveservices-computervision/esm/models';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { IPhotoRequirements } from './IPhotoRequirements';
|
||||
|
||||
|
||||
export interface IAnalysisDialogContentProps {
|
||||
imageUrl: string;
|
||||
azureKey: string;
|
||||
azureEndpoint: string;
|
||||
photoRequirements: IPhotoRequirements;
|
||||
|
||||
/**
|
||||
* The web part context we'll need to call APIs
|
||||
*/
|
||||
context: WebPartContext;
|
||||
|
||||
blob: Blob;
|
||||
|
||||
/**
|
||||
* The DOM element to attach the dialog to
|
||||
*/
|
||||
domElement: any;
|
||||
|
||||
/**
|
||||
* Dismiss handler
|
||||
*/
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { AnalyzeImageInStreamResponse } from '@azure/cognitiveservices-computervision/esm/models';
|
||||
|
||||
export interface IAnalysisDialogContentState {
|
||||
// This space for rent
|
||||
isAnalyzing: boolean;
|
||||
analysis?: AnalyzeImageInStreamResponse;
|
||||
isValid?: boolean;
|
||||
isPortrait?: boolean;
|
||||
isPortraitValid?: boolean;
|
||||
onlyOnePersonValid?: boolean;
|
||||
isClipartValid?: boolean;
|
||||
isLinedrawingValid?: boolean;
|
||||
isAdultValid?: boolean;
|
||||
isRacyValid?: boolean;
|
||||
isGoryValid?: boolean;
|
||||
keywordsValid?: boolean;
|
||||
invalidKeywords?: string[];
|
||||
isSubmitted: boolean;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface IPhotoRequirements {
|
||||
requirePortrait: boolean;
|
||||
allowClipart: boolean;
|
||||
allowLinedrawing: boolean;
|
||||
allowRacy: boolean;
|
||||
allowAdult: boolean;
|
||||
allowGory: boolean;
|
||||
forbiddenKeywords: string[];
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './AnalysisDialog';
|
||||
export * from './IPhotoRequirements';
|
|
@ -0,0 +1,19 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
|
||||
export interface IProfilePhotoEditorProps {
|
||||
instructions: string;
|
||||
context: WebPartContext;
|
||||
requirePortrait: boolean;
|
||||
allowClipart: boolean;
|
||||
allowLinedrawing: boolean;
|
||||
allowRacy: boolean;
|
||||
allowAdult: boolean;
|
||||
allowGory: boolean;
|
||||
forbiddenKeywords: string;
|
||||
azureVisionEndpoint: string;
|
||||
azureVisionKey: string;
|
||||
displayMode: DisplayMode;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export interface IProfilePhotoEditorState {
|
||||
errors: Array<any>;
|
||||
showWebCamDialog: boolean;
|
||||
imageUrl?: string;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.profilePhotoEditor {
|
||||
|
||||
.dropZone {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
border-width: 2px;
|
||||
border-color: $ms-color-neutralLight;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
:global {
|
||||
.ms-Button {
|
||||
border:none;
|
||||
}
|
||||
|
||||
.ms-CommandBar {
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.cropper-container {
|
||||
width: 100%!important;
|
||||
}
|
||||
}
|
||||
|
||||
.hiddenDropZone {
|
||||
width: 0px;
|
||||
height:0px;
|
||||
}
|
||||
|
||||
.cropper {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.placeholderDescription {
|
||||
width: 65%;
|
||||
vertical-align: middle;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
min-height: 400px;
|
||||
|
||||
.placeholderDescriptionText {
|
||||
color: $ms-color-neutralSecondary;
|
||||
font-size: 17px;
|
||||
display: block;
|
||||
font-weight: 100;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
|
||||
.cropper-point, .cropper-point.point-se {
|
||||
height:12px;
|
||||
width: 12px;
|
||||
background-color: white;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,316 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import styles from './ProfilePhotoEditor.module.scss';
|
||||
import { IProfilePhotoEditorProps } from './IProfilePhotoEditorProps';
|
||||
import { IProfilePhotoEditorState } from './IProfilePhotoEditorState';
|
||||
|
||||
// Used for localized text
|
||||
import * as strings from 'ProfilePhotoEditorWebPartStrings';
|
||||
|
||||
// Used to allow dragging and dropping files
|
||||
import Files from "react-butterfiles";
|
||||
|
||||
// Used to crop image
|
||||
import Cropper from 'react-cropper';
|
||||
import 'cropperjs/dist/cropper.css';
|
||||
|
||||
// Used for messages
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react';
|
||||
|
||||
// Used for toolbar
|
||||
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { AnalysisPanelDialog, IPhotoRequirements } from './AnalysisDialog';
|
||||
|
||||
|
||||
// Used to determine if we should be making real calls to APIs or just mock calls
|
||||
import { Environment, EnvironmentType, DisplayMode } from '@microsoft/sp-core-library';
|
||||
import { WebCamDialog } from './WebCamDialog';
|
||||
|
||||
// Used to prompt user for configuration
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
|
||||
// Constants used for file upload settings
|
||||
const maxSize: string = '4mb';
|
||||
const acceptedFiles: string[] = ["image/jpg", "image/jpeg", "image/png"];
|
||||
|
||||
|
||||
/**
|
||||
* Displays an upload area with support for webcam and allows users to crop an image
|
||||
*
|
||||
* Used in this class:
|
||||
* - Cropper: provides image cropping functionality
|
||||
* - Files: provides drag and drop file upload capability
|
||||
*/
|
||||
export default class ProfilePhotoEditor extends React.Component<IProfilePhotoEditorProps, IProfilePhotoEditorState> {
|
||||
/**
|
||||
* Holds a reference to the cropper
|
||||
*/
|
||||
private cropper: Cropper = undefined;
|
||||
|
||||
/**
|
||||
* Holds a reference to the div that can be clicked to launch the file browser
|
||||
*/
|
||||
private fileBrowser: HTMLDivElement = undefined;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(props: IProfilePhotoEditorProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
showWebCamDialog: false
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IProfilePhotoEditorProps> {
|
||||
const { azureVisionEndpoint, azureVisionKey } = this.props;
|
||||
|
||||
const needsConfiguration = azureVisionEndpoint === undefined || azureVisionKey === undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.profilePhotoEditor}>
|
||||
{ needsConfiguration ? this.renderPlaceholder() : this.renderPhotoEditor() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPlaceholder(): JSX.Element {
|
||||
return <Placeholder iconName='AzureLogo'
|
||||
iconText='Configure API key'
|
||||
description={`Before you can use this web part, you'll need to provide an API key for Azure Cognitive Services.`}
|
||||
buttonLabel='Configure'
|
||||
hideButton={this.props.displayMode === DisplayMode.Read}
|
||||
onConfigure={this._onConfigure} />;
|
||||
}
|
||||
|
||||
private renderPhotoEditor(): JSX.Element {
|
||||
return <>
|
||||
{this.state.errors.length > 0 && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
onDismiss={(_ev) => this.resetFiles()}
|
||||
dismissButtonAriaLabel={strings.CloseLabel}
|
||||
truncated={false}
|
||||
>
|
||||
{this.state.errors.map((error: any, _index: number) => {
|
||||
console.log("ERROR:", error);
|
||||
let errorMessage: string = strings.UnexpectedErrorLabel;
|
||||
switch (error.type) {
|
||||
case "unsupportedFileType":
|
||||
errorMessage = strings.UnsupportedFileTypeErrorLabel;
|
||||
break;
|
||||
case "maxSizeExceeded":
|
||||
errorMessage = strings.MaxSizeExceededErrorLabel;
|
||||
break;
|
||||
case "multipleNotAllowed":
|
||||
errorMessage = strings.MultipleFileTypeErrorLabel;
|
||||
break;
|
||||
}
|
||||
|
||||
return <p><b>{strings.ErrorLabel}:</b> {errorMessage}</p>;
|
||||
|
||||
})}
|
||||
</MessageBar>
|
||||
)}
|
||||
<CommandBar
|
||||
items={this.getCommandBarItems()}
|
||||
overflowItems={this.getOverflowItems()}
|
||||
overflowButtonProps={{ ariaLabel: strings.MoreCommandAriaLabel }}
|
||||
farItems={this.getFarItems()}
|
||||
ariaLabel={strings.CommandBarAriaLabel}
|
||||
/>
|
||||
<Files
|
||||
accept={acceptedFiles}
|
||||
convertToBase64
|
||||
maxSize={maxSize}
|
||||
onSuccess={this.handleSuccess}
|
||||
onError={this.handleErrors}
|
||||
>
|
||||
{({ browseFiles, getDropZoneProps }) => (
|
||||
<>
|
||||
{this.state.imageUrl !== undefined ? (
|
||||
<div
|
||||
{...getDropZoneProps({
|
||||
className: styles.dropZone
|
||||
})}>
|
||||
<Cropper
|
||||
className={styles.cropper}
|
||||
aspectRatio={1}
|
||||
guides={true}
|
||||
src={this.state.imageUrl}
|
||||
ref={cropper => { this.cropper = cropper; }}
|
||||
/>
|
||||
<div ref={(elm) => this.fileBrowser = elm}
|
||||
onClick={browseFiles}
|
||||
{...getDropZoneProps({
|
||||
className: styles.hiddenDropZone
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={(elm) => this.fileBrowser = elm}
|
||||
onClick={browseFiles}
|
||||
{...getDropZoneProps({
|
||||
className: styles.dropZone
|
||||
})}
|
||||
>
|
||||
<div className={styles.placeholderDescription}>
|
||||
<span className={styles.placeholderDescriptionText}>{this.props.instructions}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Files>
|
||||
{this.state.showWebCamDialog &&
|
||||
<WebCamDialog
|
||||
onDismiss={() => {
|
||||
this.setState({
|
||||
showWebCamDialog: false
|
||||
});
|
||||
}}
|
||||
onCapture={(imageUrl: string) => {
|
||||
this.setState({
|
||||
imageUrl,
|
||||
showWebCamDialog: false
|
||||
});
|
||||
}}
|
||||
/>}
|
||||
</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when a file has been successfully uploaded
|
||||
*/
|
||||
private handleSuccess = (files: any) => {
|
||||
this.setState({
|
||||
imageUrl: files[0].src.base64,
|
||||
errors: []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when an error has occurred uploading a file
|
||||
*/
|
||||
private handleErrors = (errors: any) => {
|
||||
console.log("Handle errors", errors);
|
||||
this.setState({
|
||||
imageUrl: undefined,
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the editor by removing all files and errors
|
||||
*/
|
||||
private resetFiles = () => {
|
||||
this.setState({
|
||||
imageUrl: undefined,
|
||||
errors: []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the command bar items
|
||||
*/
|
||||
private getCommandBarItems = () => {
|
||||
return [
|
||||
{
|
||||
key: 'upload',
|
||||
name: strings.UploadButtonName,
|
||||
iconProps: {
|
||||
iconName: 'Add'
|
||||
},
|
||||
onClick: () => {
|
||||
this.fileBrowser.click();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'webcam',
|
||||
name: strings.CameraButtonName,
|
||||
iconProps: {
|
||||
iconName: 'Camera'
|
||||
},
|
||||
//disabled: true,
|
||||
title: strings.CameraButtonLabel,
|
||||
onClick: () => this.getWebCamPhoto()
|
||||
},
|
||||
{
|
||||
key: 'Save',
|
||||
name: strings.SubmitButtonName,
|
||||
iconProps: {
|
||||
iconName: 'Save'
|
||||
},
|
||||
disabled: this.state.imageUrl === undefined,
|
||||
title: this.state.imageUrl === undefined ? strings.SubmitPhotoDisabledTitle : strings.SubmitPhotoTitle,
|
||||
onClick: () => this.submitPhoto()
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the overflow items -- we don't have any right now
|
||||
*/
|
||||
private getOverflowItems = () => {
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the menu items at the far right of the toolbar
|
||||
*/
|
||||
private getFarItems = () => {
|
||||
return [
|
||||
{
|
||||
key: 'reset',
|
||||
ariaLabel: strings.ResetAriaLabel,
|
||||
iconProps: {
|
||||
iconName: 'Refresh'
|
||||
},
|
||||
onClick: () => this.resetFiles()
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private getWebCamPhoto = () => {
|
||||
this.setState({
|
||||
showWebCamDialog: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the dialog to submit the photo
|
||||
*/
|
||||
private submitPhoto = () => {
|
||||
// Get the image to approve
|
||||
const imageToApprove: string = this.cropper.getCroppedCanvas().toDataURL();
|
||||
this.cropper.getCroppedCanvas().toBlob((blob: Blob)=> {
|
||||
console.log("Blob", blob);
|
||||
|
||||
const photoRequirements: IPhotoRequirements = {
|
||||
allowAdult: this.props.allowAdult,
|
||||
allowClipart: this.props.allowClipart,
|
||||
allowGory: this.props.allowGory,
|
||||
allowLinedrawing: this.props.allowLinedrawing,
|
||||
requirePortrait: this.props.requirePortrait,
|
||||
allowRacy: this.props.allowRacy,
|
||||
forbiddenKeywords: this.props.forbiddenKeywords && this.props.forbiddenKeywords.replace('; ', ';').replace(' ;', ';').split(';')
|
||||
};
|
||||
// Create a new instance of the analysis dialog
|
||||
const callout: AnalysisPanelDialog = new AnalysisPanelDialog(imageToApprove, this.props.azureVisionKey, this.props.azureVisionEndpoint, photoRequirements, this.props.context, blob);
|
||||
|
||||
// Show the dialog
|
||||
callout.show();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private _onConfigure = () => {
|
||||
// Context of the web part
|
||||
this.props.context.propertyPane.open();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.webcamdialog {
|
||||
:global(.ms-Dialog-main) {
|
||||
background-color: white;
|
||||
box-shadow: rgba(0,0,0,0.4) 0 0 5px 0;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import * as React from 'react';
|
||||
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
||||
|
||||
import * as strings from 'ProfilePhotoEditorWebPartStrings';
|
||||
|
||||
import { IWebCamDialogProps, IWebCamDialogState } from './WebCamDialog.types';
|
||||
|
||||
import styles from './WebCamDialog.module.scss';
|
||||
|
||||
import Webcam from "react-webcam";
|
||||
|
||||
const videoConstraints = {
|
||||
width: 300,
|
||||
height: 300,
|
||||
facingMode: "user"
|
||||
};
|
||||
|
||||
|
||||
export class WebCamDialog extends React.Component<IWebCamDialogProps, IWebCamDialogState> {
|
||||
private webcamRef: Webcam = undefined;
|
||||
|
||||
public render(): React.ReactElement<IWebCamDialogProps> {
|
||||
return (
|
||||
<Dialog
|
||||
className={styles.webcamdialog}
|
||||
hidden={false}
|
||||
onDismiss={this.onDismiss}
|
||||
dialogContentProps={{
|
||||
type: DialogType.normal,
|
||||
title: strings.WebCamDialogTitle,
|
||||
}}
|
||||
maxWidth={'356px'}
|
||||
modalProps={{
|
||||
isBlocking: false
|
||||
}}
|
||||
>
|
||||
<Webcam
|
||||
audio={false}
|
||||
ref={(elm) => this.webcamRef = elm}
|
||||
screenshotFormat="image/jpeg"
|
||||
videoConstraints={videoConstraints}
|
||||
imageSmoothing={true}
|
||||
onUserMedia={() => console.log("OnUserMedia")}
|
||||
onUserMediaError={() => console.log("OnUserMediaError")}
|
||||
screenshotQuality={0.92}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<PrimaryButton onClick={() => this.onCapture()} text={strings.CaptureButtonLabel} />
|
||||
<DefaultButton onClick={() => this.onDismiss()} text={strings.CancelButtonLabel} />
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
private onCapture = () => {
|
||||
const imageSrc = this.webcamRef.getScreenshot();
|
||||
console.log("ImageSrc", imageSrc);
|
||||
this.props.onCapture(imageSrc);
|
||||
}
|
||||
|
||||
private onDismiss = (_ev?: React.SyntheticEvent<HTMLElement, Event>) => {
|
||||
this.props.onDismiss();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export interface IWebCamDialogProps {
|
||||
/**
|
||||
* The DOM element to attach the dialog to
|
||||
*/
|
||||
//domElement: any;
|
||||
//hidden: boolean;
|
||||
|
||||
/**
|
||||
* Dismiss handler
|
||||
*/
|
||||
onDismiss: () => void;
|
||||
onCapture: (string) => void;
|
||||
}
|
||||
|
||||
export interface IWebCamDialogState {}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './WebCamDialog';
|
||||
export * from './WebCamDialog.types';
|
63
samples/react-smart-profile-photo-editor/src/webparts/profilePhotoEditor/loc/en-us.js
vendored
Normal file
63
samples/react-smart-profile-photo-editor/src/webparts/profilePhotoEditor/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,63 @@
|
|||
define([], function() {
|
||||
return {
|
||||
CaptureButtonLabel: "Capture",
|
||||
WebCamDialogTitle: "Insert photo from camera",
|
||||
NoKeywords: "(none)",
|
||||
ForbiddenKeywordsLabel: "Forbidden keywords",
|
||||
ForbiddenTagsFieldDescription: "Enter a semi-colon separated list of tags that should not be allowed.",
|
||||
ForbiddenTagsFieldLabel: "Forbidden tags",
|
||||
AllowGoryFieldLabel: "Allow gory images",
|
||||
AllowAdultImagesFieldLabel: "Allow adult images",
|
||||
AllowRacyFieldLabel: "Allow racy images",
|
||||
AllowLineDrawingFieldLabel: "Allow line drawing images",
|
||||
AllowClipartFieldLabel: "Allow clipart images",
|
||||
RequirePortraitFieldLabel: "Require portrait photos",
|
||||
AnalysisBadLabel: "Your photo does not meet one or more criteria. Try again with a photo that meets the following criteria:",
|
||||
NoLabel: "No",
|
||||
YesLabel: "Yes",
|
||||
GoryLabel: "Gory",
|
||||
AdultLabel: "Adult",
|
||||
RacyLabel: "Racy",
|
||||
LineDrawingLabel: "Line drawing",
|
||||
ClipartLabel: "Clip art",
|
||||
NumberOfFacesDetectedLabel: "Number of faces detected",
|
||||
PortraitLabel: "Portrait",
|
||||
PanelInstructionsLabel: "To update your profile photo, use ",
|
||||
AnalysisGoodLabel: "Your new photo meets our requirements!",
|
||||
GenderLabel: "Gender",
|
||||
EstimatedAgeLabel: "Estimated age",
|
||||
PanelTitle: "Update your profile photo",
|
||||
AnalyzingLabel: "Analyzing your photo...",
|
||||
DescriptionLabel: "Description",
|
||||
CancelButtonLabel: "Cancel",
|
||||
UpdateButtonLabel: "Update",
|
||||
SubmitButtonName: "Submit",
|
||||
UploadButtonName: "Upload",
|
||||
CameraButtonName: "Use camera",
|
||||
ResetAriaLabel: "Reset",
|
||||
CameraButtonLabel: "Camera function is disabled for this demo",
|
||||
SubmitPhotoDisabledTitle: "Upload a photo before you can submit",
|
||||
SubmitPhotoTitle: "Submit your photo",
|
||||
MultipleFileTypeErrorLabel: "You can only drop one file at a time",
|
||||
UnsupportedFileTypeErrorLabel: "You can only upload photos (e.g.: .PNG, .JPG, .JPEG)",
|
||||
UnexpectedErrorLabel: "Unexpected error",
|
||||
CommandBarAriaLabel: "Use left and right arrow keys to navigate between commands",
|
||||
MoreCommandAriaLabel: "More commands",
|
||||
MaxSizeExceededErrorLabel: "Maximum file size exceeded",
|
||||
ErrorLabel: "Error",
|
||||
CloseLabel: "Close",
|
||||
PropertyPaneDescription: "Use this web part to allow users to upload profile photos that meet your photo requirements.",
|
||||
BasicGroupName: "Photo requirements",
|
||||
InstructionsFieldLabel: "Instructions",
|
||||
AzureGroupName: "Azure Cognitive Service",
|
||||
AzureGroupMarkdown: `
|
||||
To get started, create an [Azure Cognitive Services](https://azure.microsoft.com/en-us/try/cognitive-services/) resource in your [Azure Portal](https://portal.azure.com).
|
||||
|
||||
You'll find your key and endpoint under **Keys and Endpoint**
|
||||
`,
|
||||
AzureKey: "Key",
|
||||
AzureEndPoint: "Endpoint",
|
||||
BasicInstructions: "Specify your requirements to approve a profile photo.",
|
||||
SuccessMessage: "Your profile was succesfully updated!"
|
||||
}
|
||||
});
|
62
samples/react-smart-profile-photo-editor/src/webparts/profilePhotoEditor/loc/mystrings.d.ts
vendored
Normal file
62
samples/react-smart-profile-photo-editor/src/webparts/profilePhotoEditor/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
declare interface IProfilePhotoEditorWebPartStrings {
|
||||
CaptureButtonLabel: string;
|
||||
WebCamDialogTitle: string;
|
||||
NoKeywords: string;
|
||||
ForbiddenKeywordsLabel: string;
|
||||
ForbiddenTagsFieldDescription: string;
|
||||
ForbiddenTagsFieldLabel: string;
|
||||
AllowGoryFieldLabel: string;
|
||||
AllowAdultImagesFieldLabel: string;
|
||||
AllowRacyFieldLabel: string;
|
||||
AllowLineDrawingFieldLabel: string;
|
||||
AllowClipartFieldLabel: string;
|
||||
RequirePortraitFieldLabel: string;
|
||||
AnalysisBadLabel: string;
|
||||
NoLabel: string;
|
||||
YesLabel: string;
|
||||
GoryLabel: string;
|
||||
AdultLabel: string;
|
||||
RacyLabel: string;
|
||||
LineDrawingLabel: string;
|
||||
ClipartLabel: string;
|
||||
NumberOfFacesDetectedLabel: string;
|
||||
PortraitLabel: string;
|
||||
PanelInstructionsLabel: string;
|
||||
AnalysisGoodLabel: string;
|
||||
GenderLabel: string;
|
||||
EstimatedAgeLabel: string;
|
||||
PanelTitle: string;
|
||||
AnalyzingLabel: string;
|
||||
DescriptionLabel: string;
|
||||
CancelButtonLabel: string;
|
||||
UpdateButtonLabel: string;
|
||||
SubmitButtonName: string;
|
||||
UploadButtonName: string;
|
||||
CameraButtonName: string;
|
||||
ResetAriaLabel: string;
|
||||
CameraButtonLabel: string;
|
||||
SubmitPhotoDisabledTitle: string;
|
||||
SubmitPhotoTitle: string;
|
||||
MultipleFileTypeErrorLabel: string;
|
||||
UnsupportedFileTypeErrorLabel: string;
|
||||
UnexpectedErrorLabel: string;
|
||||
CommandBarAriaLabel: string;
|
||||
MoreCommandAriaLabel: string;
|
||||
MaxSizeExceededErrorLabel: string;
|
||||
ErrorLabel: string;
|
||||
CloseLabel: string;
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
InstructionsFieldLabel: string;
|
||||
AzureGroupName: string;
|
||||
AzureGroupMarkdown: string;
|
||||
AzureKey: string;
|
||||
AzureEndPoint: string;
|
||||
BasicInstructions: string;
|
||||
SuccessMessage: string;
|
||||
}
|
||||
|
||||
declare module 'ProfilePhotoEditorWebPartStrings' {
|
||||
const strings: IProfilePhotoEditorWebPartStrings;
|
||||
export = strings;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
"noUnusedLocals": false,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue