Browse Source

UI: Value stepper widget

pull/12627/head
Artem Dzhereleiko 1 year ago
parent
commit
cc6f0b6c7b
  1. 1
      application/src/main/data/json/system/widget_bundles/buttons.json
  2. 1
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  3. 51
      application/src/main/data/json/system/widget_types/value_stepper.json
  4. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  5. 32
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html
  6. 38
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts
  7. 297
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html
  8. 225
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts
  9. 7
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts
  10. 373
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts
  11. 43
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html
  12. 90
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.scss
  13. 388
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts
  14. 239
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts
  15. 266
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html
  16. 191
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts
  17. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  18. 3
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  19. 37
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  20. 1
      ui-ngx/src/assets/widget/value-stepper/filled.svg
  21. 1
      ui-ngx/src/assets/widget/value-stepper/simplified.svg
  22. 1
      ui-ngx/src/assets/widget/value-stepper/volume.svg

1
application/src/main/data/json/system/widget_bundles/buttons.json

@ -12,6 +12,7 @@
"command_button",
"toggle_button",
"two_segment_button",
"value_stepper",
"power_button"
]
}

1
application/src/main/data/json/system/widget_bundles/control_widgets.json

@ -12,6 +12,7 @@
"command_button",
"toggle_button",
"two_segment_button",
"value_stepper",
"power_button",
"slider",
"control_widgets.switch_control",

51
application/src/main/data/json/system/widget_types/value_stepper.json

@ -0,0 +1,51 @@
{
"fqn": "slider",
"name": "Value stepper",
"deprecated": false,
"image": "tb-image;/api/images/system/value-stepper-widget.svg",
"description": "Allows users to click the buttons to send commands to devices or update attributes/time series data. Configurable settings let users define how to retrieve the initial state and specify actions for each button.",
"descriptor": {
"type": "rpc",
"sizeX": 3.5,
"sizeY": 2,
"resources": [],
"templateHtml": "<tb-value-stepper-widget\n [ctx]='ctx'\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-value-stepper-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '230px',\n previewHeight: '110px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"dataKeySettingsForm": [],
"settingsDirective": "tb-value-stepper-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-value-stepper-basic-config",
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"EXECUTE_RPC\",\"defaultValue\":0,\"executeRpc\":{\"method\":\"getState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return integer value */\\nreturn data;\"}},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"leftButtonClick\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SERVER_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"VALUE\",\"constantValue\":0,\"valueToDataFunction\":\"/* Convert input integer value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"rightButtonClick\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SERVER_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"VALUE\",\"constantValue\":0,\"valueToDataFunction\":\"/* Convert input integer value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"appearance\":{\"type\":\"simplified\",\"autoScale\":true,\"minValueRange\":-100,\"maxValueRange\":100,\"valueStep\":0.5,\"showValueBox\":true,\"valueUnits\":\"\",\"valueDecimals\":1,\"valueFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":16,\"sizeUnit\":\"px\",\"lineHeight\":\"24px\"},\"valueColor\":\"#000\",\"valueBoxBackground\":\"rgba(0, 0, 0, 0.04)\",\"showBorder\":true,\"borderWidth\":1,\"borderColor\":\"#305680\"},\"buttonAppearance\":{\"leftButton\":{\"showButton\":true,\"icon\":\"arrow_back_ios_new\",\"iconSize\":24,\"iconSizeUnit\":\"px\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"pressed\":null,\"activated\":null,\"disabled\":null}},\"rightButton\":{\"showButton\":true,\"icon\":\"arrow_forward_ios\",\"iconSize\":24,\"iconSizeUnit\":\"px\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"pressed\":null,\"activated\":null,\"disabled\":null}}},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\"},\"title\":\"Value stepper\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"configMode\":\"basic\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"datasources\":null,\"borderRadius\":null}"
},
"resources": [
{
"link": "/api/images/system/value-stepper-widget.svg",
"title": "\"Value stepper\" system widget image",
"type": "IMAGE",
"subType": "IMAGE",
"fileName": "value-stepper-widget.svg",
"publicResourceKey": "s0UKoqbiMCcKVn0pD55XZzPUR89XlXAO",
"mediaType": "image/svg+xml",
"data": "PHN2ZyB3aWR0aD0iMjE0IiBoZWlnaHQ9Ijc2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGZpbHRlcj0idXJsKCNhKSI+PHJlY3QgeD0iOC41IiB5PSI0LjUiIHdpZHRoPSIxOTciIGhlaWdodD0iNTkiIHJ4PSI0IiBmaWxsPSIjZmZmIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz48cmVjdCB4PSI5IiB5PSI1IiB3aWR0aD0iMTk2IiBoZWlnaHQ9IjU4IiByeD0iMy41IiBzdHJva2U9IiMzMDU2ODAiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPjxyZWN0IHg9IjIwLjUiIHk9IjE4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSIxNiIgZmlsbD0iIzMwNTY4MCIvPjxwYXRoIGQ9Im0zOC41IDQwIDEuNDEtMS40MUwzNS4zMyAzNGw0LjU4LTQuNTlMMzguNSAyOGwtNiA2IDYgNloiIGZpbGw9IiNmZmYiLz48cmVjdCB4PSI2NSIgeT0iMTguNSIgd2lkdGg9Ijg0IiBoZWlnaHQ9IjMxIiByeD0iMy41IiBmaWxsPSIjMzA1NjgwIiBmaWxsLW9wYWNpdHk9Ii4wNCIvPjxyZWN0IHg9IjY1IiB5PSIxOC41IiB3aWR0aD0iODQiIGhlaWdodD0iMzEiIHJ4PSIzLjUiIHN0cm9rZT0iIzMwNTY4MCIvPjxwYXRoIGQ9Ik04OC41OSAzNy41VjM5aC03LjYzdi0xLjI5bDMuNy00LjA0Yy40MS0uNDYuNzMtLjg1Ljk3LTEuMTkuMjMtLjMzLjQtLjYzLjQ5LS45YTIuMyAyLjMgMCAwIDAtLjA2LTEuNzNjLS4xMy0uMjctLjMyLS41LS41OC0uNjVhMS43IDEuNyAwIDAgMC0uOTMtLjI0Yy0uNDIgMC0uNzcuMS0xLjA2LjI3LS4yOC4xOS0uNS40NC0uNjUuNzZhMi42IDIuNiAwIDAgMC0uMjIgMS4xaC0xLjg4YzAtLjY3LjE1LTEuMjcuNDYtMS44Mi4zLS41NS43My0uOTkgMS4zLTEuM2E0LjEgNC4xIDAgMCAxIDIuMDgtLjVjLjc2IDAgMS40LjEzIDEuOTQuMzguNTMuMjYuOTMuNjIgMS4yIDEuMDlhMy4zNiAzLjM2IDAgMCAxIC4yNSAyLjcyIDUgNSAwIDAgMS0uNDkgMS4wNCA5IDkgMCAwIDEtLjc0IDEuMDRjLS4yOC4zNS0uNi43LS45NCAxLjA1bC0yLjQ2IDIuNzFoNS4yNVptOS4yNy05Ljg4djEuMDRMOTMuMyAzOWgtMS45OGw0LjU0LTkuODhoLTUuOXYtMS41aDcuODlabTEuOTggMTAuNDRjMC0uMjkuMS0uNTMuMy0uNzMuMi0uMi40Ni0uMy44LS4zcy42LjEuOC4zYy4yLjIuMy40NC4zLjczYTEgMSAwIDAgMS0uMy43NGMtLjIuMi0uNDYuMy0uOC4zcy0uNi0uMS0uOC0uM2ExIDEgMCAwIDEtLjMtLjc0Wm02LjQ5LTQuMzUtMS41LS4zNy42MS01LjcyaDYuMTR2MS42SDEwN2wtLjMyIDIuNzlhMy42NyAzLjY3IDAgMCAxIDEuODEtLjQ2Yy41NCAwIDEuMDIuMDkgMS40NC4yNi40Mi4xNy43OS40MyAxLjA4Ljc2LjMuMzMuNTMuNzMuNjggMS4yYTUgNSAwIDAgMSAwIDMuMDcgMy4xNyAzLjE3IDAgMCAxLTEuODUgMi4wM2MtLjQ2LjE5LTEuMDEuMjktMS42NS4yOWE0LjYgNC42IDAgMCAxLTEuMzYtLjIgMy43MyAzLjczIDAgMCAxLTEuMTctLjYyIDMuMTQgMy4xNCAwIDAgMS0xLjE5LTIuNDJoMS44NWMuMDUuMzcuMTUuNjkuMy45NS4xNi4yNS4zOC40NS42NC41OGEyIDIgMCAwIDAgLjkyLjJjLjMyIDAgLjYtLjA1LjgzLS4xNi4yMy0uMTEuNDItLjI3LjU3LS40OC4xNS0uMjIuMjctLjQ3LjM0LS43NWEzLjYyIDMuNjIgMCAwIDAtLjAyLTEuODcgMS45OCAxLjk4IDAgMCAwLS4zOC0uNzJjLS4xNy0uMi0uMzgtLjM2LS42My0uNDdhMi4xMyAyLjEzIDAgMCAwLS44OC0uMTdjLS40NSAwLS44LjA3LTEuMDQuMi0uMjMuMTMtLjQ1LjI5LS42NS40OFptMTEuNzUtNC4xNmMwLS4zOC4xLS43Mi4yOC0xLjA0LjE5LS4zMi40NC0uNTcuNzUtLjc2YTEuOTYgMS45NiAwIDAgMSAyLjA1IDBjLjMxLjE5LjU2LjQ0Ljc0Ljc2LjE5LjMyLjI4LjY2LjI4IDEuMDRzLS4xLjczLS4yOCAxLjA1Yy0uMTguMzEtLjQzLjU2LS43NC43NGEyLjA0IDIuMDQgMCAwIDEtMi44LS43NCAyLjAzIDIuMDMgMCAwIDEtLjI4LTEuMDVabTEuMDUgMGExIDEgMCAwIDAgMSAxIC45Ny45NyAwIDAgMCAuOTgtMSAxIDEgMCAwIDAtLjI3LS43Mi45My45MyAwIDAgMC0uNy0uM2MtLjI4IDAtLjUxLjEtLjcxLjMtLjIuMi0uMy40My0uMy43MlptMTIuMTkgNS43NWgxLjk1YTQuNSA0LjUgMCAwIDEtLjYyIDEuOTkgMy43MiAzLjcyIDAgMCAxLTEuNSAxLjM3IDUgNSAwIDAgMS0yLjMzLjUgNC4xNSA0LjE1IDAgMCAxLTMuMzQtMS40NWMtLjQtLjQ4LS43MS0xLjA0LS45Mi0xLjctLjIxLS42Ni0uMzItMS40LS4zMi0yLjIydi0uOTVjMC0uODEuMS0xLjU1LjMyLTIuMjIuMjItLjY2LjUzLTEuMjIuOTQtMS42OS40LS40Ny45LS44NCAxLjQ2LTEuMDlhNC43OCA0Ljc4IDAgMCAxIDEuOTMtLjM3Yy45IDAgMS42Ny4xNyAyLjMuNS42Mi4zMyAxLjEuOCAxLjQ1IDEuMzguMzUuNTkuNTYgMS4yNi42NCAyLjAyaC0xLjk1Yy0uMDUtLjQ4LS4xNy0uOS0uMzUtMS4yNWExLjc3IDEuNzcgMCAwIDAtLjc2LS44IDIuNzMgMi43MyAwIDAgMC0xLjMzLS4yOGMtLjQ1IDAtLjg0LjA4LTEuMTcuMjVhMi4yIDIuMiAwIDAgMC0uODQuNzNjLS4yMi4zMy0uMzkuNzItLjUgMS4yLS4xMS40Ny0uMTcgMS0uMTcgMS42di45N2MwIC41Ny4wNSAxLjEuMTUgMS41Ni4xLjQ3LjI2Ljg2LjQ3IDEuMi4yMS4zMy40OC41OS44MS43Ny4zMy4xOC43Mi4yNyAxLjE4LjI3LjU2IDAgMS0uMDggMS4zNS0uMjYuMzUtLjE4LjYxLS40NC44LS43OC4xNy0uMzQuMy0uNzYuMzUtMS4yNVoiIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjg3Ii8+PHJlY3QgeD0iMTYxLjUiIHk9IjE4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSIxNiIgZmlsbD0iIzMwNTY4MCIvPjxwYXRoIGQ9Im0xNzUuNSAyOC0xLjQxIDEuNDEgNC41OCA0LjU5LTQuNTggNC41OUwxNzUuNSA0MGw2LTYtNi02WiIgZmlsbD0iI2ZmZiIvPjwvZz48ZGVmcz48ZmlsdGVyIGlkPSJhIiB4PSIuNSIgeT0iLjUiIHdpZHRoPSIyMTMiIGhlaWdodD0iNzUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz48ZmVPZmZzZXQgZHk9IjQiLz48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+PGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+PGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA0IDAiLz48ZmVCbGVuZCBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNTY2OV8xNjA3MDUiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNTY2OV8xNjA3MDUiIHJlc3VsdD0ic2hhcGUiLz48L2ZpbHRlcj48L2RlZnM+PC9zdmc+",
"public": true
}
],
"scada": false,
"tags": [
"command",
"downlink",
"device configuration",
"device control",
"invocation",
"remote method",
"remote function",
"interface",
"subroutine call",
"inter-process communication",
"server request",
"update attribute",
"set attribute",
"add time-series"
]
}

5
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -146,6 +146,9 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/
import {
SegmentedButtonBasicConfigComponent
} from '@home/components/widget/config/basic/button/segmented-button-basic-config.component';
import {
ValueStepperBasicConfigComponent
} from '@home/components/widget/config/basic/rpc/value-stepper-basic-config.component';
@NgModule({
declarations: [
@ -182,6 +185,7 @@ import {
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent,
ValueStepperBasicConfigComponent,
TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent,
ComparisonKeysTableComponent,
@ -236,6 +240,7 @@ import {
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent,
ValueStepperBasicConfigComponent,
TimeSeriesChartBasicConfigComponent,
StatusWidgetBasicConfigComponent,
PieChartBasicConfigComponent,

32
ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html

@ -116,6 +116,38 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs" formGroupName="onButtonIcon">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.power-button.button-icon-on' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="powerButtonWidgetConfigForm.get('mainColorOn').value"
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row column-xs" formGroupName="offButtonIcon">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.power-button.button-icon-off' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="powerButtonWidgetConfigForm.get('mainColorOff').value"
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">

38
ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts

@ -87,6 +87,19 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
onButtonIcon: this.fb.group({
showIcon: [settings.onButtonIcon.showIcon, []],
iconSize: [settings.onButtonIcon.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.onButtonIcon.iconSizeUnit, []],
icon: [settings.onButtonIcon.icon, []],
}),
offButtonIcon: this.fb.group({
showIcon: [settings.offButtonIcon.showIcon, []],
iconSize: [settings.offButtonIcon.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.offButtonIcon.iconSizeUnit, []],
icon: [settings.offButtonIcon.icon, []],
}),
mainColorOn: [settings.mainColorOn, []],
backgroundColorOn: [settings.backgroundColorOn, []],
@ -128,6 +141,9 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.onButtonIcon = config.onButtonIcon;
this.widgetConfig.config.settings.offButtonIcon = config.offButtonIcon;
this.widgetConfig.config.settings.mainColorOn = config.mainColorOn;
this.widgetConfig.config.settings.backgroundColorOn = config.backgroundColorOn;
@ -148,12 +164,14 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showIcon'];
return ['showTitle', 'showIcon', 'onButtonIcon.showIcon', 'offButtonIcon.showIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.powerButtonWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.powerButtonWidgetConfigForm.get('showIcon').value;
const onButtonIcon: boolean = this.powerButtonWidgetConfigForm.get('onButtonIcon').get('showIcon').value;
const offButtonIcon: boolean = this.powerButtonWidgetConfigForm.get('offButtonIcon').get('showIcon').value;
if (showTitle) {
this.powerButtonWidgetConfigForm.get('title').enable();
this.powerButtonWidgetConfigForm.get('titleFont').enable();
@ -180,6 +198,24 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent
this.powerButtonWidgetConfigForm.get('icon').disable();
this.powerButtonWidgetConfigForm.get('iconColor').disable();
}
if (onButtonIcon) {
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSize').enable();
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSizeUnit').enable();
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('icon').enable();
} else {
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSize').disable();
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSizeUnit').disable();
this.powerButtonWidgetConfigForm.get('onButtonIcon').get('icon').disable();
}
if (offButtonIcon) {
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSize').enable();
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSizeUnit').enable();
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('icon').enable();
} else {
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSize').disable();
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSizeUnit').disable();
this.powerButtonWidgetConfigForm.get('offButtonIcon').get('icon').disable();
}
}
private getCardButtons(config: WidgetConfig): string[] {

297
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html

@ -0,0 +1,297 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="valueStepperWidgetConfigForm">
<tb-target-device formControlName="targetDevice"></tb-target-device>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.value-stepper.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.initial-state-hint' | translate}}" translate>widgets.value-stepper.initial-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.initial-state' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.left-button-click-hint' | translate}}" translate>widgets.value-stepper.left-button-click</div>
<tb-set-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.left-button-click' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="leftButtonClick"></tb-set-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.right-button-click-hint' | translate}}" translate>widgets.value-stepper.right-button-click</div>
<tb-set-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.right-button-click' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="rightButtonClick"></tb-set-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.button-state.disabled-state-hint' | translate}}" translate>widgets.button-state.disabled-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.button-state.disabled-state' | translate }}"
[valueType]="valueType.BOOLEAN"
stateLabel="{{ 'widgets.button-state.disabled' | translate }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="2:1"
[cols]="{columns: 3,
breakpoints: {
'lt-sm': 1,
'lt-md': 2
}}"
label="{{ 'widgets.button.layout' | translate }}" formControlName="type">
<tb-image-cards-select-option *ngFor="let type of valueStepperTypes"
[value]="type"
[image]="valueStepperTypeImageMap.get(type)">
{{ valueStepperTypeTranslationMap.get(type) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.value-stepper.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.value-range' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2">
<div class="tb-small-label" translate>widgets.value-stepper.min-range</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="minValueRange" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.value-stepper.max-range</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValueRange" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.value-stepper.value-increment-decrement-step' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="valueStep" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValueBox">
{{ 'widgets.value-stepper.value' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<tb-unit-input class="flex" formControlName="valueUnits"></tb-unit-input>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="valueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden" translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="valueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.value-stepper.value-box-background' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="valueBoxBackground">
</tb-color-input>
</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showBorder">
{{ 'widgets.value-stepper.border' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="borderWidth" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden">px</div>
</mat-form-field>
<tb-color-input asBoxInput
colorClearButton
formControlName="borderColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="buttonAppearance">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>widgets.value-stepper.button-appearance</div>
<tb-toggle-select [(ngModel)]="buttonAppearanceType" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="left">{{ 'widgets.value-stepper.left' | translate }}</tb-toggle-option>
<tb-toggle-option value="right">{{ 'widgets.value-stepper.right' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="leftButton" [class.!hidden]="buttonAppearanceType !== 'left'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showButton">
{{ 'widgets.value-stepper.left-button' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div>{{ 'widgets.value-stepper.icon' | translate }}</div>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.button-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorOn">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.disabled-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="rightButton" [class.!hidden]="buttonAppearanceType !== 'right'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showButton">
{{ 'widgets.value-stepper.right-button' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div>{{ 'widgets.value-stepper.icon' | translate }}</div>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.button-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorOn">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.disabled-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.value-stepper.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

225
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts

@ -0,0 +1,225 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { formatValue, isUndefined } from '@core/utils';
import { ValueType } from '@shared/models/constants';
import {
valueStepperDefaultSettings,
valueStepperTypeImages,
valueStepperTypes,
valueStepperTypeTranslations,
ValueStepperWidgetSettings
} from '@home/components/widget/lib/rpc/value-stepper-widget.models';
type ButtonAppearanceType = 'left' | 'right';
@Component({
selector: 'tb-value-stepper-basic-config',
templateUrl: './value-stepper-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class ValueStepperBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
return this.valueStepperWidgetConfigForm.get('targetDevice').value;
}
valueStepperTypeTranslationMap = valueStepperTypeTranslations;
valueStepperTypes = valueStepperTypes;
valueStepperTypeImageMap = valueStepperTypeImages;
buttonAppearanceType: ButtonAppearanceType = 'left';
valueType = ValueType;
valueStepperWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.valueStepperWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: ValueStepperWidgetSettings = {...valueStepperDefaultSettings, ...(configData.config.settings || {})};
this.valueStepperWidgetConfigForm = this.fb.group({
targetDevice: [configData.config.targetDevice, []],
initialState: [settings.initialState, []],
leftButtonClick: [settings.leftButtonClick, []],
rightButtonClick: [settings.rightButtonClick, []],
disabledState: [settings.disabledState, []],
appearance: this.fb.group({
type: [settings.appearance.type, []],
autoScale: [settings.appearance.autoScale, []],
minValueRange: [settings.appearance.minValueRange, []],
maxValueRange: [settings.appearance.maxValueRange, []],
valueStep: [settings.appearance.valueStep, [Validators.min(0)]],
showValueBox: [settings.appearance.showValueBox, []],
valueUnits: [settings.appearance.valueUnits, []],
valueDecimals: [settings.appearance.valueDecimals, []],
valueFont: [settings.appearance.valueFont, []],
valueColor: [settings.appearance.valueColor],
valueBoxBackground: [settings.appearance.valueBoxBackground, []],
showBorder: [settings.appearance.showBorder, []],
borderWidth: [settings.appearance.borderWidth, []],
borderColor: [settings.appearance.borderColor, []]
}),
buttonAppearance: this.fb.group({
leftButton: this.fb.group({
showButton: [settings.buttonAppearance.leftButton.showButton],
icon: [settings.buttonAppearance.leftButton.icon],
iconSize: [settings.buttonAppearance.leftButton.iconSize],
iconSizeUnit: [settings.buttonAppearance.leftButton.iconSizeUnit],
mainColorOn: [settings.buttonAppearance.leftButton.mainColorOn, []],
backgroundColorOn: [settings.buttonAppearance.leftButton.backgroundColorOn, []],
mainColorDisabled: [settings.buttonAppearance.leftButton.mainColorDisabled, []],
backgroundColorDisabled: [settings.buttonAppearance.leftButton.backgroundColorDisabled, []]
}),
rightButton: this.fb.group({
showButton: [settings.buttonAppearance.rightButton.showButton],
icon: [settings.buttonAppearance.rightButton.icon],
iconSize: [settings.buttonAppearance.rightButton.iconSize],
iconSizeUnit: [settings.buttonAppearance.rightButton.iconSizeUnit],
mainColorOn: [settings.buttonAppearance.rightButton.mainColorOn, []],
backgroundColorOn: [settings.buttonAppearance.rightButton.backgroundColorOn, []],
mainColorDisabled: [settings.buttonAppearance.rightButton.mainColorDisabled, []],
backgroundColorDisabled: [settings.buttonAppearance.rightButton.backgroundColorDisabled, []]
})
}),
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.targetDevice = config.targetDevice;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.initialState = config.initialState;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.leftButtonClick = config.leftButtonClick;
this.widgetConfig.config.settings.rightButtonClick = config.rightButtonClick;
this.widgetConfig.config.settings.appearance = config.appearance;
this.widgetConfig.config.settings.buttonAppearance = config.buttonAppearance;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['appearance.showValueBox', 'appearance.showBorder',
'buttonAppearance.leftButton.showButton', 'buttonAppearance.rightButton.showButton'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showValueBox: boolean = this.valueStepperWidgetConfigForm.get('appearance').get('showValueBox').value;
const showBorder: boolean = this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').value;
const showLeftButton: boolean = this.valueStepperWidgetConfigForm.get('buttonAppearance').get('leftButton').get('showButton').value;
const showRightButton: boolean = this.valueStepperWidgetConfigForm.get('buttonAppearance').get('rightButton').get('showButton').value;
if (showValueBox) {
this.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueFont').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueColor').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueBoxBackground').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').enable({emitEvent: false});
if (showBorder) {
this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').enable();
this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').enable();
} else {
this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').disable();
}
} else {
this.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueFont').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueColor').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('valueBoxBackground').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').disable({emitEvent: false});
this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').disable();
this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').disable();
}
this.buttonValidators(showLeftButton, 'leftButton');
this.buttonValidators(showRightButton, 'rightButton');
}
private buttonValidators(showButtonValue: boolean, button: string) {
if (showButtonValue) {
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('icon').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSize').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSizeUnit').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorOn').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorOn').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorDisabled').enable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').enable()
} else {
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('icon').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSize').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSizeUnit').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorOn').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorOn').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorDisabled').disable()
this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').disable()
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _valuePreviewFn(): string {
const units: string = this.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').value;
const decimals: number = this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').value;
return formatValue(48, decimals, units, false);
}
}

7
ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts

@ -33,11 +33,11 @@ import { DomSanitizer } from '@angular/platform-browser';
import { ValueType } from '@shared/models/constants';
import {
powerButtonDefaultSettings,
PowerButtonShape,
powerButtonShapeSize,
PowerButtonShape, powerButtonShapeSize,
PowerButtonWidgetSettings
} from '@home/components/widget/lib/rpc/power-button-widget.models';
import { SVG, Svg } from '@svgdotjs/svg.js';
import { MatIconRegistry } from '@angular/material/icon';
@Component({
selector: 'tb-power-button-widget',
@ -72,6 +72,7 @@ export class PowerButtonWidgetComponent extends
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private iconRegistry: MatIconRegistry,
protected cd: ChangeDetectorRef,
protected zone: NgZone) {
super(cd);
@ -180,7 +181,7 @@ export class PowerButtonWidgetComponent extends
this.renderer.setStyle(this.svgShape.node, 'user-select', 'none');
this.zone.run(() => {
this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape,
this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape, this.iconRegistry,
this.settings, this.value, this.disabledState, () => this.onClick());
});

373
ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts

@ -28,6 +28,11 @@ import { Circle, Effect, Element, G, Gradient, Path, Runner, Svg, Text, Timeline
import '@svgdotjs/svg.filter.js';
import tinycolor from 'tinycolor2';
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable, of } from 'rxjs';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { catchError, map, take } from 'rxjs/operators';
import { MatIconRegistry } from '@angular/material/icon';
import { isDefinedAndNotNull } from '@core/utils';
export enum PowerButtonLayout {
default = 'default',
@ -71,20 +76,29 @@ export const powerButtonLayoutImages = new Map<PowerButtonLayout, string>(
]
);
export interface ButtonIconSettings {
showIcon: boolean;
iconSize: number;
iconSizeUnit: string;
icon: string;
}
export interface PowerButtonWidgetSettings {
initialState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
onUpdateState: SetValueSettings;
offUpdateState: SetValueSettings;
initialState?: GetValueSettings<boolean>;
disabledState?: GetValueSettings<boolean>;
onUpdateState?: SetValueSettings;
offUpdateState?: SetValueSettings;
layout: PowerButtonLayout;
onButtonIcon: ButtonIconSettings,
offButtonIcon: ButtonIconSettings,
mainColorOn: string;
backgroundColorOn: string;
mainColorOff: string;
backgroundColorOff: string;
mainColorDisabled: string;
backgroundColorDisabled: string;
background: BackgroundSettings;
padding: string;
background?: BackgroundSettings;
padding?: string;
}
export const powerButtonDefaultSettings: PowerButtonWidgetSettings = {
@ -177,6 +191,18 @@ export const powerButtonDefaultSettings: PowerButtonWidgetSettings = {
}
},
layout: PowerButtonLayout.default,
onButtonIcon: {
showIcon: false,
iconSize: 32,
iconSizeUnit: 'px',
icon: 'power_settings_new'
},
offButtonIcon: {
showIcon: false,
iconSize: 32,
iconSizeUnit: 'px',
icon: 'power_settings_new'
},
mainColorOn: '#3F52DD',
backgroundColorOn: '#FFFFFF',
mainColorOff: '#A2A2A2',
@ -207,6 +233,11 @@ interface PowerButtonColorState {
backgroundColor: PowerButtonColor;
}
interface ButtonsIconSettings {
onButtonIcon: ButtonIconSettings;
offButtonIcon: ButtonIconSettings;
}
type PowerButtonShapeColors = Record<PowerButtonState, PowerButtonColorState>;
const createPowerButtonShapeColors = (settings: PowerButtonWidgetSettings): PowerButtonShapeColors => {
@ -257,33 +288,35 @@ export abstract class PowerButtonShape {
static fromSettings(ctx: WidgetContext,
svgShape: Svg,
iconRegistry: MatIconRegistry,
settings: PowerButtonWidgetSettings,
value: boolean,
disabled: boolean,
onClick: () => void): PowerButtonShape {
switch (settings.layout) {
case PowerButtonLayout.default:
return new DefaultPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new DefaultPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.simplified:
return new SimplifiedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new SimplifiedPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.outlined:
return new OutlinedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new OutlinedPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.default_volume:
return new DefaultVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new DefaultVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.simplified_volume:
return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new SimplifiedVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.outlined_volume:
return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new OutlinedVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.default_icon:
return new DefaultIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new DefaultIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.simplified_icon:
return new SimplifiedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new SimplifiedIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
case PowerButtonLayout.outlined_icon:
return new OutlinedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
return new OutlinedIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick);
}
}
protected readonly colors: PowerButtonShapeColors;
protected readonly icons: ButtonsIconSettings;
protected readonly onLabel: string;
protected readonly offLabel: string;
@ -293,18 +326,67 @@ export abstract class PowerButtonShape {
protected pressed = false;
protected forcePressed = false;
protected offPowerSymbolIcon: Element;
protected onPowerSymbolIcon: Element;
protected offLabelShape: Text;
protected onLabelShape: Text;
protected offPowerSymbolCircle: Path;
protected offPowerSymbolLine: Path;
protected onPowerSymbolCircle: Path;
protected onPowerSymbolLine: Path;
protected constructor(protected widgetContext: WidgetContext,
protected svgShape: Svg,
protected iconRegistry: MatIconRegistry,
protected settings: PowerButtonWidgetSettings,
protected value: boolean,
protected disabled: boolean,
protected onClick: () => void) {
this.colors = createPowerButtonShapeColors(this.settings);
this.icons = {onButtonIcon: this.settings.onButtonIcon, offButtonIcon: this.settings.offButtonIcon};
this.onLabel = this.widgetContext.translate.instant('widgets.power-button.on-label').toUpperCase();
this.offLabel = this.widgetContext.translate.instant('widgets.power-button.off-label').toUpperCase();
this._drawShape();
}
public createIconElement(icon: string, size: number): Observable<Element> {
const isSvg = isSvgIcon(icon);
if (isSvg) {
const [namespace, iconName] = splitIconName(icon);
return this.iconRegistry
.getNamedSvgIcon(iconName, namespace)
.pipe(
take(1),
map((svgElement) => {
const element = new Element(svgElement.firstChild);
const box = element.bbox();
const scale = size / box.height;
element.scale(scale);
return element;
}),
catchError(() => of(null)
));
} else {
const iconName = splitIconName(icon)[1];
const textElement = this.svgShape.text(iconName);
const fontSetClasses = (
this.iconRegistry.getDefaultFontSetClass()
).filter(className => className.length > 0);
fontSetClasses.forEach(className => textElement.addClass(className));
textElement.font({size: `${size}px`});
textElement.attr({
style: `font-size: ${size}px`,
'text-anchor': 'start'
});
const tspan = textElement.first();
tspan.attr({
'dominant-baseline': 'hanging'
});
return of(textElement);
}
}
public setValue(value: boolean) {
if (this.value !== value) {
this.value = value;
@ -330,6 +412,106 @@ export abstract class PowerButtonShape {
}
}
public drawOffShape(centerGroup: G, label: boolean, labelWeight?: string, circleStroke?: boolean) {
if (this.icons.offButtonIcon.showIcon) {
this.createIconElement(this.icons.offButtonIcon.icon, this.icons.offButtonIcon.iconSize).subscribe(icon =>
this.offPowerSymbolIcon = icon.center(cx, cy).addTo(centerGroup));
} else {
if (label) {
this.offLabelShape = this.createOffLabel(labelWeight).addTo(centerGroup);
} else {
this.offPowerSymbolCircle = this.svgShape.path(circleStroke ? powerCircle : powerCircleStroke).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(circleStroke ? powerLine : powerLineStroke).center(cx, cy-12).addTo(centerGroup);
}
}
}
public drawOnShape(onCenterGroup?: G, label?: boolean, labelWeight?: string, circleStroke?: boolean, mask?: Circle) {
if (this.icons.onButtonIcon.showIcon) {
this.createIconElement(this.icons.onButtonIcon.icon, this.icons.onButtonIcon.iconSize).subscribe(icon => {
this.onPowerSymbolIcon = icon.center(cx, cy);
if (isDefinedAndNotNull(onCenterGroup)) {
this.onPowerSymbolIcon.addTo(onCenterGroup);
}
if (isDefinedAndNotNull(mask)) {
this.createMask(mask, [this.onPowerSymbolIcon]);
}
});
} else {
if (label) {
this.onLabelShape = this.createOnLabel(labelWeight);
if (isDefinedAndNotNull(onCenterGroup)) {
this.onLabelShape.addTo(onCenterGroup);
}
if (isDefinedAndNotNull(mask)) {
this.createMask(mask, [this.onLabelShape]);
}
} else {
this.onPowerSymbolCircle = this.svgShape.path(circleStroke ? powerCircle : powerCircleStroke).center(cx, cy);
this.onPowerSymbolLine = this.svgShape.path(circleStroke ? powerLine : powerLineStroke).center(cx, cy-12);
if (isDefinedAndNotNull(onCenterGroup)) {
this.onPowerSymbolCircle.addTo(onCenterGroup);
this.onPowerSymbolLine.addTo(onCenterGroup);
}
if (isDefinedAndNotNull(mask)) {
this.createMask(mask, [this.onPowerSymbolCircle, this.onPowerSymbolLine]);
}
}
}
}
public onCenterTimeLine(timeline: Timeline, label: boolean) {
if (this.icons.onButtonIcon.showIcon) {
this.onPowerSymbolIcon.timeline(timeline);
} else {
if (label) {
this.onLabelShape.timeline(timeline);
} else {
this.onPowerSymbolCircle.timeline(timeline);
this.onPowerSymbolLine.timeline(timeline);
}
}
}
public offCenterColor(mainColor: PowerButtonColor, label: boolean) {
if (this.icons.offButtonIcon.showIcon) {
this.offPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
if (label) {
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
}
}
public onCenterColor(mainColor: PowerButtonColor, label: boolean) {
if (this.icons.onButtonIcon.showIcon) {
this.onPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
if (label) {
this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} else {
this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
}
}
public buttonAnimation(scale: number, label: boolean) {
if (this.icons.onButtonIcon.showIcon) {
powerButtonAnimation(this.onPowerSymbolIcon).transform({scale});
} else {
if (label) {
powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}});
} else {
powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}});
powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}});
}
}
}
private _drawShape() {
this.backgroundShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
@ -530,9 +712,7 @@ class DefaultPowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
@ -543,22 +723,20 @@ class DefaultPowerButtonShape extends PowerButtonShape {
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 20)
.center(cx, cy);
this.onLabelShape = this.createOnLabel();
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.drawOffShape(this.centerGroup, true);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy);
this.drawOnShape(null, true, '', false, this.onCircleShape);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 20, cx, cy, 0, 0);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.onCenterTimeLine(this.pressedTimeline, true);
this.pressedShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
@ -578,14 +756,14 @@ class DefaultPowerButtonShape extends PowerButtonShape {
this.pressedTimeline.finish();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}});
this.buttonAnimation(pressedScale, true);
this.pressedShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}});
this.buttonAnimation(1, true);
this.pressedShadow.animateRestore();
}
@ -596,8 +774,6 @@ class SimplifiedPowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private onCircleShape: Circle;
private offLabelShape: Text;
private onLabelShape: Text;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
@ -608,22 +784,21 @@ class SimplifiedPowerButtonShape extends PowerButtonShape {
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
this.drawOffShape(this.centerGroup, true);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy);
this.onLabelShape = this.createOnLabel();
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.drawOnShape(null, true, '', false, this.onCircleShape)
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.onCenterTimeLine(this.pressedTimeline, true);
this.pressedShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
}
protected drawOff() {
@ -642,14 +817,14 @@ class SimplifiedPowerButtonShape extends PowerButtonShape {
this.pressedTimeline.finish();
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}});
this.buttonAnimation(pressedScale, true);
this.pressedShadow.animate(6, 0.6);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}});
this.buttonAnimation(1, true);
this.pressedShadow.animateRestore();
}
}
@ -659,9 +834,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape {
private outerBorderMask: Circle;
private innerBorder: Circle;
private innerBorderMask: Circle;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
@ -677,25 +850,23 @@ class OutlinedPowerButtonShape extends PowerButtonShape {
this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy);
this.createMask(this.innerBorder, [this.innerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
this.drawOffShape(this.centerGroup, true);
this.onCenterGroup = this.svgShape.group();
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 28).center(cx, cy)
.addTo(this.onCenterGroup);
this.onLabelShape = this.createOnLabel();
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 28).center(cx, cy).addTo(this.onCenterGroup);
this.drawOnShape(null, true, '', false, this.onCircleShape)
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 0, 0);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onCenterGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.onCenterTimeLine(this.pressedTimeline, true);
this.pressedShadow.timeline(this.pressedTimeline);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.innerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
@ -714,7 +885,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape {
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}});
this.buttonAnimation(pressedScale / 0.98, true);
this.pressedShadow.animate(6, 0.6);
}
@ -722,7 +893,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onCenterGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}});
this.buttonAnimation(1, true);
this.pressedShadow.animateRestore();
}
}
@ -737,35 +908,29 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
private innerShadow: InnerShadowCircle;
//private innerShadowGradient: Gradient;
//private innerShadowGradientStop: Stop;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
protected onCircleShape: Circle;
private pressedTimeline: Timeline;
private centerGroup: G;
protected drawOffCenter(centerGroup: G) {
this.offLabelShape = this.createOffLabel('400').addTo(centerGroup);
this.drawOffShape(centerGroup, true, '400');
}
protected drawOnCenter() {
this.onLabelShape = this.createOnLabel('400');
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape,[this.onLabelShape]);
this.drawOnShape(null, true, '400', false, this.onCircleShape);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onLabelShape.timeline(pressedTimeline);
this.onCenterTimeLine(pressedTimeline, true);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}});
this.buttonAnimation(scale, true);
}
protected drawShape() {
@ -789,7 +954,6 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
this.drawOffCenter(this.centerGroup);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy);
this.drawOnCenter();
this.addOnCenterToMask(this.onCircleShape);
this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3);
this.pressedTimeline = new Timeline();
@ -857,38 +1021,25 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
}
class DefaultIconPowerButtonShape extends DefaultVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawOffCenter(centerGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup);
this.drawOffShape(centerGroup, false);
}
protected drawOnCenter() {
this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy);
this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12);
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]);
this.drawOnShape(null, false, '', false, this.onCircleShape);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onPowerSymbolCircle.timeline(pressedTimeline);
this.onPowerSymbolLine.timeline(pressedTimeline);
this.onCenterTimeLine(pressedTimeline, false);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, false);
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}});
powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}});
this.buttonAnimation(scale, false);
}
}
@ -896,8 +1047,6 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
private outerBorder: Circle;
private outerBorderMask: Circle;
private offLabelShape: Text;
private onLabelShape: Text;
private innerShadow: InnerShadowCircle;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
@ -905,8 +1054,8 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
private onCenterGroup: G;
protected drawCenterGroup(centerGroup: G, onCenterGroup: G) {
this.offLabelShape = this.createOffLabel().addTo(centerGroup);
this.onLabelShape = this.createOnLabel().addTo(onCenterGroup);
this.drawOffShape(centerGroup, true);
this.drawOnShape(onCenterGroup, true);
}
protected drawShape() {
@ -926,8 +1075,8 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
}
protected drawColorState(mainColor: PowerButtonColor){
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
this.onCenterColor(mainColor, true);
}
protected drawOff() {
@ -970,23 +1119,15 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
}
class SimplifiedIconPowerButtonShape extends SimplifiedVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawCenterGroup(centerGroup: G, onCenterGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup);
this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(onCenterGroup);
this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(onCenterGroup);
this.drawOffShape(centerGroup, false);
this.drawOnShape(onCenterGroup, false, '', false);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, false);
this.onCenterColor(mainColor, false);
}
}
@ -996,36 +1137,30 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
private outerBorderGradient: Gradient;
private innerBorder: Circle;
private innerBorderMask: Circle;
private offLabelShape: Text;
private onCircleShape: Circle;
private onLabelShape: Text;
protected onCircleShape: Circle;
private pressedShadow: InnerShadowCircle;
private pressedTimeline: Timeline;
private centerGroup: G;
private onCenterGroup: G;
protected drawOffCenter(centerGroup: G) {
this.offLabelShape = this.createOffLabel('800').addTo(centerGroup);
this.drawOffShape(centerGroup, true, '800');
}
protected drawOnCenter() {
this.onLabelShape = this.createOnLabel('800');
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape,[this.onLabelShape]);
this.drawOnShape(null, true, '800', false, this.onCircleShape);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onLabelShape.timeline(pressedTimeline);
this.onCenterTimeLine(pressedTimeline, true);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, true);
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}});
this.buttonAnimation(scale, true);
}
protected drawShape() {
@ -1047,7 +1182,6 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy)
.addTo(this.onCenterGroup);
this.drawOnCenter();
this.addOnCenterToMask(this.onCircleShape);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0);
this.backgroundShape.addClass('tb-small-shadow');
@ -1102,37 +1236,24 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
}
class OutlinedIconPowerButtonShape extends OutlinedVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawOffCenter(centerGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12).addTo(centerGroup);
this.drawOffShape(centerGroup, false, '', true);
}
protected drawOnCenter() {
this.onPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy);
this.onPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12);
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]);
this.drawOnShape(null, false, '', true, this.onCircleShape);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onPowerSymbolCircle.timeline(pressedTimeline);
this.onPowerSymbolLine.timeline(pressedTimeline);
this.onCenterTimeLine(pressedTimeline, false);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offCenterColor(mainColor, false);
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}});
powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}});
this.buttonAnimation(scale, false);
}
}

43
ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-value-stepper-panel" [style.padding]="padding" [style]="backgroundStyle$ | async">
<div class="tb-value-stepper-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div #stepperContent class="tb-value-stepper-content">
<div [class.!hidden]="!showLeftButton"
#leftButton
class="tb-button-shape tb-button-shape-left"
[class.tb-button-pointer]="!leftDisabledState && (loading$ | async) === false">
</div>
<div #valueBoxContainer
class="tb-value-stepper-value-box"
[class.!hidden]="!showValueBox"
[class.tb-value-stepper-value-box-disabled]="(disabledState$ | async) === true">
<div #value
class="tb-value-stepper-value"
[class.tb-value-stepper-value-disabled]="(disabledState$ | async) === true"
[style]="valueStyle">{{ valueText }}</div>
</div>
<div [class.!hidden]="!showRightButton"
#rightButton
class="tb-button-shape tb-button-shape-right"
[class.tb-button-pointer]="!rightDisabledState && (loading$ | async) === false">
</div>
</div>
<mat-progress-bar class="tb-action-widget-progress" style="height: 4px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
</div>

90
ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.scss

@ -0,0 +1,90 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-value-stepper-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 24px 24px 24px;
> div:not(.tb-value-stepper-overlay) {
z-index: 1;
}
.tb-value-stepper-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
div.tb-widget-title {
padding: 0;
}
.tb-value-stepper-content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 0;
height: 100%;
.tb-value-stepper-value-box {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 32px;
padding: 0 12px;
border-radius: 4px;
.tb-value-stepper-value-disabled {
color: rgba(0, 0, 0, 0.38) !important;
}
&-disabled {
border-color: rgba(0, 0, 0, 0.38);
}
}
.tb-button-shape {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
&-left {
margin-right: 12px;
}
&-right {
margin-left: 12px;
}
svg {
.tb-small-shadow {
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.2));
}
.tb-shadow {
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.15));
}
}
&.tb-button-pointer {
svg {
.tb-hover-circle {
cursor: pointer;
}
}
}
}
}
}

388
ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts

@ -0,0 +1,388 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
NgZone,
OnDestroy,
OnInit,
Renderer2,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { ValueType } from '@shared/models/constants';
import {
PowerButtonLayout,
PowerButtonShape,
powerButtonShapeSize,
PowerButtonWidgetSettings
} from '@home/components/widget/lib/rpc/power-button-widget.models';
import { SVG, Svg } from '@svgdotjs/svg.js';
import { MatIconRegistry } from '@angular/material/icon';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import {
valueStepperDefaultSettings,
ValueStepperWidgetSettings
} from '@home/components/widget/lib/rpc/value-stepper-widget.models';
import { UtilsService } from '@core/services/utils.service';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-value-stepper-widget',
templateUrl: './value-stepper-widget.component.html',
styleUrls: ['../action/action-widget.scss', './value-stepper-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ValueStepperWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('leftButton', {static: false})
leftButton: ElementRef<HTMLElement>;
@ViewChild('rightButton', {static: false})
rightButton: ElementRef<HTMLElement>;
@ViewChild('stepperContent', {static: false})
stepperContent: ElementRef<HTMLElement>;
@ViewChild('valueBoxContainer', {static: false})
valueBox: ElementRef<HTMLElement>;
@ViewChild('value', {static: false})
valueElement: ElementRef<HTMLElement>;
settings: ValueStepperWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
padding: string;
valueStyle: ComponentStyle = {};
value: number = null;
autoScale = false;
showValueBox = true;
showLeftButton = true;
showRightButton = true;
valueText = 'N/A';
disabledState$ = new BehaviorSubject(false);
private prevValue: number = null;
private shapeResize$: ResizeObserver;
private drawSvgShapePending = false;
private svgShapeLeft: Svg;
private svgShapeRight: Svg;
private powerButtonSvgShapeLeft: PowerButtonShape;
private powerButtonSvgShapeRight: PowerButtonShape;
private disabledState = false;
public leftDisabledState = false;
public rightDisabledState = false;
private valueSetterLeft: ValueSetter<number>;
private valueSetterRight: ValueSetter<number>;
private leftDisabledState$ = new BehaviorSubject(false);
private rightDisabledState$ = new BehaviorSubject(false);
private readonly destroy$ = new Subject<void>();
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private iconRegistry: MatIconRegistry,
private utils: UtilsService,
private elementRef: ElementRef,
protected cd: ChangeDetectorRef,
protected zone: NgZone) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...valueStepperDefaultSettings, ...this.ctx.settings};
this.autoScale = this.settings.appearance.autoScale;
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.showValueBox = this.settings.appearance.showValueBox;
this.showLeftButton = this.settings.buttonAppearance.leftButton.showButton;
this.showRightButton = this.settings.buttonAppearance.rightButton.showButton;
this.valueStyle = textStyle(this.settings.appearance.valueFont);
this.valueStyle.color = this.settings.appearance.valueColor;
if (this.showValueBox) {
const valueBoxCss = `.tb-value-stepper-value-box {\n`+
`border: ${this.settings.appearance.showBorder ?
`${this.settings.appearance.borderWidth}px solid ${this.settings.appearance.borderColor}` :
'none'};\n`+
`background-color: ${this.settings.appearance.valueBoxBackground}` +
`}`;
this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, 'tb-value-stepper-value-box', valueBoxCss);
}
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.slider.initial-value')};
this.createValueGetter(getInitialStateSettings, ValueType.INTEGER, {
next: (value) => this.onValue(value)
});
const disabledStateSettings =
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')};
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, {
next: (value) => this.disabledState$.next(value)
});
const leftButtonClick = {...this.settings.leftButtonClick,
actionLabel: this.ctx.translate.instant('widgets.slider.on-value-change')};
this.valueSetterLeft = this.createValueSetter(leftButtonClick);
const rightButtonClick = {...this.settings.rightButtonClick,
actionLabel: this.ctx.translate.instant('widgets.slider.on-value-change')};
this.valueSetterRight = this.createValueSetter(rightButtonClick);
combineLatest([
this.loading$,
this.disabledState$.asObservable(),
this.leftDisabledState$.asObservable()
]).pipe(
takeUntil(this.destroy$)
).subscribe(value => {
const state = value.includes(true);
this.updateLeftDisabledState(state)
});
combineLatest([
this.loading$,
this.disabledState$.asObservable(),
this.rightDisabledState$.asObservable()
]).pipe(
takeUntil(this.destroy$)
).subscribe(value => {
const state = value.includes(true);
this.updateRightDisabledState(state)
});
}
ngAfterViewInit(): void {
if (this.drawSvgShapePending) {
this.drawSvg();
}
super.ngAfterViewInit();
}
ngOnDestroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
public onInit() {
super.onInit();
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
if (this.leftButton || this.rightButton) {
this.drawSvg();
} else {
this.drawSvgShapePending = true;
}
this.cd.detectChanges();
}
private onValue(value: number): void {
this.value = value;
this.prevValue = value;
if ((this.value + this.settings.appearance.valueStep) >= this.settings.appearance.maxValueRange) {
this.rightDisabledState$.next(true);
} else {
this.rightDisabledState$.next(false);
}
if ((this.value - this.settings.appearance.valueStep) <= this.settings.appearance.minValueRange) {
this.leftDisabledState$.next(true);
} else {
this.leftDisabledState$.next(false);
}
this.updateValueText();
this.cd.markForCheck();
}
private updateValueText() {
if (isDefinedAndNotNull(this.value) && isNumeric(this.value)) {
this.valueText = formatValue(this.value, this.settings.appearance.valueDecimals, this.settings.appearance.valueUnits, false);
} else {
this.valueText = 'N/A';
}
}
private onClick(rightButtonClick: boolean = false) {
this.updateValueText();
if (!this.ctx.isEdit && !this.ctx.isPreview && !this.disabledState) {
const prevValue = this.prevValue;
const targetValue = rightButtonClick ?
(this.value + this.settings.appearance.valueStep) :
(this.value - this.settings.appearance.valueStep);
this.updateValue(rightButtonClick ? this.valueSetterRight : this.valueSetterLeft, targetValue, {
next: () => this.onValue(targetValue),
error: () => this.onValue(prevValue)
});
}
}
private drawSvg() {
let leftButtonSetting: PowerButtonWidgetSettings;
let rightButtonSetting: PowerButtonWidgetSettings;
if (this.showLeftButton) {
this.svgShapeLeft = SVG().addTo(this.leftButton.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize);
this.renderer.setStyle(this.svgShapeLeft.node, 'overflow', 'visible');
this.renderer.setStyle(this.svgShapeLeft.node, 'user-select', 'none');
leftButtonSetting = {
layout: PowerButtonLayout[this.settings.appearance.type],
onButtonIcon: {
showIcon: true,
icon: this.settings.buttonAppearance.leftButton.icon,
iconSize: this.settings.buttonAppearance.leftButton.iconSize * 1.7,
iconSizeUnit: this.settings.buttonAppearance.leftButton.iconSizeUnit
},
offButtonIcon: {
showIcon: true,
icon: this.settings.buttonAppearance.leftButton.icon,
iconSize: this.settings.buttonAppearance.leftButton.iconSize * 1.7,
iconSizeUnit: this.settings.buttonAppearance.leftButton.iconSizeUnit
},
mainColorOn: this.settings.buttonAppearance.leftButton.mainColorOn,
backgroundColorOn: this.settings.buttonAppearance.leftButton.backgroundColorOn,
mainColorOff: this.settings.buttonAppearance.leftButton.mainColorOff,
backgroundColorOff: this.settings.buttonAppearance.leftButton.backgroundColorOff,
mainColorDisabled: this.settings.buttonAppearance.leftButton.mainColorDisabled,
backgroundColorDisabled: this.settings.buttonAppearance.leftButton.backgroundColorDisabled
};
}
if (this.showRightButton) {
this.svgShapeRight = SVG().addTo(this.rightButton.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize);
this.renderer.setStyle(this.svgShapeRight.node, 'overflow', 'visible');
this.renderer.setStyle(this.svgShapeRight.node, 'user-select', 'none');
rightButtonSetting = {
layout: PowerButtonLayout[this.settings.appearance.type],
onButtonIcon: {
showIcon: true,
icon: this.settings.buttonAppearance.rightButton.icon,
iconSize: this.settings.buttonAppearance.rightButton.iconSize * 1.7,
iconSizeUnit: this.settings.buttonAppearance.rightButton.iconSizeUnit
},
offButtonIcon: {
showIcon: true,
icon: this.settings.buttonAppearance.rightButton.icon,
iconSize: this.settings.buttonAppearance.rightButton.iconSize * 1.7,
iconSizeUnit: this.settings.buttonAppearance.rightButton.iconSizeUnit
},
mainColorOn: this.settings.buttonAppearance.rightButton.mainColorOn,
backgroundColorOn: this.settings.buttonAppearance.rightButton.backgroundColorOn,
mainColorOff: this.settings.buttonAppearance.rightButton.mainColorOff,
backgroundColorOff: this.settings.buttonAppearance.rightButton.backgroundColorOff,
mainColorDisabled: this.settings.buttonAppearance.rightButton.mainColorDisabled,
backgroundColorDisabled: this.settings.buttonAppearance.rightButton.backgroundColorDisabled
};
}
this.zone.run(() => {
if (this.showLeftButton) {
this.powerButtonSvgShapeLeft = PowerButtonShape.fromSettings(this.ctx, this.svgShapeLeft, this.iconRegistry,
leftButtonSetting , true, this.leftDisabledState, () => this.onClick());
}
if (this.showRightButton) {
this.powerButtonSvgShapeRight = PowerButtonShape.fromSettings(this.ctx, this.svgShapeRight, this.iconRegistry,
rightButtonSetting, true, this.rightDisabledState, () => this.onClick(true));
}
});
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
if (this.autoScale) {
this.shapeResize$.observe(this.stepperContent.nativeElement);
}
if (this.showLeftButton) {
this.shapeResize$.observe(this.leftButton.nativeElement);
}
if (this.showRightButton) {
this.shapeResize$.observe(this.rightButton.nativeElement);
}
this.onResize();
}
private updateLeftDisabledState(disabled: boolean) {
this.leftDisabledState = disabled;
this.powerButtonSvgShapeLeft?.setDisabled(this.leftDisabledState);
this.cd.markForCheck();
}
private updateRightDisabledState(disabled: boolean) {
this.rightDisabledState = disabled;
this.powerButtonSvgShapeRight?.setDisabled(this.rightDisabledState);
this.cd.markForCheck();
}
private onResize() {
const panelWidth = this.stepperContent.nativeElement.getBoundingClientRect().width;
const panelHeight = this.stepperContent.nativeElement.getBoundingClientRect().height;
const minAspect = 0.2;
const avgContentHeight = 32;
const targetHeight = panelWidth * Math.min(panelHeight / panelWidth, minAspect);
const multiplier = targetHeight / avgContentHeight;
const size = avgContentHeight * multiplier;
if (this.showValueBox) {
this.renderer.setStyle(this.valueBox?.nativeElement, 'height', `${size}px`);
this.renderer.setStyle(this.valueElement?.nativeElement, 'font-size', `${this.settings.appearance.valueFont.size * multiplier}px`);
}
if (this.showLeftButton) {
this.renderer.setStyle(this.leftButton?.nativeElement, 'width', `${size}px`);
this.renderer.setStyle(this.leftButton?.nativeElement, 'height', `${size}px`);
}
if (this.showRightButton) {
this.renderer.setStyle(this.rightButton?.nativeElement, 'width', `${size}px`);
this.renderer.setStyle(this.rightButton?.nativeElement, 'height', `${size}px`);
}
if (size) {
const scale = size / powerButtonShapeSize;
if (this.showLeftButton) {
this.renderer.setStyle(this.svgShapeLeft?.node, 'transform', `scale(${scale})`);
}
if (this.showRightButton) {
this.renderer.setStyle(this.svgShapeRight?.node, 'transform', `scale(${scale})`);
}
}
}
}

239
ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts

@ -0,0 +1,239 @@
import {
DataToValueType,
GetValueAction,
GetValueSettings, SetValueAction,
SetValueSettings, ValueToDataType
} from '@shared/models/action-widget-settings.models';
import { defaultWidgetAction, WidgetAction } from '@shared/models/widget.models';
import {
ButtonToggleAppearance, segmentedButtonDefaultAppearance,
SegmentedButtonWidgetSettings
} from '@home/components/widget/lib/button/segmented-button-widget.models';
import { WidgetButtonCustomStyles, WidgetButtonType } from '@shared/components/button/widget-button.models';
import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
const defaultMainColor = '#305680';
export enum ValueStepperType {
simplified = 'simplified',
default = 'default',
default_volume = 'default_volume'
}
export const valueStepperTypes = Object.keys(ValueStepperType) as ValueStepperType[];
export const valueStepperTypeTranslations = new Map<ValueStepperType, string>(
[
[ValueStepperType.simplified, 'widgets.value-stepper.simplified'],
[ValueStepperType.default, 'widgets.value-stepper.filled'],
[ValueStepperType.default_volume, 'widgets.value-stepper.volume']
]
);
export const valueStepperTypeImages = new Map<ValueStepperType, string>(
[
[ValueStepperType.simplified, 'assets/widget/value-stepper/simplified.svg'],
[ValueStepperType.default, 'assets/widget/value-stepper/filled.svg'],
[ValueStepperType.default_volume, 'assets/widget/value-stepper/volume.svg']
]
);
export interface ValueStepperWidgetSettings {
initialState: GetValueSettings<number>;
leftButtonClick: SetValueSettings;
rightButtonClick: SetValueSettings;
disabledState: GetValueSettings<boolean>;
appearance: ValueStepperAppearance;
buttonAppearance: {
leftButton: ValueStepperButtonAppearance;
rightButton: ValueStepperButtonAppearance;
}
background: BackgroundSettings;
padding: string;
}
export interface ValueStepperAppearance {
type: ValueStepperType;
autoScale: boolean;
minValueRange: number;
maxValueRange: number;
valueStep: number;
showValueBox: boolean;
valueUnits: string;
valueDecimals: number;
valueFont: Font;
valueColor: string;
valueBoxBackground: string;
showBorder: boolean;
borderWidth: number;
borderColor: string;
}
export interface ValueStepperButtonAppearance {
showButton: boolean;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
mainColorOn: string;
backgroundColorOn: string;
mainColorOff: string;
backgroundColorOff: string;
mainColorDisabled: string;
backgroundColorDisabled: string;
customStyle: WidgetButtonCustomStyles;
}
export const valueStepperDefaultAppearance: ValueStepperAppearance = {
type: ValueStepperType.simplified,
autoScale: true,
minValueRange: -100,
maxValueRange: 100,
valueStep: 0.5,
showValueBox: true,
valueUnits: '',
valueDecimals: 1,
valueFont: {
family: 'Roboto',
weight: '500',
style: 'normal',
size: 16,
sizeUnit: 'px',
lineHeight: '24px'
},
valueColor: '#000',
valueBoxBackground: 'rgba(0, 0, 0, 0.12)',
showBorder: true,
borderWidth: 1,
borderColor: defaultMainColor
}
export const valueStepperButtonDefaultAppearance: ValueStepperButtonAppearance = {
showButton: true,
icon: '',
iconSize: 24,
iconSizeUnit: 'px',
mainColorOn: '#3F52DD',
backgroundColorOn: '#FFFFFF',
mainColorOff: '#A2A2A2',
backgroundColorOff: '#FFFFFF',
mainColorDisabled: 'rgba(0,0,0,0.12)',
backgroundColorDisabled: '#FFFFFF',
customStyle: {
enabled: null,
hovered: null,
pressed: null,
activated: null,
disabled: null
}
}
export const valueStepperDefaultSettings: ValueStepperWidgetSettings = {
initialState: {
action: GetValueAction.EXECUTE_RPC,
defaultValue: 0,
executeRpc: {
method: 'getState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
getAlarmStatus: {
severityList: null,
typeList: null
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return integer value */\nreturn data;'
}
},
disabledState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state',
scope: null
},
getTimeSeries: {
key: 'state'
},
getAlarmStatus: {
severityList: null,
typeList: null
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
leftButtonClick: {
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SERVER_SCOPE
},
putTimeSeries: {
key: 'state'
},
valueToData: {
type: ValueToDataType.VALUE,
constantValue: 0,
valueToDataFunction: '/* Convert input integer value to RPC parameters or attribute/time-series value */\nreturn value;'
}
},
rightButtonClick: {
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
requestPersistent: false,
persistentPollingInterval: 1000
},
setAttribute: {
key: 'state',
scope: AttributeScope.SERVER_SCOPE
},
putTimeSeries: {
key: 'state'
},
valueToData: {
type: ValueToDataType.VALUE,
constantValue: 0,
valueToDataFunction: '/* Convert input integer value to RPC parameters or attribute/time-series value */\nreturn value;'
}
},
appearance: valueStepperDefaultAppearance,
buttonAppearance: {
leftButton: {...valueStepperButtonDefaultAppearance, icon: 'arrow_back_ios_new'},
rightButton: {...valueStepperButtonDefaultAppearance, icon: 'arrow_forward_ios'}
},
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
padding: '12px'
};

266
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html

@ -0,0 +1,266 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="valueStepperWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.value-stepper.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.initial-state-hint' | translate}}" translate>widgets.value-stepper.initial-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.initial-state' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.left-button-click-hint' | translate}}" translate>widgets.value-stepper.left-button-click</div>
<tb-set-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.left-button-click' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="leftButtonClick"></tb-set-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-stepper.right-button-click-hint' | translate}}" translate>widgets.value-stepper.right-button-click</div>
<tb-set-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.value-stepper.right-button-click' | translate }}"
[valueType]="valueType.DOUBLE"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="rightButtonClick"></tb-set-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.button-state.disabled-state-hint' | translate}}" translate>widgets.button-state.disabled-state</div>
<tb-get-value-action-settings class="flex-1"
panelTitle="{{ 'widgets.button-state.disabled-state' | translate }}"
[valueType]="valueType.BOOLEAN"
stateLabel="{{ 'widgets.button-state.disabled' | translate }}"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel" formGroupName="appearance">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="3:1"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.button.layout' | translate }}" formControlName="type">
<tb-image-cards-select-option *ngFor="let type of valueStepperTypes"
[value]="type"
[image]="valueStepperTypeImageMap.get(type)">
{{ valueStepperTypeTranslationMap.get(type) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.value-stepper.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.value-stepper.value-range' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2">
<div class="tb-small-label" translate>widgets.value-stepper.min-range</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="minValueRange" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.value-stepper.max-range</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValueRange" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.value-stepper.value-increment-decrement-step' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="valueStep" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValueBox">
{{ 'widgets.value-stepper.value' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<tb-unit-input class="flex" formControlName="valueUnits"></tb-unit-input>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="valueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden" translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="valueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.value-stepper.value-box-background' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="valueBoxBackground">
</tb-color-input>
</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showBorder">
{{ 'widgets.value-stepper.border' | translate }}
</mat-slide-toggle>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="borderWidth" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix class="lt-md:!hidden">px</div>
</mat-form-field>
<tb-color-input asBoxInput
colorClearButton
formControlName="borderColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel" formGroupName="buttonAppearance">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>widgets.value-stepper.button-appearance</div>
<tb-toggle-select [(ngModel)]="buttonAppearanceType" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="left">{{ 'widgets.value-stepper.left' | translate }}</tb-toggle-option>
<tb-toggle-option value="right">{{ 'widgets.value-stepper.right' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="leftButton" [class.!hidden]="buttonAppearanceType !== 'left'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showButton">
{{ 'widgets.value-stepper.left-button' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div>{{ 'widgets.value-stepper.icon' | translate }}</div>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorOn">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.disabled-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" formGroupName="rightButton" [class.!hidden]="buttonAppearanceType !== 'right'">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showButton">
{{ 'widgets.value-stepper.right-button' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div>{{ 'widgets.value-stepper.icon' | translate }}</div>
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select class="flex-1" formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.power-on-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorOn">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorOn">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.power-button.disabled-colors' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-3">
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.main</div>
<tb-color-input asBoxInput
formControlName="mainColorDisabled">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div class="flex flex-row items-center justify-start gap-2">
<div translate>widgets.power-button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColorDisabled">
</tb-color-input>
</div>
</div>
</div>
</div>
</div>
</ng-container>

191
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts

@ -0,0 +1,191 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ValueType } from '@shared/models/constants';
import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models';
import {
valueStepperDefaultSettings,
valueStepperTypeImages,
valueStepperTypes,
valueStepperTypeTranslations
} from '@home/components/widget/lib/rpc/value-stepper-widget.models';
import { formatValue } from '@core/utils';
type ButtonAppearanceType = 'left' | 'right';
@Component({
selector: 'tb-value-stepper-widget-settings',
templateUrl: './value-stepper-widget-settings.component.html',
styleUrls: ['../widget-settings.scss']
})
export class ValueStepperWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
const datasources = this.widgetConfig?.config?.datasources;
return getTargetDeviceFromDatasources(datasources);
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
get borderRadius(): string {
return this.widgetConfig?.config?.borderRadius;
}
valueType = ValueType;
valueStepperWidgetSettingsForm: UntypedFormGroup;
valueStepperTypeTranslationMap = valueStepperTypeTranslations;
valueStepperTypes = valueStepperTypes;
valueStepperTypeImageMap = valueStepperTypeImages;
buttonAppearanceType: ButtonAppearanceType = 'left';
valuePreviewFn = this._valuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.valueStepperWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...valueStepperDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.valueStepperWidgetSettingsForm = this.fb.group({
initialState: [settings.initialState, []],
leftButtonClick: [settings.leftButtonClick, []],
rightButtonClick: [settings.rightButtonClick, []],
disabledState: [settings.disabledState, []],
appearance: this.fb.group({
type: [settings.appearance.type, []],
autoScale: [settings.appearance.autoScale, []],
minValueRange: [settings.appearance.minValueRange, []],
maxValueRange: [settings.appearance.maxValueRange, []],
valueStep: [settings.appearance.valueStep, [Validators.min(0)]],
showValueBox: [settings.appearance.showValueBox, []],
valueUnits: [settings.appearance.valueUnits, []],
valueDecimals: [settings.appearance.valueDecimals, []],
valueFont: [settings.appearance.valueFont, []],
valueColor: [settings.appearance.valueColor],
valueBoxBackground: [settings.appearance.valueBoxBackground, []],
showBorder: [settings.appearance.showBorder, []],
borderWidth: [settings.appearance.borderWidth, []],
borderColor: [settings.appearance.borderColor, []]
}),
buttonAppearance: this.fb.group({
leftButton: this.fb.group({
showButton: [settings.buttonAppearance.leftButton.showButton],
icon: [settings.buttonAppearance.leftButton.icon],
iconSize: [settings.buttonAppearance.leftButton.iconSize],
iconSizeUnit: [settings.buttonAppearance.leftButton.iconSizeUnit],
mainColorOn: [settings.buttonAppearance.leftButton.mainColorOn, []],
backgroundColorOn: [settings.buttonAppearance.leftButton.backgroundColorOn, []],
mainColorDisabled: [settings.buttonAppearance.leftButton.mainColorDisabled, []],
backgroundColorDisabled: [settings.buttonAppearance.leftButton.backgroundColorDisabled, []]
}),
rightButton: this.fb.group({
showButton: [settings.buttonAppearance.rightButton.showButton],
icon: [settings.buttonAppearance.rightButton.icon],
iconSize: [settings.buttonAppearance.rightButton.iconSize],
iconSizeUnit: [settings.buttonAppearance.rightButton.iconSizeUnit],
mainColorOn: [settings.buttonAppearance.rightButton.mainColorOn, []],
backgroundColorOn: [settings.buttonAppearance.rightButton.backgroundColorOn, []],
mainColorDisabled: [settings.buttonAppearance.rightButton.mainColorDisabled, []],
backgroundColorDisabled: [settings.buttonAppearance.rightButton.backgroundColorDisabled, []]
})
})
});
}
protected validatorTriggers(): string[] {
return ['appearance.showValueBox', 'appearance.showBorder',
'buttonAppearance.leftButton.showButton', 'buttonAppearance.rightButton.showButton'];
}
protected updateValidators(_emitEvent: boolean): void {
const showValueBox: boolean = this.valueStepperWidgetSettingsForm.get('appearance').get('showValueBox').value;
const showBorder: boolean = this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').value;
const showLeftButton: boolean = this.valueStepperWidgetSettingsForm.get('buttonAppearance').get('leftButton').get('showButton').value;
const showRightButton: boolean = this.valueStepperWidgetSettingsForm.get('buttonAppearance').get('rightButton').get('showButton').value;
if (showValueBox) {
this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueFont').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueColor').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueBoxBackground').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').enable({emitEvent: false});
if (showBorder) {
this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').enable();
this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').enable();
} else {
this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').disable();
}
} else {
this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueFont').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueColor').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('valueBoxBackground').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').disable({emitEvent: false});
this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').disable();
this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').disable();
}
this.buttonValidators(showLeftButton, 'leftButton');
this.buttonValidators(showRightButton, 'rightButton');
}
private buttonValidators(showButtonValue: boolean, button: string) {
if (showButtonValue) {
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('icon').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSize').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSizeUnit').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorOn').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorOn').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorDisabled').enable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').enable()
} else {
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('icon').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSize').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSizeUnit').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorOn').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorOn').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorDisabled').disable()
this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').disable()
}
}
private _valuePreviewFn(): string {
const units: string = this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').value;
const decimals: number = this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').value;
return formatValue(48, decimals, units, false);
}
}

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -371,6 +371,9 @@ import {
import {
SegmentedButtonWidgetSettingsComponent
} from '@home/components/widget/lib/settings/button/segmented-button-widget-settings.component';
import {
ValueStepperWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component';
@NgModule({
declarations: [
@ -487,6 +490,7 @@ import {
SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent,
SegmentedButtonWidgetSettingsComponent,
ValueStepperWidgetSettingsComponent,
CommandButtonWidgetSettingsComponent,
PowerButtonWidgetSettingsComponent,
SliderWidgetSettingsComponent,
@ -624,6 +628,7 @@ import {
SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent,
SegmentedButtonWidgetSettingsComponent,
ValueStepperWidgetSettingsComponent,
CommandButtonWidgetSettingsComponent,
PowerButtonWidgetSettingsComponent,
SliderWidgetSettingsComponent,

3
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -88,6 +88,7 @@ import {
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/button/two-segment-button-widget.component';
import { ValueStepperWidgetComponent } from '@home/components/widget/lib/rpc/value-stepper-widget.component';
@NgModule({
declarations: [
@ -128,6 +129,7 @@ import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/but
TwoSegmentButtonWidgetComponent,
CommandButtonWidgetComponent,
PowerButtonWidgetComponent,
ValueStepperWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,
@ -190,6 +192,7 @@ import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/but
TwoSegmentButtonWidgetComponent,
CommandButtonWidgetComponent,
PowerButtonWidgetComponent,
ValueStepperWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,

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

@ -6617,6 +6617,8 @@
"layout-outlined-icon": "Outlined.Icon",
"main": "Main",
"background": "Background",
"button-icon-on": "Button icon 'On'",
"button-icon-off": "Button icon 'Off'",
"power-on-colors": "Power 'On' colors",
"power-off-colors": "Power 'Off' colors",
"disabled-colors": "Disabled colors",
@ -6669,6 +6671,41 @@
"preview": "Preview",
"copy-style-from": "Copy style from"
},
"value-stepper": {
"behavior": "Behavior",
"simplified": "Simplified",
"filled": "Filled",
"outlined": "Outlined",
"volume": "Volume",
"initial-state": "Initial state",
"initial-state-hint": "Action to get the initial value.",
"disabled-state": "Disabled state",
"disabled-state-hint": "Configure condition under which the component is disabled.",
"right-button-click": "Right button click",
"right-button-click-hint": "Action while pressing on right button.",
"left-button-click": "Left button click",
"left-button-click-hint": "Action while pressing on left button.",
"auto-scale": "Auto scale",
"value-range": "Range",
"min-range": "Min",
"max-range": "Max",
"value-increment-decrement-step": "Value increment/decrement step",
"value": "Value",
"value-box-background": "Value box background",
"border": "Border",
"button-appearance": "Button appearance",
"left": "Left",
"right": "Right",
"left-button": "Left button",
"right-button": "Right button",
"icon": "Icon",
"color-palette": "Color palette",
"main": "Main",
"background": "Background",
"button-icon-on": "Button icon 'On'",
"button-on-colors": "Power 'On' colors",
"disabled-colors": "Disabled colors"
},
"button-state": {
"activated-state": "Activated state",
"activated-state-hint": "Configure condition under which the button is active.",

1
ui-ngx/src/assets/widget/value-stepper/filled.svg

@ -0,0 +1 @@
<svg width="214" height="76" fill="none" xmlns="http://www.w3.org/2000/svg"><g filter="url(#a)"><rect x="8.5" y="4.5" width="197" height="59" rx="4" fill="#fff" shape-rendering="crispEdges"/><rect x="21" y="18.5" width="31" height="31" rx="15.5" stroke="#305680"/><rect x="24.5" y="22" width="24" height="24" rx="12" fill="#305680"/><path d="m38.5 40 1.41-1.41L35.33 34l4.58-4.59L38.5 28l-6 6 6 6Z" fill="#fff"/><path d="M65 22a3.5 3.5 0 0 1 3.5-3.5h77A3.5 3.5 0 0 1 149 22v24a3.5 3.5 0 0 1-3.5 3.5h-77A3.5 3.5 0 0 1 65 46V22Z" fill="#305680" fill-opacity=".04"/><path d="M65 22a3.5 3.5 0 0 1 3.5-3.5h77A3.5 3.5 0 0 1 149 22v24a3.5 3.5 0 0 1-3.5 3.5h-77A3.5 3.5 0 0 1 65 46V22Z" stroke="#305680"/><path d="M88.59 37.5V39h-7.63v-1.29l3.7-4.04c.41-.46.73-.85.97-1.19.23-.33.4-.63.49-.9a2.3 2.3 0 0 0-.06-1.73c-.13-.27-.32-.5-.58-.65a1.7 1.7 0 0 0-.93-.24c-.42 0-.77.1-1.06.27-.28.19-.5.44-.65.76a2.6 2.6 0 0 0-.22 1.1h-1.88c0-.67.15-1.27.46-1.82.3-.55.73-.99 1.3-1.3a4.1 4.1 0 0 1 2.08-.5c.76 0 1.4.13 1.94.38.53.26.93.62 1.2 1.09a3.36 3.36 0 0 1 .25 2.72 5 5 0 0 1-.49 1.04 9 9 0 0 1-.74 1.04c-.28.35-.6.7-.94 1.05l-2.46 2.71h5.25Zm9.27-9.88v1.04L93.3 39h-1.98l4.54-9.88h-5.9v-1.5h7.89Zm1.98 10.44c0-.29.1-.53.3-.73.2-.2.46-.3.8-.3s.6.1.8.3c.2.2.3.44.3.73a1 1 0 0 1-.3.74c-.2.2-.46.3-.8.3s-.6-.1-.8-.3a1 1 0 0 1-.3-.74Zm6.49-4.35-1.5-.37.61-5.72h6.14v1.6H107l-.32 2.79a3.67 3.67 0 0 1 1.81-.46c.54 0 1.02.09 1.44.26.42.17.79.43 1.08.76.3.33.53.73.68 1.2a5 5 0 0 1 0 3.07 3.17 3.17 0 0 1-1.85 2.03c-.46.19-1.01.29-1.65.29a4.6 4.6 0 0 1-1.36-.2 3.73 3.73 0 0 1-1.17-.62 3.14 3.14 0 0 1-1.19-2.42h1.85c.05.37.15.69.3.95.16.25.38.45.64.58a2 2 0 0 0 .92.2c.32 0 .6-.05.83-.16.23-.11.42-.27.57-.48.15-.22.27-.47.34-.75a3.62 3.62 0 0 0-.02-1.87 1.98 1.98 0 0 0-.38-.72c-.17-.2-.38-.36-.63-.47a2.13 2.13 0 0 0-.88-.17c-.45 0-.8.07-1.04.2-.23.13-.45.29-.65.48Zm11.75-4.16c0-.38.1-.72.28-1.04.19-.32.44-.57.75-.76a1.96 1.96 0 0 1 2.05 0c.31.19.56.44.74.76.19.32.28.66.28 1.04s-.1.73-.28 1.05c-.18.31-.43.56-.74.74a2.04 2.04 0 0 1-2.8-.74 2.03 2.03 0 0 1-.28-1.05Zm1.05 0a1 1 0 0 0 1 1 .97.97 0 0 0 .98-1 1 1 0 0 0-.27-.72.93.93 0 0 0-.7-.3c-.28 0-.51.1-.71.3-.2.2-.3.43-.3.72Zm12.19 5.75h1.95a4.5 4.5 0 0 1-.62 1.99 3.72 3.72 0 0 1-1.5 1.37 5 5 0 0 1-2.33.5 4.15 4.15 0 0 1-3.34-1.45c-.4-.48-.71-1.04-.92-1.7-.21-.66-.32-1.4-.32-2.22v-.95c0-.81.1-1.55.32-2.22.22-.66.53-1.22.94-1.69.4-.47.9-.84 1.46-1.09a4.78 4.78 0 0 1 1.93-.37c.9 0 1.67.17 2.3.5.62.33 1.1.8 1.45 1.38.35.59.56 1.26.64 2.02h-1.95c-.05-.48-.17-.9-.35-1.25a1.77 1.77 0 0 0-.76-.8 2.73 2.73 0 0 0-1.33-.28c-.45 0-.84.08-1.17.25a2.2 2.2 0 0 0-.84.73c-.22.33-.39.72-.5 1.2-.11.47-.17 1-.17 1.6v.97c0 .57.05 1.1.15 1.56.1.47.26.86.47 1.2.21.33.48.59.81.77.33.18.72.27 1.18.27.56 0 1-.08 1.35-.26.35-.18.61-.44.8-.78.17-.34.3-.76.35-1.25Z" fill="#000" fill-opacity=".87"/><rect x="162" y="18.5" width="31" height="31" rx="15.5" stroke="#305680"/><rect x="165.5" y="22" width="24" height="24" rx="12" fill="#305680"/><path d="m175.5 28-1.41 1.41 4.58 4.59-4.58 4.59L175.5 40l6-6-6-6Z" fill="#fff"/></g><defs><filter id="a" x=".5" y=".5" width="213" height="75" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset dy="4"/><feGaussianBlur stdDeviation="4"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5669_166705"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_5669_166705" result="shape"/></filter></defs></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

1
ui-ngx/src/assets/widget/value-stepper/simplified.svg

@ -0,0 +1 @@
<svg width="214" height="76" fill="none" xmlns="http://www.w3.org/2000/svg"><g filter="url(#a)"><rect x="8.83" y="4.5" width="197" height="59" rx="4" fill="#fff" shape-rendering="crispEdges"/><rect x="20.83" y="18" width="32" height="32" rx="16" fill="#305680"/><path d="m38.83 40 1.41-1.41L35.66 34l4.58-4.59-1.4-1.41-6 6 6 6Z" fill="#fff"/><rect x="65.33" y="18.5" width="84" height="31" rx="3.5" fill="#305680" fill-opacity=".04"/><rect x="65.33" y="18.5" width="84" height="31" rx="3.5" stroke="#305680"/><path d="M88.92 37.5V39h-7.63v-1.29l3.7-4.04c.41-.46.73-.85.97-1.19.23-.33.4-.63.49-.9a2.3 2.3 0 0 0-.05-1.73c-.13-.27-.33-.5-.58-.65a1.7 1.7 0 0 0-.93-.24c-.42 0-.78.1-1.06.27-.3.19-.5.44-.65.76a2.6 2.6 0 0 0-.22 1.1h-1.88c0-.67.15-1.27.45-1.82.3-.55.74-.99 1.31-1.3a4.1 4.1 0 0 1 2.07-.5c.76 0 1.4.13 1.94.38.53.26.93.62 1.21 1.09.28.47.42 1.02.42 1.66 0 .36-.06.7-.17 1.06a5 5 0 0 1-.5 1.04c-.2.35-.45.7-.73 1.04-.3.35-.6.7-.95 1.05l-2.46 2.71h5.25Zm9.27-9.88v1.04L93.65 39h-1.99l4.54-9.88h-5.89v-1.5h7.88Zm1.98 10.44c0-.29.1-.53.3-.73.2-.2.46-.3.8-.3s.61.1.8.3c.2.2.3.44.3.73a1 1 0 0 1-.3.74c-.19.2-.46.3-.8.3s-.6-.1-.8-.3a1 1 0 0 1-.3-.74Zm6.5-4.35-1.5-.37.6-5.72h6.14v1.6h-4.57l-.31 2.79a3.67 3.67 0 0 1 1.8-.46c.54 0 1.02.09 1.44.26.43.17.79.43 1.09.76.3.33.52.73.68 1.2a5 5 0 0 1 0 3.07c-.16.45-.38.86-.7 1.2-.3.36-.69.63-1.16.83a4.6 4.6 0 0 1-3.02.09 3.73 3.73 0 0 1-1.16-.62 3.14 3.14 0 0 1-1.18-2.42h1.84c.05.37.15.69.3.95.17.25.38.45.65.58a2 2 0 0 0 .92.2c.32 0 .59-.05.82-.16.23-.11.42-.27.57-.48.16-.22.27-.47.34-.75a3.62 3.62 0 0 0-.01-1.87 1.98 1.98 0 0 0-.39-.72c-.16-.2-.37-.36-.63-.47a2.13 2.13 0 0 0-.88-.17c-.45 0-.8.07-1.03.2-.24.13-.46.29-.66.48Zm11.74-4.16c0-.38.1-.72.28-1.04.19-.32.44-.57.75-.76a1.96 1.96 0 0 1 2.06 0c.3.19.55.44.73.76.19.32.28.66.28 1.04s-.1.73-.28 1.05a1.97 1.97 0 0 1-1.76 1.02 2.04 2.04 0 0 1-1.78-1.02 2.03 2.03 0 0 1-.28-1.05Zm1.06 0a1 1 0 0 0 1 1 .97.97 0 0 0 .98-1 1 1 0 0 0-.28-.72.93.93 0 0 0-.7-.3c-.27 0-.5.1-.7.3-.2.2-.3.43-.3.72Zm12.18 5.75h1.96a4.5 4.5 0 0 1-.63 1.99 3.72 3.72 0 0 1-1.5 1.37 5 5 0 0 1-2.33.5 4.15 4.15 0 0 1-3.34-1.45c-.4-.48-.7-1.04-.92-1.7-.21-.66-.31-1.4-.31-2.22v-.95c0-.81.1-1.55.32-2.22a4.9 4.9 0 0 1 .93-1.69c.4-.47.9-.84 1.46-1.09a4.78 4.78 0 0 1 1.93-.37c.91 0 1.68.17 2.3.5.63.33 1.11.8 1.45 1.38.35.59.57 1.26.64 2.02h-1.95c-.05-.48-.17-.9-.34-1.25a1.77 1.77 0 0 0-.77-.8 2.73 2.73 0 0 0-1.33-.28c-.44 0-.84.08-1.17.25a2.2 2.2 0 0 0-.83.73c-.23.33-.4.72-.51 1.2-.11.47-.17 1-.17 1.6v.97c0 .57.05 1.1.15 1.56.1.47.26.86.47 1.2.22.33.49.59.81.77.33.18.72.27 1.18.27.56 0 1.01-.08 1.35-.26.35-.18.62-.44.8-.78.18-.34.3-.76.35-1.25Z" fill="#000" fill-opacity=".87"/><rect x="161.83" y="18" width="32" height="32" rx="16" fill="#305680"/><path d="m175.83 28-1.4 1.41L179 34l-4.58 4.59 1.41 1.41 6-6-6-6Z" fill="#fff"/></g><defs><filter id="a" x=".83" y=".5" width="213" height="75" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset dy="4"/><feGaussianBlur stdDeviation="4"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5669_166701"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_5669_166701" result="shape"/></filter></defs></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
ui-ngx/src/assets/widget/value-stepper/volume.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

Loading…
Cancel
Save