Add a nice locking mechanism (#139)
* Add a nice locking mecanism * Update documentation * Fix typo * Remove useless styles * Fix for touch devices * Disable ripple effect while locked * fix blank card height * Show last changed instead of label * Support for light temperature with color auto * Style for last_changed * Update documentation
This commit is contained in:
parent
364580a281
commit
880e7e37e1
34
README.md
34
README.md
|
@ -59,13 +59,15 @@ Lovelace Button card for your entities.
|
||||||
| `show_state` | boolean | `false` | `true` \| `false` | Show the state on the card. defaults to false if not set |
|
| `show_state` | boolean | `false` | `true` \| `false` | Show the state on the card. defaults to false if not set |
|
||||||
| `show_icon` | boolean | `true` | `true` \| `false` | Wether to show the icon or not. Unless redefined in `icon`, uses the default entity icon from hass |
|
| `show_icon` | boolean | `true` | `true` \| `false` | Wether to show the icon or not. Unless redefined in `icon`, uses the default entity icon from hass |
|
||||||
| `show_units` | boolean | `true` | `true` \| `false` | Display or hide the units of a sensor, if any. |
|
| `show_units` | boolean | `true` | `true` \| `false` | Display or hide the units of a sensor, if any. |
|
||||||
| `show_label` | boolean | `false` | `true` \| `false` | Display or hide the `label`/`label_template`
|
| `show_label` | boolean | `false` | `true` \| `false` | Display or hide the `label`/`label_template` |
|
||||||
|
| `show_last_changed` | boolean | `false` | `true` \| `false` | Replace the label altogether and display the the `last_changed` attribute in a nice way (eg: `12 minutes ago`) |
|
||||||
| `show_entity_picture` | boolean | `false` | `true` \| `false` | Replace the icon by the entity picture (if any) or the custom picture (if any). Falls back to using the icon if both are undefined |
|
| `show_entity_picture` | boolean | `false` | `true` \| `false` | Replace the icon by the entity picture (if any) or the custom picture (if any). Falls back to using the icon if both are undefined |
|
||||||
| `entity_picture` | string | optional | Can be any of `/local/*` file or a URL | Will override the icon/the default entity_picture with your own image. Best is to use a square image. You can also define one per state |
|
| `entity_picture` | string | optional | Can be any of `/local/*` file or a URL | Will override the icon/the default entity_picture with your own image. Best is to use a square image. You can also define one per state |
|
||||||
| `units` | string | optional | `Kb/s`, `lux`, ... | Override or define the units to display after the state of the entity. If omitted, it's using the entity's units |
|
| `units` | string | optional | `Kb/s`, `lux`, ... | Override or define the units to display after the state of the entity. If omitted, it's using the entity's units |
|
||||||
| `styles` | object list | optional | | See [styles](#styles) |
|
| `styles` | object list | optional | | See [styles](#styles) |
|
||||||
| `state` | object list | optional | See [State](#State) | State to use for the color, icon and style of the button. Multiple states can be defined |
|
| `state` | object list | optional | See [State](#State) | State to use for the color, icon and style of the button. Multiple states can be defined |
|
||||||
| `confirmation` | string | optional | Free-form text | Show a confirmation popup on tap with defined text |
|
| `confirmation` | string | optional | Free-form text | Show a confirmation popup on tap with defined text |
|
||||||
|
| `lock` | boolean | `false` | `true` \| `false` | See [lock](#lock). This will display a normal button with a lock symbol in the corner. Clicking the button will make the lock go away and enable the button to be manoeuvred for five seconds |
|
||||||
| `layout` | string | optional | See [Layout](#Layout) | The layout of the button can be modified using this option |
|
| `layout` | string | optional | See [Layout](#Layout) | The layout of the button can be modified using this option |
|
||||||
|
|
||||||
### Action
|
### Action
|
||||||
|
@ -131,7 +133,18 @@ Multiple values are possible, see the image below for examples:
|
||||||
### Templates
|
### Templates
|
||||||
|
|
||||||
`label_template` supports templating as well as `value` for `state` when `operator: template`
|
`label_template` supports templating as well as `value` for `state` when `operator: template`
|
||||||
* `label_template`: It will be interpreted as javascript code and the code should return a string
|
* `label_template`: It will be interpreted as javascript code and the code should return a string.
|
||||||
|
`label_template` supports inline HTML, so you can do stuff like:
|
||||||
|
```yaml
|
||||||
|
label_template: >
|
||||||
|
return 'Connection: '
|
||||||
|
+ (states['switch.connection'].state === 'on'
|
||||||
|
? '<span style="color: #00FF00;">enabled</span>'
|
||||||
|
: '<span style="color: #FF0000;">disabled</span>')
|
||||||
|
+ ' / '
|
||||||
|
+ (states['binary_sensor.status'].state === 'on' ? 'connected' : 'disconnected')
|
||||||
|
```
|
||||||
|
![label-template-example](examples/label_template.png)
|
||||||
* `value` for `state` when `operator: template`: It will be interpreted as javascript code and the code should return a boolean (`true` or `false`)
|
* `value` for `state` when `operator: template`: It will be interpreted as javascript code and the code should return a boolean (`true` or `false`)
|
||||||
|
|
||||||
Inside the javascript code, you'll have access to those variables:
|
Inside the javascript code, you'll have access to those variables:
|
||||||
|
@ -699,6 +712,23 @@ Example with `template`:
|
||||||
- color: green
|
- color: green
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Lock
|
||||||
|
|
||||||
|
![lock-animation](examples/lock.gif)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- type: horizontal-stack
|
||||||
|
cards:
|
||||||
|
- type: "custom:button-card"
|
||||||
|
entity: switch.test
|
||||||
|
lock: true
|
||||||
|
- type: "custom:button-card"
|
||||||
|
color_type: card
|
||||||
|
lock: true
|
||||||
|
color: black
|
||||||
|
entity: switch.test
|
||||||
|
```
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [ciotlosm](https://github.com/ciotlosm) for the readme template and the awesome examples
|
- [ciotlosm](https://github.com/ciotlosm) for the readme template and the awesome examples
|
||||||
|
|
|
@ -2353,6 +2353,47 @@ const styleMap = directive(styleInfo => part => {
|
||||||
styleMapCache.set(part, styleInfo);
|
styleMapCache.set(part, styleInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
// For each part, remember the value that was last rendered to the part by the
|
||||||
|
// unsafeHTML directive, and the DocumentFragment that was last set as a value.
|
||||||
|
// The DocumentFragment is used as a unique key to check if the last value
|
||||||
|
// rendered to the part was with unsafeHTML. If not, we'll always re-render the
|
||||||
|
// value passed to unsafeHTML.
|
||||||
|
const previousValues = new WeakMap();
|
||||||
|
/**
|
||||||
|
* Renders the result as HTML, rather than text.
|
||||||
|
*
|
||||||
|
* Note, this is unsafe to use with any user-provided input that hasn't been
|
||||||
|
* sanitized or escaped, as it may lead to cross-site-scripting
|
||||||
|
* vulnerabilities.
|
||||||
|
*/
|
||||||
|
const unsafeHTML = directive(value => part => {
|
||||||
|
if (!(part instanceof NodePart)) {
|
||||||
|
throw new Error('unsafeHTML can only be used in text bindings');
|
||||||
|
}
|
||||||
|
const previousValue = previousValues.get(part);
|
||||||
|
if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = value; // innerHTML casts to string internally
|
||||||
|
const fragment = document.importNode(template.content, true);
|
||||||
|
part.setValue(fragment);
|
||||||
|
previousValues.set(part, { value, fragment });
|
||||||
|
});
|
||||||
|
|
||||||
/** Constants to be used in the frontend. */
|
/** Constants to be used in the frontend. */
|
||||||
// Constants should be alphabetically sorted by name.
|
// Constants should be alphabetically sorted by name.
|
||||||
// Arrays with values should be alphabetically sorted if order doesn't matter.
|
// Arrays with values should be alphabetically sorted if order doesn't matter.
|
||||||
|
@ -3290,6 +3331,15 @@ var TinyColor = function () {
|
||||||
};
|
};
|
||||||
return TinyColor;
|
return TinyColor;
|
||||||
}();
|
}();
|
||||||
|
function tinycolor(color, opts) {
|
||||||
|
if (color === void 0) {
|
||||||
|
color = '';
|
||||||
|
}
|
||||||
|
if (opts === void 0) {
|
||||||
|
opts = {};
|
||||||
|
}
|
||||||
|
return new TinyColor(color, opts);
|
||||||
|
}
|
||||||
|
|
||||||
function computeDomain(entityId) {
|
function computeDomain(entityId) {
|
||||||
return entityId.substr(0, entityId.indexOf('.'));
|
return entityId.substr(0, entityId.indexOf('.'));
|
||||||
|
@ -3311,6 +3361,17 @@ function getFontColorBasedOnBackgroundColor(backgroundColor) {
|
||||||
return 'rgb(234, 234, 234)'; // dark colors - white font
|
return 'rgb(234, 234, 234)'; // dark colors - white font
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function getLightColorBasedOnTemperature(current, min, max) {
|
||||||
|
const high = new TinyColor('rgb(255, 160, 0)'); // orange-ish
|
||||||
|
const low = new TinyColor('rgb(166, 209, 255)'); // blue-ish
|
||||||
|
const middle = new TinyColor('white');
|
||||||
|
const mixAmount = (current - min) / (max - min) * 100;
|
||||||
|
if (mixAmount < 50) {
|
||||||
|
return tinycolor(low).mix(middle, mixAmount * 2).toRgbString();
|
||||||
|
} else {
|
||||||
|
return tinycolor(middle).mix(high, (mixAmount - 50) * 2).toRgbString();
|
||||||
|
}
|
||||||
|
}
|
||||||
function buildNameStateConcat(name, stateString) {
|
function buildNameStateConcat(name, stateString) {
|
||||||
if (!name && !stateString) {
|
if (!name && !stateString) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -3637,6 +3698,7 @@ const styles = css`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
ha-card.disabled {
|
ha-card.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -3661,6 +3723,34 @@ const styles = css`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
#overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
text-align: right;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
#lock {
|
||||||
|
margin-top: 8px;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-right: 7px;
|
||||||
|
-webkit-animation-duration: 5s;
|
||||||
|
animation-duration: 5s;
|
||||||
|
-webkit-animation-fill-mode: both;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
@keyframes fadeOut{
|
||||||
|
0% {opacity: 0.5;}
|
||||||
|
20% {opacity: 0;}
|
||||||
|
80% {opacity: 0;}
|
||||||
|
100% {opacity: 0.5;}
|
||||||
|
}
|
||||||
|
.fadeOut {
|
||||||
|
-webkit-animation-name: fadeOut;
|
||||||
|
animation-name: fadeOut;
|
||||||
|
}
|
||||||
@keyframes blink{
|
@keyframes blink{
|
||||||
0%{opacity:0;}
|
0%{opacity:0;}
|
||||||
50%{opacity:1;}
|
50%{opacity:1;}
|
||||||
|
@ -4031,6 +4121,11 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
if (state.attributes.brightness) {
|
if (state.attributes.brightness) {
|
||||||
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
||||||
}
|
}
|
||||||
|
} else if (state.attributes.color_temp && state.attributes.min_mireds && state.attributes.max_mireds) {
|
||||||
|
color = getLightColorBasedOnTemperature(state.attributes.color_temp, state.attributes.min_mireds, state.attributes.max_mireds);
|
||||||
|
if (state.attributes.brightness) {
|
||||||
|
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
||||||
|
}
|
||||||
} else if (state.attributes.brightness) {
|
} else if (state.attributes.brightness) {
|
||||||
color = applyBrightnessToColor(this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5);
|
color = applyBrightnessToColor(this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5);
|
||||||
} else {
|
} else {
|
||||||
|
@ -4127,6 +4222,9 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
}
|
}
|
||||||
return units;
|
return units;
|
||||||
}
|
}
|
||||||
|
_buildLastChanged(state, style) {
|
||||||
|
return state ? html`<ha-relative-time .hass="${this.hass}" .datetime="${state.last_changed}" class="label" style=${styleMap(style)}></ha-relative-time>` : html``;
|
||||||
|
}
|
||||||
_buildLabel(state, configState) {
|
_buildLabel(state, configState) {
|
||||||
if (!this.config.show_label) {
|
if (!this.config.show_label) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -4176,12 +4274,11 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
_rotate(configState) {
|
_rotate(configState) {
|
||||||
return configState && configState.spin ? true : false;
|
return configState && configState.spin ? true : false;
|
||||||
}
|
}
|
||||||
_blankCardColoredHtml(state, cardStyle) {
|
_blankCardColoredHtml(cardStyle) {
|
||||||
const color = this._buildCssColorAttribute(state, undefined);
|
const blankCardStyle = Object.assign({ background: 'none', 'box-shadow': 'none' }, cardStyle);
|
||||||
const fontColor = getFontColorBasedOnBackgroundColor(color);
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card class="disabled" style=${styleMap(cardStyle)}>
|
<ha-card class="disabled" style=${styleMap(blankCardStyle)}>
|
||||||
<div style="color: ${fontColor}; background-color: ${color};"></div>
|
<div></div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -4191,6 +4288,7 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
const color = this._buildCssColorAttribute(state, configState);
|
const color = this._buildCssColorAttribute(state, configState);
|
||||||
let buttonColor = color;
|
let buttonColor = color;
|
||||||
let cardStyle = {};
|
let cardStyle = {};
|
||||||
|
const lockStyle = {};
|
||||||
const configCardStyle = this._buildStyleGeneric(configState, 'card');
|
const configCardStyle = this._buildStyleGeneric(configState, 'card');
|
||||||
if (configCardStyle.width) {
|
if (configCardStyle.width) {
|
||||||
this.style.setProperty('flex', '0 0 auto');
|
this.style.setProperty('flex', '0 0 auto');
|
||||||
|
@ -4198,12 +4296,13 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
}
|
}
|
||||||
switch (this.config.color_type) {
|
switch (this.config.color_type) {
|
||||||
case 'blank-card':
|
case 'blank-card':
|
||||||
return this._blankCardColoredHtml(state, configCardStyle);
|
return this._blankCardColoredHtml(configCardStyle);
|
||||||
case 'card':
|
case 'card':
|
||||||
case 'label-card':
|
case 'label-card':
|
||||||
{
|
{
|
||||||
const fontColor = getFontColorBasedOnBackgroundColor(color);
|
const fontColor = getFontColorBasedOnBackgroundColor(color);
|
||||||
cardStyle.color = fontColor;
|
cardStyle.color = fontColor;
|
||||||
|
lockStyle.color = fontColor;
|
||||||
cardStyle['background-color'] = color;
|
cardStyle['background-color'] = color;
|
||||||
cardStyle = Object.assign({}, cardStyle, configCardStyle);
|
cardStyle = Object.assign({}, cardStyle, configCardStyle);
|
||||||
buttonColor = 'inherit';
|
buttonColor = 'inherit';
|
||||||
|
@ -4215,11 +4314,22 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
|
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
|
||||||
|
${this._getLock(lockStyle)}
|
||||||
${this._buttonContent(state, configState, buttonColor)}
|
${this._buttonContent(state, configState, buttonColor)}
|
||||||
<mwc-ripple></mwc-ripple>
|
${this.config.lock ? '' : html`<paper-ripple id="ripple"></paper-ripple>`}
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
_getLock(lockStyle) {
|
||||||
|
if (this.config.lock) {
|
||||||
|
return html`
|
||||||
|
<div id="overlay" style=${styleMap(lockStyle)} @click=${this._handleLock} @touchstart=${this._handleLock}>
|
||||||
|
<ha-icon id="lock" icon="mdi:lock-outline"></iron-icon>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
_buttonContent(state, configState, color) {
|
_buttonContent(state, configState, color) {
|
||||||
const name = this._buildName(state, configState);
|
const name = this._buildName(state, configState);
|
||||||
const stateString = this._buildStateString(state);
|
const stateString = this._buildStateString(state);
|
||||||
|
@ -4239,6 +4349,7 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
const nameStyleFromConfig = this._buildStyleGeneric(configState, 'name');
|
const nameStyleFromConfig = this._buildStyleGeneric(configState, 'name');
|
||||||
const stateStyleFromConfig = this._buildStyleGeneric(configState, 'state');
|
const stateStyleFromConfig = this._buildStyleGeneric(configState, 'state');
|
||||||
const labelStyleFromConfig = this._buildStyleGeneric(configState, 'label');
|
const labelStyleFromConfig = this._buildStyleGeneric(configState, 'label');
|
||||||
|
const lastChangedTemplate = this._buildLastChanged(state, labelStyleFromConfig);
|
||||||
if (!iconTemplate) itemClass.push('no-icon');
|
if (!iconTemplate) itemClass.push('no-icon');
|
||||||
if (!name) itemClass.push('no-name');
|
if (!name) itemClass.push('no-name');
|
||||||
if (!stateString) itemClass.push('no-state');
|
if (!stateString) itemClass.push('no-state');
|
||||||
|
@ -4248,7 +4359,8 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
${iconTemplate ? iconTemplate : ''}
|
${iconTemplate ? iconTemplate : ''}
|
||||||
${name ? html`<div class="name" style=${styleMap(nameStyleFromConfig)}>${name}</div>` : ''}
|
${name ? html`<div class="name" style=${styleMap(nameStyleFromConfig)}>${name}</div>` : ''}
|
||||||
${stateString ? html`<div class="state" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
|
${stateString ? html`<div class="state" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
|
||||||
${label ? html`<div class="label" style=${styleMap(labelStyleFromConfig)}>${label}</div>` : ''}
|
${label && !this.config.show_last_changed ? html`<div class="label" style=${styleMap(labelStyleFromConfig)}>${unsafeHTML(label)}</div>` : ''}
|
||||||
|
${this.config.show_last_changed ? lastChangedTemplate : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -4330,6 +4442,31 @@ let ButtonCard = class ButtonCard extends LitElement {
|
||||||
const config = ev.target.config;
|
const config = ev.target.config;
|
||||||
handleClick(this, this.hass, config, true);
|
handleClick(this, this.hass, config, true);
|
||||||
}
|
}
|
||||||
|
_handleLock(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const overlay = this.shadowRoot.getElementById('overlay');
|
||||||
|
const haCard = this.shadowRoot.firstElementChild;
|
||||||
|
overlay.style.setProperty('pointer-events', 'none');
|
||||||
|
const paperRipple = document.createElement('paper-ripple');
|
||||||
|
const lock = this.shadowRoot.getElementById('lock');
|
||||||
|
if (lock) {
|
||||||
|
haCard.appendChild(paperRipple);
|
||||||
|
const icon = document.createAttribute('icon');
|
||||||
|
icon.value = 'mdi:lock-open-outline';
|
||||||
|
lock.attributes.setNamedItem(icon);
|
||||||
|
lock.classList.add('fadeOut');
|
||||||
|
}
|
||||||
|
window.setTimeout(() => {
|
||||||
|
overlay.style.setProperty('pointer-events', '');
|
||||||
|
if (lock) {
|
||||||
|
lock.classList.remove('fadeOut');
|
||||||
|
const icon = document.createAttribute('icon');
|
||||||
|
icon.value = 'mdi:lock-outline';
|
||||||
|
lock.attributes.setNamedItem(icon);
|
||||||
|
haCard.removeChild(paperRipple);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
__decorate([property()], ButtonCard.prototype, "hass", void 0);
|
__decorate([property()], ButtonCard.prototype, "hass", void 0);
|
||||||
__decorate([property()], ButtonCard.prototype, "config", void 0);
|
__decorate([property()], ButtonCard.prototype, "config", void 0);
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 757 KiB |
|
@ -8,6 +8,7 @@ import {
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
} from 'lit-element';
|
} from 'lit-element';
|
||||||
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
|
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
|
||||||
|
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
|
||||||
import {
|
import {
|
||||||
HassEntity,
|
HassEntity,
|
||||||
} from 'home-assistant-js-websocket';
|
} from 'home-assistant-js-websocket';
|
||||||
|
@ -25,6 +26,7 @@ import {
|
||||||
buildNameStateConcat,
|
buildNameStateConcat,
|
||||||
applyBrightnessToColor,
|
applyBrightnessToColor,
|
||||||
hasConfigOrEntityChanged,
|
hasConfigOrEntityChanged,
|
||||||
|
getLightColorBasedOnTemperature,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { handleClick } from './handle-click';
|
import { handleClick } from './handle-click';
|
||||||
import { longPress } from './long-press';
|
import { longPress } from './long-press';
|
||||||
|
@ -138,6 +140,17 @@ class ButtonCard extends LitElement {
|
||||||
if (state.attributes.brightness) {
|
if (state.attributes.brightness) {
|
||||||
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
||||||
}
|
}
|
||||||
|
} else if (state.attributes.color_temp
|
||||||
|
&& state.attributes.min_mireds
|
||||||
|
&& state.attributes.max_mireds) {
|
||||||
|
color = getLightColorBasedOnTemperature(
|
||||||
|
state.attributes.color_temp,
|
||||||
|
state.attributes.min_mireds,
|
||||||
|
state.attributes.max_mireds,
|
||||||
|
);
|
||||||
|
if (state.attributes.brightness) {
|
||||||
|
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
||||||
|
}
|
||||||
} else if (state.attributes.brightness) {
|
} else if (state.attributes.brightness) {
|
||||||
color = applyBrightnessToColor(
|
color = applyBrightnessToColor(
|
||||||
this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5,
|
this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5,
|
||||||
|
@ -260,6 +273,13 @@ class ButtonCard extends LitElement {
|
||||||
return units;
|
return units;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _buildLastChanged(
|
||||||
|
state: HassEntity | undefined,
|
||||||
|
style: StyleInfo,
|
||||||
|
): TemplateResult {
|
||||||
|
return state ? html`<ha-relative-time .hass="${this.hass}" .datetime="${state.last_changed}" class="label" style=${styleMap(style)}></ha-relative-time>` : html``;
|
||||||
|
}
|
||||||
|
|
||||||
private _buildLabel(
|
private _buildLabel(
|
||||||
state: HassEntity | undefined,
|
state: HassEntity | undefined,
|
||||||
configState: StateConfig | undefined,
|
configState: StateConfig | undefined,
|
||||||
|
@ -322,14 +342,16 @@ class ButtonCard extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _blankCardColoredHtml(
|
private _blankCardColoredHtml(
|
||||||
state: HassEntity | undefined,
|
|
||||||
cardStyle: StyleInfo,
|
cardStyle: StyleInfo,
|
||||||
): TemplateResult {
|
): TemplateResult {
|
||||||
const color = this._buildCssColorAttribute(state, undefined);
|
const blankCardStyle = {
|
||||||
const fontColor = getFontColorBasedOnBackgroundColor(color);
|
background: 'none',
|
||||||
|
'box-shadow': 'none',
|
||||||
|
...cardStyle,
|
||||||
|
};
|
||||||
return html`
|
return html`
|
||||||
<ha-card class="disabled" style=${styleMap(cardStyle)}>
|
<ha-card class="disabled" style=${styleMap(blankCardStyle)}>
|
||||||
<div style="color: ${fontColor}; background-color: ${color};"></div>
|
<div></div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -340,6 +362,7 @@ class ButtonCard extends LitElement {
|
||||||
const color = this._buildCssColorAttribute(state, configState);
|
const color = this._buildCssColorAttribute(state, configState);
|
||||||
let buttonColor = color;
|
let buttonColor = color;
|
||||||
let cardStyle: StyleInfo = {};
|
let cardStyle: StyleInfo = {};
|
||||||
|
const lockStyle: StyleInfo = {};
|
||||||
const configCardStyle = this._buildStyleGeneric(configState, 'card');
|
const configCardStyle = this._buildStyleGeneric(configState, 'card');
|
||||||
|
|
||||||
if (configCardStyle.width) {
|
if (configCardStyle.width) {
|
||||||
|
@ -348,11 +371,12 @@ class ButtonCard extends LitElement {
|
||||||
}
|
}
|
||||||
switch (this.config!.color_type) {
|
switch (this.config!.color_type) {
|
||||||
case 'blank-card':
|
case 'blank-card':
|
||||||
return this._blankCardColoredHtml(state, configCardStyle);
|
return this._blankCardColoredHtml(configCardStyle);
|
||||||
case 'card':
|
case 'card':
|
||||||
case 'label-card': {
|
case 'label-card': {
|
||||||
const fontColor = getFontColorBasedOnBackgroundColor(color);
|
const fontColor = getFontColorBasedOnBackgroundColor(color);
|
||||||
cardStyle.color = fontColor;
|
cardStyle.color = fontColor;
|
||||||
|
lockStyle.color = fontColor;
|
||||||
cardStyle['background-color'] = color;
|
cardStyle['background-color'] = color;
|
||||||
cardStyle = { ...cardStyle, ...configCardStyle };
|
cardStyle = { ...cardStyle, ...configCardStyle };
|
||||||
buttonColor = 'inherit';
|
buttonColor = 'inherit';
|
||||||
|
@ -365,12 +389,24 @@ class ButtonCard extends LitElement {
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
|
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
|
||||||
|
${this._getLock(lockStyle)}
|
||||||
${this._buttonContent(state, configState, buttonColor)}
|
${this._buttonContent(state, configState, buttonColor)}
|
||||||
<mwc-ripple></mwc-ripple>
|
${this.config!.lock ? '' : html`<paper-ripple id="ripple"></paper-ripple>`}
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getLock(lockStyle: StyleInfo): TemplateResult {
|
||||||
|
if (this.config!.lock) {
|
||||||
|
return html`
|
||||||
|
<div id="overlay" style=${styleMap(lockStyle)} @click=${this._handleLock} @touchstart=${this._handleLock}>
|
||||||
|
<ha-icon id="lock" icon="mdi:lock-outline"></iron-icon>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
private _buttonContent(
|
private _buttonContent(
|
||||||
state: HassEntity | undefined,
|
state: HassEntity | undefined,
|
||||||
configState: StateConfig | undefined,
|
configState: StateConfig | undefined,
|
||||||
|
@ -405,6 +441,7 @@ class ButtonCard extends LitElement {
|
||||||
const nameStyleFromConfig = this._buildStyleGeneric(configState, 'name');
|
const nameStyleFromConfig = this._buildStyleGeneric(configState, 'name');
|
||||||
const stateStyleFromConfig = this._buildStyleGeneric(configState, 'state');
|
const stateStyleFromConfig = this._buildStyleGeneric(configState, 'state');
|
||||||
const labelStyleFromConfig = this._buildStyleGeneric(configState, 'label');
|
const labelStyleFromConfig = this._buildStyleGeneric(configState, 'label');
|
||||||
|
const lastChangedTemplate = this._buildLastChanged(state, labelStyleFromConfig);
|
||||||
if (!iconTemplate) itemClass.push('no-icon');
|
if (!iconTemplate) itemClass.push('no-icon');
|
||||||
if (!name) itemClass.push('no-name');
|
if (!name) itemClass.push('no-name');
|
||||||
if (!stateString) itemClass.push('no-state');
|
if (!stateString) itemClass.push('no-state');
|
||||||
|
@ -415,7 +452,8 @@ class ButtonCard extends LitElement {
|
||||||
${iconTemplate ? iconTemplate : ''}
|
${iconTemplate ? iconTemplate : ''}
|
||||||
${name ? html`<div class="name" style=${styleMap(nameStyleFromConfig)}>${name}</div>` : ''}
|
${name ? html`<div class="name" style=${styleMap(nameStyleFromConfig)}>${name}</div>` : ''}
|
||||||
${stateString ? html`<div class="state" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
|
${stateString ? html`<div class="state" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
|
||||||
${label ? html`<div class="label" style=${styleMap(labelStyleFromConfig)}>${label}</div>` : ''}
|
${label && !this.config!.show_last_changed ? html`<div class="label" style=${styleMap(labelStyleFromConfig)}>${unsafeHTML(label)}</div>` : ''}
|
||||||
|
${this.config!.show_last_changed ? lastChangedTemplate : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -533,4 +571,31 @@ class ButtonCard extends LitElement {
|
||||||
const config = ev.target.config;
|
const config = ev.target.config;
|
||||||
handleClick(this, this.hass!, config, true);
|
handleClick(this, this.hass!, config, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleLock(ev): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const overlay = this.shadowRoot!.getElementById('overlay') as LitElement;
|
||||||
|
const haCard = this.shadowRoot!.firstElementChild as LitElement;
|
||||||
|
overlay.style.setProperty('pointer-events', 'none');
|
||||||
|
const paperRipple = document.createElement('paper-ripple');
|
||||||
|
|
||||||
|
const lock = this.shadowRoot!.getElementById('lock') as LitElement;
|
||||||
|
if (lock) {
|
||||||
|
haCard.appendChild(paperRipple);
|
||||||
|
const icon = document.createAttribute('icon');
|
||||||
|
icon.value = 'mdi:lock-open-outline';
|
||||||
|
lock.attributes.setNamedItem(icon);
|
||||||
|
lock.classList.add('fadeOut');
|
||||||
|
}
|
||||||
|
window.setTimeout(() => {
|
||||||
|
overlay.style.setProperty('pointer-events', '');
|
||||||
|
if (lock) {
|
||||||
|
lock.classList.remove('fadeOut');
|
||||||
|
const icon = document.createAttribute('icon');
|
||||||
|
icon.value = 'mdi:lock-outline';
|
||||||
|
lock.attributes.setNamedItem(icon);
|
||||||
|
haCard.removeChild(paperRipple);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PropertyValues } from 'lit-element';
|
import { PropertyValues } from 'lit-element';
|
||||||
import { TinyColor } from '@ctrl/tinycolor';
|
import tinycolor, { TinyColor } from '@ctrl/tinycolor';
|
||||||
import { HomeAssistant } from './types';
|
import { HomeAssistant } from './types';
|
||||||
|
|
||||||
export function computeDomain(entityId: string): string {
|
export function computeDomain(entityId: string): string {
|
||||||
|
@ -27,6 +27,22 @@ export function getFontColorBasedOnBackgroundColor(backgroundColor: string): str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLightColorBasedOnTemperature(
|
||||||
|
current: number,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
): string {
|
||||||
|
const high = new TinyColor('rgb(255, 160, 0)'); // orange-ish
|
||||||
|
const low = new TinyColor('rgb(166, 209, 255)'); // blue-ish
|
||||||
|
const middle = new TinyColor('white');
|
||||||
|
const mixAmount = (current - min) / (max - min) * 100;
|
||||||
|
if (mixAmount < 50) {
|
||||||
|
return tinycolor(low).mix(middle, mixAmount * 2).toRgbString();
|
||||||
|
} else {
|
||||||
|
return tinycolor(middle).mix(high, (mixAmount - 50) * 2).toRgbString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function buildNameStateConcat(
|
export function buildNameStateConcat(
|
||||||
name: string | undefined, stateString: string | undefined,
|
name: string | undefined, stateString: string | undefined,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
|
|
@ -5,6 +5,7 @@ export const styles = css`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
ha-card.disabled {
|
ha-card.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -29,6 +30,34 @@ export const styles = css`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
#overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
text-align: right;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
#lock {
|
||||||
|
margin-top: 8px;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-right: 7px;
|
||||||
|
-webkit-animation-duration: 5s;
|
||||||
|
animation-duration: 5s;
|
||||||
|
-webkit-animation-fill-mode: both;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
@keyframes fadeOut{
|
||||||
|
0% {opacity: 0.5;}
|
||||||
|
20% {opacity: 0;}
|
||||||
|
80% {opacity: 0;}
|
||||||
|
100% {opacity: 0.5;}
|
||||||
|
}
|
||||||
|
.fadeOut {
|
||||||
|
-webkit-animation-name: fadeOut;
|
||||||
|
animation-name: fadeOut;
|
||||||
|
}
|
||||||
@keyframes blink{
|
@keyframes blink{
|
||||||
0%{opacity:0;}
|
0%{opacity:0;}
|
||||||
50%{opacity:1;}
|
50%{opacity:1;}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export interface ButtonCardConfig {
|
||||||
color_type: 'icon' | 'card' | 'label-card' | 'blank-card'
|
color_type: 'icon' | 'card' | 'label-card' | 'blank-card'
|
||||||
color?: string;
|
color?: string;
|
||||||
size: string;
|
size: string;
|
||||||
|
lock: boolean;
|
||||||
tap_action?: ActionConfig;
|
tap_action?: ActionConfig;
|
||||||
hold_action?: ActionConfig;
|
hold_action?: ActionConfig;
|
||||||
show_name?: boolean;
|
show_name?: boolean;
|
||||||
|
@ -23,6 +24,7 @@ export interface ButtonCardConfig {
|
||||||
show_icon?: boolean;
|
show_icon?: boolean;
|
||||||
show_units?: boolean;
|
show_units?: boolean;
|
||||||
show_entity_picture?: boolean;
|
show_entity_picture?: boolean;
|
||||||
|
show_last_changed?: boolean;
|
||||||
show_label?: boolean;
|
show_label?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
label_template?: string;
|
label_template?: string;
|
||||||
|
|
Loading…
Reference in New Issue