From 3c6b6cf19277306120cdb4d79577e0ea8a340ef3 Mon Sep 17 00:00:00 2001 From: Decker Dominik Date: Wed, 12 Feb 2025 17:23:29 +0100 Subject: [PATCH 1/7] GPU-1897: Added basic piktid editor impl --- cms-ui/apps/editor-ui/src/app/app.module.ts | 2 + cms-ui/apps/editor-ui/src/app/app.routes.ts | 5 + .../file-preview/file-preview.component.ts | 64 +- .../components/file-preview/file-preview.scss | 2 +- .../file-preview/file-preview.tpl.html | 133 ++- cms-ui/libs/piktid-editor/.eslintrc.json | 32 + cms-ui/libs/piktid-editor/README.md | 7 + cms-ui/libs/piktid-editor/jest.config.ts | 21 + cms-ui/libs/piktid-editor/ng-package.json | 7 + cms-ui/libs/piktid-editor/package.json | 9 + cms-ui/libs/piktid-editor/project.json | 36 + cms-ui/libs/piktid-editor/src/index.ts | 3 + .../piktid-editor/src/lib/common/models.ts | 149 +++ .../piktid-editor/src/lib/common/prompt.ts | 971 ++++++++++++++++++ .../piktid-editor/src/lib/components/index.ts | 1 + .../piktid-editor/piktid-editor.component.css | 51 + .../piktid-editor.component.html | 98 ++ .../piktid-editor/piktid-editor.component.ts | 247 +++++ .../piktid-editor/src/lib/piktid.module.ts | 13 + .../piktid-editor/src/lib/providers/index.ts | 1 + .../piktid-api/piktid-api.service.ts | 142 +++ cms-ui/libs/piktid-editor/src/test-setup.ts | 8 + cms-ui/libs/piktid-editor/tsconfig.json | 28 + cms-ui/libs/piktid-editor/tsconfig.lib.json | 17 + .../libs/piktid-editor/tsconfig.lib.prod.json | 9 + cms-ui/libs/piktid-editor/tsconfig.spec.json | 16 + cms-ui/nx.json | 5 + cms-ui/package-lock.json | 281 ++++- cms-ui/package.json | 4 + cms-ui/tsconfig.base.json | 59 ++ cms-ui/tsconfig.json | 57 +- 31 files changed, 2344 insertions(+), 134 deletions(-) create mode 100644 cms-ui/libs/piktid-editor/.eslintrc.json create mode 100644 cms-ui/libs/piktid-editor/README.md create mode 100644 cms-ui/libs/piktid-editor/jest.config.ts create mode 100644 cms-ui/libs/piktid-editor/ng-package.json create mode 100644 cms-ui/libs/piktid-editor/package.json create mode 100644 cms-ui/libs/piktid-editor/project.json create mode 100644 cms-ui/libs/piktid-editor/src/index.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/common/models.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/common/prompt.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/index.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/piktid.module.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/providers/index.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts create mode 100644 cms-ui/libs/piktid-editor/src/test-setup.ts create mode 100644 cms-ui/libs/piktid-editor/tsconfig.json create mode 100644 cms-ui/libs/piktid-editor/tsconfig.lib.json create mode 100644 cms-ui/libs/piktid-editor/tsconfig.lib.prod.json create mode 100644 cms-ui/libs/piktid-editor/tsconfig.spec.json create mode 100644 cms-ui/tsconfig.base.json diff --git a/cms-ui/apps/editor-ui/src/app/app.module.ts b/cms-ui/apps/editor-ui/src/app/app.module.ts index 2b1cd2d5f..d83a6caa7 100644 --- a/cms-ui/apps/editor-ui/src/app/app.module.ts +++ b/cms-ui/apps/editor-ui/src/app/app.module.ts @@ -3,6 +3,7 @@ import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { NgModule } from '@angular/core'; import { PreloadAllModules, RouterModule } from '@angular/router'; import { KeycloakService } from '@gentics/cms-components'; +import { PiktidModule } from '@gentics/picktid-editor'; import { AppComponent } from './app.component'; import { APP_ROUTES } from './app.routes'; import { CoreModule } from './core/core.module'; @@ -15,6 +16,7 @@ const PROVIDERS: any[] = [ @NgModule({ imports: [ CoreModule, + PiktidModule, RouterModule.forRoot(APP_ROUTES, { preloadingStrategy: PreloadAllModules, enableTracing: false, diff --git a/cms-ui/apps/editor-ui/src/app/app.routes.ts b/cms-ui/apps/editor-ui/src/app/app.routes.ts index 61563f5ff..5c67c938e 100644 --- a/cms-ui/apps/editor-ui/src/app/app.routes.ts +++ b/cms-ui/apps/editor-ui/src/app/app.routes.ts @@ -7,6 +7,7 @@ import { ToolOverviewComponent } from './embedded-tools/components/tool-overview import { ToolProxyComponent } from './embedded-tools/components/tool-proxy/tool-proxy.component'; import { ProxyRouteComponent, RessourceProxyComponent } from './shared/components'; import { EditorOutlet } from './common/models'; +import { PiktidEditorComponent } from '@gentics/picktid-editor'; export const APP_ROUTES: Route[] = [ { @@ -110,6 +111,10 @@ export const APP_ROUTES: Route[] = [ component: TagEditorRouteComponent, canActivate: [AuthGuard], }, + { + path: 'piktid-editor', + component: PiktidEditorComponent, + }, { path: 'proxy', canActivate: [AuthGuard], diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.component.ts b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.component.ts index 688310600..015f5da22 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.component.ts +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.component.ts @@ -6,6 +6,7 @@ import { Input, OnChanges, OnDestroy, + OnInit, Output, SimpleChanges, ViewChild, @@ -37,7 +38,7 @@ import { ApplicationStateService, ApplyImageDimensionsAction, FolderActionsServi styleUrls: ['./file-preview.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FilePreviewComponent implements OnChanges, OnDestroy { +export class FilePreviewComponent implements OnInit, OnChanges, OnDestroy { @Input() public file: FileModel | ImageModel; @@ -53,11 +54,12 @@ export class FilePreviewComponent implements OnChanges, OnDestroy { fileExtension: string; keepFileName = true; nodeId: number; - private maxImageFullsizeDimensions = this.calculateMaxImageDimensions(); - private subscriptions = new Subscription(); isEditableImage = isEditableImage; hasFileEditPermission$: Observable; + private maxImageFullsizeDimensions = this.calculateMaxImageDimensions(); + private subscriptions: Subscription[] = []; + get displayDimensions(): any { return (this.file.type === 'image' && this.file.sizeX !== 0 && this.file.sizeY !== 0) ? { width: this.file.sizeX, height: this.file.sizeY } : @@ -68,35 +70,41 @@ export class FilePreviewComponent implements OnChanges, OnDestroy { private dismissErrorMessage(): void {} - constructor(private resourceUrlBuilder: ResourceUrlBuilder, + constructor( + private resourceUrlBuilder: ResourceUrlBuilder, private navigationService: NavigationService, private appState: ApplicationStateService, public permissions: PermissionService, private entityResolver: EntityResolver, private notification: I18nNotification, - private folderActions: FolderActionsService) { - this.subscriptions.add( - this.appState.select(state => state.entities).subscribe(entities => { - this.hasFileEditPermission$ = this.appState.select(() => - this.file.type === 'file' ? entities.file[this.file.id].folderId : entities.image[this.file.id].folderId) - .pipe( - switchMap(folderId => this.appState.select(state => state.entities.folder[folderId])), - switchMap((folder: Folder) => { - if (folder) { - const permissionsMap = folder.permissionsMap; - const isFile = this.file.type === 'file'; - if (!permissionsMap) { - return this.permissions.getFolderPermissionMap(folder.id).pipe( - map((permissionsMap: PermissionsMapCollection) => { - return this.hasEditPermission(permissionsMap, isFile); - }), - ); - } - return of(this.hasEditPermission(permissionsMap, isFile)); - } - return of(false); - }), - ); + private folderActions: FolderActionsService, + ) {} + + ngOnInit(): void { + this.hasFileEditPermission$ = this.appState.select(state => state.entities).pipe( + map(entities => { + const folderId = this.file.type === 'file' + ? entities.file[this.file.id].folderId + : entities.image[this.file.id].folderId; + + return entities.folder[folderId]; + }), + switchMap((folder: Folder) => { + if (!folder) { + return of(false); + } + + const permissionsMap = folder.permissionsMap; + const isFile = this.file.type === 'file'; + if (permissionsMap) { + return of(this.hasEditPermission(permissionsMap, isFile)); + } + + return this.permissions.getFolderPermissionMap(folder.id).pipe( + map((permissionsMap: PermissionsMapCollection) => { + return this.hasEditPermission(permissionsMap, isFile); + }), + ); }), ); } @@ -124,7 +132,7 @@ export class FilePreviewComponent implements OnChanges, OnDestroy { ngOnDestroy(): void { this.dismissErrorMessage(); - this.subscriptions.unsubscribe(); + this.subscriptions.forEach(s => s.unsubscribe()); } editImage(): void { diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.scss b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.scss index ce88712ad..4f1662c08 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.scss +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.scss @@ -5,7 +5,7 @@ $file-color: $gcms-color-file; :host { display: flex; flex-direction: column; - max-height: 100%; + min-height: 1px; } .preview-pane { diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.tpl.html b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.tpl.html index 0ef1f0df5..bb0e24ba9 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.tpl.html +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.tpl.html @@ -1,9 +1,12 @@
- insert_drive_file - + insert_drive_file + {{ fileExtension | uppercase }}
@@ -16,14 +19,20 @@
{{ file.name }}
- -
+ +
{{ { width: file.sizeX, height: file.sizeY } | imagedimensions | i18n }} @@ -31,62 +40,92 @@ {{ file.fileSize | filesize }}
+
-
-
+
+ +
-
-
- - rotate_left {{ 'editor.rotate_image_left_button' | i18n }} + +
+
+ + rotate_left {{ 'editor.rotate_image_left_button' | i18n }}
-
- - rotate_right {{ 'editor.rotate_image_right_button' | i18n }} +
+ + rotate_right {{ 'editor.rotate_image_right_button' | i18n }}
+
-
+
- edit {{ 'editor.edit_image_button' | i18n }} + edit {{ 'editor.edit_image_button' | i18n }}
-
+ + -
+ +
- + + file_upload - {{ 'editor.upload_new_image_button' | i18n }} - {{ 'editor.upload_new_file_button' | i18n }} + {{ (file.type === 'image' + ? 'editor.upload_new_image_button' + : 'editor.upload_new_file_button' + ) | i18n }} +
- +
diff --git a/cms-ui/libs/piktid-editor/.eslintrc.json b/cms-ui/libs/piktid-editor/.eslintrc.json new file mode 100644 index 000000000..ea5fec66e --- /dev/null +++ b/cms-ui/libs/piktid-editor/.eslintrc.json @@ -0,0 +1,32 @@ +{ + "extends": ["../../.eslintrc.js"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": [ + "{projectRoot}/eslint.config.{js,cjs,mjs}" + ] + } + ] + } + } + ] +} diff --git a/cms-ui/libs/piktid-editor/README.md b/cms-ui/libs/piktid-editor/README.md new file mode 100644 index 000000000..f5f4c3d43 --- /dev/null +++ b/cms-ui/libs/piktid-editor/README.md @@ -0,0 +1,7 @@ +# piktid-editor + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test piktid-editor` to execute the unit tests. diff --git a/cms-ui/libs/piktid-editor/jest.config.ts b/cms-ui/libs/piktid-editor/jest.config.ts new file mode 100644 index 000000000..51cc6f41c --- /dev/null +++ b/cms-ui/libs/piktid-editor/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'piktid-editor', + preset: '../../jest.preset.ts', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../coverage/libs/piktid-editor', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/cms-ui/libs/piktid-editor/ng-package.json b/cms-ui/libs/piktid-editor/ng-package.json new file mode 100644 index 000000000..6e654350b --- /dev/null +++ b/cms-ui/libs/piktid-editor/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/libs/piktid-editor", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/cms-ui/libs/piktid-editor/package.json b/cms-ui/libs/piktid-editor/package.json new file mode 100644 index 000000000..c071c07f6 --- /dev/null +++ b/cms-ui/libs/piktid-editor/package.json @@ -0,0 +1,9 @@ +{ + "name": "@gentics/picktid-editor", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^18.2.0", + "@angular/core": "^18.2.0" + }, + "sideEffects": false +} diff --git a/cms-ui/libs/piktid-editor/project.json b/cms-ui/libs/piktid-editor/project.json new file mode 100644 index 000000000..cfc513e85 --- /dev/null +++ b/cms-ui/libs/piktid-editor/project.json @@ -0,0 +1,36 @@ +{ + "name": "piktid-editor", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/piktid-editor/src", + "prefix": "gtxpict", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/angular:package", + "outputs": ["{workspaceRoot}/dist/{projectRoot}"], + "options": { + "project": "libs/piktid-editor/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/piktid-editor/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "libs/piktid-editor/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/piktid-editor/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/cms-ui/libs/piktid-editor/src/index.ts b/cms-ui/libs/piktid-editor/src/index.ts new file mode 100644 index 000000000..7178539c5 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/components'; +export * from './lib/piktid.module'; +export * from './lib/providers'; diff --git a/cms-ui/libs/piktid-editor/src/lib/common/models.ts b/cms-ui/libs/piktid-editor/src/lib/common/models.ts new file mode 100644 index 000000000..1701dedc0 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/common/models.ts @@ -0,0 +1,149 @@ +import { FaceData } from './prompt'; + +/* eslint-disable @typescript-eslint/naming-convention */ +export interface AnonymizationOptions { + flag_hair: boolean; + flag_sync: boolean; + mode: 'random' | 'keep'; +} + +export interface AuthenticationResponse { + access_token: string; + refresh_token: string; +} + +export interface FileUploadResponse { + image_id: string; + msg: string; + thumbnails: Record; + blurred_faces_list: boolean[]; + face_description_list: FaceDescription[]; + faces: FaceOverview; +} + +export interface FaceOverview { + number_of_faces: number; + approved_faces: number[]; + selected_faces: number[]; + coordinates_list: Coordinates[]; +} + +export interface FaceDescription { + a: FaceData; + f: number; +} + +export interface Coordinates { + /** Points to a `FACE_ID` */ + id: number + /** Height of bounding box as percentage of image height (0-1) */ + boxHeight: number + /** Width of bounding box as percentage of image width (0-1) */ + boxWidth: number + /** Horizontal coordinate of box center as percentage of image width (0-1) */ + centerX: number + /** Vertical coordinate of box center as percentage of image height (0-1) */ + centerY: number + /** Horizontal coordinate of top-left corner as percentage of image width (0-1) */ + cornerX: number + /** Vertical coordinate of top-left corner as percentage of image height (0-1) */ + cornerY: number +} + +export interface RandomFaceResponse { + status?: string; +} + +export interface GenerateExpressionResponse { + status?: string; +} + +export interface GenerateExpressionOptions { + id_generation: number; + /** JSON Encoded {@link FaceData} */ + prompt: string; + description_type: string; + expression_strength: 0 | 1 | 2 | 3 | 4; + seed: number; + flag_sync: boolean; + options: string; +} + +export interface GenerateExpressionRequest extends Partial{ + id_image: string; + id_face: number; +} + +export interface DetectFacesResponse { + +} + +export interface SubstituteFaceOptions { + flag_reset: number; + flag_reset_single_face: number; + flag_watermark: number; + flag_quality: number; + flag_only_generated_faces: number; + flag_png: number; +} + +export interface SubstituteFaceResponse {} + +export enum NotificationName { + ERROR = 'error', + NEW_GENERATION = 'new_generation', + PROGRESS = 'progress', +} + +export interface ErrorNotificationData { + tasks: string; + msg: string; +} + +export interface ErrorNotification { + name: NotificationName.ERROR; + id: number; + timestamp: number; + data: ErrorNotificationData; +} + +export interface NewGenerationNotificationData { + task: string; + msg: string; + id_image: string; + address: string; + face_list: number[]; + /** FACE ID */ + f: number; + /** GENERATION ID */ + g: number; + link: string; +} + +export interface NewGenerationNotification { + name: NotificationName.NEW_GENERATION; + id: number; + timestamp: number; + data: NewGenerationNotificationData; +} + +export interface ProgressNotificationData { + task: string; + msg: string; + f: number; + g: number; + progress: number; +} + +export interface ProgressNotification { + name: NotificationName.PROGRESS; + id: number; + timestamp: number; + data: ProgressNotificationData; +} + +export type Notification = ErrorNotification | NewGenerationNotification | ProgressNotification; + +export interface NotificationListResponse { + notifications_list: Notification[]; +} diff --git a/cms-ui/libs/piktid-editor/src/lib/common/prompt.ts b/cms-ui/libs/piktid-editor/src/lib/common/prompt.ts new file mode 100644 index 000000000..289f0b487 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/common/prompt.ts @@ -0,0 +1,971 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +export enum Country { + AFGHANISTAN = 'Afghanistan', + ALBANIA = 'Albania', + ALGERIA = 'Algeria', + ANDORRA = 'Andorra', + ANGOLA = 'Angola', + ANTIGUA = 'Antigua', + ARGENTINA = 'Argentina', + ARMENIA = 'Armenia', + AUSTRALIA = 'Australia', + AUSTRIA = 'Austria', + AZERBAIJAN = 'Azerbaijan', + BAHAMAS = 'Bahamas', + BAHRAIN = 'Bahrain', + BANGLADESH = 'Bangladesh', + BARBADOS = 'Barbados', + BELARUS = 'Belarus', + BELGIUM = 'Belgium', + BELIZE = 'Belize', + BENIN = 'Benin', + BHUTAN = 'Bhutan', + BOLIVIA = 'Bolivia', + BOSNIA_HERZEGOVINA = 'Bosnia Herzegovina', + BOTSWANA = 'Botswana', + BRAZIL = 'Brazil', + BRUNEI = 'Brunei', + BULGARIA = 'Bulgaria', + BURKINA = 'Burkina', + BURUNDI = 'Burundi', + CAMBODIA = 'Cambodia', + CAMEROON = 'Cameroon', + CANADA = 'Canada', + CAPE_VERDE = 'Cape Verde', + CENTRAL_AFRICAN_REP = 'Central African Rep', + CHAD = 'Chad', + CHILE = 'Chile', + CHINA = 'China', + COLOMBIA = 'Colombia', + COMOROS = 'Comoros', + CONGO = 'Congo', + COSTA_RICA = 'Costa Rica', + CROATIA = 'Croatia', + CUBA = 'Cuba', + CYPRUS = 'Cyprus', + CZECH_REPUBLIC = 'Czech Republic', + DENMARK = 'Denmark', + DJIBOUTI = 'Djibouti', + DOMINICA = 'Dominica', + DOMINICAN_REPUBLIC = 'Dominican Republic', + EAST_TIMOR = 'East Timor', + ECUADOR = 'Ecuador', + EGYPT = 'Egypt', + EL_SALVADOR = 'El Salvador', + EQUATORIAL_GUINEA = 'Equatorial Guinea', + ERITREA = 'Eritrea', + ESTONIA = 'Estonia', + ETHIOPIA = 'Ethiopia', + FIJI = 'Fiji', + FINLAND = 'Finland', + FRANCE = 'France', + GABON = 'Gabon', + GAMBIA = 'Gambia', + GEORGIA = 'Georgia', + GERMANY = 'Germany', + GHANA = 'Ghana', + GREECE = 'Greece', + GRENADA = 'Grenada', + GUATEMALA = 'Guatemala', + GUINEA = 'Guinea', + GUINEA_BISSAU = 'Guinea-Bissau', + GUYANA = 'Guyana', + HAITI = 'Haiti', + HONDURAS = 'Honduras', + HUNGARY = 'Hungary', + ICELAND = 'Iceland', + INDIA = 'India', + INDONESIA = 'Indonesia', + IRAN = 'Iran', + IRAQ = 'Iraq', + IRELAND = 'Ireland', + ISRAEL = 'Israel', + ITALY = 'Italy', + IVORY_COAST = 'Ivory Coast', + JAMAICA = 'Jamaica', + JAPAN = 'Japan', + JORDAN = 'Jordan', + KAZAKHSTAN = 'Kazakhstan', + KENYA = 'Kenya', + KIRIBATI = 'Kiribati', + KOREA_NORTH = 'Korea North', + KOREA_SOUTH = 'Korea South', + KOSOVO = 'Kosovo', + KUWAIT = 'Kuwait', + KYRGYZSTAN = 'Kyrgyzstan', + LAOS = 'Laos', + LATVIA = 'Latvia', + LEBANON = 'Lebanon', + LESOTHO = 'Lesotho', + LIBERIA = 'Liberia', + LIBYA = 'Libya', + LIECHTENSTEIN = 'Liechtenstein', + LITHUANIA = 'Lithuania', + LUXEMBOURG = 'Luxembourg', + MACEDONIA = 'Macedonia', + MADAGASCAR = 'Madagascar', + MALAWI = 'Malawi', + MALAYSIA = 'Malaysia', + MALDIVES = 'Maldives', + MALI = 'Mali', + MALTA = 'Malta', + MARSHALL_ISLANDS = 'Marshall Islands', + MAURITANIA = 'Mauritania', + MAURITIUS = 'Mauritius', + MEXICO = 'Mexico', + MICRONESIA = 'Micronesia', + MOLDOVA = 'Moldova', + MONACO = 'Monaco', + MONGOLIA = 'Mongolia', + MONTENEGRO = 'Montenegro', + MOROCCO = 'Morocco', + MOZAMBIQUE = 'Mozambique', + MYANMAR = 'Myanmar', + NAMIBIA = 'Namibia', + NAURU = 'Nauru', + NEPAL = 'Nepal', + NETHERLANDS = 'Netherlands', + NEW_ZEALAND = 'New Zealand', + NICARAGUA = 'Nicaragua', + NIGER = 'Niger', + NIGERIA = 'Nigeria', + NORWAY = 'Norway', + OMAN = 'Oman', + PAKISTAN = 'Pakistan', + PALAU = 'Palau', + PANAMA = 'Panama', + PAPUA_NEW_GUINEA = 'Papua New Guinea', + PARAGUAY = 'Paraguay', + PERU = 'Peru', + PHILIPPINES = 'Philippines', + POLAND = 'Poland', + PORTUGAL = 'Portugal', + QATAR = 'Qatar', + ROMANIA = 'Romania', + RUSSIAN_FEDERATION = 'Russian Federation', + RWANDA = 'Rwanda', + ST_LUCIA = 'St Lucia', + SAINT_VINCENT = 'Saint Vincent', + SAMOA = 'Samoa', + SAN_MARINO = 'San Marino', + SAO_TOME_AND_PRINCIPE = 'Sao Tome & Principe', + SAUDI_ARABIA = 'Saudi Arabia', + SENEGAL = 'Senegal', + SERBIA = 'Serbia', + SEYCHELLES = 'Seychelles', + SIERRA_LEONE = 'Sierra Leone', + SINGAPORE = 'Singapore', + SLOVAKIA = 'Slovakia', + SLOVENIA = 'Slovenia', + SOLOMON_ISLANDS = 'Solomon Islands', + SOMALIA = 'Somalia', + SOUTH_AFRICA = 'South Africa', + SOUTH_SUDAN = 'South Sudan', + SPAIN = 'Spain', + SRI_LANKA = 'Sri Lanka', + SUDAN = 'Sudan', + SURINAME = 'Suriname', + SWAZILAND = 'Swaziland', + SWEDEN = 'Sweden', + SWITZERLAND = 'Switzerland', + SYRIA = 'Syria', + TAIWAN = 'Taiwan', + TAJIKISTAN = 'Tajikistan', + TANZANIA = 'Tanzania', + THAILAND = 'Thailand', + TOGO = 'Togo', + TONGA = 'Tonga', + TRINIDAD_AND_TOBAGO = 'Trinidad & Tobago', + TUNISIA = 'Tunisia', + TURKEY = 'Turkey', + TURKMENISTAN = 'Turkmenistan', + TUVALU = 'Tuvalu', + UGANDA = 'Uganda', + UKRAINE = 'Ukraine', + UNITED_ARAB_EMIRATES = 'United Arab Emirates', + UNITED_KINGDOM = 'United Kingdom', + UNITED_STATES = 'United States', + URUGUAY = 'Uruguay', + UZBEKISTAN = 'Uzbekistan', + VANUATU = 'Vanuatu', + VATICAN_CITY = 'Vatican City', + VENEZUELA = 'Venezuela', + VIETNAM = 'Vietnam', + YEMEN = 'Yemen', + ZAMBIA = 'Zambia', + ZIMBABWE = 'Zimbabwe', +} + +export enum Emotion { + AFFECTIONATE = 'affectionate', + AGITATED = 'agitated', + AMUSED = 'amused', + ANGRY = 'angry', + ANNOYED = 'annoyed', + ANXIOUS = 'anxious', + ASTONISHED = 'astonished', + ASTOUNDED = 'astounded', + CARING = 'caring', + COMFORTED = 'comforted', + COMPASSIONATE = 'compassionate', + CONFIDENT = 'confident', + CONFUSED = 'confused', + CONTENT = 'content', + DELIGHTED = 'delighted', + DEPRESSED = 'depressed', + DESPAIRING = 'despairing', + DEVOTED = 'devoted', + DISGUSTED = 'disgusted', + DUMBFOUNDED = 'dumbfounded', + ELATED = 'elated', + EMPATHETIC = 'empathetic', + ENAMORED = 'enamored', + ENRAGED = 'enraged', + EXCITED = 'excited', + FEARFUL = 'fearful', + FURIOUS = 'furious', + GRATEFUL = 'grateful', + GRINNING = 'grinning', + GLOOMY = 'gloomy', + HAPPY = 'happy', + HEARTBROKEN = 'heartbroken', + HOPELESS = 'hopeless', + INDIGNANT = 'indignant', + IRRITATED = 'irritated', + ISOLATED = 'isolated', + JOYFUL = 'joyful', + LONELY = 'lonely', + LOST = 'lost', + LOVED = 'loved', + MELANCHOLIC = 'melancholic', + MISERABLE = 'miserable', + MOTIVATED = 'motivated', + NAUSEATED = 'nauseated', + NERVOUS = 'nervous', + OFFENDED = 'offended', + PANICKED = 'panicked', + PASSIONATE = 'passionate', + PERPLEXED = 'perplexed', + PLEASED = 'pleased', + REJECTED = 'rejected', + RELIEVED = 'relieved', + REPULSED = 'repulsed', + RESENTFUL = 'resentful', + SAD = 'sad', + SCARED = 'scared', + SERIOUS = 'serious', + SHOCKED = 'shocked', + SPEECHLESS = 'speechless', + STRESSED = 'stressed', + SURPRISED = 'surprised', + SYMPATHETIC = 'sympathetic', + TENDER = 'tender', + TERRIFIED = 'terrified', + TENSE = 'tense', + WARM = 'warm', + WORRIED = 'worried' +} + +export enum Expression { + AMAZED = 'amazed', + AMUSED = 'amused', + ANGRY = 'angry', + ASTONISHED = 'astonished', + AWE_STRUCK = 'awe-struck', + APPALLED = 'appalled', + BEWILDERED = 'bewildered', + BORED = 'bored', + CALM = 'calm', + CHEERFUL = 'cheerful', + CONFUSED = 'confused', + CONTENT = 'content', + CURIOUS = 'curious', + DEJECTED = 'dejected', + DELIGHTED = 'delighted', + DISTRAUGHT = 'distraught', + DROWSY = 'drowsy', + DUMBFOUNDED = 'dumbfounded', + ECSTATIC = 'ecstatic', + ELATED = 'elated', + EXHAUSTED = 'exhausted', + FRUSTRATED = 'frustrated', + FURIOUS = 'furious', + GLOOMY = 'gloomy', + GRINNING = 'grinning', + HAPPY = 'happy', + HEARTBROKEN = 'heartbroken', + IMPRESSED = 'impressed', + INDIGNANT = 'indignant', + INTERESTED = 'interested', + INTRIGUED = 'intrigued', + IRRITATED = 'irritated', + JOVIAL = 'jovial', + LAUGHING = 'laughing', + MERRY = 'merry', + MISERABLE = 'miserable', + NAUSEATED = 'nauseated', + OFFENDED = 'offended', + PEACEFUL = 'peaceful', + PERPLEXED = 'perplexed', + PLEASED = 'pleased', + RADIANT = 'radiant', + RELAXED = 'relaxed', + REPULSED = 'repulsed', + RESENTFUL = 'resentful', + SAD = 'sad', + SERENE = 'serene', + SHOCKED = 'shocked', + SLEEPY = 'sleepy', + SMILING = 'smiling', + SORROWFUL = 'sorrowful', + SPEECHLESS = 'speechless', + SURPRISED = 'surprised', + THRILLED = 'thrilled', + TIRED = 'tired', + UNHAPPY = 'unhappy', + UPSET = 'upset', + WEARY = 'weary', + CRY = 'cry', + DISGUSTED = 'disgusted', + FEARFUL = 'fearful', + PATHETIC = 'pathetic', + SUSPICIOUS = 'suspicious' +} + +export enum Eyes { + ADORABLE = 'adorable', + ALLURING = 'alluring', + AMUSING = 'amusing', + ANIMATED = 'animated', + ASTUTE = 'astute', + ATTRACTIVE = 'attractive', + BEAUTIFUL = 'beautiful', + BLISSFUL = 'blissful', + BRILLIANT = 'brilliant', + CAPTIVATING = 'captivating', + CELESTIAL = 'celestial', + CHARISMATIC = 'charismatic', + CHARMING = 'charming', + CHEERFUL = 'cheerful', + CLEVER = 'clever', + CLOSED = 'closed', + COMPASSIONATE = 'compassionate', + CONFIDENT = 'confident', + CONSTRUCTIVE = 'constructive', + CREATIVE = 'creative', + CUTE = 'cute', + CURIOUS = 'curious', + DELIGHTFUL = 'delightful', + DELICATE = 'delicate', + DIVINE = 'divine', + DYNAMIC = 'dynamic', + EFFECTIVE = 'effective', + ELEGANT = 'elegant', + ENERGETIC = 'energetic', + ENCHANTING = 'enchanting', + ENDEARING = 'endearing', + ENLIGHTENED = 'enlightened', + ENTERTAINING = 'entertaining', + ENTHUSIASTIC = 'enthusiastic', + ETHEREAL = 'ethereal', + EXPRESSIVE = 'expressive', + EXPERT = 'expert', + FASCINATING = 'fascinating', + FRIENDLY = 'friendly', + FUNNY = 'funny', + GENIUS = 'genius', + GENUINE = 'genuine', + GIFTED = 'gifted', + GLISTENING = 'glistening', + GORGEOUS = 'gorgeous', + GRACEFUL = 'graceful', + HAPPY = 'happy', + HEAVENLY = 'heavenly', + HOPEFUL = 'hopeful', + HONEST = 'honest', + HYPNOTIC = 'hypnotic', + IMAGINATIVE = 'imaginative', + INNOVATIVE = 'innovative', + INQUISITIVE = 'inquisitive', + INSIGHTFUL = 'insightful', + INTELLIGENT = 'intelligent', + INTENSE = 'intense', + INVITING = 'inviting', + JOYFUL = 'joyful', + KEEN = 'keen', + KIND = 'kind', + LIVELY = 'lively', + LOVELY = 'lovely', + LOVING = 'loving', + LUMINOUS = 'luminous', + MAGICAL = 'magical', + MASTERFUL = 'masterful', + MESMERIZING = 'mesmerizing', + MERRY = 'merry', + MAGNETIC = 'magnetic', + MOTIVATING = 'motivating', + MYSTERIOUS = 'mysterious', + OBSERVANT = 'observant', + OPEN = 'open', + OPTIMISTIC = 'optimistic', + PASSIONATE = 'passionate', + PENETRATING = 'penetrating', + PERCEPTIVE = 'perceptive', + PLAYFUL = 'playful', + POSITIVE = 'positive', + PRODUCTIVE = 'productive', + PROFICIENT = 'proficient', + QUICK_WITTED = 'quick-witted', + RADIANT = 'radiant', + REFINED = 'refined', + SEDUCTIVE = 'seductive', + SHARP = 'sharp', + SHARP_WITTED = 'sharp-witted', + SHINING = 'shining', + SILKY = 'silky', + SKILLFUL = 'skillful', + SMART = 'smart', + SMOOTH = 'smooth', + SOFT = 'soft', + SOPHISTICATED = 'sophisticated', + SOULFUL = 'soulful', + SPARKLING = 'sparkling', + SPIRITUAL = 'spiritual', + STUNNING = 'stunning', + SUBTLE = 'subtle', + SUCCESSFUL = 'successful', + SUPPORTIVE = 'supportive', + SYMPATHETIC = 'sympathetic', + TALENTED = 'talented', + TENDER = 'tender', + TRANSCENDENT = 'transcendent', + TRANSLUCENT = 'translucent', + TRUSTWORTHY = 'trustworthy', + UPLIFTING = 'uplifting', + VELVETY = 'velvety', + VIBRANT = 'vibrant', + VISIONARY = 'visionary', + WARM = 'warm', + WELCOMING = 'welcoming', + WISE = 'wise', + WITTY = 'witty', + WONDERFUL = 'wonderful', + WINK_LEFT = 'wink_left', + WINK_RIGHT = 'wink_right' +} + +export enum EyesColor { + BLUE = 'blue', + LIGHT_BLUE = 'light-blue', + BLUE_GRAY = 'blue-gray', + BROWN = 'brown', + GREEN = 'green', + HAZEL = 'hazel', + GRAY = 'gray', + AMBER = 'amber', + BLACK = 'black' +} + +export enum EyesShape { + ALMOND_SHAPED = 'almond-shaped', + ROUND = 'round', + HOODED = 'hooded', + UPTURNED = 'upturned', + DOWNTURNED = 'downturned', + MONOLID = 'monolid', + DEEP_SET = 'deep-set', + WIDE_SET = 'wide-set', + CLOSE_SET = 'close-set', + PROTRUDING = 'protruding' +} + +export enum Eyelashes { + LONG = 'long', + SHORT = 'short', + CURLY = 'curly', + STRAIGHT = 'straight', + DENSE = 'dense', + SPARSE = 'sparse', + SHINY = 'shiny', + MATTE = 'matte', + BLACK = 'black', + BROWN = 'brown', + BLONDE = 'blonde', + ROUGH = 'rough', + DAMAGED = 'damaged' +} + +export enum Eyebrows { + ARCHED = 'arched', + LOW = 'low', + THICK = 'thick', + THIN = 'thin', + LONG = 'long', + SHORT = 'short', + STRAIGHT = 'straight', + CURVED = 'curved', + DENSE = 'dense', + SPARSE = 'sparse' +} + +export enum Face { + ASYMMETRICAL = 'asymmetrical', + BALANCED = 'balanced', + BULBOUS = 'bulbous', + DIAMOND_SHAPED = 'diamond-shaped', + DISPROPORTIONATE = 'disproportionate', + GRACEFUL = 'graceful', + HEART_SHAPED = 'heart-shaped', + OVAL = 'oval', + RECTANGULAR = 'rectangular', + ROUND = 'round', + SQUARE = 'square', + SYMMETRICAL = 'symmetrical' +} + +export enum FacialHair { + BEARD = 'beard', + MUSTACHE = 'mustache', + NONE = 'none' +} + +export enum Gaze { + LEFT = 'left', + LEFT_UP = 'left-up', + LEFT_DOWN = 'left-down', + RIGHT = 'right', + RIGHT_UP = 'right-up', + RIGHT_DOWN = 'right-down', + STRAIGHT = 'straight', + STRAIGHT_UP = 'straight-up', + STRAIGHT_DOWN = 'straight-down' +} + +export enum Gender { + FEMALE = 'female', + MALE = 'male', + NONE = 'none' +} + +export enum Glasses { + EYEGLASSES = 'eyeglasses', + SUNGLASSES = 'sunglasses' +} + +export enum LightPosition { + BEHIND = 'behind', + FRONTAL = 'frontal', + LEFT = 'left', + RIGHT = 'right', + TOP = 'top', + BOTTOM = 'bottom' +} + +export enum Light { + SOFT = 'soft', + DYNAMIC = 'dynamic', + WHITE = 'white', + BLUE = 'blue', + RED = 'red', + GREEN = 'green', + NEON = 'neon', + DRAMATIC = 'dramatic', + STUDIO = 'studio', + CITY = 'city', + WARM = 'warm', + DARK = 'dark', + NATURAL = 'natural', + VIVID = 'vivid' +} + +export enum Lips { + BOW_SHAPED = 'bow-shaped', + CUPIDS_BOW = 'cupid\'s bow', + DOWNTURNED = 'downturned', + FULL = 'full', + FULLER_UPPER = 'fuller upper', + FULLER_LOWER = 'fuller lower', + THIN = 'thin', + PLUMP = 'plump', + WIDE = 'wide', + NARROW = 'narrow', + STRAIGHT = 'straight', + ROUNDED = 'rounded', + HEART_SHAPED = 'heart-shaped', + TAPERED = 'tapered' +} + +export enum Hair { + BUN = 'bun', + BALD = 'bald', + COARSE = 'coarse', + DATED = 'dated', + DULL = 'dull', + ELEGANT = 'elegant', + FINE = 'fine', + FLAT = 'flat', + FLOWING = 'flowing', + FRIZZY = 'frizzy', + GREASY = 'greasy', + KEMPT = 'kempt', + MESSY = 'messy', + POLISHED = 'polished', + SILKY = 'silky', + SLEEK = 'sleek', + SOPHISTICATED = 'sophisticated', + SPARSE = 'sparse', + STRAGGLY = 'straggly', + STRINGY = 'stringy', + TEXTURED = 'textured', + THICK = 'thick', + TRENDY = 'trendy', + UNKEMPT = 'unkempt', + UNRULY = 'unruly', + VOLUMINOUS = 'voluminous', + YOUTHFUL = 'youthful' +} + +export enum HairType { + CURLY = 'curly', + COILY = 'coily', + KINKY = 'kinky', + STRAIGHT = 'straight', + WAVY = 'wavy' +} + +export enum HairColor { + ASHY = 'ashy', + AUBURN = 'auburn', + BLACK = 'black', + BLONDE = 'blonde', + CARAMEL = 'caramel', + CHESTNUT = 'chestnut', + COPPER = 'copper', + GRAY = 'gray', + GOLDEN = 'golden', + PLATINUM = 'platinum', + RED = 'red', + STRAWBERRY = 'strawberry', + WHITE = 'white' +} + +export enum HairLength { + SHORT = 'short', + MEDIUM = 'medium', + LONG = 'long', + PIXIE = 'pixie', + BOB = 'bob' +} + +export enum Makeup { + FLAWLESS = 'flawless', + NATURAL = 'natural', + MATTE = 'matte', + TANTALIZING = 'tantalizing', + PORCELAIN = 'porcelain', + RADIANT = 'radiant', + WARM = 'warm', + COOL = 'cool', + SPARKLING = 'sparkling', + VIVID = 'vivid' +} + +export enum Mouth { + ARGUING = 'arguing', + BABBLING = 'babbling', + BEAMING = 'beaming', + BICKERING = 'bickering', + BITING = 'biting', + BREATHING = 'breathing', + BROWSING = 'browsing', + CALM = 'calm', + CARING = 'caring', + CHATTING = 'chatting', + CHEWING = 'chewing', + CHUCKLING = 'chuckling', + CLOSED = 'closed', + CONFUSED = 'confused', + CONTENT = 'content', + CRYING = 'crying', + DEBATING = 'debating', + DISCUSSING = 'discussing', + DISTRESSED = 'distressed', + DROWSY = 'drowsy', + EXHAUSTED = 'exhausted', + FROWNING = 'frowning', + GASPING = 'gasping', + GARGLING = 'gargling', + GIGGLING = 'giggling', + GNAWING = 'gnawing', + GOSSIPING = 'gossiping', + GRINNING = 'grinning', + GRIMACING = 'grimacing', + GROANING = 'groaning', + HISSING = 'hissing', + HALF_OPEN = 'half-open', + INTRIGUED = 'intrigued', + JABBERING = 'jabbering', + LAUGHING = 'laughing', + LICKING = 'licking', + LISPING = 'lisping', + LIPS_POINTED = 'lips-pointed', + MOUTHING = 'mouthing', + MOANING = 'moaning', + MUNCHING = 'munching', + MUMBLING = 'mumbling', + NIBBLING = 'nibbling', + OPEN = 'open', + POUTING = 'pouting', + PRATTLING = 'prattling', + RELAXED = 'relaxed', + SCREAMING = 'screaming', + SERENE = 'serene', + SHOUTING = 'shouting', + SIGHING = 'sighing', + SINGING = 'singing', + SLEEPY = 'sleepy', + SLURRING = 'slurring', + SMILING = 'smiling', + SMIRKING = 'smirking', + SLOBBERING = 'slobbering', + STAMMERING = 'stammering', + STRESSED = 'stressed', + STUTTERING = 'stuttering', + SUCKING = 'sucking', + SURPRISED = 'surprised', + TALKING = 'talking', + TENSE = 'tense', + TIRED = 'tired', + WEARY = 'weary', + WHIMPERING = 'whimpering', + WHISPERING = 'whispering', + WHISTLING = 'whistling' +} + +export enum Nose { + BIG = 'big', + BUMPY = 'bumpy', + BUTTON = 'button', + CROOKED = 'crooked', + CUTE = 'cute', + DELICATE = 'delicate', + ELEGANT = 'elegant', + GRACEFUL = 'graceful', + HOOKED = 'hooked', + LARGE = 'large', + LONG = 'long', + PERFECT = 'perfect', + PETITE = 'petite', + PROMINENT = 'prominent', + REFINED = 'refined', + SHAPELY = 'shapely', + STRAIGHT = 'straight', + WIDE = 'wide' +} + +export enum Region { + NORTH_AMERICA = 'North America', + SOUTH_AMERICA = 'South America', + CENTRAL_AMERICA = 'Central America', + EAST_AMERICA = 'East America', + WEST_AMERICA = 'West America', + NORTH_ASIA = 'North Asia', + SOUTH_ASIA = 'South Asia', + CENTRAL_ASIA = 'Central Asia', + EAST_ASIA = 'East Asia', + WEST_ASIA = 'West Asia', + NORTH_EUROPE = 'North Europe', + SOUTH_EUROPE = 'South Europe', + CENTRAL_EUROPE = 'Central Europe', + EAST_EUROPE = 'East Europe', + WEST_EUROPE = 'West Europe', + NORTH_AFRICA = 'North Africa', + SOUTH_AFRICA = 'South Africa', + CENTRAL_AFRICA = 'Central Africa', + EAST_AFRICA = 'East Africa', + WEST_AFRICA = 'West Africa', + MIDDLE_EAST = 'Middle East', + AUSTRALIA = 'Australia', + OCEANIA = 'Oceania', + CARIBBEAN = 'Caribbean' +} + +export enum Skin { + CLEAR = 'clear', + DARK = 'dark', + DULL = 'dull', + DEHYDRATED = 'dehydrated', + FAIR = 'fair', + FLUSHED = 'flushed', + FLAWLESS = 'flawless', + FRECKLED = 'freckled', + FLOWING = 'flowing', + HYDRATED = 'hydrated', + LIGHT = 'light', + MATTE = 'matte', + NATURAL = 'natural', + OILY = 'oily', + PALE = 'pale', + PALE_WHITE = 'pale-white', + RADIANT = 'radiant', + ROUGH = 'rough', + SILKY = 'silky', + SMOOTH = 'smooth', + SOFT = 'soft', + SPOTTY = 'spotty', + SUPPLE = 'supple', + SWEATY = 'sweaty', + WRINKLED = 'wrinkled', + TANNED = 'tanned', + TAUT = 'taut' +} + +export enum Smile { + BRIGHT = 'bright', + RADIANT = 'radiant', + INFECTIOUS = 'infectious', + JOYFUL = 'joyful', + BEAMING = 'beaming', + CHEERY = 'cheery', + BLISSFUL = 'blissful', + ENCHANTING = 'enchanting', + CAPTIVATING = 'captivating', + CHARMING = 'charming', + DELIGHTFUL = 'delightful', + ENTHRALLING = 'enthralling', + EXQUISITE = 'exquisite', + FETCHING = 'fetching', + GORGEOUS = 'gorgeous', + HEAVENLY = 'heavenly', + LOVELY = 'lovely', + MAGICAL = 'magical', + MESMERIZING = 'mesmerizing', + PRETTY = 'pretty', + RAVISHING = 'ravishing', + SPLENDID = 'splendid', + STUNNING = 'stunning', + SUBLIME = 'sublime', + SUPERB = 'superb', + SWEET = 'sweet', + WONDERFUL = 'wonderful', + ADORABLE = 'adorable', + ATTRACTIVE = 'attractive', + BEAUTIFUL = 'beautiful', + BEWITCHING = 'bewitching', + CUTE = 'cute', + DIVINE = 'divine', + ELEGANT = 'elegant', + FASCINATING = 'fascinating', + GRACEFUL = 'graceful', + IRRESISTIBLE = 'irresistible', + MARVELOUS = 'marvelous', + SEDUCTIVE = 'seductive', + SENSATIONAL = 'sensational', + SENSUAL = 'sensual', + TANTALIZING = 'tantalizing', + TENDER = 'tender', + VIBRANT = 'vibrant', + ALLURING = 'alluring', + AMIABLE = 'amiable', + BEGUILING = 'beguiling', + COMFORTING = 'comforting', + ENDEARING = 'endearing', + HEARTWARMING = 'heartwarming', + LIKEABLE = 'likeable', + MAGNETIC = 'magnetic', + PERSONABLE = 'personable', + WINNING = 'winning' +} + +export enum Teeth { + ALIGNED = 'aligned', + BEAMING = 'beaming', + BLINDING = 'blinding', + BRACED = 'braced', + BRIGHT = 'bright', + BROKEN = 'broken', + CHEWY = 'chewy', + CHIPPED = 'chipped', + CLEAN = 'clean', + CLEAR = 'clear', + CRUNCHY = 'crunchy', + CRYSTALLINE = 'crystalline', + CROOKED = 'crooked', + CROWNED = 'crowned', + DAZZLING = 'dazzling', + DECAYED = 'decayed', + DISCOLORED = 'discolored', + ELASTIC = 'elastic', + FIXED = 'fixed', + FLAWLESS = 'flawless', + GAPPED = 'gapped', + GLEAMING = 'gleaming', + GLASSY = 'glassy', + IMMACULATE = 'immaculate', + IVORY = 'ivory', + JUICY = 'juicy', + LUMINOUS = 'luminous', + LUSTROUS = 'lustrous', + LUCID = 'lucid', + MISALIGNED = 'misaligned', + MISSING = 'missing', + ORTHODONTIC = 'orthodontic', + PEARLESCENT = 'pearlescent', + PERFECT = 'perfect', + PLUMP = 'plump', + POLISHED = 'polished', + RADIANT = 'radiant', + RESPLENDENT = 'resplendent', + RUBBERY = 'rubbery', + SATINY = 'satiny', + SHIMMERING = 'shimmering', + SHINY = 'shiny', + SHEER = 'sheer', + SILKY = 'silky', + SLEEK = 'sleek', + SMILE = 'smile', + SMOOTH = 'smooth', + SOFT = 'soft', + SPONGY = 'spongy', + SPOTLESS = 'spotless', + SPARKLING = 'sparkling', + STAINED = 'stained', + STRAIGHT = 'straight', + SUPPLE = 'supple', + TENDER = 'tender', + TRANSLUCENT = 'translucent', + VELVETY = 'velvety', + WHITE = 'white', + YELLOWED = 'yellowed' +} + +export interface FaceData { + Age?: number; + Country?: Country; + Emotion?: Emotion; + Expression?: Expression; + Eyes?: Eyes; + EyesColor?: EyesColor; + EyesShape?: EyesShape; + Eyelashes?: Eyelashes; + Eyebrows?: Eyebrows; + Face?: Face; + FacialHair?: FacialHair; + Gaze?: Gaze; + Gender?: Gender; + Glasses?: Glasses; + LightPosition?: LightPosition; + Light?: Light; + Lips?: Lips; + Hair?: Hair; + HairType?: HairType; + HairColor?: HairColor; + HairLength?: HairLength; + Makeup?: Makeup; + Mouth?: Mouth; + Nose?: Nose; + Region?: Region; + Skin?: Skin; + Smile?: Smile; + Teeth?: Teeth; +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/index.ts b/cms-ui/libs/piktid-editor/src/lib/components/index.ts new file mode 100644 index 000000000..c7860cb2c --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/index.ts @@ -0,0 +1 @@ +export * from './piktid-editor/piktid-editor.component'; diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css new file mode 100644 index 000000000..c05c5f1ef --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css @@ -0,0 +1,51 @@ +:host { + display: flex; + position: relative; + flex-direction: column; + gap: 1rem; + padding: 1rem +} + +.image-controls { + display: flex; + gap: 1rem; + flex-direction: row; + align-items: center; + width: 100%; +} + +.image-preview { + position: relative; + display: flex; + width: 100%; + flex-direction: row; + gap: 1rem; + flex-wrap: wrap; +} + +.image-status { + width: 100%; +} + +.image-wrapper { + position: relative; + display: inline-block; +} + +.image-wrapper .face-detection-wrapper .face-detection-item { + position: absolute; + border: 1px solid; +} + +.image-wrapper .face-detection-wrapper .face-detection-item-label { + position: absolute; + top: 0; + left: 0; + margin-top: -1.8rem; + background: black; + padding: 2px 1rem; + margin-left: -1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + color: white; +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html new file mode 100644 index 000000000..7397d1973 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html @@ -0,0 +1,98 @@ + + +
+ + image{{ 'select_image' }} + + + + upload{{ 'upload_image' }} + + + + check{{ 'confirm_editing' }} + +
+ +
Image Upload Status: {{ imageUploadStatus }}
+ +
+ +
+ Your image + +
+
+
+ {{ faceId }} +
+
+
+
+ +
+ +
+ + {{ faceId }} + +
+ +
+
+
+
+ Generated face +
+
+
+
+ +
+ + comedy_mask + {{ 'generate_new_face' }} + + + + check + {{ 'apply_face' }} + +
+
+
+
diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts new file mode 100644 index 000000000..3905fd124 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts @@ -0,0 +1,247 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { NotificationService } from '@gentics/ui-core'; +import { filter, interval, Subscription } from 'rxjs'; +import { Coordinates, NewGenerationNotificationData, NotificationName } from '../../common/models'; +import { FaceData } from '../../common/prompt'; +import { PiktidAPIService } from '../../providers/piktid-api/piktid-api.service'; + +@Component({ + selector: 'gtxpict-piktid-editor', + templateUrl: './piktid-editor.component.html', + styleUrl: './piktid-editor.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiktidEditorComponent implements OnInit, OnDestroy { + + /** The image that is currently picked by the user. */ + public pickedImage: File | null = null; + + /** The URL of the preview image. */ + public imageUrl: string | null = null; + + /** The image-id of the uploaded image. If `null`, the image is not uploaded yet. */ + public imageId: string | null = null; + + /** The status of the image upload. If `null`, the image is not uploaded yet. */ + public imageUploadStatus: 'pending' | 'success' | 'error' | null = null; + + /** The status of the face detection. If `null`, the face detection is not started yet. */ + public faceDetectionStatus: 'pending' | 'success' | 'error' | null = null; + + /** The list of face-ids detected in the image. */ + public faceIds: number[] = []; + + /** The positions of the faces in the image. */ + public facePositions: Record = {}; + + /** The descriptions of the faces in the image. */ + public faceDescriptions: Record = {}; + + /** The list of generated faces for each face-id. */ + public generatedFaces: Record = {}; + + /** The selected generation for each face-id. ID of the generation may be `-1` if no generation is selected. */ + public selectedGeneration: Record = {}; + + /** Whether the component is busy. */ + public busy = false; + + /** The list of faces that are waiting for a generation. */ + public waitingForFaces: number[] = []; + + /** The list of faces that have been confirmed. */ + public confirmedFaces: number[] = []; + + /** The ids of the notifications that have been processed. */ + private processedNotifications = new Set([]); + + private uploadSubscription: Subscription | null = null; + private notificationIntervalSubscription: Subscription | null = null; + private notificationSubscription: Subscription | null = null; + private otherSubscriptions: Subscription[] = []; + + constructor( + private changeDetector: ChangeDetectorRef, + private api: PiktidAPIService, + private notificationService: NotificationService, + ) {} + + ngOnInit(): void { + this.notificationIntervalSubscription = interval(3_000).pipe( + filter(() => this.waitingForFaces.length > 0), + ).subscribe(() => { + this.fetchNotifications(); + }); + } + + ngOnDestroy(): void { + this.uploadSubscription?.unsubscribe?.(); + this.notificationIntervalSubscription?.unsubscribe?.(); + this.notificationSubscription?.unsubscribe?.(); + this.otherSubscriptions.forEach((subscription) => subscription.unsubscribe()); + } + + public onImagePicked(files: File[]): void { + this.pickedImage = files[0]; + this.imageUrl = URL.createObjectURL(files[0]); + + // Reset the status of the image upload + this.imageUploadStatus = null; + this.imageId = null; + this.faceDetectionStatus = null; + this.faceIds = []; + this.facePositions = {}; + this.faceDescriptions = {}; + + this.changeDetector.markForCheck(); + } + + public uploadImage(): void { + // No image or already uploading + if (!this.pickedImage || this.imageUploadStatus === 'pending') { + return; + } + + this.busy = true; + this.imageUploadStatus = 'pending'; + this.faceDetectionStatus = null; + this.changeDetector.markForCheck(); + + this.uploadSubscription = this.api.uploadFile(this.pickedImage, { + mode: 'random', + // eslint-disable-next-line @typescript-eslint/naming-convention + flag_hair: true, + // eslint-disable-next-line @typescript-eslint/naming-convention + flag_sync: true, + }).subscribe({ + next: (response) => { + this.imageId = response.image_id; + this.imageUploadStatus = 'success'; + + this.selectedGeneration = {}; + this.faceIds = response.face_description_list.map((face) => { + this.selectedGeneration[face.f] = -1; + return face.f; + }); + this.facePositions = response.faces.coordinates_list.reduce((acc, face) => { + acc[face.id] = face; + return acc; + }, {} as Record); + this.faceDescriptions = response.face_description_list.reduce((acc, face) => { + acc[face.f] = face.a; + return acc; + }, {} as Record); + this.faceDetectionStatus = 'success'; + + this.busy = false; + this.changeDetector.markForCheck(); + + // Fetch notifications to get the initial list of generated faces + this.fetchNotifications(); + }, + error: (error) => { + this.imageUploadStatus = 'error'; + this.busy = false; + + console.error(error); + this.notificationService.show({ + message: error.message, + type: 'alert', + }) + + this.changeDetector.markForCheck(); + }, + }); + } + + public generateFace(faceId: number): void { + if (!this.imageId || !this.faceIds.includes(faceId)) { + return; + } + + this.otherSubscriptions.push(this.api.generateNewRandomFace(this.imageId, faceId).subscribe({ + next: (response) => { + console.log('new expression generated', response); + this.waitingForFaces.push(faceId); + this.changeDetector.markForCheck(); + }, + error: (error) => { + console.error(error); + }, + })); + } + + public confirmFaceGeneration(faceId: number): void { + if (!this.imageId || !this.faceIds.includes(faceId) || this.selectedGeneration[faceId] === -1) { + return; + } + + this.otherSubscriptions.push(this.api.substituteFace(this.imageId, faceId, this.selectedGeneration[faceId]).subscribe({ + next: (response) => { + console.log('face generation confirmed', response); + this.confirmedFaces.push(faceId); + this.changeDetector.markForCheck(); + }, + error: (error) => { + console.error(error); + }, + })); + } + + public fetchNotifications(): void { + if (!this.imageId) { + return; + } + + if (this.notificationSubscription) { + this.notificationSubscription.unsubscribe(); + } + + this.notificationSubscription = this.api.getNotificationsByName(this.imageId, [ + // NotificationName.ERROR, + NotificationName.NEW_GENERATION, + ]).subscribe({ + next: (response) => { + const stillInProgress = new Set(this.waitingForFaces); + const notifications = response.notifications_list || []; + + for (const singleNotif of notifications) { + if (this.processedNotifications.has(singleNotif.id)) { + continue; + } + + switch (singleNotif.name) { + case NotificationName.NEW_GENERATION: + if (!this.generatedFaces[singleNotif.data.f]) { + this.generatedFaces[singleNotif.data.f] = []; + } + + this.generatedFaces[singleNotif.data.f].push(singleNotif.data); + stillInProgress.delete(singleNotif.data.f); + break; + + case NotificationName.ERROR: + this.notificationService.show({ + message: singleNotif.data.msg, + type: 'alert', + }); + break; + } + + this.processedNotifications.add(singleNotif.id); + } + + this.waitingForFaces = Array.from(stillInProgress); + this.changeDetector.markForCheck(); + }, + }); + } + + public confirmEditing(): void { + if (!this.imageId || this.imageUploadStatus !== 'success' || this.confirmedFaces.length !== this.faceIds.length) { + return; + } + + // TODO: Upload the image to the CMS (Emit event here and do upload in parent) + } +} diff --git a/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts b/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts new file mode 100644 index 000000000..4ddc71c58 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { GenticsUICoreModule } from '@gentics/ui-core'; +import { PiktidEditorComponent } from './components'; +import { PiktidAPIService } from './providers'; + +@NgModule({ + imports: [CommonModule, GenticsUICoreModule], + declarations: [PiktidEditorComponent], + exports: [PiktidEditorComponent], + providers: [PiktidAPIService], +}) +export class PiktidModule {} diff --git a/cms-ui/libs/piktid-editor/src/lib/providers/index.ts b/cms-ui/libs/piktid-editor/src/lib/providers/index.ts new file mode 100644 index 000000000..a514ceb42 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/providers/index.ts @@ -0,0 +1 @@ +export * from './piktid-api/piktid-api.service'; diff --git a/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts b/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts new file mode 100644 index 000000000..ac48c3a1d --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts @@ -0,0 +1,142 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, of, tap } from 'rxjs'; +import { + AnonymizationOptions, + AuthenticationResponse, + DetectFacesResponse, + FileUploadResponse, + GenerateExpressionOptions, + GenerateExpressionRequest, + GenerateExpressionResponse, + NotificationListResponse, + NotificationName, + RandomFaceResponse, + SubstituteFaceOptions, + SubstituteFaceResponse, +} from '../../common/models'; + +@Injectable() +export class PiktidAPIService { + + private accessToken: string | null = null; + private refreshToken: string | null = null; + + constructor( + private http: HttpClient, + ) {} + + authenticate(username: string, password: string): Observable { + if (this.accessToken) { + return of({ + access_token: this.accessToken, + refresh_token: this.refreshToken, + } as AuthenticationResponse); + } + + const auth = btoa(`${username}:${password}`); + + return this.http.post('/piktid/api/tokens', null, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Basic ${auth}`, + }), + }).pipe( + tap((response) => { + this.accessToken = response.access_token; + this.refreshToken = response.refresh_token; + }), + ); + } + + uploadFile(file: File, options: AnonymizationOptions): Observable { + const data = new FormData(); + data.set('options', JSON.stringify(options)); + data.set('file', file); + + return this.http.post('/piktid/api/upload_pro', data, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }); + } + + detectFaces(imageId: string): Observable { + return this.http.post('/piktid/api/detect_faces', { + id_image: imageId, + }, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }); + } + + generateNewExpression(imageId: string, faceId: number, options?: Partial): Observable { + const req: GenerateExpressionRequest = { + id_image: imageId, + id_face: faceId, + ...options, + }; + + return this.http.post('/piktid/api/ask_new_expression', req, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }); + } + + generateNewRandomFace(imageId: string, faceId: number): Observable { + return this.http.post('/piktid/api/ask_random_face', { + id_image: imageId, + id_face: faceId, + prompt: '{}', + }, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }); + } + + getNotificationsByName(imageId: string, notificationNames: NotificationName[]): Observable { + return this.http.post('/piktid/api/notification_by_name', { + id_image: imageId, + name_list: notificationNames.join(','), + }, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }); + } + + substituteFace( + imageId: string, + faceId: number, + generationId: number, + options?: Partial, + ): Observable { + return this.http.post('/piktid/api/pick_face2', { + id_image: imageId, + id_face: faceId, + id_generation: generationId, + ...options, + }, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }); + } +} diff --git a/cms-ui/libs/piktid-editor/src/test-setup.ts b/cms-ui/libs/piktid-editor/src/test-setup.ts new file mode 100644 index 000000000..b3525effa --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/test-setup.ts @@ -0,0 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; +import 'jest-preset-angular/setup-jest'; diff --git a/cms-ui/libs/piktid-editor/tsconfig.json b/cms-ui/libs/piktid-editor/tsconfig.json new file mode 100644 index 000000000..874be7d3a --- /dev/null +++ b/cms-ui/libs/piktid-editor/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/cms-ui/libs/piktid-editor/tsconfig.lib.json b/cms-ui/libs/piktid-editor/tsconfig.lib.json new file mode 100644 index 000000000..4cdba8d9d --- /dev/null +++ b/cms-ui/libs/piktid-editor/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": ["node"] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/cms-ui/libs/piktid-editor/tsconfig.lib.prod.json b/cms-ui/libs/piktid-editor/tsconfig.lib.prod.json new file mode 100644 index 000000000..c21ffefb5 --- /dev/null +++ b/cms-ui/libs/piktid-editor/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/cms-ui/libs/piktid-editor/tsconfig.spec.json b/cms-ui/libs/piktid-editor/tsconfig.spec.json new file mode 100644 index 000000000..4272f2bb8 --- /dev/null +++ b/cms-ui/libs/piktid-editor/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/cms-ui/nx.json b/cms-ui/nx.json index 7e6f31b24..30b6e338b 100644 --- a/cms-ui/nx.json +++ b/cms-ui/nx.json @@ -121,6 +121,11 @@ "{workspaceRoot}/.eslintignore", "{workspaceRoot}/eslint.config.js" ] + }, + "@nx/angular:package": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["production", "^production"] } }, "useLegacyCache": true diff --git a/cms-ui/package-lock.json b/cms-ui/package-lock.json index cf8da152f..88772babc 100644 --- a/cms-ui/package-lock.json +++ b/cms-ui/package-lock.json @@ -83,6 +83,9 @@ "@nx/web": "20.1.3", "@nx/workspace": "20.1.3", "@schematics/angular": "18.2.9", + "@swc-node/register": "~1.9.1", + "@swc/core": "~1.5.7", + "@swc/helpers": "~0.5.11", "@types/dompurify": "^2.0.0", "@types/hammerjs": "~2.0.36", "@types/jasmine": "~4.3.0", @@ -119,6 +122,7 @@ "jest-junit": "^16.0.0", "jest-preset-angular": "14.1.0", "json-loader": "^0.5.7", + "jsonc-eslint-parser": "^2.1.0", "karma": "6.4.1", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "~2.2.0", @@ -8206,6 +8210,277 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true }, + "node_modules/@swc-node/core": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.13.3.tgz", + "integrity": "sha512-OGsvXIid2Go21kiNqeTIn79jcaX4l0G93X2rAnas4LFoDyA9wAwVK7xZdm+QsKoMn5Mus2yFLCc4OtX2dD/PWA==", + "dev": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@swc/core": ">= 1.4.13", + "@swc/types": ">= 0.1" + } + }, + "node_modules/@swc-node/register": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@swc-node/register/-/register-1.9.2.tgz", + "integrity": "sha512-BBjg0QNuEEmJSoU/++JOXhrjWdu3PTyYeJWsvchsI0Aqtj8ICkz/DqlwtXbmZVZ5vuDPpTfFlwDBZe81zgShMA==", + "dev": true, + "dependencies": { + "@swc-node/core": "^1.13.1", + "@swc-node/sourcemap-support": "^0.5.0", + "colorette": "^2.0.20", + "debug": "^4.3.4", + "pirates": "^4.0.6", + "tslib": "^2.6.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@swc/core": ">= 1.4.13", + "typescript": ">= 4.3" + } + }, + "node_modules/@swc-node/sourcemap-support": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@swc-node/sourcemap-support/-/sourcemap-support-0.5.1.tgz", + "integrity": "sha512-JxIvIo/Hrpv0JCHSyRpetAdQ6lB27oFYhv0PKCNf1g2gUXOjpeR1exrXccRxLMuAV5WAmGFBwRnNOJqN38+qtg==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.21", + "tslib": "^2.6.3" + } + }, + "node_modules/@swc/core": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.29.tgz", + "integrity": "sha512-nvTtHJI43DUSOAf3h9XsqYg8YXKc0/N4il9y4j0xAkO0ekgDNo+3+jbw6MInawjKJF9uulyr+f5bAutTsOKVlw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.5.29", + "@swc/core-darwin-x64": "1.5.29", + "@swc/core-linux-arm-gnueabihf": "1.5.29", + "@swc/core-linux-arm64-gnu": "1.5.29", + "@swc/core-linux-arm64-musl": "1.5.29", + "@swc/core-linux-x64-gnu": "1.5.29", + "@swc/core-linux-x64-musl": "1.5.29", + "@swc/core-win32-arm64-msvc": "1.5.29", + "@swc/core-win32-ia32-msvc": "1.5.29", + "@swc/core-win32-x64-msvc": "1.5.29" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.29.tgz", + "integrity": "sha512-6F/sSxpHaq3nzg2ADv9FHLi4Fu2A8w8vP8Ich8gIl16D2htStlwnaPmCLjRswO+cFkzgVqy/l01gzNGWd4DFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.29.tgz", + "integrity": "sha512-rF/rXkvUOTdTIfoYbmszbSUGsCyvqACqy1VeP3nXONS+LxFl4bRmRcUTRrblL7IE5RTMCKUuPbqbQSE2hK7bqg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.29.tgz", + "integrity": "sha512-2OAPL8iWBsmmwkjGXqvuUhbmmoLxS1xNXiMq87EsnCNMAKohGc7wJkdAOUL6J/YFpean/vwMWg64rJD4pycBeg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.29.tgz", + "integrity": "sha512-eH/Q9+8O5qhSxMestZnhuS1xqQMr6M7SolZYxiXJqxArXYILLCF+nq2R9SxuMl0CfjHSpb6+hHPk/HXy54eIRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.29.tgz", + "integrity": "sha512-TERh2OICAJz+SdDIK9+0GyTUwF6r4xDlFmpoiHKHrrD/Hh3u+6Zue0d7jQ/he/i80GDn4tJQkHlZys+RZL5UZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.29.tgz", + "integrity": "sha512-WMDPqU7Ji9dJpA+Llek2p9t7pcy7Bob8ggPUvgsIlv3R/eesF9DIzSbrgl6j3EAEPB9LFdSafsgf6kT/qnvqFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.29.tgz", + "integrity": "sha512-DO14glwpdKY4POSN0201OnGg1+ziaSVr6/RFzuSLggshwXeeyVORiHv3baj7NENhJhWhUy3NZlDsXLnRFkmhHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.29.tgz", + "integrity": "sha512-V3Y1+a1zG1zpYXUMqPIHEMEOd+rHoVnIpO/KTyFwAmKVu8v+/xPEVx/AGoYE67x4vDAAvPQrKI3Aokilqa5yVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.29.tgz", + "integrity": "sha512-OrM6yfXw4wXhnVFosOJzarw0Fdz5Y0okgHfn9oFbTPJhoqxV5Rdmd6kXxWu2RiVKs6kGSJFZXHDeUq2w5rTIMg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.29.tgz", + "integrity": "sha512-eD/gnxqKyZQQR0hR7TMkIlJ+nCF9dzYmVVNbYZWuA1Xy94aBPUsEk3Uw3oG7q6R3ErrEUPP0FNf2ztEnv+I+dw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dev": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", + "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -28789,9 +29064,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", - "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.12.tgz", + "integrity": "sha512-jDLYqo7oF8tJIttjXO6jBY5Hk8p3A8W4ttih7cCEq64fQFWmgJ4VqAQjKr7WwIDlmXKEc6QeoRb5ecjZ+2afcg==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", diff --git a/cms-ui/package.json b/cms-ui/package.json index d50c655c0..09f33f8f7 100644 --- a/cms-ui/package.json +++ b/cms-ui/package.json @@ -134,6 +134,9 @@ "@nx/web": "20.1.3", "@nx/workspace": "20.1.3", "@schematics/angular": "18.2.9", + "@swc-node/register": "~1.9.1", + "@swc/core": "~1.5.7", + "@swc/helpers": "~0.5.11", "@types/dompurify": "^2.0.0", "@types/hammerjs": "~2.0.36", "@types/jasmine": "~4.3.0", @@ -170,6 +173,7 @@ "jest-junit": "^16.0.0", "jest-preset-angular": "14.1.0", "json-loader": "^0.5.7", + "jsonc-eslint-parser": "^2.1.0", "karma": "6.4.1", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "~2.2.0", diff --git a/cms-ui/tsconfig.base.json b/cms-ui/tsconfig.base.json new file mode 100644 index 000000000..77f280d85 --- /dev/null +++ b/cms-ui/tsconfig.base.json @@ -0,0 +1,59 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "esnext", + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": { + "@admin-ui": ["apps/admin-ui/src/app"], + "@admin-ui/*": ["apps/admin-ui/src/app/*"], + "@admin-ui/testing": ["apps/admin-ui/src/testing"], + "@admin-ui/testing/*": ["apps/admin-ui/src/testing/*"], + "@editor-ui": ["apps/editor-ui/src"], + "@editor-ui/*": ["apps/editor-ui/src/*"], + "@gentics/aloha-models": ["libs/aloha-models/src/public-api"], + "@gentics/cms-components": ["libs/cms-components/src/public-api"], + "@gentics/cms-integration-api-models": [ + "libs/cms-integration-api-models/src/public-api" + ], + "@gentics/cms-models": ["libs/cms-models/src/public-api"], + "@gentics/cms-models/*": ["libs/cms-models/src/lib/*"], + "@gentics/cms-rest-client": ["libs/cms-rest-client/src/public-api"], + "@gentics/cms-rest-client-angular": [ + "libs/cms-rest-client-angular/src/public-api" + ], + "@gentics/cms-rest-client-angular/testing": [ + "libs/cms-rest-client-angular/src/lib/testing" + ], + "@gentics/cms-rest-client/*": ["libs/cms-rest-client/src/lib/*"], + "@gentics/cms-rest-clients-angular": [ + "libs/cms-rest-clients-angular/src/public-api" + ], + "@gentics/cms-rest-clients-angular/*": [ + "libs/cms-rest-clients-angular/src/lib/*" + ], + "@gentics/cms-rest-clients-angular/testing": [ + "libs/cms-rest-clients-angular/src/testing" + ], + "@gentics/e2e-utils": ["libs/e2e-utils/src/public-api"], + "@gentics/e2e-utils/*": ["libs/e2e-utils/src/lib/*"], + "@gentics/form-generator": ["libs/form-generator/src/public-api"], + "@gentics/image-editor": ["libs/image-editor/src/public-api"], + "@gentics/picktid-editor": ["libs/piktid-editor/src/index.ts"], + "@gentics/ui-core": ["libs/ui-core/src/public-api"], + "@gentics/ui-core/testing": ["libs/ui-core/src/lib/testing"], + "@gentics/ui-core/testing/*": ["libs/ui-core/src/lib/testing/*"] + } + }, + "exclude": ["node_modules", "tmp", "dist"] +} diff --git a/cms-ui/tsconfig.json b/cms-ui/tsconfig.json index 2f26dc45c..426cd691a 100644 --- a/cms-ui/tsconfig.json +++ b/cms-ui/tsconfig.json @@ -1,13 +1,5 @@ { - "compileOnSave": false, "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, "resolveJsonModule": true, "forceConsistentCasingInFileNames": true, "useDefineForClassFields": false, @@ -17,52 +9,7 @@ "noImplicitReturns": false, "noFallthroughCasesInSwitch": false, "strictPropertyInitialization": false, - "target": "ES2022", - "module": "esnext", - "lib": ["es2022", "dom"], - "typeRoots": ["./node_modules/@types", "./typings"], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@admin-ui": ["apps/admin-ui/src/app"], - "@admin-ui/*": ["apps/admin-ui/src/app/*"], - "@admin-ui/testing": ["apps/admin-ui/src/testing"], - "@admin-ui/testing/*": ["apps/admin-ui/src/testing/*"], - "@editor-ui": ["apps/editor-ui/src"], - "@editor-ui/*": ["apps/editor-ui/src/*"], - "@gentics/aloha-models": ["libs/aloha-models/src/public-api"], - "@gentics/cms-components": ["libs/cms-components/src/public-api"], - "@gentics/cms-integration-api-models": [ - "libs/cms-integration-api-models/src/public-api" - ], - "@gentics/cms-models": ["libs/cms-models/src/public-api"], - "@gentics/cms-models/*": ["libs/cms-models/src/lib/*"], - "@gentics/cms-rest-client": ["libs/cms-rest-client/src/public-api"], - "@gentics/cms-rest-client-angular": [ - "libs/cms-rest-client-angular/src/public-api" - ], - "@gentics/cms-rest-client-angular/testing": [ - "libs/cms-rest-client-angular/src/lib/testing" - ], - "@gentics/cms-rest-client/*": ["libs/cms-rest-client/src/lib/*"], - "@gentics/cms-rest-clients-angular": [ - "libs/cms-rest-clients-angular/src/public-api" - ], - "@gentics/cms-rest-clients-angular/*": [ - "libs/cms-rest-clients-angular/src/lib/*" - ], - "@gentics/cms-rest-clients-angular/testing": [ - "libs/cms-rest-clients-angular/src/testing" - ], - "@gentics/e2e-utils": ["libs/e2e-utils/src/public-api"], - "@gentics/e2e-utils/*": ["libs/e2e-utils/src/lib/*"], - "@gentics/form-generator": ["libs/form-generator/src/public-api"], - "@gentics/image-editor": ["libs/image-editor/src/public-api"], - "@gentics/ui-core": ["libs/ui-core/src/public-api"], - "@gentics/ui-core/testing": ["libs/ui-core/src/lib/testing"], - "@gentics/ui-core/testing/*": ["libs/ui-core/src/lib/testing/*"] - } + "typeRoots": ["./node_modules/@types", "./typings"] }, - "exclude": ["node_modules", "tmp", "dist"] + "extends": "./tsconfig.base.json" } From 5c5af62a84a263d1502b6e6d7bb5da25fddc2685 Mon Sep 17 00:00:00 2001 From: Decker Dominik Date: Thu, 13 Feb 2025 17:38:05 +0100 Subject: [PATCH 2/7] GPU-1897: Split functionality into separate components; Face rendering and compare --- .../piktid-editor/src/lib/common/models.ts | 45 ++- .../anonymization-editor.component.css | 35 +++ .../anonymization-editor.component.html | 49 +++ .../anonymization-editor.component.ts | 292 ++++++++++++++++++ .../face-manipulation.component.css | 49 +++ .../face-manipulation.component.html | 60 ++++ .../face-manipulation.component.ts | 61 ++++ .../image-preview/image-preview.component.css | 32 ++ .../image-preview.component.html | 40 +++ .../image-preview/image-preview.component.ts | 37 +++ .../piktid-editor/src/lib/components/index.ts | 3 + .../piktid-editor/piktid-editor.component.css | 38 +-- .../piktid-editor.component.html | 129 +++----- .../piktid-editor/piktid-editor.component.ts | 192 +++++------- .../piktid-editor/src/lib/piktid.module.ts | 17 +- .../piktid-api/piktid-api.service.ts | 94 +++++- cms-ui/libs/ui-core/src/lib/common/forms.ts | 2 +- .../lib/components/button/button.component.ts | 2 +- .../src/lib/pipes/includes/includes.pipe.ts | 25 +- 19 files changed, 948 insertions(+), 254 deletions(-) create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.css create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.html create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.css create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.html create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.ts create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.css create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.html create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.ts diff --git a/cms-ui/libs/piktid-editor/src/lib/common/models.ts b/cms-ui/libs/piktid-editor/src/lib/common/models.ts index 1701dedc0..c2e47e662 100644 --- a/cms-ui/libs/piktid-editor/src/lib/common/models.ts +++ b/cms-ui/libs/piktid-editor/src/lib/common/models.ts @@ -78,6 +78,18 @@ export interface DetectFacesResponse { } +export interface UserInfoResponse { + username: string; + email: string; + name: string; + surname: string; + affiliation: string; + credits: string; + verified: string; + contract: string; + app_name: string; +} + export interface SubstituteFaceOptions { flag_reset: number; flag_reset_single_face: number; @@ -87,7 +99,38 @@ export interface SubstituteFaceOptions { flag_png: number; } -export interface SubstituteFaceResponse {} +export interface SubstituteFaceResponse { + links: string; +} + +export interface ImageDownloadOptions { + flag_png: number; + flag_quality: number; + flag_watermark: number; +} + +export interface ImageDownloadRequest extends Partial { + id_image: string; +} + +export interface ImageDownloadResponse { + id: string; + links: string; +} + +export interface ImageLink { + /** Generated image file name */ + f: string; + /** Generated image link */ + l: string; + /** ISO Date string */ + t: string; + w: number; + /** Quality of the image? */ + q: number; + o: number; + e: number; +} export enum NotificationName { ERROR = 'error', diff --git a/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.css b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.css new file mode 100644 index 000000000..2aac92a25 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.css @@ -0,0 +1,35 @@ + +:host { + position: relative; + display: flex; + flex-direction: row; + gap: 1rem; + flex-wrap: wrap; +} + +.image-preview { + width: 50%; +} + +.controls { + flex: 1 1 auto; + display: flex; + flex-direction: column; + gap: 1rem; + + .management-controls { + padding-left: 3.2rem; + display: flex; + gap: 1rem; + flex-direction: row; + } +} + +.face-manipulation-list { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: baseline; + flex: 1 1 auto; + min-height: 1px; +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.html b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.html new file mode 100644 index 000000000..5aa547681 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.html @@ -0,0 +1,49 @@ + + +
+ +
+ {{ 'confirm_changes' }} +
+ +
+ +
+
+ + +
+ {{ 'upload_image' }} +
+
+
diff --git a/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.ts new file mode 100644 index 000000000..739cce787 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.ts @@ -0,0 +1,292 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'; +import { ChangesOf, NotificationService } from '@gentics/ui-core'; +import { filter, interval, Subscription } from 'rxjs'; +import { Coordinates, ImageLink, NewGenerationNotificationData, Notification, NotificationName } from '../../common/models'; +import { PiktidAPIService } from '../../providers'; + +@Component({ + selector: 'gtxpict-anonymization-editor', + templateUrl: './anonymization-editor.component.html', + styleUrls: ['./anonymization-editor.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy { + + @Input() + public imageUrl: string | null = null; + + @Input() + public imageId: string | null = null; + + @Input() + public faceIds: number[] = []; + + @Input() + public facePositions: Record = {}; + + @Input() + public uploading = false; + + @Output() + public uploadImage = new EventEmitter(); + + @Output() + public confirm = new EventEmitter(); + + /** If the component is currently working on something. */ + public busy = false; + + /** The list of generated faces for each face-id. */ + public faceGenerations: Record = {}; + + /** The selected generation for each face-id. ID of the generation may be `-1` if no generation is selected. */ + public selectedGeneration: Record = {}; + + /** The URL of the edited image. */ + public editedImageUrl: string | null = null; + + /** The currently active image. */ + public activeImage: 'original' | 'edited' = 'original'; + + /** The list of faces that are waiting for a generation. */ + public waitingForFaces =new Set(); + + /** The list of faces that have been confirmed. */ + public confirmedFaces = new Set(); + + /** The progress of the generation for each face. */ + public faceProgress: Record = {}; + + /** The ids of the notifications that have been processed. */ + private processedNotifications = new Set([]); + + private notificationIntervalSubscription: Subscription | null = null; + private notificationSubscription: Subscription | null = null; + private otherSubscriptions: Subscription[] = []; + + constructor( + private changeDetector: ChangeDetectorRef, + private api: PiktidAPIService, + private notificationService: NotificationService, + ) {} + + ngOnInit(): void { + this.notificationIntervalSubscription = interval(3_000).pipe( + filter(() => this.waitingForFaces.size > 0), + ).subscribe(() => { + this.fetchNotifications(); + }); + } + + ngOnChanges(changes: ChangesOf): void { + if (changes.imageId && this.imageId) { + // Fetch notifications to get the initial list of generated faces + this.fetchNotifications(); + } + if (changes.faceIds) { + this.selectedGeneration = this.faceIds.reduce((acc, faceId) => { + acc[faceId] = this.selectedGeneration[faceId] ?? -1; + return acc; + }, {} as Record); + } + } + + ngOnDestroy(): void { + this.notificationIntervalSubscription?.unsubscribe?.(); + this.notificationSubscription?.unsubscribe?.(); + this.otherSubscriptions.forEach((subscription) => subscription.unsubscribe()); + } + + public triggerUpload(): void { + this.uploadImage.emit(); + } + + public onTabChange(id: string): void { + this.activeImage = id as 'original' | 'edited'; + this.changeDetector.markForCheck(); + } + + public selectGeneration(faceId: number, generationId: number): void { + if (this.selectedGeneration[faceId] === generationId) { + this.selectedGeneration[faceId] = -1; + } else { + this.selectedGeneration[faceId] = generationId; + } + this.selectedGeneration = { ...this.selectedGeneration }; + + this.changeDetector.markForCheck(); + } + + public confirmFaceGeneration(faceId: number): void { + if (!this.imageId || !this.faceIds.includes(faceId) || this.busy) { + return; + } + + this.confirmedFaces.add(faceId); + this.confirmedFaces = new Set(this.confirmedFaces); + + if (this.selectedGeneration[faceId] === -1) { + return; + } + + this.busy = true; + this.changeDetector.markForCheck(); + + this.otherSubscriptions.push(this.api.substituteFace(this.imageId, faceId, this.selectedGeneration[faceId]).subscribe({ + next: (response) => { + this.editedImageUrl = response.l; + this.activeImage = 'edited'; + this.busy = false; + this.changeDetector.markForCheck(); + }, + error: (error) => { + this.busy = false; + this.confirmedFaces.delete(faceId); + this.confirmedFaces = new Set(this.confirmedFaces); + this.changeDetector.markForCheck(); + + console.error(error); + this.notificationService.show({ + message: error.message, + type: 'alert', + }); + }, + })); + } + + public generateFace(faceId: number): void { + if (!this.imageId || !this.faceIds.includes(faceId)) { + return; + } + + this.waitingForFaces.add(faceId); + this.waitingForFaces = new Set(this.waitingForFaces); + this.changeDetector.markForCheck(); + + this.otherSubscriptions.push(this.api.generateNewRandomFace(this.imageId, faceId).subscribe({ + next: () => { + this.changeDetector.markForCheck(); + }, + error: (error) => { + this.waitingForFaces.delete(faceId); + this.waitingForFaces = new Set(this.waitingForFaces); + this.changeDetector.markForCheck(); + console.error(error); + }, + })); + } + + public undoFaceGeneration(faceId: number): void { + if (!this.imageId || !this.faceIds.includes(faceId) || this.busy) { + return; + } + + this.busy = true; + this.confirmedFaces.delete(faceId); + this.confirmedFaces = new Set(this.confirmedFaces); + this.waitingForFaces.add(faceId); + this.waitingForFaces = new Set(this.waitingForFaces); + this.changeDetector.markForCheck(); + + this.otherSubscriptions.push(this.api.substituteFace(this.imageId, faceId, this.selectedGeneration[faceId], { + flag_reset: 1, + flag_reset_single_face: 1, + }).subscribe({ + next: (res) => { + // If there are no confirmed faces, we are back to the original image + if (this.confirmedFaces.size === 0) { + this.editedImageUrl = null; + this.activeImage = 'original'; + } else { + this.editedImageUrl = res.l; + this.activeImage = 'edited'; + } + + this.busy = false; + this.waitingForFaces.delete(faceId); + this.waitingForFaces = new Set(this.waitingForFaces); + this.selectedGeneration[faceId] = -1; + this.selectedGeneration = { ...this.selectedGeneration }; + + this.changeDetector.markForCheck(); + }, + error: (error) => { + this.busy = false; + this.waitingForFaces.delete(faceId); + this.waitingForFaces = new Set(this.waitingForFaces); + this.confirmedFaces.add(faceId); + this.confirmedFaces = new Set(this.confirmedFaces); + this.changeDetector.markForCheck(); + console.error(error); + }, + })); + } + + public fetchNotifications(): void { + if (!this.imageId) { + return; + } + + if (this.notificationSubscription) { + this.notificationSubscription.unsubscribe(); + } + + this.notificationSubscription = this.api.getNotificationsByName(this.imageId, [ + NotificationName.ERROR, + NotificationName.PROGRESS, + NotificationName.NEW_GENERATION, + ]).subscribe({ + next: (response) => { + const stillInProgress = new Set(this.waitingForFaces); + let notifications: Notification[]; + + if (Array.isArray(response)) { + notifications = response; + } else { + notifications = response.notifications_list || []; + } + + for (const singleNotif of notifications) { + if (this.processedNotifications.has(singleNotif.id)) { + continue; + } + + switch (singleNotif.name) { + case NotificationName.NEW_GENERATION: + if (!this.faceGenerations[singleNotif.data.f]) { + this.faceGenerations[singleNotif.data.f] = []; + } + + this.faceGenerations[singleNotif.data.f].push(singleNotif.data); + stillInProgress.delete(singleNotif.data.f); + break; + + case NotificationName.ERROR: + this.notificationService.show({ + message: singleNotif.data.msg, + type: 'alert', + }); + break; + + case NotificationName.PROGRESS: + this.faceProgress[singleNotif.data.f] = singleNotif.data.progress; + break; + } + + this.processedNotifications.add(singleNotif.id); + } + + this.waitingForFaces = stillInProgress; + this.changeDetector.markForCheck(); + }, + }); + } + + public confirmChanges(): void { + if (!this.imageUrl || this.busy || this.uploading || this.confirmedFaces.size === 0 ) { + return; + } + + this.confirm.emit(); + } +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.css b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.css new file mode 100644 index 000000000..7053d98d0 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.css @@ -0,0 +1,49 @@ +:host { + display: block; + width: 100%; +} + +.face-manipulation-item { + padding: 0; + width: 100%; + position: relative; + + .item-primary { + display: flex; + } + + .generation-list { + display: flex; + + .generation-item { + border: 3px solid transparent; + transition: 300ms; + + &.selected { + border-color: #0096DC; + } + + .generated-face-image-wrapper { + position: relative; + display: inline-block; + max-width: 256px; + max-height: 256px; + } + + .generated-face-image { + width: 100%; + height: 100%; + object-fit: cover; + } + } + } + + .item-actions { + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; + padding: 0.5rem 0; + } +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.html b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.html new file mode 100644 index 000000000..769b85af7 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.html @@ -0,0 +1,60 @@ + + + +
+ + {{ faceId }} + +
+ +
+
+
+
+ Generated face +
+
+
+
+ +
+ + comedy_mask + {{ 'generate_new_face' }} + + + + check + {{ 'apply_face' }} + + + + delete + {{ 'revert_face' }} + +
+
diff --git a/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.ts new file mode 100644 index 000000000..36a18ad16 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.ts @@ -0,0 +1,61 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { NewGenerationNotificationData } from '../../common/models'; + +@Component({ + selector: 'gtxpict-face-manipulation', + templateUrl: './face-manipulation.component.html', + styleUrls: ['./face-manipulation.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FaceManipulationComponent { + + @Input() + public faceId: number | null = null; + + @Input() + public generations: NewGenerationNotificationData[] = []; + + @Input() + public selectedGeneration: number | null = null; + + @Input() + public progress: number | null = null; + + @Input() + public waiting = false; + + @Input() + public confirmed = false; + + @Output() + public generationSelected = new EventEmitter(); + + @Output() + public generationConfirmed = new EventEmitter(); + + @Output() + public generateNewFace = new EventEmitter(); + + @Output() + public undoGeneration = new EventEmitter(); + + selectGeneration(generationId: number): void { + if (this.confirmed) { + return; + } + + this.generationSelected.emit(generationId); + } + + confirmGeneration(): void { + this.generationConfirmed.emit(); + } + + generateNewGeneration(): void { + this.generateNewFace.emit(); + } + + revertGeneration(): void { + this.undoGeneration.emit(); + } +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.css b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.css new file mode 100644 index 000000000..4a6c09498 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.css @@ -0,0 +1,32 @@ +.image-wrapper { + position: relative; + display: inline-block; + width: 100%; + max-height: 100%; + padding-top: 12px; + + .preview-image { + width: 100%; + height: 100%; + } + + .face-detection-wrapper { + .face-detection-item { + position: absolute; + border: 1px solid; + + .face-detection-item-label { + position: absolute; + top: 0; + left: 0; + margin-top: -1.8rem; + background: black; + padding: 2px 1rem; + margin-left: -1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + color: white; + } + } + } +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.html b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.html new file mode 100644 index 000000000..ee8ccc614 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.html @@ -0,0 +1,40 @@ + + + + + + +
+ Your image + +
+
+
+ {{ faceId }} +
+
+
+
diff --git a/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.ts new file mode 100644 index 000000000..9d586b1a8 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { Coordinates } from '../../common/models'; + +@Component({ + selector: 'gtxpict-image-preview', + templateUrl: './image-preview.component.html', + styleUrls: ['./image-preview.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ImagePreviewComponent { + + /** The URL of the original image. */ + @Input() + public originalImageUrl: string | null = null; + + /** The URL of the edited image. */ + @Input() + public editedImageUrl: string | null = null; + + /** The active image. */ + @Input() + public activeImage: 'original' | 'edited' = 'original'; + + @Input() + public faceIds: number[] = []; + + @Input() + public facePositions: Record = {}; + + /** The event emitter for the active image. */ + @Output() + public activeImageChange = new EventEmitter(); + + onTabChange(id: string): void { + this.activeImageChange.emit(id); + } +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/index.ts b/cms-ui/libs/piktid-editor/src/lib/components/index.ts index c7860cb2c..a47a14ea9 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/index.ts +++ b/cms-ui/libs/piktid-editor/src/lib/components/index.ts @@ -1 +1,4 @@ export * from './piktid-editor/piktid-editor.component'; +export * from './image-preview/image-preview.component'; +export * from './face-manipulation/face-manipulation.component'; +export * from './anonymization-editor/anonymization-editor.component'; diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css index c05c5f1ef..002aa18c3 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css +++ b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css @@ -3,7 +3,10 @@ position: relative; flex-direction: column; gap: 1rem; - padding: 1rem + padding: 1rem; + max-width: 100%; + max-height: 100%; + overflow: hidden auto; } .image-controls { @@ -14,38 +17,11 @@ width: 100%; } -.image-preview { - position: relative; - display: flex; - width: 100%; - flex-direction: row; - gap: 1rem; - flex-wrap: wrap; -} - .image-status { width: 100%; } -.image-wrapper { - position: relative; - display: inline-block; -} - -.image-wrapper .face-detection-wrapper .face-detection-item { - position: absolute; - border: 1px solid; -} - -.image-wrapper .face-detection-wrapper .face-detection-item-label { - position: absolute; - top: 0; - left: 0; - margin-top: -1.8rem; - background: black; - padding: 2px 1rem; - margin-left: -1px; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - color: white; +.image-editor { + width: 100%; + max-height: 100%; } diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html index 7397d1973..4574d0c8e 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html +++ b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html @@ -1,98 +1,43 @@ -
- - image{{ 'select_image' }} - - - - upload{{ 'upload_image' }} - - - - check{{ 'confirm_editing' }} - -
- -
Image Upload Status: {{ imageUploadStatus }}
- -
- -
- Your image - -
-
-
- {{ faceId }} -
-
+ +
+ + + Login +
+
+ + + -
- -
- - {{ faceId }} - -
- -
-
-
-
- Generated face -
-
-
-
- -
- - comedy_mask - {{ 'generate_new_face' }} - - - - check - {{ 'apply_face' }} - -
-
+
+ + image{{ 'select_image' }} +
-
+ + +
diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts index 3905fd124..a8f136e88 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts +++ b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts @@ -1,10 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion,@typescript-eslint/no-non-null-assertion */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { NotificationService } from '@gentics/ui-core'; -import { filter, interval, Subscription } from 'rxjs'; -import { Coordinates, NewGenerationNotificationData, NotificationName } from '../../common/models'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { FormProperties, NotificationService } from '@gentics/ui-core'; +import { filter, interval, map, Subscription, switchMap } from 'rxjs'; +import { Coordinates, NewGenerationNotificationData, Notification, NotificationName, UserInfoResponse } from '../../common/models'; import { FaceData } from '../../common/prompt'; import { PiktidAPIService } from '../../providers/piktid-api/piktid-api.service'; +interface LoginFormProperties { + username: string; + password: string; +} + +const LOCAL_STORAGE_KEY = 'piktid-editor-auth'; + @Component({ selector: 'gtxpict-piktid-editor', templateUrl: './piktid-editor.component.html', @@ -13,12 +22,26 @@ import { PiktidAPIService } from '../../providers/piktid-api/piktid-api.service' }) export class PiktidEditorComponent implements OnInit, OnDestroy { + /** Whether the user is logged in. */ + public loggedIn = false; + + /** The user info. */ + public userInfo: UserInfoResponse | null = null; + + public loginForm: FormGroup> | null = null; + /** The image that is currently picked by the user. */ public pickedImage: File | null = null; /** The URL of the preview image. */ public imageUrl: string | null = null; + /** The URL of the edited image. */ + public editedImageUrl: string | null = null; + + /** The active image. */ + public activeImage: 'original' | 'edited' = 'original'; + /** The image-id of the uploaded image. If `null`, the image is not uploaded yet. */ public imageId: string | null = null; @@ -46,18 +69,7 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { /** Whether the component is busy. */ public busy = false; - /** The list of faces that are waiting for a generation. */ - public waitingForFaces: number[] = []; - - /** The list of faces that have been confirmed. */ - public confirmedFaces: number[] = []; - - /** The ids of the notifications that have been processed. */ - private processedNotifications = new Set([]); - private uploadSubscription: Subscription | null = null; - private notificationIntervalSubscription: Subscription | null = null; - private notificationSubscription: Subscription | null = null; private otherSubscriptions: Subscription[] = []; constructor( @@ -67,20 +79,58 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { - this.notificationIntervalSubscription = interval(3_000).pipe( - filter(() => this.waitingForFaces.length > 0), - ).subscribe(() => { - this.fetchNotifications(); + this.loginForm = new FormGroup>({ + username: new FormControl('', Validators.required), + password: new FormControl('', Validators.required), }); + + const storedAuth = localStorage.getItem(LOCAL_STORAGE_KEY); + if (storedAuth) { + this.api.setAuth(storedAuth.split(':')[0], storedAuth.split(':')[1]); + } + + this.loggedIn = this.api.isLoggedIn(); + + if (this.loggedIn) { + this.otherSubscriptions.push(this.api.getUserInfo().subscribe({ + next: (response) => { + this.userInfo = response; + this.changeDetector.markForCheck(); + }, + })); + } } ngOnDestroy(): void { this.uploadSubscription?.unsubscribe?.(); - this.notificationIntervalSubscription?.unsubscribe?.(); - this.notificationSubscription?.unsubscribe?.(); this.otherSubscriptions.forEach((subscription) => subscription.unsubscribe()); } + public onLogin(): void { + this.busy = true; + this.changeDetector.markForCheck(); + + this.otherSubscriptions.push(this.api.authenticate(this.loginForm!.value.username!, this.loginForm!.value.password!).pipe( + switchMap(auth => this.api.getUserInfo().pipe( + map((userInfo) => ({ auth, userInfo })), + )), + ).subscribe({ + next: ({ auth, userInfo }) => { + this.loggedIn = true; + this.busy = false; + this.userInfo = userInfo; + this.changeDetector.markForCheck(); + + localStorage.setItem(LOCAL_STORAGE_KEY, `${auth.access_token}:${auth.refresh_token}`); + }, + error: (error) => { + this.busy = false; + console.error(error); + this.changeDetector.markForCheck(); + }, + })); + } + public onImagePicked(files: File[]): void { this.pickedImage = files[0]; this.imageUrl = URL.createObjectURL(files[0]); @@ -92,7 +142,9 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { this.faceIds = []; this.facePositions = {}; this.faceDescriptions = {}; - + this.editedImageUrl = null; + this.activeImage = 'original'; + this.selectedGeneration = {}; this.changeDetector.markForCheck(); } @@ -135,9 +187,6 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { this.busy = false; this.changeDetector.markForCheck(); - - // Fetch notifications to get the initial list of generated faces - this.fetchNotifications(); }, error: (error) => { this.imageUploadStatus = 'error'; @@ -154,94 +203,11 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { }); } - public generateFace(faceId: number): void { - if (!this.imageId || !this.faceIds.includes(faceId)) { - return; - } + // public confirmEditing(): void { + // if (!this.imageId || this.imageUploadStatus !== 'success' || this.confirmedFaces.size !== this.faceIds.length) { + // return; + // } - this.otherSubscriptions.push(this.api.generateNewRandomFace(this.imageId, faceId).subscribe({ - next: (response) => { - console.log('new expression generated', response); - this.waitingForFaces.push(faceId); - this.changeDetector.markForCheck(); - }, - error: (error) => { - console.error(error); - }, - })); - } - - public confirmFaceGeneration(faceId: number): void { - if (!this.imageId || !this.faceIds.includes(faceId) || this.selectedGeneration[faceId] === -1) { - return; - } - - this.otherSubscriptions.push(this.api.substituteFace(this.imageId, faceId, this.selectedGeneration[faceId]).subscribe({ - next: (response) => { - console.log('face generation confirmed', response); - this.confirmedFaces.push(faceId); - this.changeDetector.markForCheck(); - }, - error: (error) => { - console.error(error); - }, - })); - } - - public fetchNotifications(): void { - if (!this.imageId) { - return; - } - - if (this.notificationSubscription) { - this.notificationSubscription.unsubscribe(); - } - - this.notificationSubscription = this.api.getNotificationsByName(this.imageId, [ - // NotificationName.ERROR, - NotificationName.NEW_GENERATION, - ]).subscribe({ - next: (response) => { - const stillInProgress = new Set(this.waitingForFaces); - const notifications = response.notifications_list || []; - - for (const singleNotif of notifications) { - if (this.processedNotifications.has(singleNotif.id)) { - continue; - } - - switch (singleNotif.name) { - case NotificationName.NEW_GENERATION: - if (!this.generatedFaces[singleNotif.data.f]) { - this.generatedFaces[singleNotif.data.f] = []; - } - - this.generatedFaces[singleNotif.data.f].push(singleNotif.data); - stillInProgress.delete(singleNotif.data.f); - break; - - case NotificationName.ERROR: - this.notificationService.show({ - message: singleNotif.data.msg, - type: 'alert', - }); - break; - } - - this.processedNotifications.add(singleNotif.id); - } - - this.waitingForFaces = Array.from(stillInProgress); - this.changeDetector.markForCheck(); - }, - }); - } - - public confirmEditing(): void { - if (!this.imageId || this.imageUploadStatus !== 'success' || this.confirmedFaces.length !== this.faceIds.length) { - return; - } - - // TODO: Upload the image to the CMS (Emit event here and do upload in parent) - } + // // TODO: Upload the image to the CMS (Emit event here and do upload in parent) + // } } diff --git a/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts b/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts index 4ddc71c58..0b79aa833 100644 --- a/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts +++ b/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts @@ -1,12 +1,23 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { GenticsUICoreModule } from '@gentics/ui-core'; -import { PiktidEditorComponent } from './components'; +import { + AnonymizationEditorComponent, + FaceManipulationComponent, + ImagePreviewComponent, + PiktidEditorComponent, +} from './components'; import { PiktidAPIService } from './providers'; @NgModule({ - imports: [CommonModule, GenticsUICoreModule], - declarations: [PiktidEditorComponent], + imports: [CommonModule, GenticsUICoreModule, ReactiveFormsModule, FormsModule], + declarations: [ + PiktidEditorComponent, + ImagePreviewComponent, + FaceManipulationComponent, + AnonymizationEditorComponent, + ], exports: [PiktidEditorComponent], providers: [PiktidAPIService], }) diff --git a/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts b/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts index ac48c3a1d..98a15c474 100644 --- a/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts +++ b/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, of, tap } from 'rxjs'; +import { map, Observable, of, tap, throwError } from 'rxjs'; import { AnonymizationOptions, AuthenticationResponse, @@ -13,8 +13,13 @@ import { NotificationListResponse, NotificationName, RandomFaceResponse, + ImageLink, SubstituteFaceOptions, SubstituteFaceResponse, + UserInfoResponse, + ImageDownloadResponse, + ImageDownloadOptions, + ImageDownloadRequest, } from '../../common/models'; @Injectable() @@ -27,6 +32,15 @@ export class PiktidAPIService { private http: HttpClient, ) {} + public setAuth(accessToken: string, refreshToken: string): void { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public isLoggedIn(): boolean { + return !!this.accessToken; + } + authenticate(username: string, password: string): Observable { if (this.accessToken) { return of({ @@ -51,7 +65,25 @@ export class PiktidAPIService { ); } + getUserInfo(): Observable { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + + return this.http.get('/piktid/api/me', { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }); + } + uploadFile(file: File, options: AnonymizationOptions): Observable { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + const data = new FormData(); data.set('options', JSON.stringify(options)); data.set('file', file); @@ -66,6 +98,10 @@ export class PiktidAPIService { } detectFaces(imageId: string): Observable { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + return this.http.post('/piktid/api/detect_faces', { id_image: imageId, }, { @@ -78,6 +114,10 @@ export class PiktidAPIService { } generateNewExpression(imageId: string, faceId: number, options?: Partial): Observable { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + const req: GenerateExpressionRequest = { id_image: imageId, id_face: faceId, @@ -94,6 +134,10 @@ export class PiktidAPIService { } generateNewRandomFace(imageId: string, faceId: number): Observable { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + return this.http.post('/piktid/api/ask_random_face', { id_image: imageId, id_face: faceId, @@ -108,9 +152,13 @@ export class PiktidAPIService { } getNotificationsByName(imageId: string, notificationNames: NotificationName[]): Observable { - return this.http.post('/piktid/api/notification_by_name', { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + + return this.http.post('/piktid/api/notification_by_name_json', { id_image: imageId, - name_list: notificationNames.join(','), + name_list: notificationNames.join(', '), }, { observe: 'body', responseType: 'json', @@ -125,8 +173,13 @@ export class PiktidAPIService { faceId: number, generationId: number, options?: Partial, - ): Observable { - return this.http.post('/piktid/api/pick_face2', { + ): Observable { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + + // TODO: Use pick_face2 when it's actually ready - currently only throws errors + return this.http.post('/piktid/api/pick_face', { id_image: imageId, id_face: faceId, id_generation: generationId, @@ -137,6 +190,35 @@ export class PiktidAPIService { headers: new HttpHeaders({ Authorization: `Bearer ${this.accessToken}`, }), - }); + }).pipe( + map((response) => { + const links = JSON.parse(response.links); + return links; + }), + ); + } + + getImageDownloadLink(imageId: string, options?: Partial): Observable { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + + const body: ImageDownloadRequest = { + id_image: imageId, + ...options, + }; + + return this.http.post('/piktid/api/download', body, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }).pipe( + map((response) => { + const links = JSON.parse(response.links); + return links; + }), + ); } } diff --git a/cms-ui/libs/ui-core/src/lib/common/forms.ts b/cms-ui/libs/ui-core/src/lib/common/forms.ts index d77908797..3693d1ef3 100644 --- a/cms-ui/libs/ui-core/src/lib/common/forms.ts +++ b/cms-ui/libs/ui-core/src/lib/common/forms.ts @@ -57,7 +57,7 @@ export interface JsonValidationErrorModel { * ``` */ export type FormProperties = { - [P in keyof T]: AbstractControl; + [P in keyof T]: AbstractControl; }; /** diff --git a/cms-ui/libs/ui-core/src/lib/components/button/button.component.ts b/cms-ui/libs/ui-core/src/lib/components/button/button.component.ts index e9089f20f..ddcdf2605 100644 --- a/cms-ui/libs/ui-core/src/lib/components/button/button.component.ts +++ b/cms-ui/libs/ui-core/src/lib/components/button/button.component.ts @@ -37,7 +37,7 @@ export class ButtonComponent { * "success", "warning" or "alert". */ @Input() - type: 'default' | 'secondary' | 'success' | 'warning' | 'alert' = 'default'; + type: 'default' | 'secondary' | 'success' | 'warning' | 'alert' | 'primary' = 'default'; /** * Setting the "flat" attribute gives the button a transparent background diff --git a/cms-ui/libs/ui-core/src/lib/pipes/includes/includes.pipe.ts b/cms-ui/libs/ui-core/src/lib/pipes/includes/includes.pipe.ts index 185f2a92a..f9a05b19f 100644 --- a/cms-ui/libs/ui-core/src/lib/pipes/includes/includes.pipe.ts +++ b/cms-ui/libs/ui-core/src/lib/pipes/includes/includes.pipe.ts @@ -12,7 +12,7 @@ interface BasicObject { [key: string | number | symbol]: any; } -type SourceValue = BasicObject | Array | Set; +type SourceValue = BasicObject | Iterable; export const DEFAULT_COMPARE_FN: EqualityFn = (a, b, strict) => { // Null-Checks are always not strict @@ -36,12 +36,17 @@ export const DEFAULT_COMPARE_FN: EqualityFn = (a, b, strict) => { * ``` * {{ [123, 'cool'] | includes:{ strict: false, values:['foobar'] } }} * ``` + * + * For a custom compare function, you can pass a function as the `compareFn` option: + * ``` + * {{ [123, 'cool'] | includes:{ compareFn: myCompareFn, values:['123'] } }} + * ``` */ @Pipe({ name: 'gtxIncludes' }) export class IncludesPipe implements PipeTransform { - transform(sourceValue: SourceValue, optionsOrValues: IncludesOptions | Array | Set): boolean; - transform(sourceValue: SourceValue, ...args: Array): boolean { + transform(sourceValue: SourceValue, optionsOrValues: IncludesOptions | T | Iterable): boolean; + transform(sourceValue: SourceValue, ...args: Array): boolean { if (sourceValue == null || typeof sourceValue !== 'object') { return false; } @@ -90,10 +95,18 @@ export class IncludesPipe implements PipeTransform { sourceValue = Object.keys(sourceValue); } - // Compare the values - for (const singleSourceValue of (sourceValue as Array)) { + if (sourceValue[Symbol.iterator]) { + // Compare the values by iterating over the source value + for (const singleSourceValue of sourceValue as Iterable) { + for (const singleCheckValue of valuesToCheck) { + if (compareFn(singleSourceValue, singleCheckValue, strict)) { + return true; + } + } + } + } else { for (const singleCheckValue of valuesToCheck) { - if (compareFn(singleSourceValue, singleCheckValue, strict)) { + if (sourceValue.hasOwnProperty(singleCheckValue)) { return true; } } From bd04e4547dc903e6e6b02e0f0aa6217473427649 Mon Sep 17 00:00:00 2001 From: Decker Dominik Date: Mon, 17 Feb 2025 20:09:58 +0100 Subject: [PATCH 3/7] GPU-1897: Editor-UI integration and cms upload handling --- cms-ui/apps/editor-ui/project.json | 7 +- cms-ui/apps/editor-ui/src/app/app.routes.ts | 7 +- .../file-preview/file-preview.component.ts | 17 ++- .../file-preview/file-preview.tpl.html | 18 ++- .../image-anonymize-modal.component.html | 28 ++++ .../image-anonymize-modal.component.scss | 0 .../image-anonymize-modal.component.ts | 132 ++++++++++++++++++ .../src/app/content-frame/components/index.ts | 1 + .../app/content-frame/content-frame.module.ts | 4 + .../resource-url-builder.ts | 2 +- .../libs/cms-models/src/lib/models/request.ts | 6 +- cms-ui/libs/piktid-editor/src/index.ts | 2 + .../anonymization-editor.component.html | 20 +-- .../anonymization-editor.component.ts | 36 +++-- .../editor.component.css} | 0 .../components/editor/editor.component.html | 15 ++ .../editor.component.ts} | 119 +++++----------- .../face-manipulation.component.html | 6 +- .../face-manipulation.component.ts | 7 +- .../image-preview/image-preview.component.ts | 2 +- .../piktid-editor/src/lib/components/index.ts | 7 +- .../login-gate/login-gate.component.css | 0 .../login-gate/login-gate.component.html | 24 ++++ .../login-gate/login-gate.component.ts | 91 ++++++++++++ .../piktid-editor.component.html | 43 ------ .../piktid-editor/src/lib/piktid.module.ts | 20 ++- .../piktid-api/piktid-api.service.ts | 43 ++++-- 27 files changed, 454 insertions(+), 203 deletions(-) create mode 100644 cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.html create mode 100644 cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.scss create mode 100644 cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.ts rename cms-ui/libs/piktid-editor/src/lib/components/{piktid-editor/piktid-editor.component.css => editor/editor.component.css} (100%) create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.html rename cms-ui/libs/piktid-editor/src/lib/components/{piktid-editor/piktid-editor.component.ts => editor/editor.component.ts} (56%) create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.css create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.html create mode 100644 cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.ts delete mode 100644 cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html diff --git a/cms-ui/apps/editor-ui/project.json b/cms-ui/apps/editor-ui/project.json index efea472a1..9a63ed869 100644 --- a/cms-ui/apps/editor-ui/project.json +++ b/cms-ui/apps/editor-ui/project.json @@ -82,7 +82,12 @@ "executor": "@nx/angular:dev-server", "options": { "proxyConfig": "proxy.conf.json", - "liveReload": false + "liveReload": false, + "headers": { + "Access-Control-Allow-Origin": "https://d3ajjlx2rocmin.cloudfront.net", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Vary": "Origin" + } }, "configurations": { "production": { diff --git a/cms-ui/apps/editor-ui/src/app/app.routes.ts b/cms-ui/apps/editor-ui/src/app/app.routes.ts index 5c67c938e..7359246e2 100644 --- a/cms-ui/apps/editor-ui/src/app/app.routes.ts +++ b/cms-ui/apps/editor-ui/src/app/app.routes.ts @@ -1,4 +1,5 @@ import { Route } from '@angular/router'; +import { EditorOutlet } from './common/models'; import { NoNodesComponent, TagEditorRouteComponent } from './core/components'; import { ProjectEditorComponent } from './core/components/project-editor/project-editor.component'; import { AuthGuard } from './core/providers/guards/auth-guard'; @@ -6,8 +7,6 @@ import { OpenModalGuard } from './core/providers/guards/open-modal-guard'; import { ToolOverviewComponent } from './embedded-tools/components/tool-overview/tool-overview.component'; import { ToolProxyComponent } from './embedded-tools/components/tool-proxy/tool-proxy.component'; import { ProxyRouteComponent, RessourceProxyComponent } from './shared/components'; -import { EditorOutlet } from './common/models'; -import { PiktidEditorComponent } from '@gentics/picktid-editor'; export const APP_ROUTES: Route[] = [ { @@ -111,10 +110,6 @@ export const APP_ROUTES: Route[] = [ component: TagEditorRouteComponent, canActivate: [AuthGuard], }, - { - path: 'piktid-editor', - component: PiktidEditorComponent, - }, { path: 'proxy', canActivate: [AuthGuard], diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.component.ts b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.component.ts index 015f5da22..b0aabb7f9 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.component.ts +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.component.ts @@ -20,7 +20,7 @@ import { RotateParameters, User, } from '@gentics/cms-models'; -import { ProgressBarComponent } from '@gentics/ui-core'; +import { ModalService, ProgressBarComponent } from '@gentics/ui-core'; import { Observable, Subscription, of } from 'rxjs'; import { map, publishReplay, refCount, startWith, switchMap } from 'rxjs/operators'; import { getFileExtension } from '../../../common/utils/get-file-extension'; @@ -31,6 +31,7 @@ import { NavigationService } from '../../../core/providers/navigation/navigation import { PermissionService } from '../../../core/providers/permissions/permission.service'; import { ResourceUrlBuilder } from '../../../core/providers/resource-url-builder/resource-url-builder'; import { ApplicationStateService, ApplyImageDimensionsAction, FolderActionsService } from '../../../state'; +import { ImageAnonymizeModal } from '../image-anonymize-modal/image-anonymize-modal.component'; @Component({ selector: 'file-preview', @@ -78,6 +79,7 @@ export class FilePreviewComponent implements OnInit, OnChanges, OnDestroy { private entityResolver: EntityResolver, private notification: I18nNotification, private folderActions: FolderActionsService, + private modalService: ModalService, ) {} ngOnInit(): void { @@ -135,6 +137,19 @@ export class FilePreviewComponent implements OnInit, OnChanges, OnDestroy { this.subscriptions.forEach(s => s.unsubscribe()); } + anonymizeImage(): void { + const nodeId = this.appState.now.editor.nodeId; + + this.modalService.fromComponent(ImageAnonymizeModal, { + closeOnEscape: false, + closeOnOverlayClick: false, + width: '100%', + }, { + imageId: this.file.id, + nodeId, + }).then(comp => comp.open()); + } + editImage(): void { const nodeId = this.appState.now.editor.nodeId; this.navigationService.modal(nodeId, 'image', this.file.id, EditMode.EDIT).navigate(); diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.tpl.html b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.tpl.html index bb0e24ba9..7aaacded8 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.tpl.html +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/file-preview/file-preview.tpl.html @@ -85,11 +85,19 @@
-
- - edit {{ 'editor.edit_image_button' | i18n }} - -
+ +
+ + theater_comedy {{ 'editor.anonymize_people_button' | i18n }} + +
+ +
+ + edit {{ 'editor.edit_image_button' | i18n }} + +
+
diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.html b/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.html new file mode 100644 index 000000000..e77e8a626 --- /dev/null +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.html @@ -0,0 +1,28 @@ + + + + + + diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.scss b/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.ts b/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.ts new file mode 100644 index 000000000..db56c631d --- /dev/null +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.ts @@ -0,0 +1,132 @@ +import { HttpClient } from '@angular/common/http'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { I18nNotification } from '@editor-ui/app/core/providers/i18n-notification/i18n-notification.service'; +import { ResourceUrlBuilder } from '@editor-ui/app/core/providers/resource-url-builder/resource-url-builder'; +import { Image } from '@gentics/cms-models'; +import { GCMSRestClientService } from '@gentics/cms-rest-client-angular'; +import { PiktidAPIService } from '@gentics/picktid-editor'; +import { BaseModal } from '@gentics/ui-core'; +import { map, Subscription, switchMap } from 'rxjs'; + +@Component({ + selector: 'gtx-image-anonymize-modal', + templateUrl: './image-anonymize-modal.component.html', + styleUrls: ['./image-anonymize-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ImageAnonymizeModal extends BaseModal implements OnInit, OnDestroy { + + @Input() + public imageId: number | string; + + @Input() + public nodeId: number | string; + + public loadedImage: Image; + + public imageBlob: Blob; + + public loading = false; + + public piktidImageId: string; + public confirmedIds: number[] = []; + + private subscriptions: Subscription[] = []; + + constructor( + private changeDetector: ChangeDetectorRef, + private resourceUrlBuilder: ResourceUrlBuilder, + private api: GCMSRestClientService, + private piktid: PiktidAPIService, + private http: HttpClient, + private notifications: I18nNotification, + ) { + super(); + } + + ngOnInit(): void { + this.subscriptions.push(this.api.image.get(this.imageId, { nodeId: this.nodeId, update: false }).subscribe(res => { + this.loadedImage = res.image; + this.changeDetector.markForCheck(); + })); + + const originalImageUrl = this.resourceUrlBuilder.imageFullsize(this.imageId, this.nodeId); + this.subscriptions.push(this.http.get(originalImageUrl, { + observe: 'body', + responseType: 'blob', + }).subscribe(downloadedImage => { + this.imageBlob = downloadedImage; + this.changeDetector.markForCheck(); + })); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + public updateImageId(id: string): void { + this.piktidImageId = id; + } + + public updateConfirmation(ids: number[]): void { + this.confirmedIds = ids; + } + + public processImage(): void { + if (this.piktidImageId == null || this.confirmedIds?.length === 0) { + return; + } + + this.loading = true; + + // Generate the final version of the anonymized image + this.subscriptions.push(this.piktid.getImageDownloadLink(this.piktidImageId, { + flag_watermark: 0, + flag_png: 0, + flag_quality: 0, + }).pipe( + // Download the generated image + switchMap(links => this.http.get(links.l, { + observe: 'body', + responseType: 'blob', + })), + // Upload the generated image to the CMS + switchMap(newImageBlob => { + (newImageBlob as any).name = `anonymized_${this.loadedImage.name}`; + + return this.api.file.upload(newImageBlob, { + folderId: this.loadedImage.folderId, + nodeId: this.nodeId, + }); + }), + // Delete the image from piktid after successful upload + switchMap(uploaded => { + return this.piktid.deleteImage(this.piktidImageId).pipe( + map(() => uploaded), + ); + }), + ).subscribe({ + next: res => { + this.loading = false; + this.changeDetector.markForCheck(); + + console.log(res); + + this.notifications.show({ + message: 'image successfully anoonymized', + type: 'success', + }) + }, + error: err => { + this.loading = false; + this.changeDetector.markForCheck(); + + console.error(err); + this.notifications.show({ + message: err?.message ?? err, + type: 'alert', + }); + }, + })); + } +} diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/index.ts b/cms-ui/apps/editor-ui/src/app/content-frame/components/index.ts index dfbedc25c..c5d52395a 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/components/index.ts +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/index.ts @@ -29,6 +29,7 @@ export * from './dynamic-form-modal/dynamic-form-modal.component'; export * from './editor-toolbar/editor-toolbar.component'; export * from './file-preview/file-preview.component'; export * from './form-reports-list/form-reports-list.component'; +export * from './image-anonymize-modal/image-anonymize-modal.component'; export * from './image-properties-modal/image-properties-modal.component'; export * from './link-checker-controls/link-checker-controls.component'; export * from './node-properties/node-properties.component'; diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/content-frame.module.ts b/cms-ui/apps/editor-ui/src/app/content-frame/content-frame.module.ts index 1f1cb761d..e3f003aac 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/content-frame.module.ts +++ b/cms-ui/apps/editor-ui/src/app/content-frame/content-frame.module.ts @@ -3,6 +3,7 @@ import { RouterModule } from '@angular/router'; import { GenticsUICoreModule } from '@gentics/ui-core'; import { ColorAlphaModule } from 'ngx-color/alpha'; import { ColorSliderModule } from 'ngx-color/slider'; +import { PiktidModule } from '../../../../../libs/piktid-editor/src/lib/piktid.module'; import { EditorOverlayModule } from '../editor-overlay/editor-overlay.module'; import { SharedModule } from '../shared/shared.module'; import { TagEditorModule } from '../tag-editor'; @@ -38,6 +39,7 @@ import { EditorToolbarComponent, FilePreviewComponent, FormReportsListComponent, + ImageAnonymizeModal, ImagePropertiesModalComponent, LinkCheckerControlsComponent, NodePropertiesComponent, @@ -89,6 +91,7 @@ const COMPONENTS = [ EditorToolbarComponent, FilePreviewComponent, FormReportsListComponent, + ImageAnonymizeModal, LinkCheckerControlsComponent, NodePropertiesComponent, PageEditorControlsComponent, @@ -129,6 +132,7 @@ const MODULE_INITIALIZER: Provider = { ColorAlphaModule, RouterModule.forChild(contentFrameRoutes), GenticsUICoreModule, + PiktidModule, ], exports: [], declarations: [...COMPONENTS], diff --git a/cms-ui/apps/editor-ui/src/app/core/providers/resource-url-builder/resource-url-builder.ts b/cms-ui/apps/editor-ui/src/app/core/providers/resource-url-builder/resource-url-builder.ts index 34eb9eb6f..aecf69bb5 100644 --- a/cms-ui/apps/editor-ui/src/app/core/providers/resource-url-builder/resource-url-builder.ts +++ b/cms-ui/apps/editor-ui/src/app/core/providers/resource-url-builder/resource-url-builder.ts @@ -67,7 +67,7 @@ export class ResourceUrlBuilder { /** * Returns the full-size URL of an image. */ - imageFullsize(imageId: number, nodeId: number, changeDate?: number): string { + imageFullsize(imageId: number | string, nodeId: number | string, changeDate?: number): string { const cacheBust = changeDate ? String(changeDate) : Math.random().toString(36).substr(5); const data = { diff --git a/cms-ui/libs/cms-models/src/lib/models/request.ts b/cms-ui/libs/cms-models/src/lib/models/request.ts index 06a592c78..fbb3378be 100644 --- a/cms-ui/libs/cms-models/src/lib/models/request.ts +++ b/cms-ui/libs/cms-models/src/lib/models/request.ts @@ -90,7 +90,7 @@ export interface LoginOptions { export interface ItemRequestOptions { /** ID of the node (channel) for which the item shall be loaded (when multichannelling is used). */ - nodeId?: number; + nodeId?: number | string; /** true when the item should be fetched for updating */ update?: boolean; @@ -1053,12 +1053,12 @@ export interface FileCreateRequest { export interface FileUploadOptions { folderId: number; - nodeId: number; + nodeId: number | string; } export interface FileReplaceOptions { folderId?: number; - nodeId: number; + nodeId: number | string; } /** diff --git a/cms-ui/libs/piktid-editor/src/index.ts b/cms-ui/libs/piktid-editor/src/index.ts index 7178539c5..f2b4a0415 100644 --- a/cms-ui/libs/piktid-editor/src/index.ts +++ b/cms-ui/libs/piktid-editor/src/index.ts @@ -1,3 +1,5 @@ +export * from './lib/common/models'; +export * from './lib/common/prompt'; export * from './lib/components'; export * from './lib/piktid.module'; export * from './lib/providers'; diff --git a/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.html b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.html index 5aa547681..6f02b4db1 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.html +++ b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.html @@ -1,4 +1,4 @@ - +>
-
- {{ 'confirm_changes' }} -
-
- + >
@@ -41,7 +33,7 @@
{{ 'upload_image' }}
diff --git a/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.ts index 739cce787..0aa1e4735 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.ts +++ b/cms-ui/libs/piktid-editor/src/lib/components/anonymization-editor/anonymization-editor.component.ts @@ -5,7 +5,7 @@ import { Coordinates, ImageLink, NewGenerationNotificationData, Notification, No import { PiktidAPIService } from '../../providers'; @Component({ - selector: 'gtxpict-anonymization-editor', + selector: 'gtxpikt-anonymization-editor', templateUrl: './anonymization-editor.component.html', styleUrls: ['./anonymization-editor.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -27,14 +27,17 @@ export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy @Input() public uploading = false; + @Input() + public disabled = false; + @Output() public uploadImage = new EventEmitter(); @Output() - public confirm = new EventEmitter(); + public confirmChange = new EventEmitter(); /** If the component is currently working on something. */ - public busy = false; + public loading = false; /** The list of generated faces for each face-id. */ public faceGenerations: Record = {}; @@ -49,7 +52,7 @@ export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy public activeImage: 'original' | 'edited' = 'original'; /** The list of faces that are waiting for a generation. */ - public waitingForFaces =new Set(); + public waitingForFaces = new Set(); /** The list of faces that have been confirmed. */ public confirmedFaces = new Set(); @@ -118,7 +121,7 @@ export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy } public confirmFaceGeneration(faceId: number): void { - if (!this.imageId || !this.faceIds.includes(faceId) || this.busy) { + if (!this.imageId || !this.faceIds.includes(faceId) || this.loading) { return; } @@ -129,18 +132,19 @@ export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy return; } - this.busy = true; + this.loading = true; this.changeDetector.markForCheck(); this.otherSubscriptions.push(this.api.substituteFace(this.imageId, faceId, this.selectedGeneration[faceId]).subscribe({ next: (response) => { this.editedImageUrl = response.l; this.activeImage = 'edited'; - this.busy = false; + this.loading = false; + this.confirmChange.emit(Array.from(this.confirmedFaces)); this.changeDetector.markForCheck(); }, error: (error) => { - this.busy = false; + this.loading = false; this.confirmedFaces.delete(faceId); this.confirmedFaces = new Set(this.confirmedFaces); this.changeDetector.markForCheck(); @@ -177,11 +181,11 @@ export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy } public undoFaceGeneration(faceId: number): void { - if (!this.imageId || !this.faceIds.includes(faceId) || this.busy) { + if (!this.imageId || !this.faceIds.includes(faceId) || this.loading) { return; } - this.busy = true; + this.loading = true; this.confirmedFaces.delete(faceId); this.confirmedFaces = new Set(this.confirmedFaces); this.waitingForFaces.add(faceId); @@ -202,7 +206,7 @@ export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy this.activeImage = 'edited'; } - this.busy = false; + this.loading = false; this.waitingForFaces.delete(faceId); this.waitingForFaces = new Set(this.waitingForFaces); this.selectedGeneration[faceId] = -1; @@ -211,7 +215,7 @@ export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy this.changeDetector.markForCheck(); }, error: (error) => { - this.busy = false; + this.loading = false; this.waitingForFaces.delete(faceId); this.waitingForFaces = new Set(this.waitingForFaces); this.confirmedFaces.add(faceId); @@ -281,12 +285,4 @@ export class AnonymizationEditorComponent implements OnInit, OnChanges,OnDestroy }, }); } - - public confirmChanges(): void { - if (!this.imageUrl || this.busy || this.uploading || this.confirmedFaces.size === 0 ) { - return; - } - - this.confirm.emit(); - } } diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css b/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.css similarity index 100% rename from cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.css rename to cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.css diff --git a/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.html b/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.html new file mode 100644 index 000000000..0e8fffd01 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.html @@ -0,0 +1,15 @@ + + + + + diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.ts similarity index 56% rename from cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts rename to cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.ts index a8f136e88..acd57767e 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.ts +++ b/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.ts @@ -1,37 +1,30 @@ /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion,@typescript-eslint/no-non-null-assertion */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { FormProperties, NotificationService } from '@gentics/ui-core'; -import { filter, interval, map, Subscription, switchMap } from 'rxjs'; -import { Coordinates, NewGenerationNotificationData, Notification, NotificationName, UserInfoResponse } from '../../common/models'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core'; +import { ChangesOf, NotificationService } from '@gentics/ui-core'; +import { Subscription } from 'rxjs'; +import { Coordinates, NewGenerationNotificationData } from '../../common/models'; import { FaceData } from '../../common/prompt'; import { PiktidAPIService } from '../../providers/piktid-api/piktid-api.service'; -interface LoginFormProperties { - username: string; - password: string; -} - -const LOCAL_STORAGE_KEY = 'piktid-editor-auth'; - @Component({ - selector: 'gtxpict-piktid-editor', - templateUrl: './piktid-editor.component.html', - styleUrl: './piktid-editor.component.css', + selector: 'gtxpict-editor', + templateUrl: './editor.component.html', + styleUrl: './editor.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PiktidEditorComponent implements OnInit, OnDestroy { +export class EditorComponent implements OnChanges, OnDestroy { - /** Whether the user is logged in. */ - public loggedIn = false; + @Input() + public imageBlob: Blob | File | null = null; - /** The user info. */ - public userInfo: UserInfoResponse | null = null; + @Input() + public disabled = false; - public loginForm: FormGroup> | null = null; + @Output() + public imageUpload = new EventEmitter(); - /** The image that is currently picked by the user. */ - public pickedImage: File | null = null; + @Output() + public confirmChange = new EventEmitter(); /** The URL of the preview image. */ public imageUrl: string | null = null; @@ -67,7 +60,7 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { public selectedGeneration: Record = {}; /** Whether the component is busy. */ - public busy = false; + public loading = false; private uploadSubscription: Subscription | null = null; private otherSubscriptions: Subscription[] = []; @@ -78,26 +71,9 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { private notificationService: NotificationService, ) {} - ngOnInit(): void { - this.loginForm = new FormGroup>({ - username: new FormControl('', Validators.required), - password: new FormControl('', Validators.required), - }); - - const storedAuth = localStorage.getItem(LOCAL_STORAGE_KEY); - if (storedAuth) { - this.api.setAuth(storedAuth.split(':')[0], storedAuth.split(':')[1]); - } - - this.loggedIn = this.api.isLoggedIn(); - - if (this.loggedIn) { - this.otherSubscriptions.push(this.api.getUserInfo().subscribe({ - next: (response) => { - this.userInfo = response; - this.changeDetector.markForCheck(); - }, - })); + ngOnChanges(changes: ChangesOf): void { + if (changes.imageBlob) { + this.updateImage(); } } @@ -106,34 +82,12 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { this.otherSubscriptions.forEach((subscription) => subscription.unsubscribe()); } - public onLogin(): void { - this.busy = true; - this.changeDetector.markForCheck(); - - this.otherSubscriptions.push(this.api.authenticate(this.loginForm!.value.username!, this.loginForm!.value.password!).pipe( - switchMap(auth => this.api.getUserInfo().pipe( - map((userInfo) => ({ auth, userInfo })), - )), - ).subscribe({ - next: ({ auth, userInfo }) => { - this.loggedIn = true; - this.busy = false; - this.userInfo = userInfo; - this.changeDetector.markForCheck(); - - localStorage.setItem(LOCAL_STORAGE_KEY, `${auth.access_token}:${auth.refresh_token}`); - }, - error: (error) => { - this.busy = false; - console.error(error); - this.changeDetector.markForCheck(); - }, - })); - } - - public onImagePicked(files: File[]): void { - this.pickedImage = files[0]; - this.imageUrl = URL.createObjectURL(files[0]); + public updateImage(): void { + if (this.imageBlob != null) { + this.imageUrl = URL.createObjectURL(this.imageBlob); + } else { + this.imageBlob = null; + } // Reset the status of the image upload this.imageUploadStatus = null; @@ -150,16 +104,16 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { public uploadImage(): void { // No image or already uploading - if (!this.pickedImage || this.imageUploadStatus === 'pending') { + if (!this.imageBlob || this.imageUploadStatus === 'pending') { return; } - this.busy = true; + this.loading = true; this.imageUploadStatus = 'pending'; this.faceDetectionStatus = null; this.changeDetector.markForCheck(); - this.uploadSubscription = this.api.uploadFile(this.pickedImage, { + this.uploadSubscription = this.api.uploadFile(this.imageBlob, { mode: 'random', // eslint-disable-next-line @typescript-eslint/naming-convention flag_hair: true, @@ -185,12 +139,13 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { }, {} as Record); this.faceDetectionStatus = 'success'; - this.busy = false; + this.loading = false; + this.imageUpload.emit(this.imageId); this.changeDetector.markForCheck(); }, error: (error) => { this.imageUploadStatus = 'error'; - this.busy = false; + this.loading = false; console.error(error); this.notificationService.show({ @@ -203,11 +158,7 @@ export class PiktidEditorComponent implements OnInit, OnDestroy { }); } - // public confirmEditing(): void { - // if (!this.imageId || this.imageUploadStatus !== 'success' || this.confirmedFaces.size !== this.faceIds.length) { - // return; - // } - - // // TODO: Upload the image to the CMS (Emit event here and do upload in parent) - // } + public handleConfirmationChange(ids: number[]): void { + this.confirmChange.emit(ids); + } } diff --git a/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.html b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.html index 769b85af7..808f47912 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.html +++ b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.html @@ -31,7 +31,7 @@ size="small" type="primary" (click)="generateNewGeneration()" - [disabled]="waiting" + [disabled]="disabled || waiting" > comedy_mask {{ 'generate_new_face' }} @@ -41,7 +41,7 @@ size="small" type="success" (click)="confirmGeneration()" - [disabled]="waiting || confirmed || selectedGeneration === -1" + [disabled]="disabled || waiting || confirmed || selectedGeneration === -1" > check {{ 'apply_face' }} @@ -51,7 +51,7 @@ size="small" type="alert" (click)="revertGeneration()" - [disabled]="waiting || !confirmed" + [disabled]="disabled || waiting || !confirmed" > delete {{ 'revert_face' }} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.ts index 36a18ad16..4e42eb64e 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.ts +++ b/cms-ui/libs/piktid-editor/src/lib/components/face-manipulation/face-manipulation.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from import { NewGenerationNotificationData } from '../../common/models'; @Component({ - selector: 'gtxpict-face-manipulation', + selector: 'gtxpikt-face-manipulation', templateUrl: './face-manipulation.component.html', styleUrls: ['./face-manipulation.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -27,6 +27,9 @@ export class FaceManipulationComponent { @Input() public confirmed = false; + @Input() + public disabled = false; + @Output() public generationSelected = new EventEmitter(); @@ -40,7 +43,7 @@ export class FaceManipulationComponent { public undoGeneration = new EventEmitter(); selectGeneration(generationId: number): void { - if (this.confirmed) { + if (this.confirmed || this.disabled) { return; } diff --git a/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.ts index 9d586b1a8..4dec552ce 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.ts +++ b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from import { Coordinates } from '../../common/models'; @Component({ - selector: 'gtxpict-image-preview', + selector: 'gtxpikt-image-preview', templateUrl: './image-preview.component.html', styleUrls: ['./image-preview.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/cms-ui/libs/piktid-editor/src/lib/components/index.ts b/cms-ui/libs/piktid-editor/src/lib/components/index.ts index a47a14ea9..9ca23d4ba 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/index.ts +++ b/cms-ui/libs/piktid-editor/src/lib/components/index.ts @@ -1,4 +1,5 @@ -export * from './piktid-editor/piktid-editor.component'; -export * from './image-preview/image-preview.component'; -export * from './face-manipulation/face-manipulation.component'; export * from './anonymization-editor/anonymization-editor.component'; +export * from './face-manipulation/face-manipulation.component'; +export * from './image-preview/image-preview.component'; +export * from './editor/editor.component'; +export * from './login-gate/login-gate.component'; diff --git a/cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.css b/cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.html b/cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.html new file mode 100644 index 000000000..e36d8257f --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.html @@ -0,0 +1,24 @@ + + + +
+ + + Login +
+
+ + +
+ + +
+ +
+
+
diff --git a/cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.ts b/cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.ts new file mode 100644 index 000000000..e324616b6 --- /dev/null +++ b/cms-ui/libs/piktid-editor/src/lib/components/login-gate/login-gate.component.ts @@ -0,0 +1,91 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { FormProperties } from '@gentics/ui-core'; +import { map, Subscription, switchMap } from 'rxjs'; +import { UserInfoResponse } from '../../common/models'; +import { PiktidAPIService } from '../../providers'; + +interface LoginFormProperties { + username: string; + password: string; +} + +const LOCAL_STORAGE_KEY = 'piktid-editor-auth'; + +@Component({ + selector: 'gtxpikt-login-gate', + templateUrl: './login-gate.component.html', + styleUrl: './login-gate.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoginGateComponent implements OnInit, OnDestroy { + + /** Whether the user is logged in. */ + public loggedIn = false; + + /** The user info. */ + public userInfo: UserInfoResponse | null = null; + + public loginForm: FormGroup> | null = null; + + public loading = false; + + private subscriptions: Subscription[] = []; + + constructor( + private changeDetector: ChangeDetectorRef, + private api: PiktidAPIService, + ) {} + + ngOnInit(): void { + this.loginForm = new FormGroup>({ + username: new FormControl('', Validators.required), + password: new FormControl('', Validators.required), + }); + + const storedAuth = localStorage.getItem(LOCAL_STORAGE_KEY); + if (storedAuth) { + this.api.setAuth(storedAuth.split(':')[0], storedAuth.split(':')[1]); + } + + this.loggedIn = this.api.isLoggedIn(); + + if (this.loggedIn) { + this.subscriptions.push(this.api.getUserInfo().subscribe({ + next: (response) => { + this.userInfo = response; + this.changeDetector.markForCheck(); + }, + })); + } + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + public onLogin(): void { + this.loading = true; + this.changeDetector.markForCheck(); + + this.subscriptions.push(this.api.authenticate(this.loginForm.value.username, this.loginForm.value.password).pipe( + switchMap(auth => this.api.getUserInfo().pipe( + map((userInfo) => ({ auth, userInfo })), + )), + ).subscribe({ + next: ({ auth, userInfo }) => { + this.loggedIn = true; + this.loading = false; + this.userInfo = userInfo; + this.changeDetector.markForCheck(); + + localStorage.setItem(LOCAL_STORAGE_KEY, `${auth.access_token}:${auth.refresh_token}`); + }, + error: (error) => { + this.loading = false; + console.error(error); + this.changeDetector.markForCheck(); + }, + })); + } +} diff --git a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html b/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html deleted file mode 100644 index 4574d0c8e..000000000 --- a/cms-ui/libs/piktid-editor/src/lib/components/piktid-editor/piktid-editor.component.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -
- - - Login -
-
- - - - -
- - image{{ 'select_image' }} - -
- - -
diff --git a/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts b/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts index 0b79aa833..3ba6804bf 100644 --- a/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts +++ b/cms-ui/libs/piktid-editor/src/lib/piktid.module.ts @@ -6,19 +6,27 @@ import { AnonymizationEditorComponent, FaceManipulationComponent, ImagePreviewComponent, - PiktidEditorComponent, + EditorComponent, + LoginGateComponent, } from './components'; import { PiktidAPIService } from './providers'; +const COMPONENTS = [ + AnonymizationEditorComponent, + FaceManipulationComponent, + ImagePreviewComponent, + EditorComponent, + LoginGateComponent, +]; + @NgModule({ imports: [CommonModule, GenticsUICoreModule, ReactiveFormsModule, FormsModule], declarations: [ - PiktidEditorComponent, - ImagePreviewComponent, - FaceManipulationComponent, - AnonymizationEditorComponent, + ...COMPONENTS, + ], + exports: [ + ...COMPONENTS, ], - exports: [PiktidEditorComponent], providers: [PiktidAPIService], }) export class PiktidModule {} diff --git a/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts b/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts index 98a15c474..4aad52bbe 100644 --- a/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts +++ b/cms-ui/libs/piktid-editor/src/lib/providers/piktid-api/piktid-api.service.ts @@ -22,6 +22,8 @@ import { ImageDownloadRequest, } from '../../common/models'; +const BASE_URL = 'https://id.piktid.com'; + @Injectable() export class PiktidAPIService { @@ -51,7 +53,7 @@ export class PiktidAPIService { const auth = btoa(`${username}:${password}`); - return this.http.post('/piktid/api/tokens', null, { + return this.http.post(BASE_URL + '/api/tokens', null, { observe: 'body', responseType: 'json', headers: new HttpHeaders({ @@ -70,7 +72,7 @@ export class PiktidAPIService { return throwError(() => new Error('Not authenticated')); } - return this.http.get('/piktid/api/me', { + return this.http.get(BASE_URL + '/api/me', { observe: 'body', responseType: 'json', headers: new HttpHeaders({ @@ -79,7 +81,7 @@ export class PiktidAPIService { }); } - uploadFile(file: File, options: AnonymizationOptions): Observable { + uploadFile(file: File | Blob, options: AnonymizationOptions): Observable { if (!this.accessToken) { return throwError(() => new Error('Not authenticated')); } @@ -88,7 +90,7 @@ export class PiktidAPIService { data.set('options', JSON.stringify(options)); data.set('file', file); - return this.http.post('/piktid/api/upload_pro', data, { + return this.http.post(BASE_URL + '/api/upload_pro', data, { observe: 'body', responseType: 'json', headers: new HttpHeaders({ @@ -102,7 +104,7 @@ export class PiktidAPIService { return throwError(() => new Error('Not authenticated')); } - return this.http.post('/piktid/api/detect_faces', { + return this.http.post(BASE_URL + '/api/detect_faces', { id_image: imageId, }, { observe: 'body', @@ -124,7 +126,7 @@ export class PiktidAPIService { ...options, }; - return this.http.post('/piktid/api/ask_new_expression', req, { + return this.http.post(BASE_URL + '/api/ask_new_expression', req, { observe: 'body', responseType: 'json', headers: new HttpHeaders({ @@ -138,7 +140,7 @@ export class PiktidAPIService { return throwError(() => new Error('Not authenticated')); } - return this.http.post('/piktid/api/ask_random_face', { + return this.http.post(BASE_URL + '/api/ask_random_face', { id_image: imageId, id_face: faceId, prompt: '{}', @@ -156,7 +158,7 @@ export class PiktidAPIService { return throwError(() => new Error('Not authenticated')); } - return this.http.post('/piktid/api/notification_by_name_json', { + return this.http.post(BASE_URL + '/api/notification_by_name_json', { id_image: imageId, name_list: notificationNames.join(', '), }, { @@ -179,7 +181,7 @@ export class PiktidAPIService { } // TODO: Use pick_face2 when it's actually ready - currently only throws errors - return this.http.post('/piktid/api/pick_face', { + return this.http.post(BASE_URL + '/api/pick_face', { id_image: imageId, id_face: faceId, id_generation: generationId, @@ -208,7 +210,28 @@ export class PiktidAPIService { ...options, }; - return this.http.post('/piktid/api/download', body, { + return this.http.post(BASE_URL + '/api/download', body, { + observe: 'body', + responseType: 'json', + headers: new HttpHeaders({ + Authorization: `Bearer ${this.accessToken}`, + }), + }).pipe( + map((response) => { + const links = JSON.parse(response.links); + return links; + }), + ); + } + + deleteImage(imageId: string): Observable { + if (!this.accessToken) { + return throwError(() => new Error('Not authenticated')); + } + + return this.http.post(BASE_URL + '/api/remove', { + id_image: imageId, + }, { observe: 'body', responseType: 'json', headers: new HttpHeaders({ From c63f982e1cd53fb5dab5578e466afffa0e0ec321 Mon Sep 17 00:00:00 2001 From: Decker Dominik Date: Tue, 18 Feb 2025 10:25:39 +0100 Subject: [PATCH 4/7] Added File upload via URL to rest client --- .../libs/cms-models/src/lib/models/request.ts | 21 +++++++++++++++++++ .../libs/cms-rest-client/src/lib/abstracts.ts | 2 ++ cms-ui/libs/cms-rest-client/src/lib/client.ts | 1 + 3 files changed, 24 insertions(+) diff --git a/cms-ui/libs/cms-models/src/lib/models/request.ts b/cms-ui/libs/cms-models/src/lib/models/request.ts index fbb3378be..104e596a8 100644 --- a/cms-ui/libs/cms-models/src/lib/models/request.ts +++ b/cms-ui/libs/cms-models/src/lib/models/request.ts @@ -1051,6 +1051,27 @@ export interface FileCreateRequest { properties?: { [key: string]: any }; } +export interface FileUploadRequest { + /** `true` to overwrite existing files with the same name in the folder */ + overwriteExisting: boolean; + /** Target folder ID */ + folderId: number; + /** Target node ID for uploading files in channels */ + nodeId: number; + /** Source URL of the file */ + sourceURL: string; + /** Name of the file */ + name?: string; + /** Description of the file */ + description?: string; + /** Nice URL of the file */ + niceURL?: string; + /** Alternate URLs of the file */ + alternateURL?: string; + /** The additional properties of the file */ + properties?: Record; +} + export interface FileUploadOptions { folderId: number; nodeId: number | string; diff --git a/cms-ui/libs/cms-rest-client/src/lib/abstracts.ts b/cms-ui/libs/cms-rest-client/src/lib/abstracts.ts index 81705565e..7bcda7af7 100644 --- a/cms-ui/libs/cms-rest-client/src/lib/abstracts.ts +++ b/cms-ui/libs/cms-rest-client/src/lib/abstracts.ts @@ -367,6 +367,7 @@ import { LicenseContentRepositoryInfoOptions, LicenseContentRepositoryInfoResponse, PushLicenseRequest, + FileUploadRequest, } from '@gentics/cms-models'; import { LoginResponse as MeshLoginResponse } from '@gentics/mesh-models'; import { BasicAPI } from './common'; @@ -578,6 +579,7 @@ export interface AbstractFileAPI extends BasicAPI { list: (options?: FileListOptions) => FileListResponse; create: (body: FileCreateRequest) => FileUploadResponse; upload: (file: File | Blob, options: FileUploadOptions, fileName?: string) => FileUploadResponse; + uploadFromURL: (body: FileUploadRequest) => FileUploadResponse; get: (id: number | string, options?: ItemRequestOptions) => FileResponse; getMultiple: (body: MultiObjectLoadRequest) => FileListResponse; update: (id: number | string, body: FileSaveRequest) => Response; diff --git a/cms-ui/libs/cms-rest-client/src/lib/client.ts b/cms-ui/libs/cms-rest-client/src/lib/client.ts index 5a1bff9f4..03c1ac28d 100644 --- a/cms-ui/libs/cms-rest-client/src/lib/client.ts +++ b/cms-ui/libs/cms-rest-client/src/lib/client.ts @@ -406,6 +406,7 @@ export class GCMSRestClient implements GCMSRootAPI { } return this.executeMappedFormRequest(POST, '/file/create', data, options); }, + uploadFromURL: (body) => this.executeMappedJsonRequest(POST, '/file/create', body), get: (id, options) => this.executeMappedJsonRequest(GET, `/file/load/${id}`, null, options), getMultiple: (body) => this.executeMappedJsonRequest(POST, '/file/load', body), update: (id, body) => this.executeMappedJsonRequest(POST, `/file/save/${id}`, body), From 5089e12f2e47e21bb5f3c99ab403fc6dfaa8d7f2 Mon Sep 17 00:00:00 2001 From: Decker Dominik Date: Tue, 18 Feb 2025 10:26:02 +0100 Subject: [PATCH 5/7] GPU-1897: Uploading generated image via URL --- .../image-anonymize-modal.component.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.ts b/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.ts index db56c631d..ab4cdb9cf 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.ts +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/image-anonymize-modal/image-anonymize-modal.component.ts @@ -86,19 +86,14 @@ export class ImageAnonymizeModal extends BaseModal implements OnInit, OnDe flag_quality: 0, }).pipe( // Download the generated image - switchMap(links => this.http.get(links.l, { - observe: 'body', - responseType: 'blob', + switchMap(links => this.api.file.uploadFromURL({ + overwriteExisting: false, + folderId: this.loadedImage.folderId, + nodeId: this.loadedImage.masterNodeId, + sourceURL: links.l, + name: `anonymized_${this.loadedImage.name}`, + description: this.loadedImage.description, })), - // Upload the generated image to the CMS - switchMap(newImageBlob => { - (newImageBlob as any).name = `anonymized_${this.loadedImage.name}`; - - return this.api.file.upload(newImageBlob, { - folderId: this.loadedImage.folderId, - nodeId: this.nodeId, - }); - }), // Delete the image from piktid after successful upload switchMap(uploaded => { return this.piktid.deleteImage(this.piktidImageId).pipe( From 077be70d08a26b9fb5bee2451282f1b9660a9655 Mon Sep 17 00:00:00 2001 From: Decker Dominik Date: Tue, 18 Feb 2025 10:26:30 +0100 Subject: [PATCH 6/7] Removed testing config --- cms-ui/apps/editor-ui/project.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cms-ui/apps/editor-ui/project.json b/cms-ui/apps/editor-ui/project.json index 9a63ed869..efea472a1 100644 --- a/cms-ui/apps/editor-ui/project.json +++ b/cms-ui/apps/editor-ui/project.json @@ -82,12 +82,7 @@ "executor": "@nx/angular:dev-server", "options": { "proxyConfig": "proxy.conf.json", - "liveReload": false, - "headers": { - "Access-Control-Allow-Origin": "https://d3ajjlx2rocmin.cloudfront.net", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Vary": "Origin" - } + "liveReload": false }, "configurations": { "production": { From b10307677599e672d8201e18a52f33bc7966d757 Mon Sep 17 00:00:00 2001 From: Decker Dominik Date: Tue, 18 Feb 2025 12:59:30 +0100 Subject: [PATCH 7/7] GPU-1897: Fixed building errors --- .../src/lib/components/editor/editor.component.html | 2 +- .../components/image-preview/image-preview.component.html | 2 +- .../src/lib/components/login-gate/login-gate.component.ts | 8 +++++++- cms-ui/libs/ui-core/src/lib/common/mapping.ts | 4 ++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.html b/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.html index 0e8fffd01..64002cfb5 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.html +++ b/cms-ui/libs/piktid-editor/src/lib/components/editor/editor.component.html @@ -10,6 +10,6 @@ [facePositions]="facePositions" [uploading]="imageUploadStatus === 'pending'" (uploadImage)="uploadImage()" - (confirmChange)="handleConfirmationChange()" + (confirmChange)="handleConfirmationChange($event)" > diff --git a/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.html b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.html index ee8ccc614..962cdacbe 100644 --- a/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.html +++ b/cms-ui/libs/piktid-editor/src/lib/components/image-preview/image-preview.component.html @@ -1,4 +1,4 @@ - + this.api.getUserInfo().pipe( map((userInfo) => ({ auth, userInfo })), )), diff --git a/cms-ui/libs/ui-core/src/lib/common/mapping.ts b/cms-ui/libs/ui-core/src/lib/common/mapping.ts index 1be57b676..94d610a45 100644 --- a/cms-ui/libs/ui-core/src/lib/common/mapping.ts +++ b/cms-ui/libs/ui-core/src/lib/common/mapping.ts @@ -1,4 +1,4 @@ -import { SimpleChange } from '@angular/core'; +import { SimpleChange, SimpleChanges } from '@angular/core'; // Used for ...? export type MappingFn = (value: any, ...params: any[]) => any; @@ -13,4 +13,4 @@ export type MappingFn = (value: any, ...params: any[]) => any; * } * ``` */ -export type ChangesOf = { [K in keyof T]?: SimpleChange }; +export type ChangesOf = { [K in keyof T]?: SimpleChange } | SimpleChanges;