= [
14 | { name: 'Arabic', value: 'ar' },
15 | { name: 'Chinese (Simplified)', value: 'zh' },
16 | { name: 'Chinese (Traditional)', value: 'zh-TW' },
17 | { name: 'Czech', value: 'cs' },
18 | { name: 'Danish', value: 'da' },
19 | { name: 'Dutch', value: 'nl' },
20 | { name: 'English', value: 'en' },
21 | { name: 'Finnish', value: 'fi' },
22 | { name: 'French', value: 'fr' },
23 | { name: 'German', value: 'de' },
24 | { name: 'Hebrew', value: 'he' },
25 | { name: 'Hindi', value: 'hi' },
26 | { name: 'Indonesian', value: 'id' },
27 | { name: 'Italian', value: 'it' },
28 | { name: 'Japanese', value: 'ja' },
29 | { name: 'Korean', value: 'ko' },
30 | { name: 'Malay', value: 'ms' },
31 | { name: 'Norweigen', value: 'no' },
32 | { name: 'Persian', value: 'fa' },
33 | { name: 'Polish', value: 'pl' },
34 | { name: 'Portuguese', value: 'pt' },
35 | { name: 'Russian', value: 'ru' },
36 | { name: 'Spanish', value: 'es' },
37 | { name: 'Swedish', value: 'sv' },
38 | { name: 'Turkish', value: 'tr' }
39 | ];
40 |
41 | constructor() { }
42 | }
43 |
--------------------------------------------------------------------------------
/src/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Amplify Predictions",
3 | "short_name": "Predictions",
4 | "theme_color": "#feae32",
5 | "background_color": "#fafafa",
6 | "display": "standalone",
7 | "scope": "/",
8 | "start_url": "/",
9 | "icons": [
10 | {
11 | "src": "assets/icons/48x48.png",
12 | "sizes": "48x48",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "assets/icons/72x72.png",
17 | "sizes": "72x72",
18 | "type": "image/png"
19 | },
20 | {
21 | "src": "assets/icons/96x96.png",
22 | "sizes": "96x96",
23 | "type": "image/png"
24 | },
25 | {
26 | "src": "assets/icons/128x128.png",
27 | "sizes": "128x128",
28 | "type": "image/png"
29 | },
30 | {
31 | "src": "assets/icons/144x144.png",
32 | "sizes": "144x144",
33 | "type": "image/png"
34 | },
35 | {
36 | "src": "assets/icons/152x152.png",
37 | "sizes": "152x152",
38 | "type": "image/png"
39 | },
40 | {
41 | "src": "assets/icons/192x192.png",
42 | "sizes": "192x192",
43 | "type": "image/png"
44 | },
45 | {
46 | "src": "assets/icons/384x384.png",
47 | "sizes": "384x384",
48 | "type": "image/png"
49 | },
50 | {
51 | "src": "assets/icons/512x512.png",
52 | "sizes": "512x512",
53 | "type": "image/png"
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/amplify/backend/types/amplify-dependent-resources-ref.d.ts:
--------------------------------------------------------------------------------
1 | export type AmplifyDependentResourcesAttributes = {
2 | "auth": {
3 | "phototranslate1a58bd43": {
4 | "IdentityPoolId": "string",
5 | "IdentityPoolName": "string",
6 | "UserPoolId": "string",
7 | "UserPoolName": "string",
8 | "AppClientIDWeb": "string",
9 | "AppClientID": "string",
10 | "AppClientSecret": "string"
11 | }
12 | },
13 | "predictions": {
14 | "identifyText360eb9c4": {
15 | "region": "string",
16 | "format": "string"
17 | },
18 | "translateTextd8fbec95": {
19 | "region": "string",
20 | "sourceLang": "string",
21 | "targetLang": "string"
22 | },
23 | "identifyEntitiesd04d1d1d": {
24 | "region": "string",
25 | "celebrityDetectionEnabled": "string",
26 | "maxEntities": "string"
27 | },
28 | "identifyLabelsdff85f2a": {
29 | "region": "string",
30 | "type": "string"
31 | },
32 | "speechGenerator3e6f2f09": {
33 | "region": "string",
34 | "language": "string",
35 | "voice": "string"
36 | }
37 | },
38 | "api": {
39 | "ionicpredictions": {
40 | "GraphQLAPIIdOutput": "string",
41 | "GraphQLAPIEndpointOutput": "string"
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Amplify Predictions
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/amplify/backend/backend-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth": {
3 | "phototranslate1a58bd43": {
4 | "service": "Cognito",
5 | "providerPlugin": "awscloudformation"
6 | }
7 | },
8 | "predictions": {
9 | "identifyText360eb9c4": {
10 | "providerPlugin": "awscloudformation",
11 | "service": "Rekognition",
12 | "dependsOn": [],
13 | "identifyType": "identifyText"
14 | },
15 | "translateTextd8fbec95": {
16 | "providerPlugin": "awscloudformation",
17 | "service": "Translate",
18 | "convertType": "translateText"
19 | },
20 | "identifyEntitiesd04d1d1d": {
21 | "providerPlugin": "awscloudformation",
22 | "service": "Rekognition",
23 | "dependsOn": [],
24 | "identifyType": "identifyEntities"
25 | },
26 | "identifyLabelsdff85f2a": {
27 | "providerPlugin": "awscloudformation",
28 | "service": "Rekognition",
29 | "dependsOn": [],
30 | "identifyType": "identifyLabels"
31 | },
32 | "speechGenerator3e6f2f09": {
33 | "providerPlugin": "awscloudformation",
34 | "service": "Polly",
35 | "convertType": "speechGenerator"
36 | }
37 | },
38 | "hosting": {},
39 | "api": {
40 | "ionicpredictions": {
41 | "service": "AppSync",
42 | "providerPlugin": "awscloudformation",
43 | "output": {
44 | "authConfig": {
45 | "additionalAuthenticationProviders": [],
46 | "defaultAuthentication": {
47 | "authenticationType": "API_KEY",
48 | "apiKeyConfig": {
49 | "description": "",
50 | "apiKeyExpirationDays": "365"
51 | }
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Amplify Predictions PWA
2 |
3 | An Ionic app that uses [AWS Amplify Predictions](https://aws-amplify.github.io/docs/js/predictions) AI/ML services to:
4 |
5 | - Identify and Translate Text from an image using Amazon Transcribe
6 | - Identify Entities, Faces of Celebrities from an image using Amazon Rekognition
7 | - Identify and Label Entities in an image using Amazon Rekognition
8 | - Store locally & Sync settings with the cloud using Amplify DataStore and AWS AppSync
9 | - PWA Splash Screen and add to home screen Icon for both iOS and Android
10 |
11 | > This app was demoed at re:Invent 2019, here is the talk/demo: https://t.co/bPEFO9Etww?amp=1
12 |
13 |
14 |
15 | 
16 |
17 |
18 |
19 | ## Requirements
20 |
21 | - [Amplify CLI](https://docs.amplify.aws/cli/start/install/) `npm i -g @aws-amplify/cli`
22 | - Ionic `npm i -g @ionic/cli`
23 | - [AWS Account](https://portal.aws.amazon.com/billing/signup?redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start)
24 |
25 | ## Setup
26 |
27 | From the root of the project, run:
28 |
29 | ```bash
30 | $ npm i -g @aws-amplify/cli
31 | $ amplify configure
32 | $ cd IonicPredictions && npm install
33 | $ amplify init
34 | ```
35 |
36 | Choose a name for your environment i.e. "dev", then run `amplify push` to create the backend, then run `npm start` to serve the application.
37 |
38 | ## Hosting
39 |
40 | To add hosting to your PWA:
41 |
42 | ```
43 | $ amplify add hosting
44 | $ amplify push
45 | ```
46 |
47 | *Or you can connect the AWS Amplify Console and provision using git.
48 |
--------------------------------------------------------------------------------
/amplify/cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "features": {
3 | "graphqltransformer": {
4 | "addmissingownerfields": false,
5 | "improvepluralization": false,
6 | "validatetypenamereservedwords": true,
7 | "useexperimentalpipelinedtransformer": false,
8 | "enableiterativegsiupdates": false,
9 | "secondarykeyasgsi": false,
10 | "skipoverridemutationinputtypes": false,
11 | "transformerversion": 1,
12 | "suppressschemamigrationprompt": true
13 | },
14 | "frontend-ios": {
15 | "enablexcodeintegration": false
16 | },
17 | "auth": {
18 | "enablecaseinsensitivity": false,
19 | "useinclusiveterminology": false,
20 | "breakcirculardependency": false,
21 | "forcealiasattributes": false,
22 | "useenabledmfas": false
23 | },
24 | "codegen": {
25 | "useappsyncmodelgenplugin": false,
26 | "usedocsgeneratorplugin": false,
27 | "usetypesgeneratorplugin": false,
28 | "cleangeneratedmodelsdirectory": false,
29 | "retaincasestyle": false,
30 | "addtimestampfields": false,
31 | "handlelistnullabilitytransparently": false,
32 | "emitauthprovider": false,
33 | "generateindexrules": false,
34 | "enabledartnullsafety": false
35 | },
36 | "appsync": {
37 | "generategraphqlpermissions": false
38 | },
39 | "latestregionsupport": {
40 | "pinpoint": 0,
41 | "translate": 0,
42 | "transcribe": 0,
43 | "rekognition": 0,
44 | "textract": 0,
45 | "comprehend": 0
46 | },
47 | "project": {
48 | "overrides": true
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/amplify/backend/api/ionicpredictions/stacks/CustomResources.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "An auto-generated nested stack.",
4 | "Metadata": {},
5 | "Parameters": {
6 | "AppSyncApiId": {
7 | "Type": "String",
8 | "Description": "The id of the AppSync API associated with this project."
9 | },
10 | "AppSyncApiName": {
11 | "Type": "String",
12 | "Description": "The name of the AppSync API",
13 | "Default": "AppSyncSimpleTransform"
14 | },
15 | "env": {
16 | "Type": "String",
17 | "Description": "The environment name. e.g. Dev, Test, or Production",
18 | "Default": "NONE"
19 | },
20 | "S3DeploymentBucket": {
21 | "Type": "String",
22 | "Description": "The S3 bucket containing all deployment assets for the project."
23 | },
24 | "S3DeploymentRootKey": {
25 | "Type": "String",
26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory."
27 | }
28 | },
29 | "Resources": {
30 | "EmptyResource": {
31 | "Type": "Custom::EmptyResource",
32 | "Condition": "AlwaysFalse"
33 | }
34 | },
35 | "Conditions": {
36 | "HasEnvironmentParameter": {
37 | "Fn::Not": [
38 | {
39 | "Fn::Equals": [
40 | {
41 | "Ref": "env"
42 | },
43 | "NONE"
44 | ]
45 | }
46 | ]
47 | },
48 | "AlwaysFalse": {
49 | "Fn::Equals": [
50 | "true",
51 | "false"
52 | ]
53 | }
54 | },
55 | "Outputs": {
56 | "EmptyOutput": {
57 | "Description": "An empty output. You may delete this if you have at least one resource above.",
58 | "Value": ""
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/app/label/label.page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Label Entities
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 | 50" [ngStyle]="{'color': label.color}">
23 | {{label.name}}
24 |
25 |
26 |
27 |
28 | No Entities Found
29 |
30 |
31 |
32 |
33 |
34 | Click below to select or take a photo
35 |
36 |
37 |
38 |
39 |
40 |
41 | Label real world objects
42 | Take or Choose a Photo
43 |
44 |
--------------------------------------------------------------------------------
/src/app/tabs/tabs.router.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 | import { TabsPage } from './tabs.page';
4 |
5 | const routes: Routes = [
6 | {
7 | path: 'tabs',
8 | component: TabsPage,
9 | children: [
10 | {
11 | path: 'tab1',
12 | children: [
13 | {
14 | path: '',
15 | loadChildren: () =>
16 | import('../translate/translate.module').then(m => m.TranslatePageModule)
17 | }
18 | ]
19 | },
20 | {
21 | path: 'tab2',
22 | children: [
23 | {
24 | path: '',
25 | loadChildren: () =>
26 | import('../settings/settings.module').then(m => m.SettingsPageModule)
27 | }
28 | ]
29 | },
30 | {
31 | path: 'tab3',
32 | children: [
33 | {
34 | path: '',
35 | loadChildren: () =>
36 | import('../identify/identify.module').then(m => m.IdentifyPageModule)
37 | }
38 | ]
39 | },
40 | {
41 | path: 'tab4',
42 | children: [
43 | {
44 | path: '',
45 | loadChildren: () =>
46 | import('../label/label.module').then(m => m.LabelPageModule)
47 | }
48 | ]
49 | },
50 | {
51 | path: '',
52 | redirectTo: '/tabs/tab1',
53 | pathMatch: 'full'
54 | }
55 | ]
56 | },
57 | {
58 | path: '',
59 | redirectTo: '/tabs/tab1',
60 | pathMatch: 'full'
61 | }
62 | ];
63 |
64 | @NgModule({
65 | imports: [RouterModule.forChild(routes)],
66 | exports: [RouterModule]
67 | })
68 | export class TabsPageRoutingModule {}
69 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2 | import { TestBed, async } from '@angular/core/testing';
3 |
4 | import { Platform } from '@ionic/angular';
5 | import { SplashScreen } from '@ionic-native/splash-screen/ngx';
6 | import { StatusBar } from '@ionic-native/status-bar/ngx';
7 |
8 | import { AppComponent } from './app.component';
9 |
10 | describe('AppComponent', () => {
11 |
12 | let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy;
13 |
14 | beforeEach(async(() => {
15 | statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']);
16 | splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']);
17 | platformReadySpy = Promise.resolve();
18 | platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy });
19 |
20 | TestBed.configureTestingModule({
21 | declarations: [AppComponent],
22 | schemas: [CUSTOM_ELEMENTS_SCHEMA],
23 | providers: [
24 | { provide: StatusBar, useValue: statusBarSpy },
25 | { provide: SplashScreen, useValue: splashScreenSpy },
26 | { provide: Platform, useValue: platformSpy },
27 | ],
28 | }).compileComponents();
29 | }));
30 |
31 | it('should create the app', () => {
32 | const fixture = TestBed.createComponent(AppComponent);
33 | const app = fixture.debugElement.componentInstance;
34 | expect(app).toBeTruthy();
35 | });
36 |
37 | it('should initialize the app', async () => {
38 | TestBed.createComponent(AppComponent);
39 | expect(platformSpy.ready).toHaveBeenCalled();
40 | await platformReadySpy;
41 | expect(statusBarSpy.styleDefault).toHaveBeenCalled();
42 | expect(splashScreenSpy.hide).toHaveBeenCalled();
43 | });
44 |
45 | // TODO: add more tests!
46 |
47 | });
48 |
--------------------------------------------------------------------------------
/src/app/settings/settings.page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Settings
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Settings default to the values defined in your ./src/aws-exports.js file.
18 | These values are automatically generated by the Amplify CLI.
19 |
20 |
21 |
22 | Translation Settings
23 |
24 |
25 | Source Language
26 |
27 | {{lang.name}}
28 |
29 |
30 |
31 |
32 | Target Language
33 |
34 | {{lang.name}}
35 |
36 |
37 |
38 |
39 |
40 |
41 | Detect Entity Settings
42 |
43 |
44 |
45 | Celebrity Detection
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/app/identify/identify.page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Identify Entities
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 | {{entity.metadata.name || entity.metadata.confidence}}
26 |
27 |
28 |
29 |
30 | No {{ (celebDetect)?'Celebrity':'' }} Faces Found
31 |
32 |
33 |
34 |
35 |
36 | Click below to select or take a photo
37 |
38 |
39 |
40 |
41 |
42 |
43 | Celebrity detection
44 | ENABLED
45 | DISABLED
46 |
47 | Take or Choose a Photo
48 |
--------------------------------------------------------------------------------
/amplify/backend/auth/phototranslate1a58bd43/parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "identityPoolName": "phototranslate1a58bd43_identitypool_1a58bd43",
3 | "allowUnauthenticatedIdentities": true,
4 | "openIdLambdaRoleName": "photot1a58bd43_openid_lambda_role",
5 | "resourceNameTruncated": "photot1a58bd43",
6 | "userPoolName": "phototranslate1a58bd43_userpool_1a58bd43",
7 | "autoVerifiedAttributes": [
8 | "email"
9 | ],
10 | "mfaConfiguration": "OFF",
11 | "mfaTypes": [
12 | "SMS Text Message"
13 | ],
14 | "smsAuthenticationMessage": "Your authentication code is {####}",
15 | "smsVerificationMessage": "Your verification code is {####}",
16 | "emailVerificationSubject": "Your verification code",
17 | "emailVerificationMessage": "Your verification code is {####}",
18 | "defaultPasswordPolicy": false,
19 | "passwordPolicyMinLength": 8,
20 | "passwordPolicyCharacters": [],
21 | "requiredAttributes": [
22 | "email"
23 | ],
24 | "userpoolClientName": "photot1a58bd43_app_client",
25 | "userpoolClientGenerateSecret": true,
26 | "userpoolClientRefreshTokenValidity": 30,
27 | "userpoolClientWriteAttributes": [
28 | "email"
29 | ],
30 | "userpoolClientReadAttributes": [
31 | "email"
32 | ],
33 | "mfaLambdaRole": "photot1a58bd43_totp_lambda_role",
34 | "userpoolClientLambdaRole": "photot1a58bd43_userpoolclient_lambda_role",
35 | "userpoolClientSetAttributes": false,
36 | "resourceName": "phototranslate1a58bd43",
37 | "authSelections": "identityPoolAndUserPool",
38 | "authRoleName": {
39 | "Ref": "AuthRoleName"
40 | },
41 | "unauthRoleName": {
42 | "Ref": "UnauthRoleName"
43 | },
44 | "authRoleArn": {
45 | "Fn::GetAtt": [
46 | "AuthRole",
47 | "Arn"
48 | ]
49 | },
50 | "unauthRoleArn": {
51 | "Fn::GetAtt": [
52 | "UnauthRole",
53 | "Arn"
54 | ]
55 | },
56 | "useDefault": "default",
57 | "usernameAttributes": [
58 | "phone_number"
59 | ],
60 | "additionalQuestions": [],
61 | "dependsOn": []
62 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "photo-translate",
3 | "version": "0.0.1",
4 | "author": "Ionic Framework",
5 | "homepage": "https://ionicframework.com/",
6 | "scripts": {
7 | "ng": "ng",
8 | "start": "ng serve",
9 | "build": "ionic build --prod --service-worker",
10 | "test": "ng test",
11 | "lint": "ng lint",
12 | "e2e": "ng e2e"
13 | },
14 | "private": true,
15 | "dependencies": {
16 | "@angular/common": "^9.0.0",
17 | "@angular/compiler": "^9.0.0",
18 | "@angular/core": "^9.0.0",
19 | "@angular/forms": "^9.0.0",
20 | "@angular/platform-browser": "^9.0.0",
21 | "@angular/platform-browser-dynamic": "^9.0.0",
22 | "@angular/pwa": "^0.802.1",
23 | "@angular/router": "^9.0.0",
24 | "@angular/service-worker": "^9.0.0",
25 | "@aws-amplify/datastore": "^2.1.0",
26 | "@ionic-native/core": "^5.0.0",
27 | "@ionic-native/splash-screen": "^5.0.0",
28 | "@ionic-native/status-bar": "^5.0.0",
29 | "@ionic/angular": "^5.0.0",
30 | "aws-amplify": "^3.0.11",
31 | "core-js": "^2.5.4",
32 | "rxjs": "~6.5.1",
33 | "tslib": "^1.9.0",
34 | "zone.js": "~0.9.1"
35 | },
36 | "devDependencies": {
37 | "@angular-devkit/architect": "^0.900.1",
38 | "@angular-devkit/build-angular": "^0.900.2",
39 | "@angular-devkit/core": "^9.0.0",
40 | "@angular-devkit/schematics": "^9.0.0",
41 | "@angular/cli": "^9.0.0",
42 | "@angular/compiler": "^9.0.0",
43 | "@angular/compiler-cli": "^9.0.0",
44 | "@angular/language-service": "^9.0.0",
45 | "@ionic/angular-toolkit": "~2.0.0",
46 | "@types/jasmine": "~3.3.8",
47 | "@types/jasminewd2": "~2.0.3",
48 | "@types/node": "^14.0.1",
49 | "codelyzer": "^5.0.0",
50 | "jasmine-core": "~3.4.0",
51 | "jasmine-spec-reporter": "~4.2.1",
52 | "karma": "~4.1.0",
53 | "karma-chrome-launcher": "~2.2.0",
54 | "karma-coverage-istanbul-reporter": "~2.0.1",
55 | "karma-jasmine": "~2.0.1",
56 | "karma-jasmine-html-reporter": "^1.4.0",
57 | "protractor": "~5.4.0",
58 | "ts-node": "~7.0.0",
59 | "tslint": "~5.15.0",
60 | "typescript": "^3.6.4"
61 | },
62 | "description": "An Ionic project"
63 | }
64 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rulesDirectory": [
4 | "codelyzer"
5 | ],
6 | "rules": {
7 | "array-type": false,
8 | "arrow-parens": false,
9 | "deprecation": {
10 | "severity": "warn"
11 | },
12 | "import-blacklist": [
13 | true,
14 | "rxjs/Rx"
15 | ],
16 | "interface-name": false,
17 | "max-classes-per-file": false,
18 | "max-line-length": [
19 | true,
20 | 140
21 | ],
22 | "member-access": false,
23 | "member-ordering": [
24 | true,
25 | {
26 | "order": [
27 | "static-field",
28 | "instance-field",
29 | "static-method",
30 | "instance-method"
31 | ]
32 | }
33 | ],
34 | "no-consecutive-blank-lines": false,
35 | "no-console": [
36 | true,
37 | "debug",
38 | "info",
39 | "time",
40 | "timeEnd",
41 | "trace"
42 | ],
43 | "no-empty": false,
44 | "no-inferrable-types": [
45 | true,
46 | "ignore-params"
47 | ],
48 | "no-non-null-assertion": true,
49 | "no-redundant-jsdoc": true,
50 | "no-switch-case-fall-through": true,
51 | "no-use-before-declare": true,
52 | "no-var-requires": false,
53 | "object-literal-key-quotes": [
54 | true,
55 | "as-needed"
56 | ],
57 | "object-literal-sort-keys": false,
58 | "ordered-imports": false,
59 | "quotemark": [
60 | true,
61 | "single"
62 | ],
63 | "trailing-comma": false,
64 | "no-output-on-prefix": true,
65 | "no-inputs-metadata-property": true,
66 | "no-host-metadata-property": true,
67 | "no-input-rename": true,
68 | "no-output-rename": true,
69 | "use-lifecycle-interface": true,
70 | "use-pipe-transform-interface": true,
71 | "one-variable-per-declaration": false,
72 | "component-class-suffix": [true, "Page", "Component"],
73 | "directive-class-suffix": true,
74 | "directive-selector": [
75 | true,
76 | "attribute",
77 | "app",
78 | "camelCase"
79 | ],
80 | "component-selector": [
81 | true,
82 | "element",
83 | "app",
84 | "page",
85 | "kebab-case"
86 | ]
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/app/translate/translate.page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Translate
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 | {{entity.translatedText}}
22 |
23 |
24 |
25 | Speak {{sourceLang}}
26 |
27 |
28 | Speak {{targetLang}}
29 |
30 |
31 |
32 | No Text Found
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Click below to select or take a photo
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {{sourceLang}}
49 |
50 | {{targetLang}}
51 |
52 | Take or Choose a Photo
53 |
--------------------------------------------------------------------------------
/src/theme/variables.scss:
--------------------------------------------------------------------------------
1 | // Ionic Variables and Theming. For more info, please see:
2 | // http://ionicframework.com/docs/theming/
3 |
4 | /** Ionic CSS Variables **/
5 | :root {
6 | /** primary **/
7 | --ion-color-primary: #3880ff;
8 | --ion-color-primary-rgb: 56, 128, 255;
9 | --ion-color-primary-contrast: #ffffff;
10 | --ion-color-primary-contrast-rgb: 255, 255, 255;
11 | --ion-color-primary-shade: #3171e0;
12 | --ion-color-primary-tint: #4c8dff;
13 |
14 | /** secondary **/
15 | --ion-color-secondary: #0cd1e8;
16 | --ion-color-secondary-rgb: 12, 209, 232;
17 | --ion-color-secondary-contrast: #ffffff;
18 | --ion-color-secondary-contrast-rgb: 255, 255, 255;
19 | --ion-color-secondary-shade: #0bb8cc;
20 | --ion-color-secondary-tint: #24d6ea;
21 |
22 | /** tertiary **/
23 | --ion-color-tertiary: #7044ff;
24 | --ion-color-tertiary-rgb: 112, 68, 255;
25 | --ion-color-tertiary-contrast: #ffffff;
26 | --ion-color-tertiary-contrast-rgb: 255, 255, 255;
27 | --ion-color-tertiary-shade: #633ce0;
28 | --ion-color-tertiary-tint: #7e57ff;
29 |
30 | /** success **/
31 | --ion-color-success: #10dc60;
32 | --ion-color-success-rgb: 16, 220, 96;
33 | --ion-color-success-contrast: #ffffff;
34 | --ion-color-success-contrast-rgb: 255, 255, 255;
35 | --ion-color-success-shade: #0ec254;
36 | --ion-color-success-tint: #28e070;
37 |
38 | /** warning **/
39 | --ion-color-warning: #ffce00;
40 | --ion-color-warning-rgb: 255, 206, 0;
41 | --ion-color-warning-contrast: #ffffff;
42 | --ion-color-warning-contrast-rgb: 255, 255, 255;
43 | --ion-color-warning-shade: #e0b500;
44 | --ion-color-warning-tint: #ffd31a;
45 |
46 | /** danger **/
47 | --ion-color-danger: #f04141;
48 | --ion-color-danger-rgb: 245, 61, 61;
49 | --ion-color-danger-contrast: #ffffff;
50 | --ion-color-danger-contrast-rgb: 255, 255, 255;
51 | --ion-color-danger-shade: #d33939;
52 | --ion-color-danger-tint: #f25454;
53 |
54 | /** dark **/
55 | --ion-color-dark: #222428;
56 | --ion-color-dark-rgb: 34, 34, 34;
57 | --ion-color-dark-contrast: #ffffff;
58 | --ion-color-dark-contrast-rgb: 255, 255, 255;
59 | --ion-color-dark-shade: #1e2023;
60 | --ion-color-dark-tint: #383a3e;
61 |
62 | /** medium **/
63 | --ion-color-medium: #989aa2;
64 | --ion-color-medium-rgb: 152, 154, 162;
65 | --ion-color-medium-contrast: #ffffff;
66 | --ion-color-medium-contrast-rgb: 255, 255, 255;
67 | --ion-color-medium-shade: #86888f;
68 | --ion-color-medium-tint: #a2a4ab;
69 |
70 | /** light **/
71 | --ion-color-light: #f4f5f8;
72 | --ion-color-light-rgb: 244, 244, 244;
73 | --ion-color-light-contrast: #000000;
74 | --ion-color-light-contrast-rgb: 0, 0, 0;
75 | --ion-color-light-shade: #d7d8da;
76 | --ion-color-light-tint: #f5f6f9;
77 | }
78 |
--------------------------------------------------------------------------------
/src/global.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * App Global CSS
3 | * ----------------------------------------------------------------------------
4 | * Put style rules here that you want to apply globally. These styles are for
5 | * the entire app and not just one component. Additionally, this file can be
6 | * used as an entry point to import other CSS/Sass files to be included in the
7 | * output CSS.
8 | * For more information on global stylesheets, visit the documentation:
9 | * https://ionicframework.com/docs/layout/global-stylesheets
10 | */
11 |
12 | /* Core CSS required for Ionic components to work properly */
13 | @import "~@ionic/angular/css/core.css";
14 |
15 | /* Basic CSS for apps built with Ionic */
16 | @import "~@ionic/angular/css/normalize.css";
17 | @import "~@ionic/angular/css/structure.css";
18 | @import "~@ionic/angular/css/typography.css";
19 | @import '~@ionic/angular/css/display.css';
20 |
21 | /* Optional CSS utils that can be commented out */
22 | @import "~@ionic/angular/css/padding.css";
23 | @import "~@ionic/angular/css/float-elements.css";
24 | @import "~@ionic/angular/css/text-alignment.css";
25 | @import "~@ionic/angular/css/text-transformation.css";
26 | @import "~@ionic/angular/css/flex-utils.css";
27 |
28 | :root {
29 | --ion-color-primary: #feae32;
30 | --ion-color-primary-rgb: 254,174,50;
31 | --ion-color-primary-contrast: #000000;
32 | --ion-color-primary-contrast-rgb: 0,0,0;
33 | --ion-color-primary-shade: #e0992c;
34 | --ion-color-primary-tint: #feb647;
35 |
36 | --ion-color-secondary: #232F3E;
37 | --ion-color-secondary-rgb: 35,47,62;
38 | --ion-color-secondary-contrast: #ffffff;
39 | --ion-color-secondary-contrast-rgb: 255,255,255;
40 | --ion-color-secondary-shade: #1f2937;
41 | --ion-color-secondary-tint: #394451;
42 | }
43 |
44 | .ion-color-primary {
45 | --ion-color-base: var(--ion-color-primary);
46 | --ion-color-base-rgb: var(--ion-color-primary-rgb);
47 | --ion-color-contrast: var(--ion-color-primary-contrast);
48 | --ion-color-contrast-rgb: var(--ion-color-primary-contrast-rgb);
49 | --ion-color-shade: var(--ion-color-primary-shade);
50 | --ion-color-tint: var(--ion-color-primary-tint);
51 | }
52 |
53 | .ion-color-secondary {
54 | --ion-color-base: var(--ion-color-secondary);
55 | --ion-color-base-rgb: var(--ion-color-secondary-rgb);
56 | --ion-color-contrast: var(--ion-color-secondary-contrast);
57 | --ion-color-contrast-rgb: var(--ion-color-secondary-contrast-rgb);
58 | --ion-color-shade: var(--ion-color-secondary-shade);
59 | --ion-color-tint: var(--ion-color-secondary-tint);
60 | }
61 |
62 | ion-item-option {
63 | color: white;
64 | }
65 |
66 | ion-toolbar {
67 | --background: #232F3E;
68 | --color: white;
69 | }
70 |
71 | ion-button {
72 | color: white;
73 | }
74 |
75 | .toolbar-background-color {
76 | --background: #feae32;
77 | }
78 |
79 | .custom-file-input {
80 | display: none;
81 | }
82 |
83 | textarea {
84 | width: 100%;
85 | }
86 |
87 | .toolbar-logo {
88 | height: 32px;
89 | }
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | (window as any).global = window;
2 | (window as any).process = {};
3 | /**
4 | * This file includes polyfills needed by Angular and is loaded before the app.
5 | * You can add your own extra polyfills to this file.
6 | *
7 | * This file is divided into 2 sections:
8 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
9 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
10 | * file.
11 | *
12 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
13 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
14 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
15 | *
16 | * Learn more in https://angular.io/guide/browser-support
17 | */
18 |
19 | /***************************************************************************************************
20 | * BROWSER POLYFILLS
21 | */
22 |
23 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
24 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
25 |
26 | /**
27 | * Web Animations `@angular/platform-browser/animations`
28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
30 | */
31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
32 |
33 | /**
34 | * By default, zone.js will patch all possible macroTask and DomEvents
35 | * user can disable parts of macroTask/DomEvents patch by setting following flags
36 | * because those flags need to be set before `zone.js` being loaded, and webpack
37 | * will put import in the top of bundle, so user need to create a separate file
38 | * in this directory (for example: zone-flags.ts), and put the following flags
39 | * into that file, and then add the following code before importing zone.js.
40 | * import './zone-flags.ts';
41 | *
42 | * The flags allowed in zone-flags.ts are listed here.
43 | *
44 | * The following flags will work for all browsers.
45 | *
46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
48 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
49 | *
50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
52 | *
53 | * (window as any).__Zone_enable_cross_context_check = true;
54 | *
55 | */
56 |
57 | import './zone-flags.ts';
58 |
59 | /***************************************************************************************************
60 | * Zone JS is required by default for Angular itself.
61 | */
62 |
63 | import 'zone.js/dist/zone'; // Included with Angular CLI.
64 |
65 |
66 | /***************************************************************************************************
67 | * APPLICATION IMPORTS
68 | */
69 |
--------------------------------------------------------------------------------
/src/app/label/label.page.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { LoadingController } from '@ionic/angular';
3 | import Predictions from '@aws-amplify/predictions';
4 |
5 | /**
6 | * Amplify Predicitons - Identify Labels
7 | * Identify real world objects in an image.
8 | */
9 | @Component({
10 | selector: 'app-label-tab',
11 | templateUrl: './label.page.html',
12 | styleUrls: ['./label.page.scss'],
13 | })
14 | export class LabelPage {
15 |
16 | public photo:string;
17 | public loading:any;
18 | public entities:Array;
19 |
20 | constructor( public loadingController: LoadingController ) { }
21 |
22 | /**
23 | * Fired when an image is uploaded or a photo is taken
24 | * via a mobile device camera
25 | * @param evt CustomEvent
26 | */
27 | public async onChoose(evt:any) {
28 | this.loading = await this.loadingController.create({
29 | message: 'Identifying...'
30 | });
31 | this.loading.present();
32 | let file = null;
33 | if (evt.target.files) {
34 | file = evt.target.files[0];
35 | }
36 | if (!file && evt.dataTranfer.files) {
37 | file = evt.dataTranfer.files[0];
38 | }
39 | if (!file) return;
40 | const context = this, reader = new FileReader();
41 | reader.onload = function(e) {
42 | const target: any = e.target;
43 | context.photo = target.result;
44 | };
45 | reader.readAsDataURL(file);
46 | Predictions.identify({
47 | labels: {
48 | source: {
49 | file,
50 | },
51 | // "LABELS" will detect objects , "UNSAFE" will detect if content is not safe, "ALL" will do both default on aws-exports.js
52 | type: "ALL"
53 | }
54 | }).then(result => {
55 | this.entities = result.labels;
56 | this.entities.forEach((entity) => {
57 | if (entity.boundingBoxes.length > 0) {
58 | let color = "#"+Math.floor(Math.random()*16777215).toString(16);
59 | entity.color = color;
60 | setTimeout(()=> {
61 | this.drawBoundingBoxes(entity.boundingBoxes, color);
62 | });
63 | }
64 | });
65 | this.loading.dismiss();
66 | }).catch(err => {
67 | console.log(JSON.stringify(err, null, 2));
68 | this.loading.dismiss();
69 | })
70 | }
71 |
72 | /**
73 | * Draw bounding boxes around the found entities, color
74 | * coding them per found entity based on the past in color
75 | * @param entities Array
76 | * @param color String
77 | */
78 | private drawBoundingBoxes(entities:any, color:string): void {
79 | let canvas = document.getElementById('imgLabelsCanvas') as HTMLCanvasElement;
80 | let ctx = canvas.getContext("2d");
81 | let img = document.getElementById("imgLabels") as HTMLImageElement;
82 | canvas.width = img.width;
83 | canvas.height = img.height;
84 | ctx.drawImage(img,0,0,img.width,img.height);
85 | img.hidden = true;
86 | let context = canvas.getContext('2d');
87 | entities.forEach(bb => {
88 | setTimeout(() => {
89 | let width = bb.width * img.width,
90 | height = bb.height * img.height,
91 | x = bb.left * img.width,
92 | y = bb.top * img.height
93 | context.beginPath();
94 | context.rect(x, y, width, height);
95 | context.lineWidth = 10;
96 | context.strokeStyle = color;
97 | context.stroke();
98 | });
99 | });
100 | canvas.setAttribute('style','width: 100%;');
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/amplify/backend/predictions/speechGenerator3e6f2f09/speechGenerator3e6f2f09-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "{\"createdOn\":\"Linux\",\"createdBy\":\"Amplify\",\"createdWith\":\"7.5.4\",\"stackType\":\"predictions-Polly\",\"metadata\":{}}",
4 | "Parameters": {
5 | "authRoleName": {
6 | "Type": "String"
7 | },
8 | "unauthRoleName": {
9 | "Type": "String"
10 | },
11 | "convertPolicyName": {
12 | "Type": "String"
13 | },
14 | "convertType": {
15 | "Type": "String"
16 | },
17 | "access": {
18 | "Type": "String"
19 | },
20 | "resourceName": {
21 | "Type": "String"
22 | },
23 | "language": {
24 | "Type": "String"
25 | },
26 | "voice": {
27 | "Type": "String"
28 | },
29 | "env": {
30 | "Type": "String"
31 | }
32 | },
33 | "Conditions": {
34 | "AuthGuestRoleAccess": {
35 | "Fn::Equals": [
36 | {
37 | "Ref": "access"
38 | },
39 | "authAndGuest"
40 | ]
41 | }
42 | },
43 | "Outputs": {
44 | "region": {
45 | "Value": {
46 | "Fn::FindInMap": [
47 | "RegionMapping",
48 | {
49 | "Ref": "AWS::Region"
50 | },
51 | {
52 | "Ref": "convertType"
53 | }
54 | ]
55 | }
56 | },
57 | "language": {
58 | "Value": {
59 | "Ref": "language"
60 | }
61 | },
62 | "voice": {
63 | "Value": {
64 | "Ref": "voice"
65 | }
66 | }
67 | },
68 | "Resources": {
69 | "PollyPolicy": {
70 | "Type": "AWS::IAM::Policy",
71 | "Properties": {
72 | "PolicyName": {
73 | "Ref": "convertPolicyName"
74 | },
75 | "Roles": {
76 | "Fn::If": [
77 | "AuthGuestRoleAccess",
78 | [
79 | {
80 | "Ref": "authRoleName"
81 | },
82 | {
83 | "Ref": "unauthRoleName"
84 | }
85 | ],
86 | [
87 | {
88 | "Ref": "authRoleName"
89 | }
90 | ]
91 | ]
92 | },
93 | "PolicyDocument": {
94 | "Version": "2012-10-17",
95 | "Statement": [
96 | {
97 | "Effect": "Allow",
98 | "Action": [
99 | "polly:SynthesizeSpeech"
100 | ],
101 | "Resource": "*"
102 | }
103 | ]
104 | }
105 | }
106 | }
107 | },
108 | "Mappings": {
109 | "RegionMapping": {
110 | "us-east-1": {
111 | "speechGenerator": "us-east-1"
112 | },
113 | "us-east-2": {
114 | "speechGenerator": "us-east-2"
115 | },
116 | "us-west-2": {
117 | "speechGenerator": "us-west-2"
118 | },
119 | "eu-west-1": {
120 | "speechGenerator": "eu-west-1"
121 | },
122 | "eu-west-2": {
123 | "speechGenerator": "eu-west-2"
124 | },
125 | "eu-central-1": {
126 | "speechGenerator": "eu-central-1"
127 | },
128 | "ap-northeast-1": {
129 | "speechGenerator": "ap-northeast-1"
130 | },
131 | "ap-northeast-2": {
132 | "speechGenerator": "ap-northeast-2"
133 | },
134 | "ap-southeast-1": {
135 | "speechGenerator": "ap-southeast-1"
136 | },
137 | "ap-southeast-2": {
138 | "speechGenerator": "ap-southeast-2"
139 | },
140 | "ap-south-1": {
141 | "speechGenerator": "ap-south-1"
142 | }
143 | }
144 | }
145 | }
--------------------------------------------------------------------------------
/amplify/backend/predictions/translateTextd8fbec95/translateTextd8fbec95-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "{\"createdOn\":\"Linux\",\"createdBy\":\"Amplify\",\"createdWith\":\"7.5.4\",\"stackType\":\"predictions-Translate\",\"metadata\":{}}",
4 | "Parameters": {
5 | "authRoleName": {
6 | "Type": "String"
7 | },
8 | "unauthRoleName": {
9 | "Type": "String"
10 | },
11 | "convertPolicyName": {
12 | "Type": "String"
13 | },
14 | "convertType": {
15 | "Type": "String"
16 | },
17 | "access": {
18 | "Type": "String"
19 | },
20 | "resourceName": {
21 | "Type": "String"
22 | },
23 | "sourceLang": {
24 | "Type": "String"
25 | },
26 | "targetLang": {
27 | "Type": "String"
28 | },
29 | "env": {
30 | "Type": "String"
31 | }
32 | },
33 | "Conditions": {
34 | "AuthGuestRoleAccess": {
35 | "Fn::Equals": [
36 | {
37 | "Ref": "access"
38 | },
39 | "authAndGuest"
40 | ]
41 | }
42 | },
43 | "Outputs": {
44 | "region": {
45 | "Value": {
46 | "Fn::FindInMap": [
47 | "RegionMapping",
48 | {
49 | "Ref": "AWS::Region"
50 | },
51 | {
52 | "Ref": "convertType"
53 | }
54 | ]
55 | }
56 | },
57 | "sourceLang": {
58 | "Value": {
59 | "Ref": "sourceLang"
60 | }
61 | },
62 | "targetLang": {
63 | "Value": {
64 | "Ref": "targetLang"
65 | }
66 | }
67 | },
68 | "Resources": {
69 | "TranslatePolicy": {
70 | "Type": "AWS::IAM::Policy",
71 | "Properties": {
72 | "PolicyName": {
73 | "Ref": "convertPolicyName"
74 | },
75 | "Roles": {
76 | "Fn::If": [
77 | "AuthGuestRoleAccess",
78 | [
79 | {
80 | "Ref": "authRoleName"
81 | },
82 | {
83 | "Ref": "unauthRoleName"
84 | }
85 | ],
86 | [
87 | {
88 | "Ref": "authRoleName"
89 | }
90 | ]
91 | ]
92 | },
93 | "PolicyDocument": {
94 | "Version": "2012-10-17",
95 | "Statement": [
96 | {
97 | "Effect": "Allow",
98 | "Action": [
99 | "translate:TranslateText"
100 | ],
101 | "Resource": "*"
102 | }
103 | ]
104 | }
105 | }
106 | }
107 | },
108 | "Mappings": {
109 | "RegionMapping": {
110 | "us-east-1": {
111 | "translateText": "us-east-1"
112 | },
113 | "us-east-2": {
114 | "translateText": "us-east-1"
115 | },
116 | "us-west-2": {
117 | "translateText": "us-west-2"
118 | },
119 | "eu-west-1": {
120 | "translateText": "eu-west-1"
121 | },
122 | "eu-west-2": {
123 | "translateText": "eu-west-1"
124 | },
125 | "eu-central-1": {
126 | "translateText": "eu-central-1"
127 | },
128 | "ap-northeast-1": {
129 | "translateText": "ap-northeast-1"
130 | },
131 | "ap-northeast-2": {
132 | "translateText": "ap-northeast-2"
133 | },
134 | "ap-southeast-1": {
135 | "translateText": "ap-southeast-1"
136 | },
137 | "ap-southeast-2": {
138 | "translateText": "ap-southeast-1"
139 | },
140 | "ap-south-1": {
141 | "translateText": "ap-south-1"
142 | }
143 | }
144 | }
145 | }
--------------------------------------------------------------------------------
/src/app/identify/identify.page.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import Predictions from '@aws-amplify/predictions';
3 | import { LoadingController } from '@ionic/angular';
4 | import { Hub } from '@aws-amplify/core';
5 | import awsconfig from 'src/aws-exports';
6 | import { LoggerService } from '../logger.service';
7 |
8 | /**
9 | * Amplify Predictions - Identify Entities
10 | * Identify entities from an image, optionally detecting
11 | * celebrities and displaying their name. If celebrity detection
12 | * is disabled, the confidence score will be rendered.
13 | */
14 | @Component({
15 | selector: 'app-identify-tab',
16 | templateUrl: 'identify.page.html',
17 | styleUrls: ['identify.page.scss']
18 | })
19 | export class IdentifyPage {
20 |
21 | public photo:string;
22 | public loading:any;
23 | public entities:Array;
24 | public celebDetect = awsconfig.predictions.identify.identifyEntities.celebrityDetectionEnabled;
25 |
26 | constructor(
27 | public loadingController: LoadingController,
28 | private logger: LoggerService ) {
29 | // Listen for settings changes via the settings UI
30 | Hub.listen('settings', (data) => {
31 | const { payload } = data;
32 | this.celebDetect = payload.data;
33 | });
34 | }
35 |
36 | /**
37 | * Fired when a image is selected, or a photo is taken
38 | * via a mobile device camera.
39 | * @param evt CustomEvent
40 | */
41 | public async onChoose(evt) {
42 | this.loading = await this.loadingController.create({
43 | message: 'Identifying...'
44 | });
45 | this.loading.present();
46 | let file = null;
47 | if (evt.target.files) {
48 | file = evt.target.files[0];
49 | }
50 | if (!file && evt.dataTranfer.files) {
51 | file = evt.dataTranfer.files[0];
52 | }
53 | if (!file) return;
54 | const context = this, reader = new FileReader();
55 | reader.onload = function(e) {
56 | const target: any = e.target;
57 | context.photo = target.result;
58 | };
59 | reader.readAsDataURL(file);
60 | Predictions.identify({
61 | entities: {
62 | source: {
63 | file
64 | },
65 | celebrityDetection: this.celebDetect
66 | }
67 | }).then(result => {
68 | this.logger.log('Predictions.identify', result);
69 | this.entities = result.entities;
70 | this.entities.forEach((entity) => entity.color = "#"+Math.floor(Math.random()*16777215).toString(16))
71 | setTimeout(()=> {
72 | this.drawBoundingBoxes(this.entities);
73 | });
74 | this.loading.dismiss();
75 | }).catch(err => {
76 | this.logger.error('Predictions.identify', err);
77 | this.loading.dismiss();
78 | })
79 | }
80 |
81 | /**
82 | * Draw bounding boxes around the entities that are found
83 | * using the boundingBox values returned from the service
84 | * @param entities Array
85 | */
86 | private drawBoundingBoxes(entities:any) {
87 | let canvas = document.getElementById('imgEntitiesCanvas') as HTMLCanvasElement;
88 | let ctx = canvas.getContext("2d");
89 | let img = document.getElementById("imgEntities") as HTMLImageElement;
90 | canvas.width = img.width;
91 | canvas.height = img.height;
92 | ctx.drawImage(img,0,0,img.width,img.height);
93 | img.hidden = true;
94 | let context = canvas.getContext('2d');
95 | entities.forEach(entity => {
96 | setTimeout(()=>{
97 | let bb = entity.boundingBox,
98 | width = bb.width * img.width,
99 | height = bb.height * img.height,
100 | x = bb.left * img.width,
101 | y = bb.top * img.height
102 | context.beginPath();
103 | context.rect(x, y, width, height);
104 | context.lineWidth = 10;
105 | context.strokeStyle = entity.color;
106 | context.stroke();
107 | });
108 | });
109 | canvas.setAttribute('style','width: 100%;');
110 | }
111 |
112 | /**
113 | * Toggles celebrity detection on/off and fires a Hub
114 | * event to change the value in the settings UI
115 | */
116 | toggleCelebDetect() {
117 | this.celebDetect = !this.celebDetect;
118 | Hub.dispatch(
119 | 'settings',
120 | {
121 | event: 'celebrityDetectionEnabled',
122 | data: this.celebDetect
123 | }
124 | )
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/amplify/backend/predictions/identifyLabelsdff85f2a/identifyLabelsdff85f2a-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "{\"createdOn\":\"Linux\",\"createdBy\":\"Amplify\",\"createdWith\":\"7.5.4\",\"stackType\":\"predictions-Rekognition\",\"metadata\":{}}",
4 | "Parameters": {
5 | "resourceName": {
6 | "Type": "String"
7 | },
8 | "identifyPolicyName": {
9 | "Type": "String"
10 | },
11 | "access": {
12 | "Type": "String"
13 | },
14 | "authRoleName": {
15 | "Type": "String"
16 | },
17 | "unauthRoleName": {
18 | "Type": "String"
19 | },
20 | "adminAuthProtected": {
21 | "Type": "String"
22 | },
23 | "adminGuestProtected": {
24 | "Type": "String"
25 | },
26 | "identifyType": {
27 | "Type": "String"
28 | },
29 | "type": {
30 | "Type": "String"
31 | },
32 | "env": {
33 | "Type": "String"
34 | }
35 | },
36 | "Conditions": {
37 | "CreateAdminAuthProtected": {
38 | "Fn::Not": [
39 | {
40 | "Fn::Equals": [
41 | {
42 | "Ref": "adminAuthProtected"
43 | },
44 | "DISALLOW"
45 | ]
46 | }
47 | ]
48 | },
49 | "CreateAdminGuestProtected": {
50 | "Fn::Not": [
51 | {
52 | "Fn::Equals": [
53 | {
54 | "Ref": "adminGuestProtected"
55 | },
56 | "DISALLOW"
57 | ]
58 | }
59 | ]
60 | },
61 | "ShouldNotCreateEnvResources": {
62 | "Fn::Equals": [
63 | {
64 | "Ref": "env"
65 | },
66 | "NONE"
67 | ]
68 | },
69 | "AuthGuestRoleAccess": {
70 | "Fn::Equals": [
71 | {
72 | "Ref": "access"
73 | },
74 | "authAndGuest"
75 | ]
76 | }
77 | },
78 | "Outputs": {
79 | "region": {
80 | "Value": {
81 | "Fn::FindInMap": [
82 | "RegionMapping",
83 | {
84 | "Ref": "AWS::Region"
85 | },
86 | {
87 | "Ref": "identifyType"
88 | }
89 | ]
90 | }
91 | },
92 | "type": {
93 | "Value": {
94 | "Ref": "type"
95 | }
96 | }
97 | },
98 | "Resources": {
99 | "identifyLabelssPolicy": {
100 | "Type": "AWS::IAM::Policy",
101 | "Properties": {
102 | "PolicyName": {
103 | "Ref": "identifyPolicyName"
104 | },
105 | "Roles": {
106 | "Fn::If": [
107 | "AuthGuestRoleAccess",
108 | [
109 | {
110 | "Ref": "authRoleName"
111 | },
112 | {
113 | "Ref": "unauthRoleName"
114 | }
115 | ],
116 | [
117 | {
118 | "Ref": "authRoleName"
119 | }
120 | ]
121 | ]
122 | },
123 | "PolicyDocument": {
124 | "Version": "2012-10-17",
125 | "Statement": [
126 | {
127 | "Effect": "Allow",
128 | "Action": [
129 | "rekognition:DetectLabels",
130 | "rekognition:DetectModerationLabels"
131 | ],
132 | "Resource": "*"
133 | }
134 | ]
135 | }
136 | }
137 | }
138 | },
139 | "Mappings": {
140 | "RegionMapping": {
141 | "us-east-1": {
142 | "identifyLabels": "us-east-1"
143 | },
144 | "us-east-2": {
145 | "identifyLabels": "us-east-2"
146 | },
147 | "us-west-2": {
148 | "identifyLabels": "us-west-2"
149 | },
150 | "eu-west-1": {
151 | "identifyLabels": "eu-west-1"
152 | },
153 | "eu-west-2": {
154 | "identifyLabels": "eu-west-2"
155 | },
156 | "eu-central-1": {
157 | "identifyLabels": "eu-central-1"
158 | },
159 | "ap-northeast-1": {
160 | "identifyLabels": "ap-northeast-1"
161 | },
162 | "ap-northeast-2": {
163 | "identifyLabels": "ap-northeast-2"
164 | },
165 | "ap-southeast-1": {
166 | "identifyLabels": "ap-southeast-1"
167 | },
168 | "ap-southeast-2": {
169 | "identifyLabels": "ap-southeast-2"
170 | },
171 | "ap-south-1": {
172 | "identifyLabels": "ap-south-1"
173 | }
174 | }
175 | }
176 | }
--------------------------------------------------------------------------------
/amplify/backend/predictions/identifyText360eb9c4/identifyText360eb9c4-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "{\"createdOn\":\"Linux\",\"createdBy\":\"Amplify\",\"createdWith\":\"7.5.4\",\"stackType\":\"predictions-Rekognition\",\"metadata\":{}}",
4 | "Parameters": {
5 | "resourceName": {
6 | "Type": "String"
7 | },
8 | "identifyPolicyName": {
9 | "Type": "String"
10 | },
11 | "access": {
12 | "Type": "String"
13 | },
14 | "authRoleName": {
15 | "Type": "String"
16 | },
17 | "unauthRoleName": {
18 | "Type": "String"
19 | },
20 | "adminAuthProtected": {
21 | "Type": "String"
22 | },
23 | "adminGuestProtected": {
24 | "Type": "String"
25 | },
26 | "identifyType": {
27 | "Type": "String"
28 | },
29 | "identifyDoc": {
30 | "Type": "String"
31 | },
32 | "format": {
33 | "Type": "String"
34 | },
35 | "env": {
36 | "Type": "String"
37 | }
38 | },
39 | "Conditions": {
40 | "CreateAdminAuthProtected": {
41 | "Fn::Not": [
42 | {
43 | "Fn::Equals": [
44 | {
45 | "Ref": "adminAuthProtected"
46 | },
47 | "DISALLOW"
48 | ]
49 | }
50 | ]
51 | },
52 | "CreateAdminGuestProtected": {
53 | "Fn::Not": [
54 | {
55 | "Fn::Equals": [
56 | {
57 | "Ref": "adminGuestProtected"
58 | },
59 | "DISALLOW"
60 | ]
61 | }
62 | ]
63 | },
64 | "ShouldNotCreateEnvResources": {
65 | "Fn::Equals": [
66 | {
67 | "Ref": "env"
68 | },
69 | "NONE"
70 | ]
71 | },
72 | "AuthGuestRoleAccess": {
73 | "Fn::Equals": [
74 | {
75 | "Ref": "access"
76 | },
77 | "authAndGuest"
78 | ]
79 | }
80 | },
81 | "Outputs": {
82 | "region": {
83 | "Value": {
84 | "Fn::FindInMap": [
85 | "RegionMapping",
86 | {
87 | "Ref": "AWS::Region"
88 | },
89 | {
90 | "Ref": "identifyType"
91 | }
92 | ]
93 | }
94 | },
95 | "format": {
96 | "Value": {
97 | "Ref": "format"
98 | }
99 | }
100 | },
101 | "Resources": {
102 | "IdentifyTextPolicy": {
103 | "Type": "AWS::IAM::Policy",
104 | "Properties": {
105 | "PolicyName": {
106 | "Ref": "identifyPolicyName"
107 | },
108 | "Roles": {
109 | "Fn::If": [
110 | "AuthGuestRoleAccess",
111 | [
112 | {
113 | "Ref": "authRoleName"
114 | },
115 | {
116 | "Ref": "unauthRoleName"
117 | }
118 | ],
119 | [
120 | {
121 | "Ref": "authRoleName"
122 | }
123 | ]
124 | ]
125 | },
126 | "PolicyDocument": {
127 | "Version": "2012-10-17",
128 | "Statement": [
129 | {
130 | "Effect": "Allow",
131 | "Action": [
132 | "rekognition:DetectText",
133 | "rekognition:DetectLabel"
134 | ],
135 | "Resource": "*"
136 | }
137 | ]
138 | }
139 | }
140 | }
141 | },
142 | "Mappings": {
143 | "RegionMapping": {
144 | "us-east-1": {
145 | "identifyText": "us-east-1"
146 | },
147 | "us-east-2": {
148 | "identifyText": "us-east-2"
149 | },
150 | "us-west-2": {
151 | "identifyText": "us-west-2"
152 | },
153 | "eu-west-1": {
154 | "identifyText": "eu-west-1"
155 | },
156 | "eu-west-2": {
157 | "identifyText": "eu-west-2"
158 | },
159 | "eu-central-1": {
160 | "identifyText": "eu-central-1"
161 | },
162 | "ap-northeast-1": {
163 | "identifyText": "ap-northeast-1"
164 | },
165 | "ap-northeast-2": {
166 | "identifyText": "ap-northeast-2"
167 | },
168 | "ap-southeast-1": {
169 | "identifyText": "ap-southeast-1"
170 | },
171 | "ap-southeast-2": {
172 | "identifyText": "ap-southeast-2"
173 | },
174 | "ap-south-1": {
175 | "identifyText": "ap-south-1"
176 | }
177 | }
178 | }
179 | }
--------------------------------------------------------------------------------
/amplify/backend/predictions/identifyEntitiesd04d1d1d/identifyEntitiesd04d1d1d-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSTemplateFormatVersion": "2010-09-09",
3 | "Description": "{\"createdOn\":\"Linux\",\"createdBy\":\"Amplify\",\"createdWith\":\"7.5.4\",\"stackType\":\"predictions-Rekognition\",\"metadata\":{}}",
4 | "Parameters": {
5 | "resourceName": {
6 | "Type": "String"
7 | },
8 | "identifyPolicyName": {
9 | "Type": "String"
10 | },
11 | "access": {
12 | "Type": "String"
13 | },
14 | "authRoleName": {
15 | "Type": "String"
16 | },
17 | "unauthRoleName": {
18 | "Type": "String"
19 | },
20 | "adminAuthProtected": {
21 | "Type": "String"
22 | },
23 | "adminGuestProtected": {
24 | "Type": "String"
25 | },
26 | "identifyType": {
27 | "Type": "String"
28 | },
29 | "celebrityDetectionEnabled": {
30 | "Type": "String"
31 | },
32 | "adminTask": {
33 | "Type": "String"
34 | },
35 | "maxEntities": {
36 | "Type": "Number"
37 | },
38 | "folderPolicies": {
39 | "Type": "String"
40 | },
41 | "env": {
42 | "Type": "String"
43 | }
44 | },
45 | "Conditions": {
46 | "CreateAdminAuthProtected": {
47 | "Fn::Not": [
48 | {
49 | "Fn::Equals": [
50 | {
51 | "Ref": "adminAuthProtected"
52 | },
53 | "DISALLOW"
54 | ]
55 | }
56 | ]
57 | },
58 | "CreateAdminGuestProtected": {
59 | "Fn::Not": [
60 | {
61 | "Fn::Equals": [
62 | {
63 | "Ref": "adminGuestProtected"
64 | },
65 | "DISALLOW"
66 | ]
67 | }
68 | ]
69 | },
70 | "ShouldNotCreateEnvResources": {
71 | "Fn::Equals": [
72 | {
73 | "Ref": "env"
74 | },
75 | "NONE"
76 | ]
77 | },
78 | "AuthGuestRoleAccess": {
79 | "Fn::Equals": [
80 | {
81 | "Ref": "access"
82 | },
83 | "authAndGuest"
84 | ]
85 | }
86 | },
87 | "Outputs": {
88 | "region": {
89 | "Value": {
90 | "Fn::FindInMap": [
91 | "RegionMapping",
92 | {
93 | "Ref": "AWS::Region"
94 | },
95 | {
96 | "Ref": "identifyType"
97 | }
98 | ]
99 | }
100 | },
101 | "celebrityDetectionEnabled": {
102 | "Value": {
103 | "Ref": "celebrityDetectionEnabled"
104 | }
105 | },
106 | "maxEntities": {
107 | "Value": {
108 | "Ref": "maxEntities"
109 | }
110 | }
111 | },
112 | "Resources": {
113 | "IdentifyEntitiesPolicy": {
114 | "Type": "AWS::IAM::Policy",
115 | "Properties": {
116 | "PolicyName": {
117 | "Ref": "identifyPolicyName"
118 | },
119 | "Roles": {
120 | "Fn::If": [
121 | "AuthGuestRoleAccess",
122 | [
123 | {
124 | "Ref": "authRoleName"
125 | },
126 | {
127 | "Ref": "unauthRoleName"
128 | }
129 | ],
130 | [
131 | {
132 | "Ref": "authRoleName"
133 | }
134 | ]
135 | ]
136 | },
137 | "PolicyDocument": {
138 | "Version": "2012-10-17",
139 | "Statement": [
140 | {
141 | "Effect": "Allow",
142 | "Action": [
143 | "rekognition:DetectFaces",
144 | "rekognition:RecognizeCelebrities"
145 | ],
146 | "Resource": "*"
147 | }
148 | ]
149 | }
150 | }
151 | }
152 | },
153 | "Mappings": {
154 | "RegionMapping": {
155 | "us-east-1": {
156 | "identifyEntities": "us-east-1"
157 | },
158 | "us-east-2": {
159 | "identifyEntities": "us-east-2"
160 | },
161 | "us-west-2": {
162 | "identifyEntities": "us-west-2"
163 | },
164 | "eu-west-1": {
165 | "identifyEntities": "eu-west-1"
166 | },
167 | "eu-west-2": {
168 | "identifyEntities": "eu-west-2"
169 | },
170 | "eu-central-1": {
171 | "identifyEntities": "eu-central-1"
172 | },
173 | "ap-northeast-1": {
174 | "identifyEntities": "ap-northeast-1"
175 | },
176 | "ap-northeast-2": {
177 | "identifyEntities": "ap-northeast-2"
178 | },
179 | "ap-southeast-1": {
180 | "identifyEntities": "ap-southeast-1"
181 | },
182 | "ap-southeast-2": {
183 | "identifyEntities": "ap-southeast-2"
184 | },
185 | "ap-south-1": {
186 | "identifyEntities": "ap-south-1"
187 | }
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/src/app/settings/settings.page.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import awsconfig from 'src/aws-exports';
3 | import { Hub } from '@aws-amplify/core';
4 | import { DataStore } from "@aws-amplify/datastore";
5 | import { Setting } from "src/models";
6 | import { DataService, Language } from '../data.service';
7 | /**
8 | * Amplify Predictions - Settings UI
9 | * Configure settings for the other tabs. Initial settings are
10 | * pulled from the aws-exports.js file, which is automatically
11 | * generated via the Amplify CLI
12 | */
13 | @Component({
14 | selector: 'app-settings-tab',
15 | templateUrl: 'settings.page.html',
16 | styleUrls: ['settings.page.scss']
17 | })
18 | export class SettingsPage {
19 |
20 | // Source language
21 | public defaultSource = awsconfig.predictions.convert.translateText.defaults.sourceLanguage;
22 | // Target language
23 | public defaultTarget = awsconfig.predictions.convert.translateText.defaults.targetLanguage;
24 | // Enable celebrity detection in identify
25 | public celebDetect = awsconfig.predictions.identify.identifyEntities.celebrityDetectionEnabled as boolean;
26 | // Supported translate languages
27 | public langs: Array;
28 |
29 | constructor(private data: DataService) {
30 | // supported translate languages
31 | this.langs = this.data.langs;
32 | // Listen for settings changed from other views
33 | Hub.listen('settings', (data) => {
34 | const { payload } = data;
35 | this.celebDetect = payload.data;
36 | });
37 | this.getSettings('celebDetect')
38 | .then((setting: Setting) => {
39 | if (setting) this.celebDetect = (setting.value) ? true : false;
40 | });
41 | this.getSettings('translateSource')
42 | .then((setting: Setting) => {
43 | if (setting) {
44 | this.defaultSource = setting.value;
45 | Hub.dispatch(
46 | 'settings',
47 | {
48 | event: 'source',
49 | data: setting.value
50 | });
51 | }
52 | });
53 | this.getSettings('translateTarget')
54 | .then((setting: Setting) => {
55 | if (setting) {
56 | this.defaultTarget = setting.value;
57 | Hub.dispatch(
58 | 'settings',
59 | {
60 | event: 'target',
61 | data: setting.value
62 | });
63 | }
64 | });
65 | }
66 |
67 | /**
68 | * Load a setting from local DataStore
69 | * @param name name of the setting to load from the datastore
70 | */
71 | private async getSettings(name: string): Promise {
72 | const setting = await DataStore.query(Setting, c => c.name('eq', name));
73 | return (setting) ? setting[0] : null;
74 | }
75 |
76 | /**
77 | * Select the source language to translate from
78 | * and dispatch a Hub event for other views to handle
79 | * @param evt CustomEvent
80 | */
81 | public selectSource(evt): void {
82 | this.defaultSource = evt.target.value;
83 | Hub.dispatch(
84 | 'settings',
85 | {
86 | event: 'source',
87 | data: evt.target.value
88 | });
89 | this.getSettings('translateSource')
90 | .then((setting: Setting) => {
91 | if (setting) {
92 | this.save(setting.name, evt.target.value, setting.id);
93 | } else {
94 | this.save('translateSource', evt.target.value);
95 | }
96 | });
97 | }
98 |
99 | /**
100 | * Select the target language to translate to and
101 | * dispatch Hub event for other views
102 | * @param evt CustomEvent
103 | */
104 | public selectTarget(evt): void {
105 | this.defaultTarget = evt.target.value;
106 | Hub.dispatch(
107 | 'settings',
108 | {
109 | event: 'target',
110 | data: evt.target.value
111 | });
112 | this.getSettings('translateTarget')
113 | .then((setting: Setting) => {
114 | if (setting) {
115 | this.save(setting.name, evt.target.value, setting.id);
116 | } else {
117 | this.save('translateTarget', evt.target.value);
118 | }
119 | });
120 | }
121 |
122 | /**
123 | * Toggle celebrity detection on/off for identify entities
124 | * @param evt CustomEvent
125 | */
126 | public toggleCelebDetect(evt): void {
127 | this.celebDetect = evt.detail.checked;
128 | Hub.dispatch(
129 | 'settings',
130 | {
131 | event: 'celebrityDetectionEnabled',
132 | data: evt.detail.checked
133 | }
134 | );
135 | this.getSettings('celebDetect')
136 | .then((setting: Setting) => {
137 | if (setting) {
138 | this.save(setting.name, evt.detail.checked, setting.id);
139 | } else {
140 | this.save('celebDetect', evt.detail.checked);
141 | }
142 | });
143 | }
144 |
145 | /**
146 | * Save a setting to the local datastore
147 | * @param name Setting name
148 | * @param value Setting value
149 | */
150 | private async save(name: string, value: string, id?: string) {
151 | if (id) {
152 | console.log('updating existing setting');
153 | const setting: Setting = await DataStore.query(Setting, id);
154 | await DataStore.save(Setting.copyOf(setting, updated => {
155 | updated.value = value;
156 | updated.name = setting.name;
157 | })
158 | );
159 | } else {
160 | console.log('saving new setting');
161 | await DataStore.save(new Setting({
162 | 'name': name,
163 | 'value': value
164 | }));
165 | }
166 | }
167 |
168 | }
169 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json",
3 | "version": 1,
4 | "defaultProject": "app",
5 | "newProjectRoot": "projects",
6 | "projects": {
7 | "app": {
8 | "root": "",
9 | "sourceRoot": "src",
10 | "projectType": "application",
11 | "prefix": "app",
12 | "schematics": {},
13 | "architect": {
14 | "build": {
15 | "builder": "@angular-devkit/build-angular:browser",
16 | "options": {
17 | "outputPath": "www",
18 | "index": "src/index.html",
19 | "main": "src/main.ts",
20 | "polyfills": "src/polyfills.ts",
21 | "tsConfig": "tsconfig.app.json",
22 | "assets": [
23 | {
24 | "glob": "**/*",
25 | "input": "src/assets",
26 | "output": "assets"
27 | },
28 | {
29 | "glob": "**/*.svg",
30 | "input": "node_modules/ionicons/dist/ionicons/svg",
31 | "output": "./svg"
32 | },
33 | "src/manifest.webmanifest",
34 | "src/pwacompat.js"
35 | ],
36 | "styles": [
37 | {
38 | "input": "src/theme/variables.scss"
39 | },
40 | {
41 | "input": "src/global.scss"
42 | }
43 | ],
44 | "scripts": []
45 | },
46 | "configurations": {
47 | "production": {
48 | "fileReplacements": [
49 | {
50 | "replace": "src/environments/environment.ts",
51 | "with": "src/environments/environment.prod.ts"
52 | }
53 | ],
54 | "optimization": true,
55 | "outputHashing": "all",
56 | "sourceMap": false,
57 | "extractCss": true,
58 | "namedChunks": false,
59 | "aot": true,
60 | "extractLicenses": true,
61 | "vendorChunk": false,
62 | "buildOptimizer": true,
63 | "budgets": [
64 | {
65 | "type": "initial",
66 | "maximumWarning": "2mb",
67 | "maximumError": "5mb"
68 | }
69 | ],
70 | "serviceWorker": true,
71 | "ngswConfigPath": "ngsw-config.json"
72 | },
73 | "ci": {
74 | "progress": false
75 | }
76 | }
77 | },
78 | "serve": {
79 | "builder": "@angular-devkit/build-angular:dev-server",
80 | "options": {
81 | "browserTarget": "app:build"
82 | },
83 | "configurations": {
84 | "production": {
85 | "browserTarget": "app:build:production"
86 | },
87 | "ci": {
88 | "progress": false
89 | }
90 | }
91 | },
92 | "extract-i18n": {
93 | "builder": "@angular-devkit/build-angular:extract-i18n",
94 | "options": {
95 | "browserTarget": "app:build"
96 | }
97 | },
98 | "test": {
99 | "builder": "@angular-devkit/build-angular:karma",
100 | "options": {
101 | "main": "src/test.ts",
102 | "polyfills": "src/polyfills.ts",
103 | "tsConfig": "tsconfig.spec.json",
104 | "karmaConfig": "karma.conf.js",
105 | "styles": [],
106 | "scripts": [],
107 | "assets": [
108 | {
109 | "glob": "favicon.ico",
110 | "input": "src/",
111 | "output": "/"
112 | },
113 | {
114 | "glob": "**/*",
115 | "input": "src/assets",
116 | "output": "/assets"
117 | },
118 | "src/manifest.webmanifest"
119 | ]
120 | },
121 | "configurations": {
122 | "ci": {
123 | "progress": false,
124 | "watch": false
125 | }
126 | }
127 | },
128 | "lint": {
129 | "builder": "@angular-devkit/build-angular:tslint",
130 | "options": {
131 | "tsConfig": [
132 | "tsconfig.app.json",
133 | "tsconfig.spec.json",
134 | "e2e/tsconfig.json"
135 | ],
136 | "exclude": ["**/node_modules/**"]
137 | }
138 | },
139 | "e2e": {
140 | "builder": "@angular-devkit/build-angular:protractor",
141 | "options": {
142 | "protractorConfig": "e2e/protractor.conf.js",
143 | "devServerTarget": "app:serve"
144 | },
145 | "configurations": {
146 | "production": {
147 | "devServerTarget": "app:serve:production"
148 | },
149 | "ci": {
150 | "devServerTarget": "app:serve:ci"
151 | }
152 | }
153 | },
154 | "ionic-cordova-build": {
155 | "builder": "@ionic/angular-toolkit:cordova-build",
156 | "options": {
157 | "browserTarget": "app:build"
158 | },
159 | "configurations": {
160 | "production": {
161 | "browserTarget": "app:build:production"
162 | }
163 | }
164 | },
165 | "ionic-cordova-serve": {
166 | "builder": "@ionic/angular-toolkit:cordova-serve",
167 | "options": {
168 | "cordovaBuildTarget": "app:ionic-cordova-build",
169 | "devServerTarget": "app:serve"
170 | },
171 | "configurations": {
172 | "production": {
173 | "cordovaBuildTarget": "app:ionic-cordova-build:production",
174 | "devServerTarget": "app:serve:production"
175 | }
176 | }
177 | }
178 | }
179 | }
180 | },
181 | "cli": {
182 | "defaultCollection": "@ionic/angular-toolkit"
183 | },
184 | "schematics": {
185 | "@ionic/angular-toolkit:component": {
186 | "styleext": "scss"
187 | },
188 | "@ionic/angular-toolkit:page": {
189 | "styleext": "scss"
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/app/translate/translate.page.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import Predictions from '@aws-amplify/predictions';
3 | import { LoadingController, ModalController } from '@ionic/angular';
4 | import { Hub } from '@aws-amplify/core';
5 | import awsconfig from 'src/aws-exports';
6 | import { LoggerService } from '../logger.service';
7 | import { DataStore, Predicates } from "@aws-amplify/datastore";
8 | import { Setting } from "src/models";
9 | import { Language, DataService } from '../data.service';
10 | import { PopoverController } from '@ionic/angular';
11 | import { LanguageSelectComponent } from './language-select/language-select.component';
12 |
13 | /**
14 | * Amplify Predictions - Translation
15 | * Settings are pulled from the aws-exports.js file and
16 | * can be changed via the Settings (tab2) UI.
17 | */
18 | @Component({
19 | selector: 'app-translate-tab',
20 | templateUrl: 'translate.page.html',
21 | styleUrls: ['translate.page.scss']
22 | })
23 | export class TranslatePage {
24 |
25 | public translatedText = "Choose or Take a Photo"
26 | public identifiedText:string;
27 | public photo:string;
28 | public loading:any;
29 | public entities: Array;
30 | public sourceLang = awsconfig.predictions.convert.translateText.defaults.sourceLanguage;
31 | public targetLang = awsconfig.predictions.convert.translateText.defaults.targetLanguage;
32 | public langs:Array;
33 |
34 | constructor(
35 | public loadingController: LoadingController,
36 | private logger: LoggerService,
37 | private data: DataService,
38 | private popoverController: PopoverController,
39 | public modalController: ModalController ) {
40 | this.langs = data.langs;
41 | // Listen for changes in settings from the settings view
42 | Hub.listen('settings', (data) => {
43 | const { payload } = data;
44 | if (payload.event === 'source')
45 | this.sourceLang = payload.data;
46 |
47 | if (payload.event === 'target')
48 | this.targetLang = payload.data;
49 | });
50 | DataStore.query(Setting, c => c.name('eq','translateSource'))
51 | .then((setting: Setting[]) => {
52 | if (setting && setting[0]) this.sourceLang = setting[0].value;
53 | });
54 | DataStore.query(Setting, c => c.name('eq','translateTarget'))
55 | .then((setting: Setting[]) => {
56 | if (setting && setting[0]) this.targetLang = setting[0].value;
57 | });
58 | }
59 |
60 | /**
61 | * Fired when a photo is chosen from the file inspector
62 | * or when a photo is taken via a mobile device. Will
63 | * initially identify text from an image, then will call
64 | * translate()
65 | * @param evt CustomEvent
66 | */
67 | public async onChoose(evt:any) {
68 | this.loading = await this.loadingController.create({
69 | message: 'Analyzing...'
70 | });
71 | this.translatedText = "";
72 | this.loading.present();
73 | let file = null;
74 | if (evt.target.files) {
75 | file = evt.target.files[0];
76 | }
77 | if (!file && evt.dataTransfer.files) {
78 | file = evt.dataTransfer.files[0];
79 | }
80 | if (!file) { return; }
81 | const context = this, reader = new FileReader();
82 | reader.onload = function(e) {
83 | const target: any = e.target;
84 | context.photo = target.result;
85 | };
86 | reader.readAsDataURL(file);
87 | // First, identify the text in the image
88 | Predictions.identify({
89 | text: {
90 | source: {
91 | file,
92 | },
93 | // Available options "PLAIN", "FORM", "TABLE", "ALL"
94 | format: "PLAIN",
95 | }
96 | }).then((result:any) => {
97 | this.logger.log('Predictions.identify',result);
98 | this.identifiedText = result.text.fullText;
99 | // Draw the bounding boxes
100 | this.entities = result.text.words;
101 | this.entities.forEach((entity) => {
102 | entity.color = "#"+Math.floor(Math.random()*16777215).toString(16)
103 | this.translate(entity);
104 | });
105 | this.loading.dismiss();
106 | setTimeout(()=> {
107 | this.drawBoundingBoxes(this.entities);
108 | });
109 | })
110 | .catch(err => {
111 | this.logger.log('Predictions.identify -> Error', err);
112 | this.loading.dismiss();
113 | })
114 | }
115 |
116 | /**
117 | * Translate the text returned from Predictions.identify
118 | * @param entity Object
119 | */
120 | private translate(entity:any): void {
121 | this.loading.message = "Translating..."
122 | Predictions.convert({
123 | translateText: {
124 | source: {
125 | text: entity.text,
126 | // defaults configured on aws-exports.js
127 | // update-able via the settings ui
128 | language : this.sourceLang
129 | // supported languages https://docs.aws.amazon.com/translate/latest/dg/how-it-works.html#how-it-works-language-codes
130 | },
131 | targetLanguage: this.targetLang
132 | }
133 | }).then(result => {
134 | this.logger.log('Predictions.convert', result);
135 | this.translatedText = result.text;
136 | entity.translatedText = this.translatedText;
137 | this.loading.dismiss();
138 | }).catch(err => {
139 | this.logger.error('Predictions.convert', err);
140 | this.loading.dismiss();
141 | })
142 | }
143 |
144 | generateTextToSpeech(text:string) {
145 | Predictions.convert({
146 | textToSpeech: {
147 | source: {
148 | text: text,
149 | },
150 | voiceId: "Amy" // default configured on aws-exports.js
151 | // list of different options are here https://docs.aws.amazon.com/polly/latest/dg/voicelist.html
152 | }
153 | }).then(result => {
154 | let AudioContext = (window as any).AudioContext || (window as any).webkitAudioContext;
155 | // console.log({ AudioContext });
156 | const audioCtx = new AudioContext();
157 | const source = audioCtx.createBufferSource();
158 | audioCtx.decodeAudioData(result.audioStream, (buffer) => {
159 | source.buffer = buffer;
160 | source.connect(audioCtx.destination);
161 | source.start(0);
162 | }, (err) => this.logger.error('audioCtx.decodeAudioData',err));
163 | }).catch(err => this.logger.error('Predictions.convert', err));
164 | }
165 |
166 | /**
167 | * Draw bounding boxes around the entities that are found
168 | * using the boundingBox values returned from the service
169 | * @param entities Array
170 | */
171 | private drawBoundingBoxes(entities:any) {
172 | let canvas = document.getElementById('imgTranslateCanvas') as HTMLCanvasElement;
173 | let ctx = canvas.getContext("2d");
174 | let img = document.getElementById("imgTranslate") as HTMLImageElement;
175 | canvas.width = img.width;
176 | canvas.height = img.height;
177 | ctx.drawImage(img,0,0,img.width,img.height);
178 | let context = canvas.getContext('2d');
179 | entities.forEach(entity => {
180 | setTimeout(()=>{
181 | try {
182 | let bb = entity.boundingBox,
183 | width = bb.width * img.width,
184 | height = bb.height * img.height,
185 | x = bb.left * img.width,
186 | y = bb.top * img.height
187 | context.beginPath();
188 | context.rect(x, y, width, height);
189 | context.lineWidth = 10;
190 | context.strokeStyle = entity.color;
191 | context.stroke();
192 | } catch(error) {
193 | this.logger.log('context.stroke', error);
194 | }
195 | });
196 | });
197 | img.hidden = true;
198 | canvas.setAttribute('style','width: 100%;');
199 | }
200 |
201 | public onSourceSelect(evt: any):void {
202 | this.presentModal('source');
203 | }
204 |
205 | public onTargetSelect(evt: any):void {
206 | this.presentModal('target');
207 | }
208 |
209 | /**
210 | * Show the popover for selecting a language
211 | * @param ev CustomEvent
212 | */
213 | public async presentPopover(ev: any) {
214 | const popover = await this.popoverController.create({
215 | component: LanguageSelectComponent,
216 | event: ev,
217 | translucent: true
218 | });
219 | return await popover.present();
220 | }
221 |
222 | /**
223 | * Present a modal for language selection
224 | * @param type String - type of selection i.e. source or target language
225 | */
226 | public async presentModal(type:string) {
227 | const modal = await this.modalController.create({
228 | component: LanguageSelectComponent,
229 | componentProps: {
230 | 'selected': (type === 'source')?this.sourceLang:this.targetLang,
231 | 'type': type
232 | }
233 | });
234 | return await modal.present();
235 | }
236 |
237 | /**
238 | * Copy text to the clipboard
239 | * @param textArea HTMLTextArea
240 | */
241 | public copyText(textArea:any) {
242 | textArea.select();
243 | (document as any).execCommand('copy');
244 | }
245 |
246 | }
247 |
--------------------------------------------------------------------------------
/src/app/API.service.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // This file was automatically generated and should not be edited.
4 | import { Injectable } from "@angular/core";
5 | import API, { graphqlOperation, GraphQLResult } from "@aws-amplify/api-graphql";
6 | import { Observable } from "zen-observable-ts";
7 |
8 | export interface SubscriptionResponse {
9 | value: GraphQLResult;
10 | }
11 |
12 | export type CreateSettingInput = {
13 | id?: string | null;
14 | name: string;
15 | value?: string | null;
16 | };
17 |
18 | export type ModelSettingConditionInput = {
19 | name?: ModelStringInput | null;
20 | value?: ModelStringInput | null;
21 | and?: Array | null;
22 | or?: Array | null;
23 | not?: ModelSettingConditionInput | null;
24 | };
25 |
26 | export type ModelStringInput = {
27 | ne?: string | null;
28 | eq?: string | null;
29 | le?: string | null;
30 | lt?: string | null;
31 | ge?: string | null;
32 | gt?: string | null;
33 | contains?: string | null;
34 | notContains?: string | null;
35 | between?: Array | null;
36 | beginsWith?: string | null;
37 | attributeExists?: boolean | null;
38 | attributeType?: ModelAttributeTypes | null;
39 | size?: ModelSizeInput | null;
40 | };
41 |
42 | export enum ModelAttributeTypes {
43 | binary = "binary",
44 | binarySet = "binarySet",
45 | bool = "bool",
46 | list = "list",
47 | map = "map",
48 | number = "number",
49 | numberSet = "numberSet",
50 | string = "string",
51 | stringSet = "stringSet",
52 | _null = "_null"
53 | }
54 |
55 | export type ModelSizeInput = {
56 | ne?: number | null;
57 | eq?: number | null;
58 | le?: number | null;
59 | lt?: number | null;
60 | ge?: number | null;
61 | gt?: number | null;
62 | between?: Array | null;
63 | };
64 |
65 | export type Setting = {
66 | __typename: "Setting";
67 | id?: string;
68 | name?: string;
69 | value?: string | null;
70 | createdAt?: string;
71 | updatedAt?: string;
72 | };
73 |
74 | export type UpdateSettingInput = {
75 | id: string;
76 | name?: string | null;
77 | value?: string | null;
78 | };
79 |
80 | export type DeleteSettingInput = {
81 | id: string;
82 | };
83 |
84 | export type ModelSettingFilterInput = {
85 | id?: ModelIDInput | null;
86 | name?: ModelStringInput | null;
87 | value?: ModelStringInput | null;
88 | and?: Array | null;
89 | or?: Array | null;
90 | not?: ModelSettingFilterInput | null;
91 | };
92 |
93 | export type ModelIDInput = {
94 | ne?: string | null;
95 | eq?: string | null;
96 | le?: string | null;
97 | lt?: string | null;
98 | ge?: string | null;
99 | gt?: string | null;
100 | contains?: string | null;
101 | notContains?: string | null;
102 | between?: Array | null;
103 | beginsWith?: string | null;
104 | attributeExists?: boolean | null;
105 | attributeType?: ModelAttributeTypes | null;
106 | size?: ModelSizeInput | null;
107 | };
108 |
109 | export type ModelSettingConnection = {
110 | __typename: "ModelSettingConnection";
111 | items?: Array;
112 | nextToken?: string | null;
113 | };
114 |
115 | export type CreateSettingMutation = {
116 | __typename: "Setting";
117 | id: string;
118 | name: string;
119 | value?: string | null;
120 | createdAt: string;
121 | updatedAt: string;
122 | };
123 |
124 | export type UpdateSettingMutation = {
125 | __typename: "Setting";
126 | id: string;
127 | name: string;
128 | value?: string | null;
129 | createdAt: string;
130 | updatedAt: string;
131 | };
132 |
133 | export type DeleteSettingMutation = {
134 | __typename: "Setting";
135 | id: string;
136 | name: string;
137 | value?: string | null;
138 | createdAt: string;
139 | updatedAt: string;
140 | };
141 |
142 | export type GetSettingQuery = {
143 | __typename: "Setting";
144 | id: string;
145 | name: string;
146 | value?: string | null;
147 | createdAt: string;
148 | updatedAt: string;
149 | };
150 |
151 | export type ListSettingsQuery = {
152 | __typename: "ModelSettingConnection";
153 | items: Array<{
154 | __typename: "Setting";
155 | id: string;
156 | name: string;
157 | value?: string | null;
158 | createdAt: string;
159 | updatedAt: string;
160 | }>;
161 | nextToken?: string | null;
162 | };
163 |
164 | export type OnCreateSettingSubscription = {
165 | __typename: "Setting";
166 | id: string;
167 | name: string;
168 | value?: string | null;
169 | createdAt: string;
170 | updatedAt: string;
171 | };
172 |
173 | export type OnUpdateSettingSubscription = {
174 | __typename: "Setting";
175 | id: string;
176 | name: string;
177 | value?: string | null;
178 | createdAt: string;
179 | updatedAt: string;
180 | };
181 |
182 | export type OnDeleteSettingSubscription = {
183 | __typename: "Setting";
184 | id: string;
185 | name: string;
186 | value?: string | null;
187 | createdAt: string;
188 | updatedAt: string;
189 | };
190 |
191 | @Injectable({
192 | providedIn: "root"
193 | })
194 | export class APIService {
195 | async CreateSetting(
196 | input: CreateSettingInput,
197 | condition?: ModelSettingConditionInput
198 | ): Promise {
199 | const statement = `mutation CreateSetting($input: CreateSettingInput!, $condition: ModelSettingConditionInput) {
200 | createSetting(input: $input, condition: $condition) {
201 | __typename
202 | id
203 | name
204 | value
205 | createdAt
206 | updatedAt
207 | }
208 | }`;
209 | const gqlAPIServiceArguments: any = {
210 | input
211 | };
212 | if (condition) {
213 | gqlAPIServiceArguments.condition = condition;
214 | }
215 | const response = (await API.graphql(
216 | graphqlOperation(statement, gqlAPIServiceArguments)
217 | )) as any;
218 | return response.data.createSetting;
219 | }
220 | async UpdateSetting(
221 | input: UpdateSettingInput,
222 | condition?: ModelSettingConditionInput
223 | ): Promise {
224 | const statement = `mutation UpdateSetting($input: UpdateSettingInput!, $condition: ModelSettingConditionInput) {
225 | updateSetting(input: $input, condition: $condition) {
226 | __typename
227 | id
228 | name
229 | value
230 | createdAt
231 | updatedAt
232 | }
233 | }`;
234 | const gqlAPIServiceArguments: any = {
235 | input
236 | };
237 | if (condition) {
238 | gqlAPIServiceArguments.condition = condition;
239 | }
240 | const response = (await API.graphql(
241 | graphqlOperation(statement, gqlAPIServiceArguments)
242 | )) as any;
243 | return response.data.updateSetting;
244 | }
245 | async DeleteSetting(
246 | input: DeleteSettingInput,
247 | condition?: ModelSettingConditionInput
248 | ): Promise {
249 | const statement = `mutation DeleteSetting($input: DeleteSettingInput!, $condition: ModelSettingConditionInput) {
250 | deleteSetting(input: $input, condition: $condition) {
251 | __typename
252 | id
253 | name
254 | value
255 | createdAt
256 | updatedAt
257 | }
258 | }`;
259 | const gqlAPIServiceArguments: any = {
260 | input
261 | };
262 | if (condition) {
263 | gqlAPIServiceArguments.condition = condition;
264 | }
265 | const response = (await API.graphql(
266 | graphqlOperation(statement, gqlAPIServiceArguments)
267 | )) as any;
268 | return response.data.deleteSetting;
269 | }
270 | async GetSetting(id: string): Promise {
271 | const statement = `query GetSetting($id: ID!) {
272 | getSetting(id: $id) {
273 | __typename
274 | id
275 | name
276 | value
277 | createdAt
278 | updatedAt
279 | }
280 | }`;
281 | const gqlAPIServiceArguments: any = {
282 | id
283 | };
284 | const response = (await API.graphql(
285 | graphqlOperation(statement, gqlAPIServiceArguments)
286 | )) as any;
287 | return response.data.getSetting;
288 | }
289 | async ListSettings(
290 | filter?: ModelSettingFilterInput,
291 | limit?: number,
292 | nextToken?: string
293 | ): Promise {
294 | const statement = `query ListSettings($filter: ModelSettingFilterInput, $limit: Int, $nextToken: String) {
295 | listSettings(filter: $filter, limit: $limit, nextToken: $nextToken) {
296 | __typename
297 | items {
298 | __typename
299 | id
300 | name
301 | value
302 | createdAt
303 | updatedAt
304 | }
305 | nextToken
306 | }
307 | }`;
308 | const gqlAPIServiceArguments: any = {};
309 | if (filter) {
310 | gqlAPIServiceArguments.filter = filter;
311 | }
312 | if (limit) {
313 | gqlAPIServiceArguments.limit = limit;
314 | }
315 | if (nextToken) {
316 | gqlAPIServiceArguments.nextToken = nextToken;
317 | }
318 | const response = (await API.graphql(
319 | graphqlOperation(statement, gqlAPIServiceArguments)
320 | )) as any;
321 | return response.data.listSettings;
322 | }
323 | OnCreateSettingListener: Observable<
324 | SubscriptionResponse
325 | > = API.graphql(
326 | graphqlOperation(
327 | `subscription OnCreateSetting {
328 | onCreateSetting {
329 | __typename
330 | id
331 | name
332 | value
333 | createdAt
334 | updatedAt
335 | }
336 | }`
337 | )
338 | ) as Observable>;
339 |
340 | OnUpdateSettingListener: Observable<
341 | SubscriptionResponse
342 | > = API.graphql(
343 | graphqlOperation(
344 | `subscription OnUpdateSetting {
345 | onUpdateSetting {
346 | __typename
347 | id
348 | name
349 | value
350 | createdAt
351 | updatedAt
352 | }
353 | }`
354 | )
355 | ) as Observable>;
356 |
357 | OnDeleteSettingListener: Observable<
358 | SubscriptionResponse
359 | > = API.graphql(
360 | graphqlOperation(
361 | `subscription OnDeleteSetting {
362 | onDeleteSetting {
363 | __typename
364 | id
365 | name
366 | value
367 | createdAt
368 | updatedAt
369 | }
370 | }`
371 | )
372 | ) as Observable>;
373 | }
374 |
--------------------------------------------------------------------------------
/amplify/backend/auth/phototranslate1a58bd43/phototranslate1a58bd43-cloudformation-template.yml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Parameters:
3 | env:
4 | Type: String
5 | authRoleName:
6 | Type: String
7 | unauthRoleName:
8 | Type: String
9 | authRoleArn:
10 | Type: String
11 | unauthRoleArn:
12 | Type: String
13 | identityPoolName:
14 | Type: String
15 | allowUnauthenticatedIdentities:
16 | Type: String
17 | openIdLambdaRoleName:
18 | Type: String
19 | resourceNameTruncated:
20 | Type: String
21 | userPoolName:
22 | Type: String
23 | autoVerifiedAttributes:
24 | Type: CommaDelimitedList
25 | mfaConfiguration:
26 | Type: String
27 | mfaTypes:
28 | Type: CommaDelimitedList
29 | smsAuthenticationMessage:
30 | Type: String
31 | smsVerificationMessage:
32 | Type: String
33 | emailVerificationSubject:
34 | Type: String
35 | emailVerificationMessage:
36 | Type: String
37 | defaultPasswordPolicy:
38 | Type: String
39 | passwordPolicyMinLength:
40 | Type: Number
41 | passwordPolicyCharacters:
42 | Type: CommaDelimitedList
43 | requiredAttributes:
44 | Type: CommaDelimitedList
45 | userpoolClientName:
46 | Type: String
47 | userpoolClientGenerateSecret:
48 | Type: String
49 | userpoolClientRefreshTokenValidity:
50 | Type: Number
51 | userpoolClientWriteAttributes:
52 | Type: CommaDelimitedList
53 | userpoolClientReadAttributes:
54 | Type: CommaDelimitedList
55 | mfaLambdaRole:
56 | Type: String
57 | userpoolClientLambdaRole:
58 | Type: String
59 | userpoolClientSetAttributes:
60 | Type: String
61 | resourceName:
62 | Type: String
63 | authSelections:
64 | Type: String
65 | useDefault:
66 | Type: String
67 | usernameAttributes:
68 | Type: CommaDelimitedList
69 | additionalQuestions:
70 | Type: CommaDelimitedList
71 | dependsOn:
72 | Type: CommaDelimitedList
73 | Conditions:
74 | ShouldNotCreateEnvResources:
75 | Fn::Equals:
76 | - Ref: env
77 | - NONE
78 | Resources:
79 | SNSRole:
80 | Type: AWS::IAM::Role
81 | Properties:
82 | RoleName:
83 | Fn::If:
84 | - ShouldNotCreateEnvResources
85 | - photot1a58bd43_sns-role
86 | - Fn::Join:
87 | - ''
88 | - - photot1a58bd43_sns-role
89 | - '-'
90 | - Ref: env
91 | AssumeRolePolicyDocument:
92 | Version: '2012-10-17'
93 | Statement:
94 | - Sid: ''
95 | Effect: Allow
96 | Principal:
97 | Service: cognito-idp.amazonaws.com
98 | Action:
99 | - sts:AssumeRole
100 | Condition:
101 | StringEquals:
102 | sts:ExternalId: photot1a58bd43_role_external_id
103 | Policies:
104 | - PolicyName: photot1a58bd43-sns-policy
105 | PolicyDocument:
106 | Version: '2012-10-17'
107 | Statement:
108 | - Effect: Allow
109 | Action:
110 | - sns:Publish
111 | Resource: '*'
112 | UserPool:
113 | Type: AWS::Cognito::UserPool
114 | UpdateReplacePolicy: Retain
115 | Properties:
116 | UserPoolName:
117 | Fn::If:
118 | - ShouldNotCreateEnvResources
119 | - Ref: userPoolName
120 | - Fn::Join:
121 | - ''
122 | - - Ref: userPoolName
123 | - '-'
124 | - Ref: env
125 | Schema:
126 | - Name: email
127 | Required: true
128 | Mutable: true
129 | AutoVerifiedAttributes:
130 | Ref: autoVerifiedAttributes
131 | EmailVerificationMessage:
132 | Ref: emailVerificationMessage
133 | EmailVerificationSubject:
134 | Ref: emailVerificationSubject
135 | Policies:
136 | PasswordPolicy:
137 | MinimumLength:
138 | Ref: passwordPolicyMinLength
139 | RequireLowercase: false
140 | RequireNumbers: false
141 | RequireSymbols: false
142 | RequireUppercase: false
143 | UsernameAttributes:
144 | Ref: usernameAttributes
145 | MfaConfiguration:
146 | Ref: mfaConfiguration
147 | SmsVerificationMessage:
148 | Ref: smsVerificationMessage
149 | SmsConfiguration:
150 | SnsCallerArn:
151 | Fn::GetAtt:
152 | - SNSRole
153 | - Arn
154 | ExternalId: photot1a58bd43_role_external_id
155 | UserPoolClientWeb:
156 | Type: AWS::Cognito::UserPoolClient
157 | Properties:
158 | ClientName: photot1a58bd43_app_clientWeb
159 | RefreshTokenValidity:
160 | Ref: userpoolClientRefreshTokenValidity
161 | UserPoolId:
162 | Ref: UserPool
163 | DependsOn: UserPool
164 | UserPoolClient:
165 | Type: AWS::Cognito::UserPoolClient
166 | Properties:
167 | ClientName:
168 | Ref: userpoolClientName
169 | GenerateSecret:
170 | Ref: userpoolClientGenerateSecret
171 | RefreshTokenValidity:
172 | Ref: userpoolClientRefreshTokenValidity
173 | UserPoolId:
174 | Ref: UserPool
175 | DependsOn: UserPool
176 | UserPoolClientRole:
177 | Type: AWS::IAM::Role
178 | Properties:
179 | RoleName:
180 | Fn::If:
181 | - ShouldNotCreateEnvResources
182 | - Ref: userpoolClientLambdaRole
183 | - Fn::Join:
184 | - ''
185 | - - Ref: userpoolClientLambdaRole
186 | - '-'
187 | - Ref: env
188 | AssumeRolePolicyDocument:
189 | Version: '2012-10-17'
190 | Statement:
191 | - Effect: Allow
192 | Principal:
193 | Service:
194 | - lambda.amazonaws.com
195 | Action:
196 | - sts:AssumeRole
197 | DependsOn: UserPoolClient
198 | UserPoolClientLambda:
199 | Type: AWS::Lambda::Function
200 | Properties:
201 | Code:
202 | ZipFile:
203 | Fn::Join:
204 | - ''
205 | - - const response = require('cfn-response');
206 | - const aws = require('aws-sdk');
207 | - const identity = new aws.CognitoIdentityServiceProvider();
208 | - exports.handler = (event, context, callback) => {
209 | - ' if (event.RequestType == ''Delete'') { '
210 | - ' response.send(event, context, response.SUCCESS, {})'
211 | - ' }'
212 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {'
213 | - ' const params = {'
214 | - ' ClientId: event.ResourceProperties.clientId,'
215 | - ' UserPoolId: event.ResourceProperties.userpoolId'
216 | - ' };'
217 | - ' identity.describeUserPoolClient(params).promise()'
218 | - ' .then((res) => {'
219 | - ' response.send(event, context, response.SUCCESS, {''appSecret'': res.UserPoolClient.ClientSecret});'
220 | - ' })'
221 | - ' .catch((err) => {'
222 | - ' response.send(event, context, response.FAILED, {err});'
223 | - ' });'
224 | - ' }'
225 | - '};'
226 | Handler: index.handler
227 | Runtime: nodejs12.x
228 | Timeout: '300'
229 | Role:
230 | Fn::GetAtt:
231 | - UserPoolClientRole
232 | - Arn
233 | DependsOn: UserPoolClientRole
234 | UserPoolClientLambdaPolicy:
235 | Type: AWS::IAM::Policy
236 | Properties:
237 | PolicyName: photot1a58bd43_userpoolclient_lambda_iam_policy
238 | Roles:
239 | - Fn::If:
240 | - ShouldNotCreateEnvResources
241 | - Ref: userpoolClientLambdaRole
242 | - Fn::Join:
243 | - ''
244 | - - Ref: userpoolClientLambdaRole
245 | - '-'
246 | - Ref: env
247 | PolicyDocument:
248 | Version: '2012-10-17'
249 | Statement:
250 | - Effect: Allow
251 | Action:
252 | - cognito-idp:DescribeUserPoolClient
253 | Resource:
254 | Fn::GetAtt:
255 | - UserPool
256 | - Arn
257 | DependsOn: UserPoolClientLambda
258 | UserPoolClientLogPolicy:
259 | Type: AWS::IAM::Policy
260 | Properties:
261 | PolicyName: photot1a58bd43_userpoolclient_lambda_log_policy
262 | Roles:
263 | - Fn::If:
264 | - ShouldNotCreateEnvResources
265 | - Ref: userpoolClientLambdaRole
266 | - Fn::Join:
267 | - ''
268 | - - Ref: userpoolClientLambdaRole
269 | - '-'
270 | - Ref: env
271 | PolicyDocument:
272 | Version: '2012-10-17'
273 | Statement:
274 | - Effect: Allow
275 | Action:
276 | - logs:CreateLogGroup
277 | - logs:CreateLogStream
278 | - logs:PutLogEvents
279 | Resource:
280 | Fn::Sub:
281 | - >-
282 | arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*
283 | - region:
284 | Ref: AWS::Region
285 | account:
286 | Ref: AWS::AccountId
287 | lambda:
288 | Ref: UserPoolClientLambda
289 | DependsOn: UserPoolClientLambdaPolicy
290 | UserPoolClientInputs:
291 | Type: Custom::LambdaCallout
292 | Properties:
293 | ServiceToken:
294 | Fn::GetAtt:
295 | - UserPoolClientLambda
296 | - Arn
297 | clientId:
298 | Ref: UserPoolClient
299 | userpoolId:
300 | Ref: UserPool
301 | DependsOn: UserPoolClientLogPolicy
302 | IdentityPool:
303 | Type: AWS::Cognito::IdentityPool
304 | Properties:
305 | IdentityPoolName:
306 | Fn::If:
307 | - ShouldNotCreateEnvResources
308 | - phototranslate1a58bd43_identitypool_1a58bd43
309 | - Fn::Join:
310 | - ''
311 | - - phototranslate1a58bd43_identitypool_1a58bd43
312 | - __
313 | - Ref: env
314 | CognitoIdentityProviders:
315 | - ClientId:
316 | Ref: UserPoolClient
317 | ProviderName:
318 | Fn::Sub:
319 | - cognito-idp.${region}.amazonaws.com/${client}
320 | - region:
321 | Ref: AWS::Region
322 | client:
323 | Ref: UserPool
324 | - ClientId:
325 | Ref: UserPoolClientWeb
326 | ProviderName:
327 | Fn::Sub:
328 | - cognito-idp.${region}.amazonaws.com/${client}
329 | - region:
330 | Ref: AWS::Region
331 | client:
332 | Ref: UserPool
333 | AllowUnauthenticatedIdentities:
334 | Ref: allowUnauthenticatedIdentities
335 | DependsOn: UserPoolClientInputs
336 | IdentityPoolRoleMap:
337 | Type: AWS::Cognito::IdentityPoolRoleAttachment
338 | Properties:
339 | IdentityPoolId:
340 | Ref: IdentityPool
341 | Roles:
342 | unauthenticated:
343 | Ref: unauthRoleArn
344 | authenticated:
345 | Ref: authRoleArn
346 | DependsOn: IdentityPool
347 | Outputs:
348 | IdentityPoolId:
349 | Value:
350 | Ref: IdentityPool
351 | Description: Id for the identity pool
352 | IdentityPoolName:
353 | Value:
354 | Fn::GetAtt:
355 | - IdentityPool
356 | - Name
357 | UserPoolId:
358 | Value:
359 | Ref: UserPool
360 | Description: Id for the user pool
361 | UserPoolName:
362 | Value:
363 | Ref: userPoolName
364 | AppClientIDWeb:
365 | Value:
366 | Ref: UserPoolClientWeb
367 | Description: The user pool app client id for web
368 | AppClientID:
369 | Value:
370 | Ref: UserPoolClient
371 | Description: The user pool app client id
372 | AppClientSecret:
373 | Value:
374 | Fn::GetAtt:
375 | - UserPoolClientInputs
376 | - appSecret
377 | Description: >-
378 | {"createdOn":"Linux","createdBy":"Amplify","createdWith":"7.5.4","stackType":"auth-Cognito","metadata":{}}
379 |
--------------------------------------------------------------------------------
/src/pwacompat.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google Inc. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 | * use this file except in compliance with the License. You may obtain a copy of
6 | * the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | * License for the specific language governing permissions and limitations under
14 | * the License.
15 | */
16 |
17 | 'use strict';
18 |
19 | (function() {
20 | if (!('fetch' in window)) {
21 | return; // basic feature detection: from Mobile Safari 10.3+
22 | }
23 |
24 | const capableDisplayModes = ['standalone', 'fullscreen', 'minimal-ui'];
25 | const defaultSplashColor = '#f8f9fa';
26 | const defaultSplashTextSize = 24;
27 | const idealSplashIconSize = 128;
28 | const minimumSplashIconSize = 48;
29 | const splashIconPadding = 32;
30 |
31 | const isSafari = (navigator.vendor && navigator.vendor.indexOf('Apple') !== -1);
32 | const isEdge = (navigator.userAgent && navigator.userAgent.indexOf('Edge') !== -1);
33 | const isEdgePWA = (typeof Windows !== 'undefined');
34 |
35 | function setup() {
36 | const manifestEl = document.head.querySelector('link[rel="pwa-setup"]');
37 | const manifestHref = manifestEl ? manifestEl.href : '';
38 | const hrefFactory = buildHrefFactory([manifestHref, window.location]);
39 |
40 | Promise.resolve()
41 | .then(() => {
42 | if (!manifestHref) {
43 | throw `can't find '`;
44 | }
45 | const opts = /** @type {!RequestInit} */ ({});
46 | if (manifestHref.crossOrigin === 'use-credentials') {
47 | opts.credentials = 'include';
48 | }
49 | return window.fetch(manifestHref, opts);
50 | })
51 | .then((response) => response.json())
52 | .then((data) => process(data, hrefFactory))
53 | .catch((err) => console.warn('pwacompat.js error', err));
54 | }
55 |
56 | /**
57 | * @param {!Array} options
58 | * @return {function(string): string}
59 | */
60 | function buildHrefFactory(options) {
61 | for (let i = 0; i < options.length; ++i) {
62 | const opt = options[i];
63 | try {
64 | new URL('', opt);
65 | return (part) => (new URL(part, opt)).toString();
66 | } catch (e) {}
67 | }
68 | return (part) => part;
69 | }
70 |
71 | function push(localName, attr) {
72 | const node = document.createElement(localName);
73 | for (const k in attr) {
74 | node.setAttribute(k, attr[k]);
75 | }
76 | document.head.appendChild(node);
77 | return node;
78 | }
79 |
80 | function meta(name, content) {
81 | if (content) {
82 | if (content === true) {
83 | content = 'yes';
84 | }
85 | push('meta', {name, content});
86 | }
87 | }
88 |
89 | /**
90 | * @param {!Object} manifest
91 | * @param {function(string): string} urlFactory
92 | */
93 | function process(manifest, urlFactory) {
94 | const icons = manifest['icons'] || [];
95 | icons.sort((a, b) => parseInt(b.sizes, 10) - parseInt(a.sizes, 10)); // largest first
96 | const appleTouchIcons = icons.map((icon) => {
97 | // create icons as byproduct
98 | const attr = {'rel': 'icon', 'href': urlFactory(icon['src']), 'sizes': icon['sizes']};
99 | push('link', attr);
100 | if (isSafari) {
101 | attr['rel'] = 'apple-touch-icon';
102 | return push('link', attr);
103 | }
104 | });
105 |
106 | const display = manifest['display'];
107 | const isCapable = capableDisplayModes.indexOf(display) !== -1;
108 | meta('mobile-web-app-capable', isCapable);
109 | updateThemeColorRender(/** @type {string} */ (manifest['theme_color']) || 'black');
110 |
111 | if (isEdge) {
112 | meta('msapplication-starturl', manifest['start_url'] || '/');
113 | meta('msapplication-TileColor', manifest['theme_color']);
114 | }
115 |
116 | // nb: we check, but this won't override any _earlier_ (in DOM order) theme-color
117 | if (!document.head.querySelector('[name="theme-color"]')) {
118 | meta('theme-color', manifest['theme_color']);
119 | }
120 |
121 | // TODO(samthor): We don't detect QQ or UC, we just set the vars anyway.
122 | const orientation = simpleOrientationFor(manifest['orientation']);
123 | meta('x5-orientation', orientation); // QQ
124 | meta('screen-orientation', orientation); // UC
125 | if (display === 'fullscreen') {
126 | meta('x5-fullscreen', 'true'); // QQ
127 | meta('full-screen', 'yes'); // UC
128 | } else if (isCapable) {
129 | meta('x5-page-mode', 'app'); // QQ
130 | meta('browsermode', 'application'); // UC
131 | }
132 |
133 | if (!isSafari) {
134 | return; // the rest of this file is for Safari
135 | }
136 |
137 | const backgroundIsLight = shouldUseLightForeground(
138 | /** @type {string} */ (manifest['background_color']) || defaultSplashColor);
139 | const title = manifest['name'] || manifest['short_name'] || document.title;
140 |
141 | // Add related iTunes app from manifest.
142 | const itunes = findAppleId(manifest['related_applications']);
143 | itunes && meta('apple-itunes-app', `app-id=${itunes}`);
144 |
145 | // General iOS meta tags.
146 | meta('apple-mobile-web-app-capable', isCapable);
147 | meta('apple-mobile-web-app-title', title);
148 |
149 | function splashFor({width, height}, orientation, icon) {
150 | const ratio = window.devicePixelRatio;
151 | const ctx = contextForCanvas({width: width * ratio, height: height * ratio});
152 |
153 | ctx.scale(ratio, ratio);
154 | ctx.fillStyle = manifest['background_color'] || defaultSplashColor;
155 | ctx.fillRect(0, 0, width, height);
156 | ctx.translate(width / 2, (height - splashIconPadding) / 2);
157 |
158 | ctx.font = `${defaultSplashTextSize}px HelveticaNeue-CondensedBold`;
159 | ctx.fillStyle = backgroundIsLight ? 'white' : 'black';
160 | const textWidth = ctx.measureText(title).width;
161 |
162 | if (icon) {
163 | // nb: on Chrome, we need the image >=48px, use the big layout >=80dp, ideal is >=128dp
164 | let iconWidth = (icon.width / ratio);
165 | let iconHeight = (icon.height / ratio);
166 | if (iconHeight > idealSplashIconSize) {
167 | // clamp to 128px height max
168 | iconWidth /= (iconHeight / idealSplashIconSize);
169 | iconHeight = idealSplashIconSize;
170 | }
171 |
172 | if (iconWidth >= minimumSplashIconSize && iconHeight >= minimumSplashIconSize) {
173 | ctx.drawImage(icon, iconWidth / -2, iconHeight / -2, iconWidth, iconHeight);
174 | ctx.translate(0, iconHeight / 2 + splashIconPadding);
175 | }
176 | }
177 | ctx.fillText(title, textWidth / -2, 0);
178 |
179 | const generatedSplash = document.createElement('link');
180 | generatedSplash.setAttribute('rel', 'apple-touch-startup-image');
181 | generatedSplash.setAttribute('media', `(orientation: ${orientation})`);
182 | generatedSplash.setAttribute('href', ctx.canvas.toDataURL());
183 |
184 | return generatedSplash;
185 | }
186 |
187 | const previous = new Set();
188 | function updateSplash(applicationIcon) {
189 | const portrait = splashFor(window.screen, 'portrait', applicationIcon);
190 | const landscape = splashFor({
191 | width: window.screen.height,
192 | height: window.screen.width,
193 | }, 'landscape', applicationIcon);
194 |
195 | previous.forEach((prev) => prev.remove());
196 |
197 | document.head.appendChild(portrait);
198 | document.head.appendChild(landscape);
199 | previous.add(portrait);
200 | previous.add(landscape);
201 | }
202 | updateSplash(null);
203 |
204 | // fetch the largest icon to generate a splash screen
205 | if (!appleTouchIcons.length) {
206 | return;
207 | }
208 | const icon = appleTouchIcons[0];
209 | const img = new Image();
210 | img.crossOrigin = 'anonymous';
211 | img.onload = () => {
212 | updateSplash(img);
213 |
214 | // also check and redraw icon
215 | if (!manifest['background_color']) {
216 | return;
217 | }
218 | const redrawn = updateTransparent(img, manifest['background_color']);
219 | if (redrawn === null) {
220 | return; // the rest probably aren't interesting either
221 | }
222 | icon.href = redrawn;
223 |
224 | // fetch and fix all remaining icons
225 | appleTouchIcons.slice(1).forEach((icon) => {
226 | const img = new Image();
227 | img.crossOrigin = 'anonymous';
228 | img.onload = () => {
229 | const redrawn = updateTransparent(img, manifest['background_color'], true);
230 | icon.href = redrawn;
231 | };
232 | img.src = icon.href;
233 | });
234 |
235 | };
236 | img.src = icon.href;
237 | }
238 |
239 | function findAppleId(related) {
240 | let itunes;
241 | (related || [])
242 | .filter((app) => app['platform'] === 'itunes')
243 | .forEach((app) => {
244 | if (app['id']) {
245 | itunes = app['id'];
246 | } else {
247 | const match = app['url'].match(/id(\d+)/);
248 | if (match) {
249 | itunes = match[1];
250 | }
251 | }
252 | });
253 | return itunes;
254 | }
255 |
256 | function simpleOrientationFor(v) {
257 | v = String(v || '');
258 | const prefix = v.substr(0, 3);
259 | if (prefix === 'por') {
260 | return 'portrait';
261 | } else if (prefix === 'lan') {
262 | return 'landscape';
263 | }
264 | return '';
265 | }
266 |
267 | /**
268 | * @param {string} color
269 | */
270 | function updateThemeColorRender(color) {
271 | if (!(isSafari || isEdgePWA)) {
272 | return;
273 | }
274 |
275 | const themeIsLight = shouldUseLightForeground(color);
276 | if (isSafari) {
277 | // nb. Safari 11.3+ gives a deprecation warning about this meta tag.
278 | // TODO(samthor): Potentially set black-translucent in 'fullscreen'.
279 | meta('apple-mobile-web-app-status-bar-style', themeIsLight ? 'black' : 'default');
280 | } else {
281 | // Edge PWA
282 | const t = getEdgeTitleBar();
283 | if (t === null) {
284 | console.debug('UWP no titleBar')
285 | return;
286 | }
287 | t.foregroundColor = colorToWindowsRGBA(themeIsLight ? 'black' : 'white');
288 | t.backgroundColor = colorToWindowsRGBA(color);
289 | }
290 | }
291 |
292 | /**
293 | * @return {?ApplicationViewTitleBar}
294 | */
295 | function getEdgeTitleBar() {
296 | try {
297 | return Windows.UI.ViewManagement.ApplicationView.getForCurrentView().titleBar;
298 | } catch (e) {
299 | return null;
300 | }
301 | }
302 |
303 | /**
304 | * The Windows titlebar APIs expect an object of {r, g, b, a}.
305 | *
306 | * @param {string} color
307 | * @return {WindowsColor}
308 | */
309 | function colorToWindowsRGBA(color) {
310 | const data = readColor(color);
311 | return /** @type {WindowsColor} */ ({
312 | 'r': data[0],
313 | 'g': data[1],
314 | 'b': data[2],
315 | 'a': data[3],
316 | });
317 | }
318 |
319 | /**
320 | * @param {string} color
321 | * @return {!Uint8ClampedArray}
322 | */
323 | function readColor(color) {
324 | const c = contextForCanvas();
325 | c.fillStyle = color;
326 | c.fillRect(0, 0, 1, 1);
327 | return c.getImageData(0, 0, 1, 1).data;
328 | }
329 |
330 | /**
331 | * @param {string} color
332 | * @return {boolean}
333 | */
334 | function shouldUseLightForeground(color) {
335 | const pixelData = readColor(color);
336 |
337 | // From https://cs.chromium.org/chromium/src/chrome/android/java/src/org/chromium/chrome/browser/util/ColorUtils.java
338 | const data = pixelData.map((v) => {
339 | const f = v / 255;
340 | return (f < 0.03928) ? f / 12.92 : Math.pow((f + 0.055) / 1.055, 2.4);
341 | });
342 | const lum = 0.2126 * data[0] + 0.7152 * data[1] + 0.0722 * data[2];
343 | const contrast = Math.abs((1.05) / (lum + 0.05));
344 | return contrast > 3;
345 | }
346 |
347 | function updateTransparent(image, background, force=false) {
348 | const context = contextForCanvas(image);
349 | context.drawImage(image, 0, 0);
350 |
351 | // look for transparent pixel in top-left
352 | // TODO: Chrome actually checks the four corners for some cases.
353 | if (!force) {
354 | const imageData = context.getImageData(0, 0, 1, 1);
355 | if (imageData.data[3] == 255) {
356 | return null;
357 | }
358 | }
359 |
360 | context.globalCompositeOperation = 'destination-over'; // only replace transparent areas
361 | context.fillStyle = background;
362 | context.fillRect(0, 0, image.width, image.height);
363 | return context.canvas.toDataURL();
364 | }
365 |
366 | function contextForCanvas({width, height} = {width: 1, height: 1}) {
367 | const canvas = document.createElement('canvas');
368 | canvas.width = width;
369 | canvas.height = height;
370 | return canvas.getContext('2d');
371 | }
372 |
373 | // actually run PWACompat here
374 | if (document.readyState === 'complete') {
375 | setup();
376 | } else {
377 | window.addEventListener('load', setup);
378 | }
379 | }());
380 |
--------------------------------------------------------------------------------