diff --git a/application/src/main/data/json/system/widget_bundles/buttons.json b/application/src/main/data/json/system/widget_bundles/buttons.json index a48fba892b..00036a0cba 100644 --- a/application/src/main/data/json/system/widget_bundles/buttons.json +++ b/application/src/main/data/json/system/widget_bundles/buttons.json @@ -9,6 +9,7 @@ }, "widgetTypeFqns": [ "action_button", - "command_button" + "command_button", + "power_button" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 5dce630e64..568a144874 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -9,6 +9,8 @@ }, "widgetTypeFqns": [ "single_switch", + "command_button", + "power_button", "control_widgets.switch_control", "control_widgets.slide_toggle_control", "control_widgets.round_switch", diff --git a/application/src/main/data/json/system/widget_types/power_button.json b/application/src/main/data/json/system/widget_types/power_button.json new file mode 100644 index 0000000000..2f825c9e74 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/power_button.json @@ -0,0 +1,36 @@ +{ + "fqn": "power_button", + "name": "Power button", + "deprecated": false, + "image": "tb-image:cG93ZXItYnV0dG9uLnN2Zw==:IlBvd2VyIGJ1dHRvbiIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2lfNDU4OF84ODQyMCkiPgo8cGF0aCBkPSJNMTQ2LjUgODBDMTQ2LjUgMTA1LjEyOSAxMjUuNjgxIDEyNS41IDEwMCAxMjUuNUM3NC4zMTg4IDEyNS41IDUzLjUgMTA1LjEyOSA1My41IDgwQzUzLjUgNTQuODcxIDc0LjMxODggMzQuNSAxMDAgMzQuNUMxMjUuNjgxIDM0LjUgMTQ2LjUgNTQuODcxIDE0Ni41IDgwWiIgZmlsbD0iIzNGNTJERCIvPgo8L2c+CjxwYXRoIGQ9Ik0xNDUuNSA4MEMxNDUuNSAxMDQuNTU2IDEyNS4xNSAxMjQuNSAxMDAgMTI0LjVDNzQuODUwNCAxMjQuNSA1NC41IDEwNC41NTYgNTQuNSA4MEM1NC41IDU1LjQ0MzcgNzQuODUwNCAzNS41IDEwMCAzNS41QzEyNS4xNSAzNS41IDE0NS41IDU1LjQ0MzcgMTQ1LjUgODBaIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTg4Xzg4NDIwKSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAwIDEzNS41QzEzMS4yMDQgMTM1LjUgMTU2LjUgMTEwLjY1MiAxNTYuNSA4MEMxNTYuNSA0OS4zNDgyIDEzMS4yMDQgMjQuNSAxMDAgMjQuNUM2OC43OTU5IDI0LjUgNDMuNSA0OS4zNDgyIDQzLjUgODBDNDMuNSAxMTAuNjUyIDY4Ljc5NTkgMTM1LjUgMTAwIDEzNS41Wk0xMDAgMTI1LjVDMTI1LjY4MSAxMjUuNSAxNDYuNSAxMDUuMTI5IDE0Ni41IDgwQzE0Ni41IDU0Ljg3MSAxMjUuNjgxIDM0LjUgMTAwIDM0LjVDNzQuMzE4OCAzNC41IDUzLjUgNTQuODcxIDUzLjUgODBDNTMuNSAxMDUuMTI5IDc0LjMxODggMTI1LjUgMTAwIDEyNS41WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzQ1ODhfODg0MjApIi8+CjwvZz4KPHBhdGggZD0iTTk4LjM1NzQgNzkuMTg1NVY4MC4xNzM4Qzk4LjM1NzQgODEuMzQ4MyA5OC4yMTA2IDgyLjQwMSA5Ny45MTcgODMuMzMyQzk3LjYyMzQgODQuMjYzIDk3LjIwMDggODUuMDU0NCA5Ni42NDk0IDg1LjcwNjFDOTYuMDk4IDg2LjM1NzcgOTUuNDM1NSA4Ni44NTU1IDk0LjY2MjEgODcuMTk5MkM5My44OTU4IDg3LjU0MyA5My4wMzY1IDg3LjcxNDggOTIuMDg0IDg3LjcxNDhDOTEuMTYwMiA4Ny43MTQ4IDkwLjMxMTUgODcuNTQzIDg5LjUzODEgODcuMTk5MkM4OC43NzE4IDg2Ljg1NTUgODguMTA1OCA4Ni4zNTc3IDg3LjU0IDg1LjcwNjFDODYuOTgxNCA4NS4wNTQ0IDg2LjU0ODIgODQuMjYzIDg2LjI0MDIgODMuMzMyQzg1LjkzMjMgODIuNDAxIDg1Ljc3ODMgODEuMzQ4MyA4NS43NzgzIDgwLjE3MzhWNzkuMTg1NUM4NS43NzgzIDc4LjAxMTEgODUuOTI4NyA3Ni45NjE5IDg2LjIyOTUgNzYuMDM4MUM4Ni41Mzc0IDc1LjEwNzEgODYuOTcwNyA3NC4zMTU4IDg3LjUyOTMgNzMuNjY0MUM4OC4wODc5IDczLjAwNTIgODguNzUwMyA3Mi41MDM5IDg5LjUxNjYgNzIuMTYwMkM5MC4yOSA3MS44MTY0IDkxLjEzODcgNzEuNjQ0NSA5Mi4wNjI1IDcxLjY0NDVDOTMuMDE1IDcxLjY0NDUgOTMuODc0MyA3MS44MTY0IDk0LjY0MDYgNzIuMTYwMkM5NS40MTQxIDcyLjUwMzkgOTYuMDc2NSA3My4wMDUyIDk2LjYyNzkgNzMuNjY0MUM5Ny4xODY1IDc0LjMxNTggOTcuNjEyNiA3NS4xMDcxIDk3LjkwNjIgNzYuMDM4MUM5OC4yMDcgNzYuOTYxOSA5OC4zNTc0IDc4LjAxMTEgOTguMzU3NCA3OS4xODU1Wk05Ni4zMDU3IDgwLjE3MzhWNzkuMTY0MUM5Ni4zMDU3IDc4LjIzMzEgOTYuMjA5IDc3LjQwOTUgOTYuMDE1NiA3Ni42OTM0Qzk1LjgyOTQgNzUuOTc3MiA5NS41NTM3IDc1LjM3NTcgOTUuMTg4NSA3NC44ODg3Qzk0LjgyMzIgNzQuNDAxNyA5NC4zNzU3IDc0LjAzMjkgOTMuODQ1NyA3My43ODIyQzkzLjMyMjkgNzMuNTMxNiA5Mi43Mjg1IDczLjQwNjIgOTIuMDYyNSA3My40MDYyQzkxLjQxOCA3My40MDYyIDkwLjgzNDMgNzMuNTMxNiA5MC4zMTE1IDczLjc4MjJDODkuNzk1OSA3NC4wMzI5IDg5LjM1MTkgNzQuNDAxNyA4OC45Nzk1IDc0Ljg4ODdDODguNjE0MyA3NS4zNzU3IDg4LjMzMTQgNzUuOTc3MiA4OC4xMzA5IDc2LjY5MzRDODcuOTMwMyA3Ny40MDk1IDg3LjgzMDEgNzguMjMzMSA4Ny44MzAxIDc5LjE2NDFWODAuMTczOEM4Ny44MzAxIDgxLjExMiA4Ny45MzAzIDgxLjk0MjcgODguMTMwOSA4Mi42NjZDODguMzMxNCA4My4zODIyIDg4LjYxNzggODMuOTg3MyA4OC45OTAyIDg0LjQ4MTRDODkuMzY5OCA4NC45Njg0IDg5LjgxNzQgODUuMzM3MiA5MC4zMzMgODUuNTg3OUM5MC44NTU4IDg1LjgzODUgOTEuNDM5NSA4NS45NjM5IDkyLjA4NCA4NS45NjM5QzkyLjc1NzIgODUuOTYzOSA5My4zNTUxIDg1LjgzODUgOTMuODc3OSA4NS41ODc5Qzk0LjQwMDcgODUuMzM3MiA5NC44NDExIDg0Ljk2ODQgOTUuMTk5MiA4NC40ODE0Qzk1LjU2NDUgODMuOTg3MyA5NS44NDAyIDgzLjM4MjIgOTYuMDI2NCA4Mi42NjZDOTYuMjEyNiA4MS45NDI3IDk2LjMwNTcgODEuMTEyIDk2LjMwNTcgODAuMTczOFpNMTEzLjQ5MyA3MS44NTk0Vjg3LjVIMTExLjQwOUwxMDMuNTM1IDc1LjQzNjVWODcuNUgxMDEuNDYyVjcxLjg1OTRIMTAzLjUzNUwxMTEuNDQxIDgzLjk1NTFWNzEuODU5NEgxMTMuNDkzWiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfaV80NTg4Xzg4NDIwIiB4PSI0OC41IiB5PSIzNC41IiB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeD0iLTUiIGR5PSI1Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJzaGFwZSIgcmVzdWx0PSJlZmZlY3QxX2lubmVyU2hhZG93XzQ1ODhfODg0MjAiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTg4Xzg4NDIwIiB4PSIzNS41IiB5PSIyMC41IiB3aWR0aD0iMTI5IiBoZWlnaHQ9IjEyNyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSI0Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDU4OF84ODQyMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTg4Xzg4NDIwIiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNDU4OF84ODQyMCIgeDE9IjY1Ljg5NjQiIHkxPSIxMjQuNSIgeDI9IjEyOS41MTkiIHkyPSIzMy45MjQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDQ0NDQ0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=", + "description": "Sends the command to the device or updates attribute/time-series when the user pushes the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when power on/off states.", + "descriptor": { + "type": "rpc", + "sizeX": 3.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-power-button-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-power-button-basic-config", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"EXECUTE_RPC\",\"defaultValue\":false,\"executeRpc\":{\"method\":\"getState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"onUpdateState\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SHARED_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"CONSTANT\",\"constantValue\":true,\"valueToDataFunction\":\"/* Convert input boolean value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"offUpdateState\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SHARED_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"CONSTANT\",\"constantValue\":false,\"valueToDataFunction\":\"/* Convert input boolean value to RPC parameters or attribute/time-series value */ \\n return value;\"}},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"layout\":\"default\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorOff\":\"#A2A2A2\",\"backgroundColorOff\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Power\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":null}" + }, + "tags": [ + "command", + "downlink", + "device configuration", + "device control", + "invocation", + "remote method", + "remote function", + "interface", + "subroutine call", + "inter-process communication", + "server request", + "power" + ] +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index bb31daf7bb..c268be7bcd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -100,6 +100,9 @@ import { import { CommandButtonBasicConfigComponent } from '@home/components/widget/config/basic/button/command-button-basic-config.component'; +import { + PowerButtonBasicConfigComponent +} from '@home/components/widget/config/basic/button/power-button-basic-config.component'; @NgModule({ declarations: [ @@ -131,7 +134,8 @@ import { BarChartWithLabelsBasicConfigComponent, SingleSwitchBasicConfigComponent, ActionButtonBasicConfigComponent, - CommandButtonBasicConfigComponent + CommandButtonBasicConfigComponent, + PowerButtonBasicConfigComponent ], imports: [ CommonModule, @@ -167,7 +171,8 @@ import { BarChartWithLabelsBasicConfigComponent, SingleSwitchBasicConfigComponent, ActionButtonBasicConfigComponent, - CommandButtonBasicConfigComponent + CommandButtonBasicConfigComponent, + PowerButtonBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -197,5 +202,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + + + widgets.power-button.behavior + + widgets.rpc-state.initial-state + + + + widgets.power-button.power-on + + + + widgets.power-button.power-off + + + + widgets.rpc-state.disabled-state + + + + + widget-config.appearance + + + {{ powerButtonLayoutTranslationMap.get(layout) | translate }} + + + + + {{ 'widget-config.title' | translate }} + + + + + + + + + + + + + + {{ 'widget-config.card-icon' | translate }} + + + + + + + + + + + + + + {{ 'widgets.power-button.power-on-colors' | translate }} + + + widgets.power-button.main + + + + + + widgets.power-button.background + + + + + + + {{ 'widgets.power-button.power-off-colors' | translate }} + + + widgets.power-button.main + + + + + + widgets.power-button.background + + + + + + + {{ 'widgets.power-button.disabled-colors' | translate }} + + + widgets.power-button.main + + + + + + widgets.power-button.background + + + + + + + + widget-config.card-appearance + + {{ 'widgets.background.background' | translate }} + + + + + widget-config.show-card-buttons + + {{ 'fullscreen.fullscreen' | translate }} + + + + {{ 'widget-config.card-border-radius' | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts new file mode 100644 index 0000000000..174ded9ed3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts @@ -0,0 +1,195 @@ +/// +/// 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 { isUndefined } from '@core/utils'; +import { ValueType } from '@shared/models/constants'; +import { + powerButtonDefaultSettings, + powerButtonLayoutImages, + powerButtonLayouts, + powerButtonLayoutTranslations, + PowerButtonWidgetSettings +} from '@home/components/widget/lib/rpc/power-button-widget.models'; +import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-power-button-basic-config', + templateUrl: './power-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + return this.powerButtonWidgetConfigForm.get('targetDevice').value; + } + + powerButtonLayouts = powerButtonLayouts; + + powerButtonLayoutTranslationMap = powerButtonLayoutTranslations; + powerButtonLayoutImageMap = powerButtonLayoutImages; + + valueType = ValueType; + + powerButtonWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.powerButtonWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: PowerButtonWidgetSettings = {...powerButtonDefaultSettings, ...(configData.config.settings || {})}; + const iconSize = resolveCssSize(configData.config.iconSize); + this.powerButtonWidgetConfigForm = this.fb.group({ + targetDevice: [configData.config.targetDevice, []], + + initialState: [settings.initialState, []], + onUpdateState: [settings.onUpdateState, []], + offUpdateState: [settings.offUpdateState, []], + disabledState: [settings.disabledState, []], + + layout: [settings.layout, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + + showIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + icon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + mainColorOn: [settings.mainColorOn, []], + backgroundColorOn: [settings.backgroundColorOn, []], + + mainColorOff: [settings.mainColorOff, []], + backgroundColorOff: [settings.backgroundColorOff, []], + + mainColorDisabled: [settings.mainColorDisabled, []], + backgroundColorDisabled: [settings.backgroundColorDisabled, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.targetDevice = config.targetDevice; + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.icon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.initialState = config.initialState; + this.widgetConfig.config.settings.onUpdateState = config.onUpdateState; + this.widgetConfig.config.settings.offUpdateState = config.offUpdateState; + this.widgetConfig.config.settings.disabledState = config.disabledState; + + this.widgetConfig.config.settings.layout = config.layout; + + this.widgetConfig.config.settings.mainColorOn = config.mainColorOn; + this.widgetConfig.config.settings.backgroundColorOn = config.backgroundColorOn; + + this.widgetConfig.config.settings.mainColorOff = config.mainColorOff; + this.widgetConfig.config.settings.backgroundColorOff = config.backgroundColorOff; + + this.widgetConfig.config.settings.mainColorDisabled = config.mainColorDisabled; + this.widgetConfig.config.settings.backgroundColorDisabled = config.backgroundColorDisabled; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.powerButtonWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.powerButtonWidgetConfigForm.get('showIcon').value; + if (showTitle) { + this.powerButtonWidgetConfigForm.get('title').enable(); + this.powerButtonWidgetConfigForm.get('titleFont').enable(); + this.powerButtonWidgetConfigForm.get('titleColor').enable(); + this.powerButtonWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.powerButtonWidgetConfigForm.get('iconSize').enable(); + this.powerButtonWidgetConfigForm.get('iconSizeUnit').enable(); + this.powerButtonWidgetConfigForm.get('icon').enable(); + this.powerButtonWidgetConfigForm.get('iconColor').enable(); + } else { + this.powerButtonWidgetConfigForm.get('iconSize').disable(); + this.powerButtonWidgetConfigForm.get('iconSizeUnit').disable(); + this.powerButtonWidgetConfigForm.get('icon').disable(); + this.powerButtonWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.powerButtonWidgetConfigForm.get('title').disable(); + this.powerButtonWidgetConfigForm.get('titleFont').disable(); + this.powerButtonWidgetConfigForm.get('titleColor').disable(); + this.powerButtonWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.powerButtonWidgetConfigForm.get('iconSize').disable(); + this.powerButtonWidgetConfigForm.get('iconSizeUnit').disable(); + this.powerButtonWidgetConfigForm.get('icon').disable(); + this.powerButtonWidgetConfigForm.get('iconColor').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'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts index 26dc76866a..dc56a1e992 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts @@ -91,6 +91,8 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A ngOnDestroy() { this.valueActions.forEach(v => v.destroy()); + this.loadingSubject.complete(); + this.loadingSubject.unsubscribe(); } public onInit() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html new file mode 100644 index 0000000000..93e394ac5e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + close + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.scss new file mode 100644 index 0000000000..e0192630f9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.scss @@ -0,0 +1,64 @@ +/** + * 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-power-button-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px 24px 24px 24px; + > div:not(.tb-power-button-overlay) { + z-index: 1; + } + .tb-power-button-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-power-button-content { + flex: 1; + min-width: 0; + min-height: 0; + .tb-power-button-shape { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + svg { + .tb-small-shadow { + filter: drop-shadow(0px 4px 6px rgba(0, 0, 0, 0.08)); + } + .tb-shadow { + filter: drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.1)); + } + } + &.tb-power-button-pointer { + svg { + .tb-hover-circle { + cursor: pointer; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts new file mode 100644 index 0000000000..e68a2e4baa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts @@ -0,0 +1,200 @@ +/// +/// 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, + OnDestroy, + OnInit, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; +import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; +import { Observable } from 'rxjs'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ValueType } from '@shared/models/constants'; +import { + powerButtonDefaultSettings, + PowerButtonShape, + powerButtonShapeSize, + PowerButtonWidgetSettings +} from '@home/components/widget/lib/rpc/power-button-widget.models'; +import { SVG, Svg } from '@svgdotjs/svg.js'; + +@Component({ + selector: 'tb-power-button-widget', + templateUrl: './power-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './power-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class PowerButtonWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('powerButtonShape', {static: false}) + powerButtonShape: ElementRef; + + settings: PowerButtonWidgetSettings; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + value = false; + disabled = false; + + private shapeResize$: ResizeObserver; + private drawSvgShapePending = false; + private svgShape: Svg; + private powerButtonSvgShape: PowerButtonShape; + private disabledState = false; + + private onValueSetter: ValueSetter; + private offValueSetter: ValueSetter; + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + private renderer: Renderer2, + protected cd: ChangeDetectorRef) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...powerButtonDefaultSettings, ...this.ctx.settings}; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + const getInitialStateSettings = + {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')}; + this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, { + 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.onDisabled(value) + }); + + const onUpdateStateSettings = {...this.settings.onUpdateState, + actionLabel: this.ctx.translate.instant('widgets.power-button.power-on')}; + this.onValueSetter = this.createValueSetter(onUpdateStateSettings); + + const offUpdateStateSettings = {...this.settings.offUpdateState, + actionLabel: this.ctx.translate.instant('widgets.power-button.power-off')}; + this.offValueSetter = this.createValueSetter(offUpdateStateSettings); + + this.loading$.subscribe((loading) => { + this.updateDisabledState(loading || this.disabled); + }); + } + + ngAfterViewInit(): void { + if (this.drawSvgShapePending) { + this.drawSvg(); + } + super.ngAfterViewInit(); + } + + ngOnDestroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + if (this.powerButtonShape) { + this.drawSvg(); + } else { + this.drawSvgShapePending = true; + } + this.cd.detectChanges(); + } + + private onValue(value: boolean): void { + const newValue = !!value; + if (this.value !== newValue) { + this.value = newValue; + this.powerButtonSvgShape?.setValue(this.value); + } + } + + private onDisabled(value: boolean): void { + const newDisabled = !!value; + if (this.disabled !== newDisabled) { + this.disabled = newDisabled; + this.updateDisabledState(this.disabled); + } + } + + private onClick() { + if (!this.ctx.isEdit && !this.ctx.isPreview && !this.disabledState) { + this.onValue(!this.value); + const targetValue = this.value; + const targetSetter = targetValue ? this.onValueSetter : this.offValueSetter; + this.powerButtonSvgShape?.setPressed(true); + this.updateValue(targetSetter, targetValue, { + next: () => { + this.powerButtonSvgShape?.setPressed(false); + this.onValue(targetValue); + }, + error: () => { + this.powerButtonSvgShape?.setPressed(false); + this.onValue(!targetValue); + } + }); + } + } + + private drawSvg() { + this.svgShape = SVG().addTo(this.powerButtonShape.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize); + this.renderer.setStyle(this.svgShape.node, 'overflow', 'visible'); + this.renderer.setStyle(this.svgShape.node, 'user-select', 'none'); + + this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape, + this.settings, this.value, this.disabledState, () => this.onClick()); + + this.shapeResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.shapeResize$.observe(this.powerButtonShape.nativeElement); + this.onResize(); + } + + private updateDisabledState(disabled: boolean) { + this.disabledState = disabled; + this.powerButtonSvgShape?.setDisabled(this.disabledState); + } + + private onResize() { + const shapeWidth = this.powerButtonShape.nativeElement.getBoundingClientRect().width; + const shapeHeight = this.powerButtonShape.nativeElement.getBoundingClientRect().height; + const size = Math.min(shapeWidth, shapeHeight); + const scale = size / powerButtonShapeSize; + this.renderer.setStyle(this.svgShape.node, 'transform', `scale(${scale})`); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts new file mode 100644 index 0000000000..b1372a9660 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -0,0 +1,912 @@ +/// +/// 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 { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { + DataToValueType, + GetValueAction, + GetValueSettings, + SetValueAction, + SetValueSettings, + ValueToDataType +} from '@shared/models/action-widget-settings.models'; +import { Circle, Element, G, Gradient, Runner, Stop, Svg, Text, Timeline } from '@svgdotjs/svg.js'; +import tinycolor from 'tinycolor2'; +import { WidgetContext } from '@home/models/widget-component.models'; + +export enum PowerButtonLayout { + default = 'default', + simplified = 'simplified', + outlined = 'outlined', + default_volume = 'default_volume', + simplified_volume = 'simplified_volume', + outlined_volume = 'outlined_volume' +} + +export const powerButtonLayouts = Object.keys(PowerButtonLayout) as PowerButtonLayout[]; + +export const powerButtonLayoutTranslations = new Map( + [ + [PowerButtonLayout.default, 'widgets.power-button.layout-default'], + [PowerButtonLayout.simplified, 'widgets.power-button.layout-simplified'], + [PowerButtonLayout.outlined, 'widgets.power-button.layout-outlined'], + [PowerButtonLayout.default_volume, 'widgets.power-button.layout-default-volume'], + [PowerButtonLayout.simplified_volume, 'widgets.power-button.layout-simplified-volume'], + [PowerButtonLayout.outlined_volume, 'widgets.power-button.layout-outlined-volume'] + ] +); + +export const powerButtonLayoutImages = new Map( + [ + [PowerButtonLayout.default, 'assets/widget/power-button/default-layout.svg'], + [PowerButtonLayout.simplified, 'assets/widget/power-button/simplified-layout.svg'], + [PowerButtonLayout.outlined, 'assets/widget/power-button/outlined-layout.svg'], + [PowerButtonLayout.default_volume, 'assets/widget/power-button/default-volume-layout.svg'], + [PowerButtonLayout.simplified_volume, 'assets/widget/power-button/simplified-volume-layout.svg'], + [PowerButtonLayout.outlined_volume, 'assets/widget/power-button/outlined-volume-layout.svg'] + ] +); + +export interface PowerButtonWidgetSettings { + initialState: GetValueSettings; + disabledState: GetValueSettings; + onUpdateState: SetValueSettings; + offUpdateState: SetValueSettings; + layout: PowerButtonLayout; + mainColorOn: string; + backgroundColorOn: string; + mainColorOff: string; + backgroundColorOff: string; + mainColorDisabled: string; + backgroundColorDisabled: string; + background: BackgroundSettings; +} + +export const powerButtonDefaultSettings: PowerButtonWidgetSettings = { + initialState: { + action: GetValueAction.EXECUTE_RPC, + defaultValue: false, + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + disabledState: { + action: GetValueAction.DO_NOTHING, + defaultValue: false, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + onUpdateState: { + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SHARED_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: ValueToDataType.CONSTANT, + constantValue: true, + valueToDataFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' + } + }, + offUpdateState: { + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SHARED_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: ValueToDataType.CONSTANT, + constantValue: false, + valueToDataFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */ \n return value;' + } + }, + layout: PowerButtonLayout.default, + mainColorOn: '#3F52DD', + backgroundColorOn: '#FFFFFF', + mainColorOff: '#A2A2A2', + backgroundColorOff: '#FFFFFF', + mainColorDisabled: 'rgba(0,0,0,0.12)', + backgroundColorDisabled: '#FFFFFF', + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; + +interface PowerButtonColor { + hex: string; + opacity: number; +} + +type PowerButtonState = 'on' | 'off' | 'disabled'; + +interface PowerButtonColorState { + mainColor: PowerButtonColor; + backgroundColor: PowerButtonColor; +} + +type PowerButtonShapeColors = Record; + +const createPowerButtonShapeColors = (settings: PowerButtonWidgetSettings): PowerButtonShapeColors => { + const mainColorOn = tinycolor(settings.mainColorOn); + const backgroundColorOn = tinycolor(settings.backgroundColorOn); + const mainColorOff = tinycolor(settings.mainColorOff); + const backgroundColorOff = tinycolor(settings.backgroundColorOff); + const mainColorDisabled = tinycolor(settings.mainColorDisabled); + const backgroundColorDisabled = tinycolor(settings.backgroundColorDisabled); + return { + on: { + mainColor: {hex: mainColorOn.toHexString(), opacity: mainColorOn.getAlpha()}, + backgroundColor: {hex: backgroundColorOn.toHexString(), opacity: backgroundColorOn.getAlpha()}, + }, + off: { + mainColor: {hex: mainColorOff.toHexString(), opacity: mainColorOff.getAlpha()}, + backgroundColor: {hex: backgroundColorOff.toHexString(), opacity: backgroundColorOff.getAlpha()}, + }, + disabled: { + mainColor: {hex: mainColorDisabled.toHexString(), opacity: mainColorDisabled.getAlpha()}, + backgroundColor: {hex: backgroundColorDisabled.toHexString(), opacity: backgroundColorDisabled.getAlpha()}, + } + }; +}; + +export const powerButtonShapeSize = 110; +const cx = powerButtonShapeSize / 2; +const cy = powerButtonShapeSize / 2; + +export abstract class PowerButtonShape { + + static fromSettings(ctx: WidgetContext, + svgShape: Svg, + settings: PowerButtonWidgetSettings, + value: boolean, + disabled: boolean, + onClick: () => void): PowerButtonShape { + switch (settings.layout) { + case PowerButtonLayout.default: + return new DefaultPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.simplified: + return new SimplifiedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.outlined: + return new OutlinedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.default_volume: + return new DefaultVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.simplified_volume: + return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.outlined_volume: + return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + } + } + + protected readonly colors: PowerButtonShapeColors; + protected readonly onLabel: string; + protected readonly offLabel: string; + + protected backgroundShape: Circle; + protected hoverShape: Circle; + protected hovered = false; + protected pressed = false; + protected forcePressed = false; + + protected constructor(protected widgetContext: WidgetContext, + protected svgShape: Svg, + protected settings: PowerButtonWidgetSettings, + protected value: boolean, + protected disabled: boolean, + protected onClick: () => void) { + this.colors = createPowerButtonShapeColors(this.settings); + 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 setValue(value: boolean) { + if (this.value !== value) { + this.value = value; + this._drawState(); + } + } + + public setDisabled(disabled: boolean) { + if (this.disabled !== disabled) { + this.disabled = disabled; + this._drawState(); + } + } + + public setPressed(pressed: boolean) { + if (this.forcePressed !== pressed) { + this.forcePressed = pressed; + if (this.forcePressed && !this.pressed) { + this.onPressStart(); + } else if (!this.forcePressed && !this.pressed) { + this.onPressEnd(); + } + } + } + + private _drawShape() { + + this.backgroundShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + + this.drawShape(); + + this.hoverShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy).addClass('tb-hover-circle') + .fill({color: '#000000', opacity: 0}); + this.hoverShape.on('mouseover', () => { + this.hovered = true; + if (!this.disabled) { + this.hoverShape.timeline().finish(); + this.hoverShape.animate(200).attr({'fill-opacity': 0.06}); + } + }); + this.hoverShape.on('mouseout', () => { + this.hovered = false; + this.hoverShape.timeline().finish(); + this.hoverShape.animate(200).attr({'fill-opacity': 0}); + this._cancelPressed(); + }); + this.hoverShape.on('touchmove', (event: TouchEvent) => { + const touch = event.touches[0]; + const element = document.elementFromPoint(touch.pageX,touch.pageY); + if (this.hoverShape.node !== element) { + this._cancelPressed(); + } + }); + this.hoverShape.on('touchcancel', () => { + this._cancelPressed(); + }); + this.hoverShape.on('mousedown touchstart', (event: Event) => { + if (event.type === 'mousedown') { + if ((event as MouseEvent).button !== 0) { + return; + } + } + if (!this.disabled && !this.pressed) { + this.pressed = true; + if (!this.forcePressed) { + this.onPressStart(); + } + } + }); + this.hoverShape.on('mouseup touchend touchcancel', () => { + if (this.pressed && !this.disabled) { + this.onClick(); + } + this._cancelPressed(); + }); + this._drawState(); + } + + private _cancelPressed() { + if (this.pressed) { + this.pressed = false; + if (!this.forcePressed) { + this.onPressEnd(); + } + } + } + + private _drawState() { + let colorState: PowerButtonColorState; + if (this.disabled) { + colorState = this.colors.disabled; + } else { + colorState = this.value ? this.colors.on : this.colors.off; + } + this.drawBackgroundState(colorState.backgroundColor); + this.drawColorState(colorState.mainColor); + if (this.value) { + this.drawOn(); + } else { + this.drawOff(); + } + if (this.disabled) { + this.hoverShape.timeline().finish(); + this.hoverShape.attr({'fill-opacity': 0}); + } else if (this.hovered) { + this.hoverShape.timeline().finish(); + this.hoverShape.animate(200).attr({'fill-opacity': 0.06}); + } + } + + private drawBackgroundState(backgroundColor: PowerButtonColor) { + this.backgroundShape.attr({ fill: backgroundColor.hex, 'fill-opacity': backgroundColor.opacity}); + } + + protected drawShape() {} + + protected drawColorState(_mainColor: PowerButtonColor) {} + + protected drawOff() {} + + protected drawOn() {} + + protected onPressStart() {} + + protected onPressEnd() {} + + protected createMask(shape: Element, maskElements: Element[]) { + const mask = + this.svgShape.mask().add(this.svgShape.rect().width('100%').height('100%').fill('#fff')); + maskElements.forEach(e => { + mask.add(e.fill('#000').attr({'fill-opacity': 1})); + }); + shape.maskWith(mask); + } + + protected createPressedShadow(diameter: number, rightElseLeft = true): Gradient { + const innerShadowGradient = this.svgShape.gradient('radial', (add) => { + add.stop(0.7, '#000000', 0); + add.stop(1, '#000000', 0.4); + }).attr({ cx: rightElseLeft ? '45%' : '55%', cy: '55%', r: '100%'}); + this.svgShape.circle(diameter).center(cx, cy) + .fill(innerShadowGradient).stroke({width: 0}); + return innerShadowGradient; + } + + protected pressedAnimation(element: Element): Runner { + return element.animate(200, 0, 'now'); + } + + protected createOnLabel(fontWeight = '500'): Text { + return this.createLabel(this.onLabel, fontWeight); + } + + protected createOffLabel(fontWeight = '500'): Text { + return this.createLabel(this.offLabel, fontWeight); + } + + private createLabel(text: string, fontWeight = '500'): Text { + return this.svgShape.text(text).font({ + family: 'Roboto', + weight: fontWeight, + style: 'normal', + size: '22px' + }).attr({x: '50%', y: '50%', 'text-anchor': 'middle', 'dominant-baseline': 'middle'}); + } + +} + +class DefaultPowerButtonShape extends PowerButtonShape { + + private outerBorder: Circle; + private outerBorderMask: Circle; + private offLabelShape: Text; + private onCircleShape: Circle; + private onLabelShape: Text; + private pressedShadow: Gradient; + private pressedTimeline: Timeline; + private centerGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + 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.pressedShadow = this.createPressedShadow(powerButtonShapeSize - 20); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + 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.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.outerBorderMask.radius((powerButtonShapeSize - 20)/2); + this.onCircleShape.hide(); + this.centerGroup.show(); + } + + protected drawOn() { + this.outerBorderMask.radius((powerButtonShapeSize - 2)/2); + this.centerGroup.hide(); + this.onCircleShape.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + this.pressedAnimation(this.centerGroup).transform({scale: pressedScale}); + this.pressedAnimation(this.onLabelShape).transform({scale: pressedScale}); + this.pressedAnimation(this.pressedShadow).attr({r: '62%'}); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + this.pressedAnimation(this.centerGroup).transform({scale: 1}); + this.pressedAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedAnimation(this.pressedShadow).attr({r: '100%'}); + } + +} + +class SimplifiedPowerButtonShape extends PowerButtonShape { + + private outerBorder: Circle; + private outerBorderMask: Circle; + private onCircleShape: Circle; + private offLabelShape: Text; + private onLabelShape: Text; + private pressedShadow: Gradient; + private pressedTimeline: Timeline; + private centerGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + 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.onCircleShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy); + this.onLabelShape = this.createOnLabel(); + this.createMask(this.onCircleShape, [this.onLabelShape]); + this.pressedShadow = this.createPressedShadow(powerButtonShapeSize - 4); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + 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}); + } + + protected drawOff() { + this.onCircleShape.hide(); + this.outerBorder.show(); + this.centerGroup.show(); + } + + protected drawOn() { + this.centerGroup.hide(); + this.outerBorder.hide(); + this.onCircleShape.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + this.pressedAnimation(this.centerGroup).transform({scale: pressedScale}); + this.pressedAnimation(this.onLabelShape).transform({scale: pressedScale}); + this.pressedAnimation(this.pressedShadow).attr({r: '62%'}); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + this.pressedAnimation(this.centerGroup).transform({scale: 1}); + this.pressedAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedAnimation(this.pressedShadow).attr({r: '100%'}); + } +} + +class OutlinedPowerButtonShape extends PowerButtonShape { + private outerBorder: Circle; + private outerBorderMask: Circle; + private innerBorder: Circle; + private innerBorderMask: Circle; + private offLabelShape: Text; + private onCircleShape: Circle; + private onLabelShape: Text; + private pressedShadow: Gradient; + private pressedTimeline: Timeline; + private centerGroup: G; + private onCenterGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 2).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + 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.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.pressedShadow = this.createPressedShadow(powerButtonShapeSize - 24); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onCenterGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + 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.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.onCenterGroup.hide(); + this.centerGroup.show(); + } + + protected drawOn() { + this.centerGroup.hide(); + this.onCenterGroup.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + this.pressedAnimation(this.centerGroup).transform({scale: pressedScale}); + this.pressedAnimation(this.onCenterGroup).transform({scale: 0.98}); + this.pressedAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98}); + this.pressedAnimation(this.pressedShadow).attr({r: '62%'}); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + this.pressedAnimation(this.centerGroup).transform({scale: 1}); + this.pressedAnimation(this.onCenterGroup).transform({scale: 1}); + this.pressedAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedAnimation(this.pressedShadow).attr({r: '100%'}); + } +} + +class DefaultVolumePowerButtonShape extends PowerButtonShape { + private outerBorder: Circle; + private outerBorderMask: Circle; + private outerBorderGradient: Gradient; + private innerBorder: Circle; + private innerBorderMask: Circle; + private innerBorderGradient: Gradient; + private innerShadow: Circle; + private innerShadowGradient: Gradient; + private innerShadowGradientStop: Stop; + private offLabelShape: Text; + private onCircleShape: Circle; + private onLabelShape: Text; + private pressedTimeline: Timeline; + private centerGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.outerBorderGradient = this.svgShape.gradient('linear', (add) => { + add.stop(0, '#CCCCCC', 1); + add.stop(1, '#FFFFFF', 1); + }).from(0.268, 0.92).to(0.832, 0.1188); + this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); + this.createMask(this.innerBorder, [this.innerBorderMask]); + this.innerBorderGradient = this.svgShape.gradient('linear', (add) => { + add.stop(0, '#CCCCCC', 1); + add.stop(1, '#FFFFFF', 1); + }).from(0.832, 0.1188).to(0.268, 0.92); + this.centerGroup = this.svgShape.group(); + this.offLabelShape = this.createOffLabel('400').addTo(this.centerGroup); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); + this.onLabelShape = this.createOnLabel('400'); + this.createMask(this.onCircleShape, [this.onLabelShape]); + this.innerShadow = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); + this.innerShadowGradient = this.svgShape.gradient('radial', (add) => { + add.stop(0.7, '#000000', 0); + this.innerShadowGradientStop = add.stop(1, '#000000', 0.2); + }).attr({ cx: '45%', cy: '55%', r: '65%'}); + this.innerShadow.fill(this.innerShadowGradient); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + this.innerShadowGradient.timeline(this.pressedTimeline); + this.innerShadowGradientStop.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor){ + if (this.disabled) { + this.backgroundShape.removeClass('tb-small-shadow'); + if (!this.forcePressed) { + this.innerShadow.hide(); + } + this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + this.backgroundShape.addClass('tb-small-shadow'); + this.innerShadow.show(); + this.outerBorder.fill(this.outerBorderGradient); + this.outerBorder.attr({ 'fill-opacity': 1 }); + this.innerBorder.fill(this.innerBorderGradient); + this.innerBorder.attr({ 'fill-opacity': 1 }); + } + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.onCircleShape.hide(); + this.centerGroup.show(); + this.innerBorder.show(); + } + + protected drawOn() { + if (this.disabled) { + this.innerBorder.hide(); + } else { + this.innerBorder.show(); + } + this.centerGroup.hide(); + this.onCircleShape.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + this.innerShadow.show(); + const pressedScale = 0.75; + this.pressedAnimation(this.centerGroup).transform({scale: pressedScale}); + this.pressedAnimation(this.onLabelShape).transform({scale: pressedScale}); + this.pressedAnimation(this.innerShadowGradient).attr({ r: '60%'}); + this.pressedAnimation(this.innerShadowGradientStop).update({ opacity: 0.4 }); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + this.pressedAnimation(this.centerGroup).transform({scale: 1}); + this.pressedAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedAnimation(this.innerShadowGradient).attr({ r: '65%'}).after(() => { + if (this.disabled) { + this.innerShadow.hide(); + } + }); + this.pressedAnimation(this.innerShadowGradientStop).update({ opacity: 0.2 }); + } + +} + +class SimplifiedVolumePowerButtonShape extends PowerButtonShape { + + private outerBorder: Circle; + private outerBorderMask: Circle; + private offLabelShape: Text; + private onLabelShape: Text; + private innerShadow: Circle; + private pressedShadow: Gradient; + private pressedTimeline: Timeline; + private centerGroup: G; + private onCenterGroup: G; + + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({color: '#FAFAFA', opacity: 1}).stroke({width: 0}); + 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.onCenterGroup = this.svgShape.group(); + this.onLabelShape = this.createOnLabel().addTo(this.onCenterGroup); + this.innerShadow = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy); + const innerShadowGradient = this.svgShape.gradient('radial', (add) => { + add.stop(0.7, '#000000', 0); + add.stop(1, '#000000', 0.2); + }).attr({ cx: '55%', cy: '55%', r: '65%'}); + this.innerShadow.fill(innerShadowGradient); + this.pressedShadow = this.createPressedShadow(powerButtonShapeSize - 4, false); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onCenterGroup.timeline(this.pressedTimeline); + this.pressedShadow.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor){ + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + if (!this.pressed) { + this.backgroundShape.addClass('tb-shadow'); + } + this.innerShadow.hide(); + this.onCenterGroup.hide(); + this.centerGroup.show(); + } + + protected drawOn() { + this.backgroundShape.removeClass('tb-shadow'); + this.centerGroup.hide(); + this.onCenterGroup.show(); + this.innerShadow.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + if (!this.value) { + this.backgroundShape.removeClass('tb-shadow'); + } + this.pressedAnimation(this.centerGroup).transform({scale: pressedScale}); + this.pressedAnimation(this.onCenterGroup).transform({scale: pressedScale}); + this.pressedAnimation(this.pressedShadow).attr({r: '62%'}); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + this.pressedAnimation(this.centerGroup).transform({scale: 1}); + this.pressedAnimation(this.onCenterGroup).transform({scale: 1}); + this.pressedAnimation(this.pressedShadow).attr({r: '100%'}).after(() => { + if (!this.value) { + this.backgroundShape.addClass('tb-shadow'); + } + }); + } +} + +class OutlinedVolumePowerButtonShape extends PowerButtonShape { + private outerBorder: Circle; + private outerBorderMask: Circle; + private outerBorderGradient: Gradient; + private innerBorder: Circle; + private innerBorderMask: Circle; + private offLabelShape: Text; + private onCircleShape: Circle; + private onLabelShape: Text; + private pressedShadow: Gradient; + private pressedTimeline: Timeline; + private centerGroup: G; + private onCenterGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.outerBorderGradient = this.svgShape.gradient('linear', (add) => { + add.stop(0, '#CCCCCC', 1); + add.stop(1, '#FFFFFF', 1); + }).from(0.268, 0.92).to(0.832, 0.1188); + this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy); + this.createMask(this.innerBorder, [this.innerBorderMask]); + this.centerGroup = this.svgShape.group(); + this.offLabelShape = this.createOffLabel('800').addTo(this.centerGroup); + this.onCenterGroup = this.svgShape.group(); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy) + .addTo(this.onCenterGroup); + this.onLabelShape = this.createOnLabel('800'); + this.createMask(this.onCircleShape, [this.onLabelShape]); + this.pressedShadow = this.createPressedShadow(powerButtonShapeSize - 30); + this.backgroundShape.addClass('tb-small-shadow'); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onCenterGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + this.pressedShadow.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor){ + if (this.disabled) { + this.outerBorder.attr({ fill: '#000000', 'fill-opacity': 0.03}); + } else { + this.outerBorder.fill(this.outerBorderGradient); + this.outerBorder.attr({ 'fill-opacity': 1 }); + } + this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.onCenterGroup.hide(); + this.centerGroup.show(); + this.innerBorder.show(); + } + + protected drawOn() { + this.innerBorder.hide(); + this.centerGroup.hide(); + this.onCenterGroup.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + this.pressedAnimation(this.centerGroup).transform({scale: pressedScale}); + this.pressedAnimation(this.onCenterGroup).transform({scale: 0.98}); + this.pressedAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98}); + this.pressedAnimation(this.pressedShadow).attr({r: '62%'}); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + this.pressedAnimation(this.centerGroup).transform({scale: 1}); + this.pressedAnimation(this.onCenterGroup).transform({scale: 1}); + this.pressedAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedAnimation(this.pressedShadow).attr({r: '100%'}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html new file mode 100644 index 0000000000..ced9bf9575 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html @@ -0,0 +1,142 @@ + + + + widgets.power-button.behavior + + widgets.rpc-state.initial-state + + + + widgets.power-button.power-on + + + + widgets.power-button.power-off + + + + widgets.rpc-state.disabled-state + + + + + widget-config.card-style + + + {{ powerButtonLayoutTranslationMap.get(layout) | translate }} + + + + {{ 'widgets.background.background' | translate }} + + + + + + widgets.power-button.button + + {{ 'widgets.power-button.power-on-colors' | translate }} + + + widgets.power-button.main + + + + + + widgets.power-button.background + + + + + + + {{ 'widgets.power-button.power-off-colors' | translate }} + + + widgets.power-button.main + + + + + + widgets.power-button.background + + + + + + + {{ 'widgets.power-button.disabled-colors' | translate }} + + + widgets.power-button.main + + + + + + widgets.power-button.background + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts new file mode 100644 index 0000000000..c1d9f98460 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts @@ -0,0 +1,88 @@ +/// +/// 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 } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ValueType } from '@shared/models/constants'; +import { + powerButtonDefaultSettings, + powerButtonLayoutImages, + powerButtonLayouts, + powerButtonLayoutTranslations +} from '@home/components/widget/lib/rpc/power-button-widget.models'; + +@Component({ + selector: 'tb-power-button-widget-settings', + templateUrl: './power-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class PowerButtonWidgetSettingsComponent extends WidgetSettingsComponent { + + get targetDevice(): TargetDevice { + return this.widgetConfig?.config?.targetDevice; + } + + get widgetType(): widgetType { + return this.widgetConfig?.widgetType; + } + + powerButtonLayouts = powerButtonLayouts; + + powerButtonLayoutTranslationMap = powerButtonLayoutTranslations; + powerButtonLayoutImageMap = powerButtonLayoutImages; + + valueType = ValueType; + + powerButtonWidgetSettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.powerButtonWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...powerButtonDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.powerButtonWidgetSettingsForm = this.fb.group({ + initialState: [settings.initialState, []], + onUpdateState: [settings.onUpdateState, []], + offUpdateState: [settings.offUpdateState, []], + disabledState: [settings.disabledState, []], + + layout: [settings.layout, []], + + mainColorOn: [settings.mainColorOn, []], + backgroundColorOn: [settings.backgroundColorOn, []], + + mainColorOff: [settings.mainColorOff, []], + backgroundColorOff: [settings.backgroundColorOff, []], + + mainColorDisabled: [settings.mainColorDisabled, []], + backgroundColorDisabled: [settings.backgroundColorDisabled, []], + + background: [settings.background, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 3f4a619c49..d3fe9dac3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -321,6 +321,9 @@ import { import { CommandButtonWidgetSettingsComponent } from '@home/components/widget/lib/settings/button/command-button-widget-settings.component'; +import { + PowerButtonWidgetSettingsComponent +} from '@home/components/widget/lib/settings/button/power-button-widget-settings.component'; @NgModule({ declarations: [ @@ -436,7 +439,8 @@ import { BarChartWithLabelsWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent, ActionButtonWidgetSettingsComponent, - CommandButtonWidgetSettingsComponent + CommandButtonWidgetSettingsComponent, + PowerButtonWidgetSettingsComponent ], imports: [ CommonModule, @@ -557,7 +561,8 @@ import { BarChartWithLabelsWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent, ActionButtonWidgetSettingsComponent, - CommandButtonWidgetSettingsComponent + CommandButtonWidgetSettingsComponent, + PowerButtonWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -645,5 +650,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/default-volume-layout.svg b/ui-ngx/src/assets/widget/power-button/default-volume-layout.svg new file mode 100644 index 0000000000..9a502cc8f0 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/default-volume-layout.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/outlined-layout.svg b/ui-ngx/src/assets/widget/power-button/outlined-layout.svg new file mode 100644 index 0000000000..9510d68721 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/outlined-layout.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/outlined-volume-layout.svg b/ui-ngx/src/assets/widget/power-button/outlined-volume-layout.svg new file mode 100644 index 0000000000..9e511892b3 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/outlined-volume-layout.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/simplified-layout.svg b/ui-ngx/src/assets/widget/power-button/simplified-layout.svg new file mode 100644 index 0000000000..eecb1e9cbe --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/simplified-layout.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/simplified-volume-layout.svg b/ui-ngx/src/assets/widget/power-button/simplified-volume-layout.svg new file mode 100644 index 0000000000..500d9a0c97 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/simplified-volume-layout.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +