Browse Source

UI: Implement battery level widget

pull/9294/head
Igor Kulikov 3 years ago
parent
commit
be850f506a
  1. 1
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 23
      application/src/main/data/json/system/widget_types/battery_level.json
  3. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  4. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  5. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  6. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  7. 129
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html
  8. 221
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts
  9. 2
      ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html
  10. 11
      ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts
  11. 44
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html
  12. 158
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss
  13. 293
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts
  14. 108
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts
  15. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
  16. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss
  17. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts
  18. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts
  19. 63
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html
  20. 104
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts
  21. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  22. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  23. 14
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  24. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  25. 4
      ui-ngx/src/assets/widget/battery-level/battery-shape-horizontal.svg
  26. 4
      ui-ngx/src/assets/widget/battery-level/battery-shape-vertical.svg
  27. 42
      ui-ngx/src/assets/widget/battery-level/horizontal-divided-layout.svg
  28. 44
      ui-ngx/src/assets/widget/battery-level/horizontal-solid-layout.svg
  29. 33
      ui-ngx/src/assets/widget/battery-level/vertical-divided-layout.svg
  30. 44
      ui-ngx/src/assets/widget/battery-level/vertical-solid-layout.svg

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

@ -11,6 +11,7 @@
"cards.value_card",
"cards.horizontal_value_card",
"cards.aggregated_value_card",
"battery_level",
"cards.label_widget",
"cards.dashboard_state_widget",
"cards.qr_code",

23
application/src/main/data/json/system/widget_types/battery_level.json

@ -0,0 +1,23 @@
{
"fqn": "battery_level",
"name": "Battery level",
"deprecated": false,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAilBMVEXg4ODf39/g4OAAAADg4ODf39////9c35Dg4ODL9dz5/vzX9+QhISHl+u3f+enB89XC89XHx8c9PT2QkJCu8MiF56x0dHRw453j4+Px8fFYWFisrKz0/fcvLy/S9uDq+/GCgoK6uro8PDzV1dVKSkpw457O9t6enp7i+etmZmarq6tm4ZeZ67p65aQon2vGAAAABnRSTlPvIL8Ar7DvmsykAAAEcklEQVR42u3dbVebMBjG8Tq3Sym3zARlSSA8ax+2ff+vtyCds666oOsSuvxf5CCnL/o7gUIRThfnZx8Wl3Pv49n54mwhMfukYXw8AYeRmM0KJ9HiVCCXAeJZAeJbAeJbAeJbAeJbAeJb/wOkzky5wDx6BcJJKU1cPqHxDmh4Aw97DcLNkFEO06jJiAH5uAaPQD++Yf4JsqKVGTRRJqA06bo2gwJYb1ZJMF5z6ngNs5TCQXYQIRjXAoz6JqUNGkUpYxllDQS1eU01GJHKoLVETQwOsoLQUAp0uRhYTzetjMwqpQ0kA5Ca9byFg+xnZEMNwLKe0x6kJ6WUJsEGqOHUwogcZL+PCNogp75h+zPCKRuSIwRKr9xsWfYQZiAZdc83rc2wChI7SEq6hYMsITpNa05bpFQ3NRlITTVDQ4oZAG8apX5CJDnasux3drUF5IZI9SQhWsog28G0bYn6boSYFHVwl/25lhQYE3I3mFHgV6rFm/LspLGraYuJeQlRpJyfp/wViOjgvP/h+8i8ChDfChDfChDfChDfChDfChDfChDfejekiKOnxUvY5RtkGT3r3lLiGySJnreGVZ5BzIR4MiXvgxRl9HtlAYs8gBTruBy7jw53X47Fycsm55BlYt5obJeBJhYbmhPIsoziwn7ykqi0kLiAlNEVplRZSFxArgZH/O3WsmsjsZA7gJQlcHNh341hH/48dgspogrVxZTM66MKB3ILqaIC8STINZaHj/VuIVdvgiQ4kGvIMkCeFSABEiABEiABEiAm95BqEiT2F4JvExy38BiCm+9fLLspvIZMKUD+BSS5sa3yGnI95eKDx5Di7mJChb+QkzkgBsh+ARIgARIgM4KINMdQrnifwiRTzlU+M4hcKaL0wUE6aykzSxnpTUv5vCAp6X6EcBKQLTEwamEGjbEmFXOAMIb0AdKRMmNNq+HvnWuo7jMuwbyHmMY3zh4gOdW7508VMQz1DJyJVhwZkkyCxBaQ7BlEpVJ3fIsjQ3A7wXGHN0C6Vqd5hqNDim/WjttqKmSX6MUsvrM/7uy75ZpW+5AsTTXfzgUiSZtxQ83DrACaJMbyVpJo+LEhxWfLoupVCBSlXUOthNSUi9podnEhNNixIcnXC9u+Xr8KES0R6cYsbbVZevzAlQxos74+MuTO2mEkxUGIYAIPNatcjmuaFdt/SdPM4IA4dBJnv6YACZAACZAACZAAMQVIgLzv4kPkL2TaxQeP/xmK6vuEiw8+Qwwltqvy/IaBk7nzAUlsmd+Q6s5+X098hky6+HAydz54fBw5mSN7gOwXIAESIAESIAEyZ8iJPBlaRdXkiw+FxXPgDp6eXk87jb8rkJhZPJBTyPhw+tpe8nWNZVniUI4hRRQvDeezXcNcJBaPszuA4GqQ2LeMLZ5mdwIxkigpiqVV1fr+xY8s5xBUZWRfWeGF3EOAorqyqyrwYj5AvCxAfCtAfCtAfCtAfCtAfOtyscBJdLn44PyHKv5G8tPp/Ij2+dnHy7m3+HB2/gO3i0/vBj05fgAAAABJRU5ErkJggg==",
"description": "Displays current battery level of device.",
"descriptor": {
"type": "latest",
"sizeX": 2.5,
"sizeY": 2.5,
"resources": [],
"templateHtml": "<tb-battery-level-widget \n [ctx]=\"ctx\"\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-battery-level-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.batteryLevelWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.batteryLevelWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '200px',\n previewHeight: '200px',\n embedTitlePanel: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-battery-level-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-battery-level-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"batteryLevel\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"layout\":\"vertical_solid\",\"showValue\":true,\"autoScaleValueSize\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryLevelColor\":{\"color\":\"rgba(92, 223, 144, 1)\",\"type\":\"range\",\"rangeList\":[{\"from\":0,\"to\":25,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":50,\"to\":100,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryShapeColor\":{\"color\":\"rgba(92, 223, 144, 0.32)\",\"type\":\"range\",\"rangeList\":[{\"from\":0,\"to\":25,\"color\":\"rgba(227, 71, 71, 0.32)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 0.32)\"},{\"from\":50,\"to\":100,\"color\":\"rgba(92, 223, 144, 0.32)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"}},\"title\":\"Battery level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
},
"externalId": null
}

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -89,6 +89,7 @@ export interface WidgetActionsApi {
handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor,
entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void;
elementClick: ($event: Event) => void;
cardClick: ($event: Event) => void;
getActiveEntityInfo: () => SubscriptionEntityInfo;
openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void;

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -351,7 +351,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer,
public elRef: ElementRef) {
public elRef: ElementRef,
private injector: Injector) {
super(store);
if (isDefinedAndNotNull(embeddedValue)) {
this.embedded = embeddedValue;
@ -1177,6 +1178,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
maxWidth: '95vw',
injector: this.injector,
data: {
dashboard: this.dashboard,
aliasController: this.dashboardCtx.aliasController,

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

@ -55,6 +55,9 @@ import {
import {
EntityCountBasicConfigComponent
} from '@home/components/widget/config/basic/entity/entity-count-basic-config.component';
import {
BatteryLevelBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/battery-level-basic-config.component';
@NgModule({
declarations: [
@ -71,7 +74,8 @@ import {
DataKeyRowComponent,
DataKeysPanelComponent,
AlarmCountBasicConfigComponent,
EntityCountBasicConfigComponent
EntityCountBasicConfigComponent,
BatteryLevelBasicConfigComponent
],
imports: [
CommonModule,
@ -92,7 +96,8 @@ import {
DataKeyRowComponent,
DataKeysPanelComponent,
AlarmCountBasicConfigComponent,
EntityCountBasicConfigComponent
EntityCountBasicConfigComponent,
BatteryLevelBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -107,5 +112,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-value-card-basic-config': ValueCardBasicConfigComponent,
'tb-aggregated-value-card-basic-config': AggregatedValueCardBasicConfigComponent,
'tb-alarm-count-basic-config': AlarmCountBasicConfigComponent,
'tb-entity-count-basic-config': EntityCountBasicConfigComponent
'tb-entity-count-basic-config': EntityCountBasicConfigComponent,
'tb-battery-level-basic-config': BatteryLevelBasicConfigComponent
};

1
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -381,6 +381,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false});
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false});
this.updateModel();
this.cd.markForCheck();
}
});
}

129
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html

@ -0,0 +1,129 @@
<!--
Copyright © 2016-2023 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]="batteryLevelWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeyLabel
hideDataKeyColor
hideDataKeyUnits
hideDataKeyDecimals
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
cols="4"
colsLtMd="2"
label="{{ 'widgets.battery-level.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of batteryLevelLayouts"
[value]="layout"
[image]="batteryLevelLayoutImageMap.get(layout)">
{{ batteryLevelLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="batteryLevelWidgetConfigForm.get('title').value"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.battery-level.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="batteryLevelWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue">
{{ 'widgets.battery-level.value' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-chip-listbox formControlName="autoScaleValueSize">
<mat-chip-option [value]="true">{{ 'widgets.battery-level.auto-scale' | translate }}</mat-chip-option>
</mat-chip-listbox>
<tb-font-settings formControlName="valueFont"
[autoScale]="batteryLevelWidgetConfigForm.get('autoScaleValueSize').value"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.battery-level.battery-level-color' | translate }}</div>
<tb-color-settings formControlName="batteryLevelColor">
</tb-color-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.battery-level.battery-shape-color' | translate }}</div>
<tb-color-settings formControlName="batteryShapeColor">
</tb-color-settings>
</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>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

221
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts

@ -0,0 +1,221 @@
///
/// Copyright © 2016-2023 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, Injector } 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 {
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isUndefined } from '@core/utils';
import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models';
import {
batteryLevelDefaultSettings,
batteryLevelLayoutImages,
batteryLevelLayouts,
batteryLevelLayoutTranslations,
BatteryLevelWidgetSettings
} from '@home/components/widget/lib/indicator/battery-level-widget.models';
@Component({
selector: 'tb-battery-level-basic-config',
templateUrl: './battery-level-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class BatteryLevelBasicConfigComponent extends BasicWidgetConfigComponent {
public get displayTimewindowConfig(): boolean {
const datasources = this.batteryLevelWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.batteryLevelWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
batteryLevelLayouts = batteryLevelLayouts;
batteryLevelLayoutTranslationMap = batteryLevelLayoutTranslations;
batteryLevelLayoutImageMap = batteryLevelLayoutImages;
batteryLevelWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.batteryLevelWidgetConfigForm;
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData, [{ name: 'batteryLevel', label: 'batteryLevel', type: DataKeyType.timeseries }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: BatteryLevelWidgetSettings = {...batteryLevelDefaultSettings, ...(configData.config.settings || {})};
const iconSize = resolveCssSize(configData.config.iconSize);
this.batteryLevelWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
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, []],
showValue: [settings.showValue, []],
autoScaleValueSize: [settings.autoScaleValueSize, []],
valueFont: [settings.valueFont, []],
valueColor: [settings.valueColor, []],
batteryLevelColor: [settings.batteryLevelColor, []],
batteryShapeColor: [settings.batteryShapeColor, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
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.layout = config.layout;
this.widgetConfig.config.settings.showValue = config.showValue;
this.widgetConfig.config.settings.autoScaleValueSize = config.autoScaleValueSize === true;
this.widgetConfig.config.settings.valueFont = config.valueFont;
this.widgetConfig.config.settings.valueColor = config.valueColor;
this.widgetConfig.config.settings.batteryLevelColor = config.batteryLevelColor;
this.widgetConfig.config.settings.batteryShapeColor = config.batteryShapeColor;
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', 'showValue'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.batteryLevelWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.batteryLevelWidgetConfigForm.get('showIcon').value;
const showValue: boolean = this.batteryLevelWidgetConfigForm.get('showValue').value;
if (showTitle) {
this.batteryLevelWidgetConfigForm.get('title').enable();
this.batteryLevelWidgetConfigForm.get('titleFont').enable();
this.batteryLevelWidgetConfigForm.get('titleColor').enable();
this.batteryLevelWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.batteryLevelWidgetConfigForm.get('iconSize').enable();
this.batteryLevelWidgetConfigForm.get('iconSizeUnit').enable();
this.batteryLevelWidgetConfigForm.get('icon').enable();
this.batteryLevelWidgetConfigForm.get('iconColor').enable();
} else {
this.batteryLevelWidgetConfigForm.get('iconSize').disable();
this.batteryLevelWidgetConfigForm.get('iconSizeUnit').disable();
this.batteryLevelWidgetConfigForm.get('icon').disable();
this.batteryLevelWidgetConfigForm.get('iconColor').disable();
}
} else {
this.batteryLevelWidgetConfigForm.get('title').disable();
this.batteryLevelWidgetConfigForm.get('titleFont').disable();
this.batteryLevelWidgetConfigForm.get('titleColor').disable();
this.batteryLevelWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.batteryLevelWidgetConfigForm.get('iconSize').disable();
this.batteryLevelWidgetConfigForm.get('iconSizeUnit').disable();
this.batteryLevelWidgetConfigForm.get('icon').disable();
this.batteryLevelWidgetConfigForm.get('iconColor').disable();
}
if (showValue) {
this.batteryLevelWidgetConfigForm.get('autoScaleValueSize').enable();
this.batteryLevelWidgetConfigForm.get('valueFont').enable();
this.batteryLevelWidgetConfigForm.get('valueColor').enable();
} else {
this.batteryLevelWidgetConfigForm.get('autoScaleValueSize').disable();
this.batteryLevelWidgetConfigForm.get('valueFont').disable();
this.batteryLevelWidgetConfigForm.get('valueColor').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.widgetConfig.config.units;
const decimals: number = this.widgetConfig.config.decimals;
return formatValue(55, decimals, units, true);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-count-panel" [class.tb-count-pointer]="showChevron" (click)="cardClick($event)">
<div class="tb-count-panel" [class.tb-count-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div class="tb-count-panel-column">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-count-panel-row">

11
ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts

@ -77,6 +77,8 @@ export class CountWidgetComponent implements OnInit {
showChevron = false;
chevronStyle: ComponentStyle = {};
hasCardClickAction = false;
constructor(private widgetComponent: WidgetComponent,
private cd: ChangeDetectorRef) {
}
@ -116,6 +118,8 @@ export class CountWidgetComponent implements OnInit {
this.showChevron = this.settings.showChevron;
this.chevronStyle = iconStyle(this.settings.chevronSize, this.settings.chevronSizeUnit);
this.chevronStyle.color = this.settings.chevronColor;
this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
}
public onInit() {
@ -138,11 +142,6 @@ export class CountWidgetComponent implements OnInit {
}
public cardClick($event: Event) {
const descriptors = this.ctx.actionsApi.getActionDescriptors('cardClick');
if (descriptors.length) {
$event.stopPropagation();
const descriptor = descriptors[0];
this.ctx.actionsApi.handleWidgetAction($event, descriptor);
}
this.ctx.actionsApi.cardClick($event);
}
}

44
ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2023 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-battery-level-panel" [style]="backgroundStyle" [class.tb-battery-level-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div class="tb-battery-level-overlay" [style]="overlayStyle"></div>
<div class="tb-battery-level-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<div #batteryLevelContent class="tb-battery-level-content" [class]="layoutClass">
<div #batteryLevelBox class="tb-battery-level-box">
<div #batteryLevelRectangle class="tb-battery-level-rectangle" [class]="layoutClass" [class.solid]="solid" [class.divided]="!solid">
<div class="tb-battery-level-shape" [style.background]="batteryShapeColor.color"></div>
<div class="tb-battery-level-container">
<div *ngIf="solid; else dividedIndicator" class="tb-battery-level-indicator-box solid"
[style.background-image]="'linear-gradient(0deg, ' + batteryLevelColor.color + ' 0% 100%)'"
[style.background-size]="vertical ? '100% ' + (value + 1) + '%' : (value + 1) + '% 100%'">
</div>
</div>
</div>
</div>
<div *ngIf="showValue" #batteryLevelValueBox class="tb-battery-level-value-box">
<div #batteryLevelValue class="tb-battery-level-value" [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>
</div>
</div>
</div>
<ng-template #dividedIndicator>
<div *ngFor="let section of batterySections; trackBy: trackBySection" class="tb-battery-level-indicator-box divided"
[style.background]="batteryLevelColor.color"
[style.opacity]="section ? '1': '0'"></div>
</ng-template>

158
ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss

@ -0,0 +1,158 @@
/**
* Copyright © 2016-2023 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.
*/
:host {
.tb-battery-level-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 24px 24px 24px;
&.tb-battery-level-pointer {
cursor: pointer;
}
> div:not(.tb-battery-level-overlay) {
z-index: 1;
}
.tb-battery-level-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
.tb-battery-level-content {
min-height: 0;
flex: 1;
display: flex;
justify-content: center;
&.vertical {
flex-direction: row;
gap: 16px;
.tb-battery-level-value-box {
align-items: center;
.tb-battery-level-value {
padding: 8px 12px;
}
}
}
&.horizontal {
flex-direction: column-reverse;
gap: 8px;
align-items: center;
.tb-battery-level-value-box {
.tb-battery-level-value {
padding: 4px 6px;
}
}
}
.tb-battery-level-box {
display: flex;
align-items: center;
.tb-battery-level-rectangle {
width: 100%;
height: 100%;
position: relative;
.tb-battery-level-shape {
position: absolute;
inset: 0;
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center;
}
.tb-battery-level-container {
position: absolute;
display: flex;
gap: 3%;
}
.tb-battery-level-indicator-box {
width: 100%;
height: 100%;
&.solid {
background-repeat: no-repeat;
transition: background 0.2s ease-out;
}
&.divided {
transition: opacity 0.2s ease-out;
}
}
&.vertical {
.tb-battery-level-shape {
mask-image: url(/assets/widget/battery-level/battery-shape-vertical.svg);
}
.tb-battery-level-container {
flex-direction: column-reverse;
}
&.solid {
.tb-battery-level-container {
inset: 8.85% 6.25% 3.54% 6.25%;
}
}
&.divided {
.tb-battery-level-container {
inset: 9.73% 7.81% 4.42% 7.81%;
}
}
.tb-battery-level-indicator-box {
&.solid {
border-radius: 10.7% / 6%;
background-position: 0 101%;
}
&.divided {
border-radius: 7.14% / 17.8%;
}
}
}
&.horizontal {
.tb-battery-level-shape {
mask-image: url(/assets/widget/battery-level/battery-shape-horizontal.svg);
}
.tb-battery-level-container {
inset: 6.25% 8.85% 6.25% 3.54%;
flex-direction: row;
}
&.solid {
.tb-battery-level-container {
inset: 6.25% 8.85% 6.25% 3.54%;
}
}
&.divided {
.tb-battery-level-container {
inset: 7.81% 9.73% 7.81% 4.42%;
}
}
.tb-battery-level-indicator-box {
&.solid {
border-radius: 6% / 10.7%;
background-position: -1% 0%;
}
&.divided {
border-radius: 17.8% / 7.14%;
}
}
}
}
}
.tb-battery-level-value-box {
display: flex;
.tb-battery-level-value {
white-space: nowrap;
}
}
}
}
}

293
ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts

@ -0,0 +1,293 @@
///
/// Copyright © 2016-2023 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,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild
} from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import { DatePipe } from '@angular/common';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle,
getDataKey,
getSingleTsValue,
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
import {
batteryLevelDefaultSettings,
BatteryLevelLayout,
BatteryLevelWidgetSettings
} from '@home/components/widget/lib/indicator/battery-level-widget.models';
import { ResizeObserver } from '@juggle/resize-observer';
const verticalBatteryDimensions = {
shapeAspectRatio: 64 / 113,
widthRatio: {
valueTopBottomPaddingRatio: 8 / 64,
valueLeftRightPaddingRatio: 12 / 64,
valueFontSizeRatio: 20 / 64,
valueLineHeightRaio: 24 / 64
},
heightRatio: {
valueTopBottomPaddingRatio: 8 / 113,
valueLeftRightPaddingRatio: 12 / 113,
valueFontSizeRatio: 20 / 113,
valueLineHeightRaio: 24 / 113
}
};
const horizontalBatteryDimensions = {
shapeAspectRatio: 113 / 64,
heightRatio: {
valueTopBottomPaddingRatio: 4 / 64,
valueFontSizeRatio: 20 / 64,
valueLineHeightRatio: 24 / 64
}
};
@Component({
selector: 'tb-battery-level-widget',
templateUrl: './battery-level-widget.component.html',
styleUrls: ['./battery-level-widget.component.scss']
})
export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('batteryLevelContent', {static: true})
batteryLevelContent: ElementRef<HTMLElement>;
@ViewChild('batteryLevelBox', {static: true})
batteryLevelBox: ElementRef<HTMLElement>;
@ViewChild('batteryLevelRectangle', {static: true})
batteryLevelRectangle: ElementRef<HTMLElement>;
@ViewChild('batteryLevelValueBox', {static: false})
batteryLevelValueBox: ElementRef<HTMLElement>;
@ViewChild('batteryLevelValue', {static: false})
batteryLevelValue: ElementRef<HTMLElement>;
settings: BatteryLevelWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
layout: BatteryLevelLayout;
layoutClass = 'vertical';
vertical = true;
solid = true;
showValue = true;
autoScaleValueSize = true;
valueText = 'N/A';
valueStyle: ComponentStyle = {};
valueColor: ColorProcessor;
value: number;
batterySections: boolean[] = [false, false, false, false];
batteryLevelColor: ColorProcessor;
batteryShapeColor: ColorProcessor;
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
batteryBoxResize$: ResizeObserver;
hasCardClickAction = false;
private decimals = 0;
private units = '';
constructor(private date: DatePipe,
private widgetComponent: WidgetComponent,
private renderer: Renderer2,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.batteryLevelWidget = this;
this.settings = {...batteryLevelDefaultSettings, ...this.ctx.settings};
this.decimals = this.ctx.decimals;
this.units = this.ctx.units;
const dataKey = getDataKey(this.ctx.datasources);
if (isDefinedAndNotNull(dataKey?.decimals)) {
this.decimals = dataKey.decimals;
}
if (dataKey?.units) {
this.units = dataKey.units;
}
this.layout = this.settings.layout;
this.vertical = [BatteryLevelLayout.vertical_solid, BatteryLevelLayout.vertical_divided].includes(this.layout);
this.layoutClass = this.vertical ? 'vertical' : 'horizontal';
this.solid = [BatteryLevelLayout.vertical_solid, BatteryLevelLayout.horizontal_solid].includes(this.layout);
this.showValue = this.settings.showValue;
this.autoScaleValueSize = this.showValue && this.settings.autoScaleValueSize;
this.valueStyle = textStyle(this.settings.valueFont, '0.1px');
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
this.batteryLevelColor = ColorProcessor.fromSettings(this.settings.batteryLevelColor);
this.batteryShapeColor = ColorProcessor.fromSettings(this.settings.batteryShapeColor);
this.backgroundStyle = backgroundStyle(this.settings.background);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
}
ngAfterViewInit() {
this.batteryBoxResize$ = new ResizeObserver(() => {
this.onResize();
});
this.batteryBoxResize$.observe(this.batteryLevelContent.nativeElement);
if (this.showValue) {
this.batteryBoxResize$.observe(this.batteryLevelValueBox.nativeElement);
}
this.onResize();
}
ngOnDestroy() {
if (this.batteryBoxResize$) {
this.batteryBoxResize$.disconnect();
}
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public onDataUpdated() {
const tsValue = getSingleTsValue(this.ctx.data);
this.value = 0;
if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) {
this.value = tsValue[1];
this.valueText = formatValue(this.value, this.decimals, this.units, true);
} else {
this.valueText = 'N/A';
}
if (!this.solid) {
const sectionSize = 100 / this.batterySections.length;
for (let i=0; i<this.batterySections.length; i++) {
this.batterySections[i] = this.value > sectionSize * i;
}
}
this.valueColor.update(this.value);
this.batteryLevelColor.update(this.value);
this.batteryShapeColor.update(this.value);
this.cd.detectChanges();
}
public trackBySection(index: number): number {
return index;
}
public cardClick($event: Event) {
this.ctx.actionsApi.cardClick($event);
}
private onResize() {
if (this.vertical) {
if (this.batteryLevelValue) {
const contentWidth = this.batteryLevelContent.nativeElement.getBoundingClientRect().width;
const boxWidth = (contentWidth - 16) / 2;
const boxHeight = this.batteryLevelContent.nativeElement.getBoundingClientRect().height;
const ratios = contentWidth > boxHeight ? verticalBatteryDimensions.heightRatio : verticalBatteryDimensions.widthRatio;
const boxSize = contentWidth > boxHeight ? boxHeight : boxWidth;
const topBottomValuePadding = ratios.valueTopBottomPaddingRatio * boxSize;
const leftRightValuePadding = ratios.valueLeftRightPaddingRatio * boxSize;
const valuePadding = `${topBottomValuePadding}px ${leftRightValuePadding}px`;
this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'padding', valuePadding);
if (this.autoScaleValueSize) {
const valueFontSize = ratios.valueFontSizeRatio * boxSize;
const valueLineHeight = ratios.valueLineHeightRaio * boxSize;
this.setValueFontSize(valueFontSize, valueLineHeight, boxWidth);
}
}
let height = this.batteryLevelContent.nativeElement.getBoundingClientRect().height;
const width = height * verticalBatteryDimensions.shapeAspectRatio;
this.renderer.setStyle(this.batteryLevelBox.nativeElement, 'width', width + 'px');
const realWidth = this.batteryLevelBox.nativeElement.getBoundingClientRect().width;
if (realWidth < width) {
height = realWidth / verticalBatteryDimensions.shapeAspectRatio;
this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'height', height + 'px');
} else {
this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'height', null);
}
} else {
const width = this.batteryLevelContent.nativeElement.getBoundingClientRect().width;
let height = width / horizontalBatteryDimensions.shapeAspectRatio;
this.renderer.setStyle(this.batteryLevelBox.nativeElement, 'height', height + 'px');
const realHeight = this.batteryLevelBox.nativeElement.getBoundingClientRect().height;
if (realHeight < height) {
height = realHeight;
const newWidth = height * horizontalBatteryDimensions.shapeAspectRatio;
this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'width', newWidth + 'px');
} else {
this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'width', null);
}
if (this.batteryLevelValue) {
const ratios = horizontalBatteryDimensions.heightRatio;
const valuePadding = `${(ratios.valueTopBottomPaddingRatio * height)}px 6px`;
this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'padding', valuePadding);
if (this.autoScaleValueSize) {
const valueFontSize = ratios.valueFontSizeRatio * height;
const valueLineHeight = ratios.valueLineHeightRatio * height;
const boxWidth = this.batteryLevelContent.nativeElement.getBoundingClientRect().width;
this.setValueFontSize(valueFontSize, valueLineHeight, boxWidth);
}
}
}
}
private setValueFontSize(valueFontSize: number, valueLineHeight: number, maxWidth: number) {
this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'fontSize', valueFontSize + 'px');
this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'lineHeight', valueLineHeight + 'px');
let valueWidth = this.batteryLevelValue.nativeElement.getBoundingClientRect().width;
while (valueWidth > maxWidth && valueFontSize > 6) {
valueFontSize--;
valueLineHeight--;
this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'fontSize', valueFontSize + 'px');
this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'lineHeight', valueLineHeight + 'px');
valueWidth = this.batteryLevelValue.nativeElement.getBoundingClientRect().width;
}
}
}

108
ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts

@ -0,0 +1,108 @@
///
/// Copyright © 2016-2023 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,
ColorSettings,
ColorType,
constantColor,
defaultColorFunction,
Font
} from '@shared/models/widget-settings.models';
export enum BatteryLevelLayout {
vertical_solid = 'vertical_solid',
horizontal_solid = 'horizontal_solid',
vertical_divided = 'vertical_divided',
horizontal_divided = 'horizontal_divided'
}
export const batteryLevelLayouts = Object.keys(BatteryLevelLayout) as BatteryLevelLayout[];
export const batteryLevelLayoutTranslations = new Map<BatteryLevelLayout, string>(
[
[BatteryLevelLayout.vertical_solid, 'widgets.battery-level.layout-vertical-solid'],
[BatteryLevelLayout.horizontal_solid, 'widgets.battery-level.layout-horizontal-solid'],
[BatteryLevelLayout.vertical_divided, 'widgets.battery-level.layout-vertical-divided'],
[BatteryLevelLayout.horizontal_divided, 'widgets.battery-level.layout-horizontal-divided']
]
);
export const batteryLevelLayoutImages = new Map<BatteryLevelLayout, string>(
[
[BatteryLevelLayout.vertical_solid, 'assets/widget/battery-level/vertical-solid-layout.svg'],
[BatteryLevelLayout.horizontal_solid, 'assets/widget/battery-level/horizontal-solid-layout.svg'],
[BatteryLevelLayout.vertical_divided, 'assets/widget/battery-level/vertical-divided-layout.svg'],
[BatteryLevelLayout.horizontal_divided, 'assets/widget/battery-level/horizontal-divided-layout.svg']
]
);
export interface BatteryLevelWidgetSettings {
layout: BatteryLevelLayout;
showValue: boolean;
autoScaleValueSize: boolean;
valueFont: Font;
valueColor: ColorSettings;
batteryLevelColor: ColorSettings;
batteryShapeColor: ColorSettings;
background: BackgroundSettings;
}
export const batteryLevelDefaultSettings: BatteryLevelWidgetSettings = {
layout: BatteryLevelLayout.vertical_solid,
showValue: true,
autoScaleValueSize: true,
valueFont: {
family: 'Roboto',
size: 20,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '24px'
},
valueColor: constantColor('rgba(0, 0, 0, 0.87)'),
batteryLevelColor: {
color: 'rgba(92, 223, 144, 1)',
type: ColorType.range,
rangeList: [
{from: 0, to: 25, color: 'rgba(227, 71, 71, 1)'},
{from: 25, to: 50, color: 'rgba(246, 206, 67, 1)'},
{from: 50, to: 100, color: 'rgba(92, 223, 144, 1)'}
],
colorFunction: defaultColorFunction
},
batteryShapeColor: {
color: 'rgba(92, 223, 144, 0.32)',
type: ColorType.range,
rangeList: [
{from: 0, to: 25, color: 'rgba(227, 71, 71, 0.32)'},
{from: 25, to: 50, color: 'rgba(246, 206, 67, 0.32)'},
{from: 50, to: 100, color: 'rgba(92, 223, 144, 0.32)'}
],
colorFunction: defaultColorFunction
},
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
};

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html

@ -19,12 +19,13 @@
<div class="tb-font-settings-title" translate>widgets.widget-font.font-settings</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.size</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div *ngIf="!autoScale" fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="size" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="sizeUnit"></tb-css-unit-select>
</div>
<div *ngIf="autoScale" class="tb-font-settings-auto" translate>widgets.widget-font.auto</div>
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.font-family</div>
@ -74,9 +75,10 @@
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.line-height</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-form-field *ngIf="!autoScale" fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="lineHeight" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div *ngIf="autoScale" class="tb-font-settings-auto" translate>widgets.widget-font.auto</div>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding font-preview">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss

@ -25,6 +25,10 @@
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-font-settings-auto {
padding: 8px 12px;
color: rgba(0, 0, 0, 0.38);
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts

@ -65,6 +65,10 @@ export class FontSettingsPanelComponent extends PageComponent implements OnInit
@coerceBoolean()
clearButton = false;
@Input()
@coerceBoolean()
autoScale = false;
@Input()
popover: TbPopoverComponent<FontSettingsPanelComponent>;
@ -97,12 +101,12 @@ export class FontSettingsPanelComponent extends PageComponent implements OnInit
ngOnInit(): void {
this.fontFormGroup = this.fb.group(
{
size: [this.font?.size, [Validators.min(0)]],
sizeUnit: [(this.font?.sizeUnit || 'px'), []],
size: [{value: this.font?.size, disabled: this.autoScale}, [Validators.min(0)]],
sizeUnit: [{ value: (this.font?.sizeUnit || 'px'), disabled: this.autoScale}, []],
family: [this.font?.family, []],
weight: [this.font?.weight, []],
style: [this.font?.style, []],
lineHeight: [this.font?.lineHeight, []]
lineHeight: [{ value: this.font?.lineHeight, disabled: this.autoScale }, []]
}
);
this.updatePreviewStyle(this.font);

7
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts

@ -50,6 +50,10 @@ export class FontSettingsComponent implements OnInit, ControlValueAccessor {
@coerceBoolean()
clearButton = false;
@Input()
@coerceBoolean()
autoScale = false;
private modelValue: Font;
private propagateChange = null;
@ -87,7 +91,8 @@ export class FontSettingsComponent implements OnInit, ControlValueAccessor {
const ctx: any = {
font: this.modelValue,
initialPreviewStyle: this.initialPreviewStyle,
clearButton: this.clearButton
clearButton: this.clearButton,
autoScale: this.autoScale
};
if (isDefinedAndNotNull(this.previewText)) {
const previewText = typeof this.previewText === 'string' ? this.previewText : this.previewText();

63
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2023 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]="batteryLevelWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.battery-level.battery-level-card-style</div>
<tb-image-cards-select rowHeight="1:1"
cols="4"
colsLtMd="2"
label="{{ 'widgets.battery-level.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of batteryLevelLayouts"
[value]="layout"
[image]="batteryLevelLayoutImageMap.get(layout)">
{{ batteryLevelLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showValue">
{{ 'widgets.battery-level.value' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-chip-listbox formControlName="autoScaleValueSize">
<mat-chip-option [value]="true">{{ 'widgets.battery-level.auto-scale' | translate }}</mat-chip-option>
</mat-chip-listbox>
<tb-font-settings formControlName="valueFont"
[autoScale]="batteryLevelWidgetSettingsForm.get('autoScaleValueSize').value"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.battery-level.battery-level-color' | translate }}</div>
<tb-color-settings formControlName="batteryLevelColor">
</tb-color-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.battery-level.battery-shape-color' | translate }}</div>
<tb-color-settings formControlName="batteryShapeColor">
</tb-color-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
</ng-container>

104
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts

@ -0,0 +1,104 @@
///
/// Copyright © 2016-2023 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, Injector } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { formatValue } from '@core/utils';
import {
batteryLevelDefaultSettings,
batteryLevelLayoutImages,
batteryLevelLayouts,
batteryLevelLayoutTranslations
} from '@home/components/widget/lib/indicator/battery-level-widget.models';
@Component({
selector: 'tb-battery-level-widget-settings',
templateUrl: './battery-level-widget-settings.component.html',
styleUrls: []
})
export class BatteryLevelWidgetSettingsComponent extends WidgetSettingsComponent {
batteryLevelLayouts = batteryLevelLayouts;
batteryLevelLayoutTranslationMap = batteryLevelLayoutTranslations;
batteryLevelLayoutImageMap = batteryLevelLayoutImages;
batteryLevelWidgetSettingsForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.batteryLevelWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...batteryLevelDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.batteryLevelWidgetSettingsForm = this.fb.group({
layout: [settings.layout, []],
showValue: [settings.showValue, []],
autoScaleValueSize: [settings.autoScaleValueSize, []],
valueFont: [settings.valueFont, []],
valueColor: [settings.valueColor, []],
batteryLevelColor: [settings.batteryLevelColor, []],
batteryShapeColor: [settings.batteryShapeColor, []],
background: [settings.background, []]
});
}
protected validatorTriggers(): string[] {
return ['showValue'];
}
protected updateValidators(emitEvent: boolean) {
const showValue: boolean = this.batteryLevelWidgetSettingsForm.get('showValue').value;
if (showValue) {
this.batteryLevelWidgetSettingsForm.get('autoScaleValueSize').enable();
this.batteryLevelWidgetSettingsForm.get('valueFont').enable();
this.batteryLevelWidgetSettingsForm.get('valueColor').enable();
} else {
this.batteryLevelWidgetSettingsForm.get('autoScaleValueSize').disable();
this.batteryLevelWidgetSettingsForm.get('valueFont').disable();
this.batteryLevelWidgetSettingsForm.get('valueColor').disable();
}
this.batteryLevelWidgetSettingsForm.get('autoScaleValueSize').updateValueAndValidity({emitEvent});
this.batteryLevelWidgetSettingsForm.get('valueFont').updateValueAndValidity({emitEvent});
this.batteryLevelWidgetSettingsForm.get('valueColor').updateValueAndValidity({emitEvent});
}
private _valuePreviewFn(): string {
const units: string = this.widgetConfig.config.units;
const decimals: number = this.widgetConfig.config.decimals;
return formatValue(22, decimals, units, true);
}
}

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

@ -285,6 +285,9 @@ import {
import {
EntityCountWidgetSettingsComponent
} from '@home/components/widget/lib/settings/entity/entity-count-widget-settings.component';
import {
BatteryLevelWidgetSettingsComponent
} from '@home/components/widget/lib/settings/indicator/battery-level-widget-settings.component';
@NgModule({
declarations: [
@ -390,7 +393,8 @@ import {
AggregatedValueCardKeySettingsComponent,
AggregatedValueCardWidgetSettingsComponent,
AlarmCountWidgetSettingsComponent,
EntityCountWidgetSettingsComponent
EntityCountWidgetSettingsComponent,
BatteryLevelWidgetSettingsComponent
],
imports: [
CommonModule,
@ -501,7 +505,8 @@ import {
AggregatedValueCardKeySettingsComponent,
AggregatedValueCardWidgetSettingsComponent,
AlarmCountWidgetSettingsComponent,
EntityCountWidgetSettingsComponent
EntityCountWidgetSettingsComponent,
BatteryLevelWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -577,5 +582,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-aggregated-value-card-key-settings': AggregatedValueCardKeySettingsComponent,
'tb-aggregated-value-card-widget-settings': AggregatedValueCardWidgetSettingsComponent,
'tb-alarm-count-widget-settings': AlarmCountWidgetSettingsComponent,
'tb-entity-count-widget-settings': EntityCountWidgetSettingsComponent
'tb-entity-count-widget-settings': EntityCountWidgetSettingsComponent,
'tb-battery-level-widget-settings': BatteryLevelWidgetSettingsComponent
};

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

@ -57,6 +57,7 @@ import {
AggregatedValueCardWidgetComponent
} from '@home/components/widget/lib/cards/aggregated-value-card-widget.component';
import { CountWidgetComponent } from '@home/components/widget/lib/count/count-widget.component';
import { BatteryLevelWidgetComponent } from '@home/components/widget/lib/indicator/battery-level-widget.component';
@NgModule({
declarations:
@ -90,7 +91,8 @@ import { CountWidgetComponent } from '@home/components/widget/lib/count/count-wi
GatewayRemoteConfigurationDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent
CountWidgetComponent,
BatteryLevelWidgetComponent
],
imports: [
CommonModule,
@ -128,7 +130,8 @@ import { CountWidgetComponent } from '@home/components/widget/lib/count/count-wi
GatewayRemoteConfigurationDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent
CountWidgetComponent,
BatteryLevelWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

14
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -253,6 +253,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
getActionDescriptors: this.getActionDescriptors.bind(this),
handleWidgetAction: this.handleWidgetAction.bind(this),
elementClick: this.elementClick.bind(this),
cardClick: this.cardClick.bind(this),
getActiveEntityInfo: this.getActiveEntityInfo.bind(this),
openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this),
openDashboardStateInPopover: this.openDashboardStateInPopover.bind(this)
@ -1418,6 +1419,19 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
private cardClick($event: Event) {
const descriptors = this.getActionDescriptors('cardClick');
if (descriptors.length) {
$event.stopPropagation();
const descriptor = descriptors[0];
const entityInfo = this.getActiveEntityInfo();
const entityId = entityInfo ? entityInfo.entityId : null;
const entityName = entityInfo ? entityInfo.entityName : null;
const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
}
}
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>,
actionDescriptor: WidgetActionDescriptor): Observable<any> {
const resourceTasks: Observable<string>[] = [];

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

@ -5036,6 +5036,19 @@
"blur": "Blur",
"preview": "Preview"
},
"battery-level": {
"layout": "Layout",
"layout-vertical-solid": "Vertical. Solid",
"layout-horizontal-solid": "Horizontal. Solid",
"layout-vertical-divided": "Vertical. Divided",
"layout-horizontal-divided": "Horizontal. Divided",
"icon": "Icon",
"value": "Value",
"auto-scale": "Auto scale",
"battery-level-color": "Battery level color",
"battery-shape-color": "Battery shape color",
"battery-level-card-style": "Battery level card style"
},
"chart": {
"common-settings": "Common settings",
"enable-stacking-mode": "Enable stacking mode",
@ -6146,7 +6159,8 @@
"color": "Color",
"shadow-color": "Shadow color",
"preview": "Preview",
"line-height": "Line height"
"line-height": "Line height",
"auto": "Auto"
},
"home": {
"no-data-available": "No data available"

4
ui-ngx/src/assets/widget/battery-level/battery-shape-horizontal.svg

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="113" height="64" fill="none" version="1.1" viewBox="0 0 113 64" xmlns="http://www.w3.org/2000/svg">
<path d="m113 25.028c0-1.6651-1.0316-3.1561-2.5898-3.7432l-0.82038-0.309c-1.5582-0.5871-2.5898-2.078-2.5898-3.7432v-7.2325c0-5.5228-4.4772-10-10-10h-87c-5.523 0-10 4.4772-10 10v44c0 5.5228 4.477 10 10 10h87c5.5228 0 10-4.4772 10-10v-7.3597c0-1.6015 0.9552-3.0486 2.4278-3.6781l1.1444-0.4892c1.4726-0.6294 2.4278-2.0765 2.4278-3.678zm-8-15.028v44c0 4.4183-3.5817 8-8 8h-87c-4.418 0-8-3.5817-8-8v-44c0-4.4183 3.582-8 8-8h87c4.4183 0 8 3.5817 8 8z" clip-rule="evenodd" fill="#000" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 659 B

4
ui-ngx/src/assets/widget/battery-level/battery-shape-vertical.svg

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="64" height="113" fill="none" version="1.1" viewBox="0 0 64 113" xmlns="http://www.w3.org/2000/svg">
<path d="m25.028 0c-1.6651 0-3.1561 1.0316-3.7432 2.5898l-0.309 0.82038c-0.5871 1.5582-2.078 2.5898-3.7432 2.5898h-7.2325c-5.5228 0-10 4.4772-10 10v87c0 5.523 4.4772 10 10 10h44c5.5228 0 10-4.477 10-10v-87c0-5.5228-4.4772-10-10-10h-7.3597c-1.6015 0-3.0486-0.9552-3.6781-2.4278l-0.4892-1.1444c-0.6294-1.4726-2.0765-2.4278-3.678-2.4278zm-15.028 8h44c4.4183 0 8 3.5817 8 8v87c0 4.418-3.5817 8-8 8h-44c-4.4183 0-8-3.582-8-8v-87c0-4.4183 3.5817-8 8-8z" clip-rule="evenodd" fill="#000" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 661 B

42
ui-ngx/src/assets/widget/battery-level/horizontal-divided-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

44
ui-ngx/src/assets/widget/battery-level/horizontal-solid-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

33
ui-ngx/src/assets/widget/battery-level/vertical-divided-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

44
ui-ngx/src/assets/widget/battery-level/vertical-solid-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

Loading…
Cancel
Save