Browse Source

Merge pull request #14153 from mtsymbarov-del/fix/camera-widget

Added ability to save pictures from Photo Camera widget to Image library
pull/14268/head
Igor Kulikov 7 months ago
committed by GitHub
parent
commit
aa017696d5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/src/main/data/json/system/widget_types/photo_camera_input.json
  2. 2
      ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html
  3. 105
      ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts
  4. 84
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html
  5. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts
  6. 13
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/data/json/system/widget_types/photo_camera_input.json

@ -15,7 +15,7 @@
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-photo-camera-input-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"saveToGallery\":true,\"usePublicGalleryLink\":false,\"imageFormat\":\"image/png\",\"imageQuality\":0.92,\"maxWidth\":640,\"maxHeight\":480},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}"
},
"resources": [
{

2
ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html

@ -20,7 +20,7 @@
<div [class.!hidden]="isShowCamera" class="flex size-full flex-col items-center justify-between">
<div class="tb-web-camera__last-photo flex-1">
<span [class.!hidden]="lastPhoto" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span>
<img [class.!hidden]="!lastPhoto" class="tb-web-camera__last-photo_img" [src]="lastPhoto" alt="last photo"/>
<img [class.!hidden]="!lastPhoto" class="tb-web-camera__last-photo_img" [src]="lastPhoto | image | async" alt="last photo"/>
</div>
<button mat-raised-button color="primary" (click)="takePhoto()" *ngIf="!textMessage">
{{ "widgets.input-widgets.take-photo" | translate }}

105
ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts

@ -19,29 +19,30 @@ import {
ElementRef,
Inject,
Input,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ImageService } from '@app/core/public-api';
import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay';
import { AttributeService } from '@core/http/attribute.service';
import { UtilsService } from '@core/services/utils.service';
import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
import { WINDOW } from '@core/services/window.service';
import { AttributeService } from '@core/http/attribute.service';
import { isString } from '@core/utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { PageComponent } from '@shared/components/page.component';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable } from 'rxjs';
import { isString } from '@core/utils';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
import { map, Observable, of, switchMap } from 'rxjs';
interface PhotoCameraInputWidgetSettings {
widgetTitle: string;
saveToGallery: boolean;
usePublicGalleryLink: boolean;
imageQuality: number;
imageFormat: string;
maxWidth: number;
@ -59,9 +60,7 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
constructor(@Inject(WINDOW) private window: Window,
protected store: Store<AppState>,
private elementRef: ElementRef,
private ngZone: NgZone,
private overlay: Overlay,
private imageService: ImageService,
private utils: UtilsService,
private attributeService: AttributeService,
private sanitizer: DomSanitizer
@ -119,6 +118,9 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
lastPhoto: SafeUrl;
datasourceDetected = false;
private mimeType: string;
private quality: number;
private static hasGetUserMedia(): boolean {
return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia);
}
@ -160,6 +162,9 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
this.dataKeyDetected = true;
}
}
this.mimeType = this.settings.imageFormat || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
this.quality = this.settings.imageQuality || PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.detectAvailableDevices();
}
@ -169,8 +174,8 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
private updateWidgetData(data: Array<DatasourceData>) {
const keyData = data[0].data;
if (keyData?.length && isString(keyData[0][1]) && keyData[0][1].startsWith('data:image/')) {
this.lastPhoto = this.sanitizer.bypassSecurityTrustUrl(keyData[0][1]);
if (keyData?.length && isString(keyData[0][1])) {
this.lastPhoto = keyData[0][1].startsWith('data:image/') ? this.sanitizer.bypassSecurityTrustUrl(keyData[0][1]) : keyData[0][1];
}
}
@ -237,26 +242,34 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
savePhoto() {
this.updatePhoto = true;
let task: Observable<any>;
const entityId: EntityId = {
entityType: this.datasource.entityType,
id: this.datasource.entityId
};
const saveData = [{
key: this.datasource.dataKeys[0].name,
value: this.previewPhoto
}];
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
task = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
} else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
task = this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
}
task.subscribe(() => {
this.isPreviewPhoto = false;
this.updatePhoto = false;
this.closeCamera();
}, () => {
this.updatePhoto = false;
const uploadImage$ = this.settings.saveToGallery ? this.saveImageToGallery() : of(this.previewPhoto);
uploadImage$.pipe(
switchMap((image) => {
const saveData = [{
key: this.datasource.dataKeys[0].name,
value: image
}];
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
return this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
} else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
return this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
}
})
).subscribe({
next: () => {
this.isPreviewPhoto = false;
this.updatePhoto = false;
this.closeCamera();
},
error: () => {
this.updatePhoto = false;
}
});
}
@ -271,12 +284,36 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
this.canvasElement.height = this.videoHeight;
this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);
const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality);
this.previewPhoto = this.canvasElement.toDataURL(this.mimeType, this.quality);
this.isPreviewPhoto = true;
}
private saveImageToGallery(): Observable<string> {
return new Observable<Blob>((observer) => {
this.canvasElement.toBlob(
(blob) => {
if (blob) {
observer.next(blob);
observer.complete();
} else {
observer.error('Failed to create image blob');
}
},
this.mimeType,
this.quality
);
}).pipe(
switchMap((blob) => {
const fileName = this.datasource.dataKeys[0].name;
const file = new File([blob], fileName, { type: this.mimeType });
return this.imageService.uploadImage(file, fileName);
}),
map((imageInfo) =>
this.settings.usePublicGalleryLink ? imageInfo.publicLink : imageInfo.link
)
);
}
private inititedVideoStream(deviceId?: string, init = false) {
if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
const videoTrackConstraints = {

84
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html

@ -15,19 +15,32 @@
limitations under the License.
-->
<section class="tb-widget-settings" [formGroup]="photoCameraInputWidgetSettingsForm">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.general-settings</legend>
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.input-widgets.widget-title</mat-label>
<input matInput formControlName="widgetTitle">
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.image-settings</legend>
<section class="flex flex-col gt-xs:flex-row gt-xs:items-center gt-xs:justify-start gt-xs:gap-2">
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.input-widgets.image-format</mat-label>
<section class="flex flex-col gap-4" [formGroup]="photoCameraInputWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.input-widgets.general-settings</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>widgets.input-widgets.widget-title</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="widgetTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.input-widgets.save-image</div>
<mat-slide-toggle class="mat-slide" formControlName="saveToGallery">
{{ 'widgets.input-widgets.save-to-gallery' | translate }}
</mat-slide-toggle>
@if (photoCameraInputWidgetSettingsForm.get('saveToGallery').value) {
<mat-slide-toggle class="mat-slide" formControlName="usePublicGalleryLink">
{{ 'widgets.input-widgets.public-image' | translate }}
</mat-slide-toggle>
}
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.input-widgets.image-settings</div>
<div class="tb-form-row space-between">
<div translate>widgets.input-widgets.image-format</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="imageFormat">
<mat-option [value]="'image/jpeg'">
{{ 'widgets.input-widgets.image-format-jpeg' | translate }}
@ -40,20 +53,33 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.input-widgets.image-quality</mat-label>
<input matInput type="number" min="0" max="1" formControlName="imageQuality">
</mat-form-field>
</section>
<section class="flex flex-col gt-xs:flex-row gt-xs:items-center gt-xs:justify-start gt-xs:gap-2">
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.input-widgets.max-image-width</mat-label>
<input matInput type="number" min="1" formControlName="maxWidth">
</mat-form-field>
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.input-widgets.max-image-height</mat-label>
<input matInput type="number" min="1" formControlName="maxHeight">
</mat-form-field>
</section>
</fieldset>
</div>
@if (photoCameraInputWidgetSettingsForm.get('imageFormat').value !== 'image/png') {
<div class="tb-form-row space-between">
<div translate>widgets.input-widgets.image-quality</div>
<mat-form-field class="number" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" max="100" formControlName="imageQuality">
<span matSuffix>%</span>
</mat-form-field>
</div>
}
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width !min-w-30">Size</div>
<div class="flex flex-row items-center justify-star gap-2">
<div class="tb-small-label" translate>widgets.input-widgets.max-image-width</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="1" formControlName="maxWidth">
<span matSuffix>px</span>
</mat-form-field>
<mat-divider vertical class="xs:hidden"></mat-divider>
<div class="tb-small-label" translate>widgets.input-widgets.max-image-height</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="1" formControlName="maxHeight">
<span matSuffix>px</span>
</mat-form-field>
</div>
</div>
</div>
</section>

27
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts

@ -15,10 +15,10 @@
///
import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
@Component({
selector: 'tb-photo-camera-input-widget-settings',
@ -42,6 +42,8 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo
return {
widgetTitle: '',
saveToGallery: false,
usePublicGalleryLink: true,
imageFormat: 'image/png',
imageQuality: 0.92,
maxWidth: 640,
@ -57,11 +59,28 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo
widgetTitle: [settings.widgetTitle, []],
// Image settings
saveToGallery: [settings.saveToGallery],
usePublicGalleryLink: [settings.usePublicGalleryLink],
imageFormat: [settings.imageFormat, []],
imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(1)]],
imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(100)]],
maxWidth: [settings.maxWidth, [Validators.min(1)]],
maxHeight: [settings.maxHeight, [Validators.min(1)]]
});
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
return {
...settings,
saveToGallery: settings.saveToGallery ?? false,
usePublicGalleryLink: settings.usePublicGalleryLink ?? false,
imageQuality: settings.imageQuality * 100
}
}
protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings {
return {
...settings,
imageQuality: settings.imageQuality / 100
}
}
}

13
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -8101,14 +8101,14 @@
"attribute-scope-server": "Server attribute",
"attribute-scope-shared": "Shared attribute",
"value-required": "Value required",
"image-settings": "Image settings",
"image-settings": "Image output settings",
"image-format": "Image format",
"image-format-jpeg": "JPEG",
"image-format-png": "PNG",
"image-format-webp": "WEBP",
"image-quality": "Image quality that use lossy compression such as jpeg and webp",
"max-image-width": "Maximum image width",
"max-image-height": "Maximum image height",
"image-quality": "Image quality",
"max-image-width": "Max width",
"max-image-height": "Max height",
"action-buttons": "Action buttons",
"show-action-buttons": "Show action buttons",
"update-all-values": "Update all values, not only modified",
@ -8189,7 +8189,10 @@
"add-radio-option": "Add radio option",
"radio-label-position": "Label position",
"radio-label-position-before": "Before",
"radio-label-position-after": "After"
"radio-label-position-after": "After",
"save-image": "Save image",
"save-to-gallery": "Automatically store captured images in Image Gallery",
"public-image": "Makes image avaliable for any unauthorized user"
},
"invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type",
"qr-code": {

Loading…
Cancel
Save