├── src ├── app │ ├── app.component.scss │ ├── tabs │ │ ├── tabs.page.scss │ │ ├── tabs.page.ts │ │ ├── tabs.module.ts │ │ ├── tabs.page.html │ │ ├── tabs.page.spec.ts │ │ └── tabs.router.module.ts │ ├── identify │ │ ├── identify.page.scss │ │ ├── identify.module.ts │ │ ├── identify.page.spec.ts │ │ ├── identify.page.html │ │ └── identify.page.ts │ ├── settings │ │ ├── settings.page.scss │ │ ├── settings.module.ts │ │ ├── settings.page.spec.ts │ │ ├── settings.page.html │ │ └── settings.page.ts │ ├── label │ │ ├── label.page.scss │ │ ├── label.module.ts │ │ ├── label.page.spec.ts │ │ ├── label.page.html │ │ └── label.page.ts │ ├── app.component.html │ ├── translate │ │ ├── translate.page.scss │ │ ├── language-select │ │ │ ├── language-select.component.scss │ │ │ ├── language-select.component.html │ │ │ ├── language-select.component.spec.ts │ │ │ └── language-select.component.ts │ │ ├── translate.page.spec.ts │ │ ├── translate.module.ts │ │ ├── translate.page.html │ │ └── translate.page.ts │ ├── data.service.spec.ts │ ├── logger.service.spec.ts │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── logger.service.ts │ ├── app.module.ts │ ├── data.service.ts │ ├── app.component.spec.ts │ └── API.service.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── assets │ ├── amplify.png │ ├── icon │ │ └── favicon.png │ ├── icons │ │ ├── 48x48.png │ │ ├── 72x72.png │ │ ├── 96x96.png │ │ ├── 128x128.png │ │ ├── 144x144.png │ │ ├── 152x152.png │ │ ├── 192x192.png │ │ ├── 384x384.png │ │ ├── 512x512.png │ │ ├── ios │ │ │ ├── Icon.png │ │ │ ├── icon_20pt.png │ │ │ ├── icon_29pt.png │ │ │ ├── icon_40pt.png │ │ │ ├── icon_76pt.png │ │ │ ├── icon_20pt@2x.png │ │ │ ├── icon_20pt@3x.png │ │ │ ├── icon_29pt@2x.png │ │ │ ├── icon_29pt@3x.png │ │ │ ├── icon_40pt@2x.png │ │ │ ├── icon_40pt@3x.png │ │ │ ├── icon_60pt@2x.png │ │ │ ├── icon_60pt@3x.png │ │ │ ├── icon_76pt@2x.png │ │ │ └── icon_83.5@2x.png │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png │ └── shapes.svg ├── models │ ├── schema.d.ts │ ├── index.js │ ├── index.d.ts │ └── schema.js ├── zone-flags.ts ├── graphql │ ├── subscriptions.graphql │ ├── queries.graphql │ └── mutations.graphql ├── test.ts ├── main.ts ├── manifest.webmanifest ├── index.html ├── theme │ └── variables.scss ├── global.scss ├── polyfills.ts └── pwacompat.js ├── demo.gif ├── ionic.config.json ├── amplify ├── backend │ ├── api │ │ └── ionicpredictions │ │ │ ├── transform.conf.json │ │ │ ├── schema.graphql │ │ │ ├── parameters.json │ │ │ └── stacks │ │ │ └── CustomResources.json │ ├── predictions │ │ ├── translateTextd8fbec95 │ │ │ ├── parameters.json │ │ │ └── translateTextd8fbec95-template.json │ │ ├── speechGenerator3e6f2f09 │ │ │ ├── parameters.json │ │ │ └── speechGenerator3e6f2f09-template.json │ │ ├── identifyLabelsdff85f2a │ │ │ ├── parameters.json │ │ │ └── identifyLabelsdff85f2a-template.json │ │ ├── identifyText360eb9c4 │ │ │ ├── parameters.json │ │ │ └── identifyText360eb9c4-template.json │ │ └── identifyEntitiesd04d1d1d │ │ │ ├── parameters.json │ │ │ └── identifyEntitiesd04d1d1d-template.json │ ├── types │ │ └── amplify-dependent-resources-ref.d.ts │ ├── backend-config.json │ └── auth │ │ └── phototranslate1a58bd43 │ │ ├── parameters.json │ │ └── phototranslate1a58bd43-cloudformation-template.yml ├── README.md ├── .config │ └── project-config.json ├── hooks │ ├── README.md │ ├── post-push.sh.sample │ └── pre-push.js.sample └── cli.json ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.json └── protractor.conf.js ├── tsconfig.app.json ├── tsconfig.spec.json ├── .graphqlconfig.yml ├── browserlist ├── tsconfig.json ├── ngsw-config.json ├── .gitignore ├── karma.conf.js ├── README.md ├── package.json ├── tslint.json └── angular.json /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/identify/identify.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/settings/settings.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/label/label.page.scss: -------------------------------------------------------------------------------- 1 | .confident { 2 | font-weight: bold; 3 | } -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/demo.gif -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/app/translate/translate.page.scss: -------------------------------------------------------------------------------- 1 | .lang-btn { 2 | text-decoration: none; 3 | color:#333333; 4 | } -------------------------------------------------------------------------------- /src/assets/amplify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/amplify.png -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "photo-translate", 3 | "integrations": {}, 4 | "type": "angular" 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /src/assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/48x48.png -------------------------------------------------------------------------------- /src/assets/icons/72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/72x72.png -------------------------------------------------------------------------------- /src/assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/96x96.png -------------------------------------------------------------------------------- /src/models/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '@aws-amplify/datastore'; 2 | 3 | export declare const schema: Schema; -------------------------------------------------------------------------------- /amplify/backend/api/ionicpredictions/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true 4 | } -------------------------------------------------------------------------------- /src/assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/128x128.png -------------------------------------------------------------------------------- /src/assets/icons/144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/144x144.png -------------------------------------------------------------------------------- /src/assets/icons/152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/152x152.png -------------------------------------------------------------------------------- /src/assets/icons/192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/192x192.png -------------------------------------------------------------------------------- /src/assets/icons/384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/384x384.png -------------------------------------------------------------------------------- /src/assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/512x512.png -------------------------------------------------------------------------------- /src/assets/icons/ios/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/Icon.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /amplify/backend/api/ionicpredictions/schema.graphql: -------------------------------------------------------------------------------- 1 | type Setting @model { 2 | id: ID! 3 | name: String! 4 | value: String 5 | } -------------------------------------------------------------------------------- /src/app/translate/language-select/language-select.component.scss: -------------------------------------------------------------------------------- 1 | .selected { 2 | color: orange; 3 | font-weight: bold; 4 | } -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_20pt.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_29pt.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_40pt.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_76pt.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_20pt@2x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_20pt@3x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_29pt@2x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_29pt@3x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_40pt@2x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_40pt@3x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_60pt@2x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_60pt@3x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_76pt@2x.png -------------------------------------------------------------------------------- /src/assets/icons/ios/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlabieniec/IonicPredictions/HEAD/src/assets/icons/ios/icon_83.5@2x.png -------------------------------------------------------------------------------- /src/zone-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents Angular change detection from 3 | * running with certain Web Component callbacks 4 | */ 5 | (window as any).__Zone_disable_customElements = true; 6 | -------------------------------------------------------------------------------- /amplify/backend/api/ionicpredictions/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "ionicpredictions", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": "false" 5 | } -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { initSchema } from '@aws-amplify/datastore'; 3 | import { schema } from './schema'; 4 | 5 | 6 | 7 | const { Setting } = initSchema(schema); 8 | 9 | export { 10 | Setting 11 | }; -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getPageTitle() { 9 | return element(by.css('ion-title')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tabs', 5 | templateUrl: 'tabs.page.html', 6 | styleUrls: ['tabs.page.scss'] 7 | }) 8 | export class TabsPage { 9 | 10 | constructor() {} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": ["node"] 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "src/test.ts", 12 | "src/**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getPageTitle()).toContain('Tab One'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/data.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DataService } from './data.service'; 4 | 5 | describe('DataService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: DataService = TestBed.get(DataService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/zone-flags.ts", 13 | "src/polyfills.ts" 14 | ], 15 | "include": [ 16 | "src/**/*.spec.ts", 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/app/logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LoggerService } from './logger.service'; 4 | 5 | describe('LoggerService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: LoggerService = TestBed.get(LoggerService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | ionicpredictions: 3 | schemaPath: src/graphql/schema.json 4 | includes: 5 | - src/graphql/**/*.graphql 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: angular 11 | generatedFileName: src/app/API.service.ts 12 | docsFilePath: src/graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /amplify/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Amplify CLI 2 | This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). 3 | 4 | Helpful resources: 5 | - Amplify documentation: https://docs.amplify.aws 6 | - Amplify CLI documentation: https://docs.amplify.aws/cli 7 | - More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files 8 | - Join Amplify's community: https://amplify.aws/community/ 9 | -------------------------------------------------------------------------------- /src/models/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ModelInit, MutableModel, PersistentModelConstructor } from "@aws-amplify/datastore"; 2 | 3 | 4 | 5 | 6 | 7 | export declare class Setting { 8 | readonly id: string; 9 | readonly name: string; 10 | readonly value?: string; 11 | constructor(init: ModelInit); 12 | static copyOf(source: Setting, mutator: (draft: MutableModel) => MutableModel | void): Setting; 13 | } -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "photo-translate", 3 | "version": "3.1", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "ionic", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "www", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /amplify/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Command Hooks 2 | 3 | Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. 4 | 5 | To get started, add your script files based on the expected naming convention in this directory. 6 | 7 | Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks 8 | -------------------------------------------------------------------------------- /amplify/backend/predictions/translateTextd8fbec95/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceName": "translateTextd8fbec95", 3 | "convertPolicyName": "translateTextPolicyd8fbec95", 4 | "authRoleName": { 5 | "Ref": "AuthRoleName" 6 | }, 7 | "unauthRoleName": { 8 | "Ref": "UnauthRoleName" 9 | }, 10 | "convertType": "translateText", 11 | "sourceLang": "no", 12 | "targetLang": "en", 13 | "access": "authAndGuest" 14 | } -------------------------------------------------------------------------------- /amplify/backend/predictions/speechGenerator3e6f2f09/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceName": "speechGenerator3e6f2f09", 3 | "convertPolicyName": "speechGeneratorPolicy3e6f2f09", 4 | "authRoleName": { 5 | "Ref": "AuthRoleName" 6 | }, 7 | "unauthRoleName": { 8 | "Ref": "UnauthRoleName" 9 | }, 10 | "convertType": "speechGenerator", 11 | "language": "nb-NO", 12 | "voice": "Liv", 13 | "access": "authAndGuest" 14 | } -------------------------------------------------------------------------------- /browserlist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. 13 | -------------------------------------------------------------------------------- /amplify/backend/predictions/identifyLabelsdff85f2a/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceName": "identifyLabelsdff85f2a", 3 | "identifyPolicyName": "identifyPolicydff85f2a", 4 | "authRoleName": { 5 | "Ref": "AuthRoleName" 6 | }, 7 | "unauthRoleName": { 8 | "Ref": "UnauthRoleName" 9 | }, 10 | "adminAuthProtected": "DISALLOW", 11 | "adminGuestProtected": "DISALLOW", 12 | "identifyType": "identifyLabels", 13 | "access": "authAndGuest", 14 | "type": "LABELS" 15 | } -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule) 8 | } 9 | ]; 10 | @NgModule({ 11 | imports: [ 12 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) 13 | ], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /amplify/backend/predictions/identifyText360eb9c4/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceName": "identifyText360eb9c4", 3 | "identifyPolicyName": "identifyPolicy360eb9c4", 4 | "authRoleName": { 5 | "Ref": "AuthRoleName" 6 | }, 7 | "unauthRoleName": { 8 | "Ref": "UnauthRoleName" 9 | }, 10 | "adminAuthProtected": "DISALLOW", 11 | "adminGuestProtected": "DISALLOW", 12 | "identifyType": "identifyText", 13 | "identifyDoc": false, 14 | "access": "authAndGuest", 15 | "format": "PLAIN" 16 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/graphql/subscriptions.graphql: -------------------------------------------------------------------------------- 1 | # this is an auto generated file. This will be overwritten 2 | subscription OnCreateSetting { 3 | onCreateSetting { 4 | id 5 | name 6 | value 7 | createdAt 8 | updatedAt 9 | } 10 | } 11 | subscription OnUpdateSetting { 12 | onUpdateSetting { 13 | id 14 | name 15 | value 16 | createdAt 17 | updatedAt 18 | } 19 | } 20 | subscription OnDeleteSetting { 21 | onDeleteSetting { 22 | id 23 | name 24 | value 25 | createdAt 26 | updatedAt 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | 6 | import { TabsPageRoutingModule } from './tabs.router.module'; 7 | 8 | import { TabsPage } from './tabs.page'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | IonicModule, 13 | CommonModule, 14 | FormsModule, 15 | TabsPageRoutingModule 16 | ], 17 | declarations: [TabsPage] 18 | }) 19 | export class TabsPageModule {} 20 | -------------------------------------------------------------------------------- /src/graphql/queries.graphql: -------------------------------------------------------------------------------- 1 | # this is an auto generated file. This will be overwritten 2 | query GetSetting($id: ID!) { 3 | getSetting(id: $id) { 4 | id 5 | name 6 | value 7 | createdAt 8 | updatedAt 9 | } 10 | } 11 | query ListSettings( 12 | $filter: ModelSettingFilterInput 13 | $limit: Int 14 | $nextToken: String 15 | ) { 16 | listSettings(filter: $filter, limit: $limit, nextToken: $nextToken) { 17 | items { 18 | id 19 | name 20 | value 21 | createdAt 22 | updatedAt 23 | } 24 | nextToken 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/identify/identify.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { IdentifyPage } from './identify.page'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | IonicModule, 11 | CommonModule, 12 | FormsModule, 13 | RouterModule.forChild([{ path: '', component: IdentifyPage }]) 14 | ], 15 | declarations: [IdentifyPage] 16 | }) 17 | export class IdentifyPageModule {} 18 | -------------------------------------------------------------------------------- /amplify/backend/predictions/identifyEntitiesd04d1d1d/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceName": "identifyEntitiesd04d1d1d", 3 | "identifyPolicyName": "identifyPolicyd04d1d1d", 4 | "authRoleName": { 5 | "Ref": "AuthRoleName" 6 | }, 7 | "unauthRoleName": { 8 | "Ref": "UnauthRoleName" 9 | }, 10 | "adminAuthProtected": "DISALLOW", 11 | "adminGuestProtected": "DISALLOW", 12 | "identifyType": "identifyEntities", 13 | "access": "authAndGuest", 14 | "celebrityDetectionEnabled": true, 15 | "maxEntities": 0, 16 | "adminTask": false, 17 | "folderPolicies": "" 18 | } -------------------------------------------------------------------------------- /src/app/translate/language-select/language-select.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{(type === 'source')?'Source':'Target'}} Language 4 | 5 | Close 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{lang.name}} 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/label/label.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { LabelPage } from './label.page'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: LabelPage 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | FormsModule, 21 | IonicModule, 22 | RouterModule.forChild(routes) 23 | ], 24 | declarations: [LabelPage] 25 | }) 26 | export class LabelPageModule {} 27 | -------------------------------------------------------------------------------- /src/app/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { SettingsPage } from './settings.page'; 7 | import { DataService } from '../data.service'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | IonicModule, 12 | CommonModule, 13 | FormsModule, 14 | RouterModule.forChild([{ path: '', component: SettingsPage }]) 15 | ], 16 | providers: [DataService], 17 | declarations: [SettingsPage] 18 | }) 19 | export class SettingsPageModule {} 20 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/*.css", 13 | "/*.js" 14 | ] 15 | } 16 | }, { 17 | "name": "assets", 18 | "installMode": "lazy", 19 | "updateMode": "prefetch", 20 | "resources": { 21 | "files": [ 22 | "/assets/**", 23 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 24 | ] 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Translate 7 | 8 | 9 | 10 | Entity 11 | 12 | 13 | 14 | Label 15 | 16 | 17 | 18 | Settings 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { Platform } from '@ionic/angular'; 4 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 5 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: 'app.component.html', 10 | styleUrls: ['app.component.scss'] 11 | }) 12 | export class AppComponent { 13 | constructor( 14 | private platform: Platform, 15 | private splashScreen: SplashScreen, 16 | private statusBar: StatusBar 17 | ) { 18 | this.initializeApp(); 19 | } 20 | 21 | initializeApp() { 22 | this.platform.ready().then(() => { 23 | this.statusBar.styleDefault(); 24 | this.splashScreen.hide(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/identify/identify.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | 4 | import { IdentifyPage } from './identify.page'; 5 | 6 | describe('IdentifyPage', () => { 7 | let component: IdentifyPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [IdentifyPage], 13 | imports: [IonicModule.forRoot()] 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(IdentifyPage); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | })); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/settings/settings.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | 4 | import { SettingsPage } from './settings.page'; 5 | 6 | describe('Tab2Page', () => { 7 | let component: SettingsPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [SettingsPage], 13 | imports: [IonicModule.forRoot()] 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(SettingsPage); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | })); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /amplify/hooks/post-push.sh.sample: -------------------------------------------------------------------------------- 1 | # This is a sample hook script created by Amplify CLI. 2 | # To start using this post-push hook please change the filename: 3 | # post-push.sh.sample -> post-push.sh 4 | # 5 | # learn more: https://docs.amplify.aws/cli/usage/command-hooks 6 | 7 | if [ -z "$(which jq)" ]; then 8 | echo "Please install jq to run the sample script." 9 | exit 0 10 | fi 11 | 12 | parameters=`cat` 13 | error=$(jq -r '.error // empty' <<< "$parameters") 14 | data=$(jq -r '.data' <<< "$parameters") 15 | 16 | # 17 | # Write code here: 18 | # 19 | if [ ! -z "$error" ]; then 20 | echo "Amplify CLI emitted an error:" $(jq -r '.message' <<< "$error") 21 | exit 0 22 | fi 23 | echo "project root path:" $(pwd); 24 | echo "Amplify CLI command:" $(jq -r '.amplify | .command' <<< "$data") -------------------------------------------------------------------------------- /src/app/translate/translate.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | 4 | import { TranslatePage } from './translate.page'; 5 | 6 | describe('TranslatePage', () => { 7 | let component: TranslatePage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [TranslatePage], 13 | imports: [IonicModule.forRoot()] 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(TranslatePage); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | })); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/tabs/tabs.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { TabsPage } from './tabs.page'; 5 | 6 | describe('TabsPage', () => { 7 | let component: TabsPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [TabsPage], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(TabsPage); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/label/label.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { LabelPage } from './label.page'; 5 | 6 | describe('Tab4Page', () => { 7 | let component: LabelPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ LabelPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(LabelPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | import Amplify from '@aws-amplify/core'; 8 | import Auth from '@aws-amplify/auth'; 9 | import Predictions, { AmazonAIPredictionsProvider } from '@aws-amplify/predictions'; 10 | import awsconfig from './aws-exports'; 11 | 12 | Amplify.configure(awsconfig); 13 | Amplify.addPluggable(new AmazonAIPredictionsProvider()); 14 | 15 | Auth.configure(awsconfig); 16 | Predictions.configure(awsconfig); 17 | 18 | if (environment.production) { 19 | enableProdMode(); 20 | } 21 | 22 | platformBrowserDynamic().bootstrapModule(AppModule) 23 | .catch(err => console.log(err)); 24 | -------------------------------------------------------------------------------- /src/app/translate/translate.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { RouterModule } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { TranslatePage } from './translate.page'; 7 | import { DataService } from '../data.service'; 8 | import { LanguageSelectComponent } from './language-select/language-select.component'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | IonicModule, 13 | CommonModule, 14 | FormsModule, 15 | RouterModule.forChild([{ path: '', component: TranslatePage }]) 16 | ], 17 | providers: [DataService], 18 | declarations: [TranslatePage, LanguageSelectComponent], 19 | entryComponents: [LanguageSelectComponent] 20 | }) 21 | export class TranslatePageModule {} 22 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/graphql/mutations.graphql: -------------------------------------------------------------------------------- 1 | # this is an auto generated file. This will be overwritten 2 | mutation CreateSetting( 3 | $input: CreateSettingInput! 4 | $condition: ModelSettingConditionInput 5 | ) { 6 | createSetting(input: $input, condition: $condition) { 7 | id 8 | name 9 | value 10 | createdAt 11 | updatedAt 12 | } 13 | } 14 | mutation UpdateSetting( 15 | $input: UpdateSettingInput! 16 | $condition: ModelSettingConditionInput 17 | ) { 18 | updateSetting(input: $input, condition: $condition) { 19 | id 20 | name 21 | value 22 | createdAt 23 | updatedAt 24 | } 25 | } 26 | mutation DeleteSetting( 27 | $input: DeleteSettingInput! 28 | $condition: ModelSettingConditionInput 29 | ) { 30 | deleteSetting(input: $input, condition: $condition) { 31 | id 32 | name 33 | value 34 | createdAt 35 | updatedAt 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /amplify/hooks/pre-push.js.sample: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a sample hook script created by Amplify CLI. 3 | * To start using this pre-push hook please change the filename: 4 | * pre-push.js.sample -> pre-push.js 5 | * 6 | * learn more: https://docs.amplify.aws/cli/usage/command-hooks 7 | */ 8 | 9 | /** 10 | * @param data { { amplify: { environment: string, command: string, subCommand: string, argv: string[] } } } 11 | * @param error { { message: string, stack: string } } 12 | */ 13 | const hookHandler = async (data, error) => { 14 | // TODO write your hook handler here 15 | }; 16 | 17 | const getParameters = async () => { 18 | const fs = require("fs"); 19 | return JSON.parse(fs.readFileSync(0, { encoding: "utf8" })); 20 | }; 21 | 22 | getParameters() 23 | .then((event) => hookHandler(event.data, event.error)) 24 | .catch((err) => { 25 | console.error(err); 26 | process.exitCode = 1; 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/logger.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class LoggerService { 7 | 8 | constructor() { } 9 | 10 | /** 11 | * Log a service call result 12 | * @param service String - service name i.e. Identify, Translate etc. 13 | * @param object Object - payload to print out 14 | */ 15 | public log(service:string,object:any): void { 16 | console.log(); 17 | console.info(`+++ Begin: ${service} +++`); 18 | console.log(JSON.stringify(object, null, 2)); 19 | console.info(`--- End: ${service} ---`); 20 | console.log(); 21 | } 22 | 23 | public error(service:string,object:any): void { 24 | console.log(); 25 | console.error(`+++ Begin: ${service} +++`); 26 | console.log(JSON.stringify(object, null, 2)); 27 | console.error(`--- End: ${service} ---`); 28 | console.log(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/translate/language-select/language-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { LanguageSelectComponent } from './language-select.component'; 5 | 6 | describe('LanguageSelectComponent', () => { 7 | let component: LanguageSelectComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ LanguageSelectComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(LanguageSelectComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .ionic/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | www/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | $RECYCLE.BIN/ 31 | 32 | .DS_Store 33 | Thumbs.db 34 | UserInterfaceState.xcuserstate 35 | 36 | #amplify 37 | amplify/\#current-cloud-backend 38 | amplify/.config/local-* 39 | amplify/mock-data 40 | amplify/backend/amplify-meta.json 41 | amplify/backend/awscloudformation 42 | build/ 43 | dist/ 44 | node_modules/ 45 | aws-exports.js 46 | awsconfiguration.json 47 | amplifyconfiguration.json 48 | amplify-build-config.json 49 | amplify-gradle-config.json 50 | amplifyxc.config 51 | amplify/team-provider-info.json -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouteReuseStrategy } from '@angular/router'; 4 | 5 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 6 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 7 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 8 | 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { AppComponent } from './app.component'; 11 | import { ServiceWorkerModule } from '@angular/service-worker'; 12 | import { environment } from '../environments/environment'; 13 | 14 | @NgModule({ 15 | declarations: [AppComponent], 16 | entryComponents: [], 17 | imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })], 18 | providers: [ 19 | StatusBar, 20 | SplashScreen, 21 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } 22 | ], 23 | bootstrap: [AppComponent] 24 | }) 25 | export class AppModule {} 26 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/translate/language-select/language-select.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { DataService } from 'src/app/data.service'; 3 | import { ModalController } from '@ionic/angular'; 4 | import { Hub } from '@aws-amplify/core'; 5 | 6 | @Component({ 7 | selector: 'app-language-select', 8 | templateUrl: './language-select.component.html', 9 | styleUrls: ['./language-select.component.scss'], 10 | }) 11 | export class LanguageSelectComponent implements OnInit { 12 | 13 | @Input() selected:string; 14 | @Input() type:string; 15 | 16 | constructor( 17 | public data: DataService, 18 | private modalCtrl: ModalController ) { 19 | } 20 | 21 | ngOnInit() { 22 | console.log(`selected: ${this.selected}`); 23 | console.log(`type: ${this.type}`); 24 | } 25 | 26 | public select(lang:string): void { 27 | console.log('selectSource: ', lang); 28 | Hub.dispatch( 29 | 'settings', 30 | { 31 | event: this.type, 32 | data: lang 33 | }); 34 | this.dismiss(); 35 | } 36 | 37 | public dismiss() { 38 | // using the injected ModalController this page 39 | // can "dismiss" itself and optionally pass back data 40 | this.modalCtrl.dismiss({ 41 | 'dismissed': true 42 | }); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/models/schema.js: -------------------------------------------------------------------------------- 1 | export const schema = { 2 | "models": { 3 | "Setting": { 4 | "name": "Setting", 5 | "fields": { 6 | "id": { 7 | "name": "id", 8 | "isArray": false, 9 | "type": "ID", 10 | "isRequired": true, 11 | "attributes": [] 12 | }, 13 | "name": { 14 | "name": "name", 15 | "isArray": false, 16 | "type": "String", 17 | "isRequired": true, 18 | "attributes": [] 19 | }, 20 | "value": { 21 | "name": "value", 22 | "isArray": false, 23 | "type": "String", 24 | "isRequired": false, 25 | "attributes": [] 26 | } 27 | }, 28 | "syncable": true, 29 | "pluralName": "Settings", 30 | "attributes": [ 31 | { 32 | "type": "model", 33 | "properties": {} 34 | } 35 | ] 36 | } 37 | }, 38 | "enums": {}, 39 | "nonModels": {}, 40 | "version": "143746525d423107bde2b38a05bee30a" 41 | }; -------------------------------------------------------------------------------- /src/app/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | export interface Language { 4 | name: string, 5 | value: string 6 | }; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class DataService { 12 | 13 | public langs: Array = [ 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 | ![Demo Gif](./demo.gif) 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 | 18 | Predictions Detect Entities 19 | 20 | 21 | 22 | 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 | 20 | Predictions Detect Entities 21 | 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 | 15 | Predictions Translate 16 | 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 | --------------------------------------------------------------------------------