Merge branch 'dev' into persistent_notification_trigger
This commit is contained in:
commit
ecba22d301
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
title: Control Circular Slider
|
||||
---
|
|
@ -0,0 +1,153 @@
|
|||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-circular-slider";
|
||||
import "../../../../src/components/ha-slider";
|
||||
|
||||
@customElement("demo-components-ha-control-circular-slider")
|
||||
export class DemoHaCircularSlider extends LitElement {
|
||||
@state()
|
||||
private current = 22;
|
||||
|
||||
@state()
|
||||
private value = 19;
|
||||
|
||||
@state()
|
||||
private high = 25;
|
||||
|
||||
@state()
|
||||
private changingValue?: number;
|
||||
|
||||
@state()
|
||||
private changingHigh?: number;
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.value = ev.detail.value;
|
||||
}
|
||||
|
||||
private _valueChanging(ev) {
|
||||
this.changingValue = ev.detail.value;
|
||||
}
|
||||
|
||||
private _highChanged(ev) {
|
||||
this.high = ev.detail.value;
|
||||
}
|
||||
|
||||
private _highChanging(ev) {
|
||||
this.changingHigh = ev.detail.value;
|
||||
}
|
||||
|
||||
private _currentChanged(ev) {
|
||||
this.current = ev.currentTarget.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Config</b></p>
|
||||
<div class="field">
|
||||
<p>Current</p>
|
||||
<ha-slider
|
||||
min="10"
|
||||
max="30"
|
||||
.value=${this.current}
|
||||
@change=${this._currentChanged}
|
||||
pin
|
||||
></ha-slider>
|
||||
<p>${this.current} °C</p>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Single</b></p>
|
||||
<ha-control-circular-slider
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changing=${this._valueChanging}
|
||||
.value=${this.value}
|
||||
.current=${this.current}
|
||||
step="1"
|
||||
min="10"
|
||||
max="30"
|
||||
></ha-control-circular-slider>
|
||||
<div>
|
||||
Value: ${this.value} °C
|
||||
<br />
|
||||
Changing:
|
||||
${this.changingValue != null ? `${this.changingValue} °C` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Dual</b></p>
|
||||
<ha-control-circular-slider
|
||||
dual
|
||||
@low-changed=${this._valueChanged}
|
||||
@low-changing=${this._valueChanging}
|
||||
@high-changed=${this._highChanged}
|
||||
@high-changing=${this._highChanging}
|
||||
.low=${this.value}
|
||||
.high=${this.high}
|
||||
.current=${this.current}
|
||||
step="1"
|
||||
min="10"
|
||||
max="30"
|
||||
></ha-control-circular-slider>
|
||||
<div>
|
||||
Low value: ${this.value} °C
|
||||
<br />
|
||||
Low changing:
|
||||
${this.changingValue != null ? `${this.changingValue} °C` : "-"}
|
||||
<br />
|
||||
High value: ${this.high} °C
|
||||
<br />
|
||||
High changing:
|
||||
${this.changingHigh != null ? `${this.changingHigh} °C` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
p.title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
ha-control-circular-slider {
|
||||
--control-circular-slider-color: #ff9800;
|
||||
--control-circular-slider-background: #ff9800;
|
||||
--control-circular-slider-background-opacity: 0.3;
|
||||
}
|
||||
ha-control-circular-slider[dual] {
|
||||
--control-circular-slider-high-color: #2196f3;
|
||||
--control-circular-slider-low-color: #ff9800;
|
||||
--control-circular-slider-background: var(--disabled-color);
|
||||
}
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-control-circular-slider": DemoHaCircularSlider;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ const CONFIGS = [
|
|||
heading: "markdown-it demo",
|
||||
config: `
|
||||
- type: markdown
|
||||
content: >-
|
||||
content: |
|
||||
# h1 Heading 8-)
|
||||
|
||||
## h2 Heading
|
||||
|
@ -65,6 +65,15 @@ const CONFIGS = [
|
|||
>> ...by using additional greater-than signs right next to each other...
|
||||
> > > ...or with spaces between arrows.
|
||||
|
||||
> **Warning** Hey there
|
||||
> This is a warning with a title
|
||||
|
||||
> **Note**
|
||||
> This is a note
|
||||
|
||||
> **Note**
|
||||
> This is a multiline note
|
||||
> Lorem ipsum...
|
||||
|
||||
## Lists
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
mdiGoogleCirclesCommunities,
|
||||
mdiHomeAssistant,
|
||||
mdiHomeAutomation,
|
||||
mdiImage,
|
||||
mdiImageFilterFrames,
|
||||
mdiLightbulb,
|
||||
mdiLightningBolt,
|
||||
|
@ -90,6 +91,7 @@ export const FIXED_DOMAIN_ICONS = {
|
|||
group: mdiGoogleCirclesCommunities,
|
||||
homeassistant: mdiHomeAssistant,
|
||||
homekit: mdiHomeAutomation,
|
||||
image: mdiImage,
|
||||
image_processing: mdiImageFilterFrames,
|
||||
input_button: mdiGestureTapButton,
|
||||
input_datetime: mdiCalendarClock,
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { PropertyDeclaration, ReactiveElement } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { InternalPropertyDeclaration } from "lit/decorators";
|
||||
import type { ClassElement } from "../../types";
|
||||
|
||||
type Callback = (oldValue: any, newValue: any) => void;
|
||||
|
||||
class Storage {
|
||||
constructor(subscribe = true, storage = window.localStorage) {
|
||||
class StorageClass {
|
||||
constructor(storage = window.localStorage) {
|
||||
this.storage = storage;
|
||||
if (!subscribe) {
|
||||
if (storage !== window.localStorage) {
|
||||
// storage events only work for localStorage
|
||||
return;
|
||||
}
|
||||
window.addEventListener("storage", (ev: StorageEvent) => {
|
||||
|
@ -77,6 +79,7 @@ class Storage {
|
|||
}
|
||||
|
||||
public setValue(storageKey: string, value: any): any {
|
||||
const oldValue = this._storage[storageKey];
|
||||
this._storage[storageKey] = value;
|
||||
try {
|
||||
if (value === undefined) {
|
||||
|
@ -86,49 +89,68 @@ class Storage {
|
|||
}
|
||||
} catch (err: any) {
|
||||
// Safari in private mode doesn't allow localstorage
|
||||
} finally {
|
||||
if (this._listeners[storageKey]) {
|
||||
this._listeners[storageKey].forEach((listener) =>
|
||||
listener(oldValue, value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subscribeStorage = new Storage();
|
||||
const storages: Record<string, StorageClass> = {};
|
||||
|
||||
export const LocalStorage =
|
||||
(
|
||||
storageKey?: string,
|
||||
property?: boolean,
|
||||
subscribe = true,
|
||||
storageType?: globalThis.Storage,
|
||||
propertyOptions?: PropertyDeclaration
|
||||
): any =>
|
||||
export const storage =
|
||||
(options: {
|
||||
key?: string;
|
||||
storage?: "localStorage" | "sessionStorage";
|
||||
subscribe?: boolean;
|
||||
state?: boolean;
|
||||
stateOptions?: InternalPropertyDeclaration;
|
||||
}): any =>
|
||||
(clsElement: ClassElement) => {
|
||||
const storage =
|
||||
subscribe && !storageType
|
||||
? subscribeStorage
|
||||
: new Storage(subscribe, storageType);
|
||||
const storageName = options.storage || "localStorage";
|
||||
|
||||
let storageInstance: StorageClass;
|
||||
if (storageName && storageName in storages) {
|
||||
storageInstance = storages[storageName];
|
||||
} else {
|
||||
storageInstance = new StorageClass(window[storageName]);
|
||||
storages[storageName] = storageInstance;
|
||||
}
|
||||
|
||||
const key = String(clsElement.key);
|
||||
storageKey = storageKey || String(clsElement.key);
|
||||
const storageKey = options.key || String(clsElement.key);
|
||||
const initVal = clsElement.initializer
|
||||
? clsElement.initializer()
|
||||
: undefined;
|
||||
|
||||
storage.addFromStorage(storageKey);
|
||||
storageInstance.addFromStorage(storageKey);
|
||||
|
||||
const subscribeChanges = (el: ReactiveElement): UnsubscribeFunc =>
|
||||
storage.subscribeChanges(storageKey!, (oldValue) => {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
});
|
||||
const subscribeChanges =
|
||||
options.subscribe !== false
|
||||
? (el: ReactiveElement): UnsubscribeFunc =>
|
||||
storageInstance.subscribeChanges(
|
||||
storageKey!,
|
||||
(oldValue, _newValue) => {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
}
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const getValue = (): any =>
|
||||
storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal;
|
||||
storageInstance.hasKey(storageKey!)
|
||||
? storageInstance.getValue(storageKey!)
|
||||
: initVal;
|
||||
|
||||
const setValue = (el: ReactiveElement, value: any) => {
|
||||
let oldValue: unknown | undefined;
|
||||
if (property) {
|
||||
if (options.state) {
|
||||
oldValue = getValue();
|
||||
}
|
||||
storage.setValue(storageKey!, value);
|
||||
if (property) {
|
||||
storageInstance.setValue(storageKey!, value);
|
||||
if (options.state) {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
}
|
||||
};
|
||||
|
@ -148,22 +170,23 @@ export const LocalStorage =
|
|||
configurable: true,
|
||||
},
|
||||
finisher(cls: typeof ReactiveElement) {
|
||||
if (property && subscribe) {
|
||||
if (options.state && options.subscribe) {
|
||||
const connectedCallback = cls.prototype.connectedCallback;
|
||||
const disconnectedCallback = cls.prototype.disconnectedCallback;
|
||||
cls.prototype.connectedCallback = function () {
|
||||
connectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`] = subscribeChanges(this);
|
||||
this[`__unbsubLocalStorage${key}`] = subscribeChanges?.(this);
|
||||
};
|
||||
cls.prototype.disconnectedCallback = function () {
|
||||
disconnectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`]();
|
||||
this[`__unbsubLocalStorage${key}`]?.();
|
||||
this[`__unbsubLocalStorage${key}`] = undefined;
|
||||
};
|
||||
}
|
||||
if (property) {
|
||||
if (options.state) {
|
||||
cls.createProperty(clsElement.key, {
|
||||
noAccessor: true,
|
||||
...propertyOptions,
|
||||
...options.stateOptions,
|
||||
});
|
||||
}
|
||||
},
|
|
@ -191,7 +191,9 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||
|
||||
// state is a timestamp
|
||||
if (
|
||||
["button", "input_button", "scene", "stt", "tts"].includes(domain) ||
|
||||
["button", "image", "input_button", "scene", "stt", "tts"].includes(
|
||||
domain
|
||||
) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,546 @@
|
|||
import {
|
||||
DIRECTION_ALL,
|
||||
Manager,
|
||||
Pan,
|
||||
Tap,
|
||||
TouchMouseInput,
|
||||
} from "@egjs/hammerjs";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
svg,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { clamp } from "../common/number/clamp";
|
||||
import { arc } from "../resources/svg-arc";
|
||||
|
||||
const MAX_ANGLE = 270;
|
||||
const ROTATE_ANGLE = 360 - MAX_ANGLE / 2 - 90;
|
||||
const RADIUS = 145;
|
||||
|
||||
function xy2polar(x: number, y: number) {
|
||||
const r = Math.sqrt(x * x + y * y);
|
||||
const phi = Math.atan2(y, x);
|
||||
return [r, phi];
|
||||
}
|
||||
|
||||
function rad2deg(rad: number) {
|
||||
return (rad / (2 * Math.PI)) * 360;
|
||||
}
|
||||
|
||||
type ActiveSlider = "low" | "high" | "value";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"value-changing": { value: unknown };
|
||||
"low-changing": { value: unknown };
|
||||
"low-changed": { value: unknown };
|
||||
"high-changing": { value: unknown };
|
||||
"high-changed": { value: unknown };
|
||||
}
|
||||
}
|
||||
|
||||
const A11Y_KEY_CODES = new Set([
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Home",
|
||||
"End",
|
||||
]);
|
||||
|
||||
@customElement("ha-control-circular-slider")
|
||||
export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public dual?: boolean;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@property({ type: String, attribute: "low-label" })
|
||||
public lowLabel?: string;
|
||||
|
||||
@property({ type: String, attribute: "high-label" })
|
||||
public highLabel?: string;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public current?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public low?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public high?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public step = 1;
|
||||
|
||||
@property({ type: Number })
|
||||
public min = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
public max = 100;
|
||||
|
||||
@state()
|
||||
public _activeSlider?: ActiveSlider;
|
||||
|
||||
@state()
|
||||
public _lastSlider?: ActiveSlider;
|
||||
|
||||
private _valueToPercentage(value: number) {
|
||||
return (
|
||||
(clamp(value, this.min, this.max) - this.min) / (this.max - this.min)
|
||||
);
|
||||
}
|
||||
|
||||
private _percentageToValue(value: number) {
|
||||
return (this.max - this.min) * value + this.min;
|
||||
}
|
||||
|
||||
private _steppedValue(value: number) {
|
||||
return Math.round(value / this.step) * this.step;
|
||||
}
|
||||
|
||||
private _boundedValue(value: number) {
|
||||
const min =
|
||||
this._activeSlider === "high" ? Math.min(this.low ?? this.max) : this.min;
|
||||
const max =
|
||||
this._activeSlider === "low" ? Math.max(this.high ?? this.min) : this.max;
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private _mc?: HammerManager;
|
||||
|
||||
private _getPercentageFromEvent = (e: HammerInput) => {
|
||||
const bound = this._slider.getBoundingClientRect();
|
||||
const x = (2 * (e.center.x - bound.left - bound.width / 2)) / bound.width;
|
||||
const y = (2 * (e.center.y - bound.top - bound.height / 2)) / bound.height;
|
||||
|
||||
const [, phi] = xy2polar(x, y);
|
||||
|
||||
const offset = (360 - MAX_ANGLE) / 2;
|
||||
|
||||
const angle = ((rad2deg(phi) + offset - ROTATE_ANGLE + 360) % 360) - offset;
|
||||
|
||||
return Math.max(Math.min(angle / MAX_ANGLE, 1), 0);
|
||||
};
|
||||
|
||||
@query("#slider")
|
||||
private _slider;
|
||||
|
||||
@query("#interaction")
|
||||
private _interaction;
|
||||
|
||||
private _findActiveSlider(value: number): ActiveSlider {
|
||||
if (!this.dual) return "value";
|
||||
const low = Math.max(this.low ?? this.min, this.min);
|
||||
const high = Math.min(this.high ?? this.max, this.max);
|
||||
if (low >= value) {
|
||||
return "low";
|
||||
}
|
||||
if (high <= value) {
|
||||
return "high";
|
||||
}
|
||||
const lowDistance = Math.abs(value - low);
|
||||
const highDistance = Math.abs(value - high);
|
||||
return lowDistance <= highDistance ? "low" : "high";
|
||||
}
|
||||
|
||||
private _setActiveValue(value: number) {
|
||||
if (!this._activeSlider) return;
|
||||
this[this._activeSlider] = value;
|
||||
}
|
||||
|
||||
private _getActiveValue(): number | undefined {
|
||||
if (!this._activeSlider) return undefined;
|
||||
return this[this._activeSlider];
|
||||
}
|
||||
|
||||
_setupListeners() {
|
||||
if (this._interaction && !this._mc) {
|
||||
this._mc = new Manager(this._interaction, {
|
||||
inputClass: TouchMouseInput,
|
||||
});
|
||||
this._mc.add(
|
||||
new Pan({
|
||||
direction: DIRECTION_ALL,
|
||||
enable: true,
|
||||
threshold: 0,
|
||||
})
|
||||
);
|
||||
|
||||
this._mc.add(new Tap({ event: "singletap" }));
|
||||
|
||||
this._mc.on("pan", (e) => {
|
||||
e.srcEvent.stopPropagation();
|
||||
e.srcEvent.preventDefault();
|
||||
});
|
||||
this._mc.on("panstart", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const raw = this._percentageToValue(percentage);
|
||||
this._activeSlider = this._findActiveSlider(raw);
|
||||
this._lastSlider = this._activeSlider;
|
||||
this.shadowRoot?.getElementById("#slider")?.focus();
|
||||
});
|
||||
this._mc.on("pancancel", () => {
|
||||
if (this.disabled) return;
|
||||
this._activeSlider = undefined;
|
||||
});
|
||||
this._mc.on("panmove", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const raw = this._percentageToValue(percentage);
|
||||
const bounded = this._boundedValue(raw);
|
||||
this._setActiveValue(bounded);
|
||||
const stepped = this._steppedValue(bounded);
|
||||
if (this._activeSlider) {
|
||||
fireEvent(this, `${this._activeSlider}-changing`, { value: stepped });
|
||||
}
|
||||
});
|
||||
this._mc.on("panend", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const raw = this._percentageToValue(percentage);
|
||||
const bounded = this._boundedValue(raw);
|
||||
const stepped = this._steppedValue(bounded);
|
||||
if (this._activeSlider) {
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: undefined,
|
||||
});
|
||||
fireEvent(this, `${this._activeSlider}-changed`, { value: stepped });
|
||||
}
|
||||
this._activeSlider = undefined;
|
||||
});
|
||||
this._mc.on("singletap", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const raw = this._percentageToValue(percentage);
|
||||
this._activeSlider = this._findActiveSlider(raw);
|
||||
const bounded = this._boundedValue(raw);
|
||||
const stepped = this._steppedValue(bounded);
|
||||
this._setActiveValue(stepped);
|
||||
if (this._activeSlider) {
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: undefined,
|
||||
});
|
||||
fireEvent(this, `${this._activeSlider}-changed`, { value: stepped });
|
||||
}
|
||||
this._lastSlider = this._activeSlider;
|
||||
this.shadowRoot?.getElementById("#slider")?.focus();
|
||||
this._activeSlider = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private get _tenPercentStep() {
|
||||
return Math.max(this.step, (this.max - this.min) / 10);
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
if (this._lastSlider) {
|
||||
this.shadowRoot?.getElementById(this._lastSlider)?.focus();
|
||||
}
|
||||
this._activeSlider =
|
||||
this._lastSlider ?? ((e.currentTarget as any).id as ActiveSlider);
|
||||
this._lastSlider = undefined;
|
||||
|
||||
const value = this._getActiveValue();
|
||||
|
||||
switch (e.code) {
|
||||
case "ArrowRight":
|
||||
case "ArrowUp":
|
||||
this._setActiveValue(
|
||||
this._boundedValue((value ?? this.min) + this.step)
|
||||
);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
case "ArrowDown":
|
||||
this._setActiveValue(
|
||||
this._boundedValue((value ?? this.min) - this.step)
|
||||
);
|
||||
break;
|
||||
case "PageUp":
|
||||
this._setActiveValue(
|
||||
this._steppedValue(
|
||||
this._boundedValue((value ?? this.min) + this._tenPercentStep)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "PageDown":
|
||||
this._setActiveValue(
|
||||
this._steppedValue(
|
||||
this._boundedValue((value ?? this.min) - this._tenPercentStep)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "Home":
|
||||
this._setActiveValue(this._boundedValue(this.min));
|
||||
break;
|
||||
case "End":
|
||||
this._setActiveValue(this._boundedValue(this.max));
|
||||
break;
|
||||
}
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: this._getActiveValue(),
|
||||
});
|
||||
this._activeSlider = undefined;
|
||||
}
|
||||
|
||||
_handleKeyUp(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
this._activeSlider = (e.currentTarget as any).id as ActiveSlider;
|
||||
e.preventDefault();
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: undefined,
|
||||
});
|
||||
fireEvent(this, `${this._activeSlider}-changed`, {
|
||||
value: this._getActiveValue(),
|
||||
});
|
||||
this._activeSlider = undefined;
|
||||
}
|
||||
|
||||
destroyListeners() {
|
||||
if (this._mc) {
|
||||
this._mc.destroy();
|
||||
this._mc = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS });
|
||||
|
||||
const maxRatio = MAX_ANGLE / 360;
|
||||
|
||||
const f = RADIUS * 2 * Math.PI;
|
||||
const lowValue = this.dual ? this.low : this.value;
|
||||
const highValue = this.high;
|
||||
const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
|
||||
const highPercentage = this._valueToPercentage(highValue ?? this.max);
|
||||
|
||||
const lowArcLength = lowPercentage * f * maxRatio;
|
||||
const lowStrokeDasharray = `${lowArcLength} ${f - lowArcLength}`;
|
||||
|
||||
const highArcLength = (1 - highPercentage) * f * maxRatio;
|
||||
const highStrokeDasharray = `${highArcLength} ${f - highArcLength}`;
|
||||
const highStrokeDashOffset = `${highArcLength + f * (1 - maxRatio)}`;
|
||||
|
||||
const currentPercentage = this._valueToPercentage(this.current ?? 0);
|
||||
const currentAngle = currentPercentage * MAX_ANGLE;
|
||||
|
||||
return html`
|
||||
<svg
|
||||
id="slider"
|
||||
viewBox="0 0 320 320"
|
||||
overflow="visible"
|
||||
class=${classMap({
|
||||
pressed: Boolean(this._activeSlider),
|
||||
})}
|
||||
@keydown=${this._handleKeyDown}
|
||||
tabindex=${this._lastSlider ? "0" : "-1"}
|
||||
>
|
||||
<g
|
||||
id="container"
|
||||
transform="translate(160 160) rotate(${ROTATE_ANGLE})"
|
||||
>
|
||||
<g id="interaction">
|
||||
<path d=${trackPath} />
|
||||
</g>
|
||||
<g id="display">
|
||||
<path class="background" d=${trackPath} />
|
||||
<circle
|
||||
.id=${this.dual ? "low" : "value"}
|
||||
class="track"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r=${RADIUS}
|
||||
stroke-dasharray=${lowStrokeDasharray}
|
||||
stroke-dashoffset="0"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${lowValue != null
|
||||
? this._steppedValue(lowValue)
|
||||
: undefined}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
${this.dual
|
||||
? svg`
|
||||
<circle
|
||||
id="high"
|
||||
class="track"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r=${RADIUS}
|
||||
stroke-dasharray=${highStrokeDasharray}
|
||||
stroke-dashoffset=${highStrokeDashOffset}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${
|
||||
highValue != null
|
||||
? this._steppedValue(highValue)
|
||||
: undefined
|
||||
}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.highLabel)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${this.current != null
|
||||
? svg`
|
||||
<g
|
||||
style=${styleMap({ "--current-angle": `${currentAngle}deg` })}
|
||||
class="current"
|
||||
>
|
||||
<line
|
||||
x1=${RADIUS - 12}
|
||||
y1="0"
|
||||
x2=${RADIUS - 15}
|
||||
y2="0"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<line
|
||||
x1=${RADIUS - 15}
|
||||
y1="0"
|
||||
x2=${RADIUS - 20}
|
||||
y2="0"
|
||||
stroke-linecap="round"
|
||||
stroke-width="4"
|
||||
/>
|
||||
</g>
|
||||
`
|
||||
: nothing}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
--control-circular-slider-color: var(--primary-color);
|
||||
--control-circular-slider-background: #8b97a3;
|
||||
--control-circular-slider-background-opacity: 0.3;
|
||||
--control-circular-slider-low-color: var(
|
||||
--control-circular-slider-color
|
||||
);
|
||||
--control-circular-slider-high-color: var(
|
||||
--control-circular-slider-color
|
||||
);
|
||||
}
|
||||
svg {
|
||||
width: 320px;
|
||||
display: block;
|
||||
}
|
||||
#slider {
|
||||
outline: none;
|
||||
}
|
||||
#interaction {
|
||||
display: flex;
|
||||
fill: none;
|
||||
stroke: transparent;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#display {
|
||||
pointer-events: none;
|
||||
}
|
||||
:host([disabled]) #interaction {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.background {
|
||||
fill: none;
|
||||
stroke: var(--control-circular-slider-background);
|
||||
opacity: var(--control-circular-slider-background-opacity);
|
||||
stroke-linecap: round;
|
||||
stroke-width: 24px;
|
||||
}
|
||||
|
||||
.track {
|
||||
outline: none;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 24px;
|
||||
transition: stroke-width 300ms ease-in-out,
|
||||
stroke-dasharray 300ms ease-in-out,
|
||||
stroke-dashoffset 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.track:focus-visible {
|
||||
stroke-width: 28px;
|
||||
}
|
||||
|
||||
.pressed .track {
|
||||
transition: stroke-width 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.current {
|
||||
stroke: var(--primary-text-color);
|
||||
transform: rotate(var(--current-angle, 0));
|
||||
transition: transform 300ms ease-in-out;
|
||||
}
|
||||
|
||||
#value {
|
||||
stroke: var(--control-circular-slider-color);
|
||||
}
|
||||
|
||||
#low {
|
||||
stroke: var(--control-circular-slider-low-color);
|
||||
}
|
||||
|
||||
#high {
|
||||
stroke: var(--control-circular-slider-high-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-control-circular-slider": HaControlCircularSlider;
|
||||
}
|
||||
}
|
|
@ -176,7 +176,7 @@ export class HaControlSlider extends LitElement {
|
|||
this._mc = undefined;
|
||||
}
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
this.removeEventListener("keyup", this._handleKeyDown);
|
||||
this.removeEventListener("keyup", this._handleKeyUp);
|
||||
}
|
||||
|
||||
private get _tenPercentStep() {
|
||||
|
|
|
@ -3,6 +3,8 @@ import { customElement, property } from "lit/decorators";
|
|||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
|
||||
const _blockQuoteToAlert = { Note: "info", Warning: "warning" };
|
||||
|
||||
@customElement("ha-markdown-element")
|
||||
class HaMarkdownElement extends ReactiveElement {
|
||||
@property() public content?;
|
||||
|
@ -65,6 +67,34 @@ class HaMarkdownElement extends ReactiveElement {
|
|||
node.loading = "lazy";
|
||||
}
|
||||
node.addEventListener("load", this._resize);
|
||||
} else if (node instanceof HTMLQuoteElement) {
|
||||
// Map GitHub blockquote elements to our ha-alert element
|
||||
const firstElementChild = node.firstElementChild;
|
||||
const quoteTitleElement = firstElementChild?.firstElementChild;
|
||||
const quoteType =
|
||||
quoteTitleElement?.textContent &&
|
||||
_blockQuoteToAlert[quoteTitleElement.textContent];
|
||||
|
||||
// GitHub is strict on how these are defined, we need to make sure we know what we have before starting to replace it
|
||||
if (quoteTitleElement?.nodeName === "STRONG" && quoteType) {
|
||||
const alertNote = document.createElement("ha-alert");
|
||||
alertNote.alertType = quoteType;
|
||||
alertNote.title =
|
||||
(firstElementChild!.childNodes[1].nodeName === "#text" &&
|
||||
firstElementChild!.childNodes[1].textContent?.trimStart()) ||
|
||||
"";
|
||||
|
||||
const childNodes = Array.from(firstElementChild!.childNodes);
|
||||
for (const child of childNodes.slice(
|
||||
childNodes.findIndex(
|
||||
// There is always a line break between the title and the content, we want to skip that
|
||||
(childNode) => childNode instanceof HTMLBRElement
|
||||
) + 1
|
||||
)) {
|
||||
alertNote.appendChild(child);
|
||||
}
|
||||
node.firstElementChild!.replaceWith(alertNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { mdiImagePlus } from "@mdi/js";
|
|||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { createImage, generateImageThumbnailUrl } from "../data/image";
|
||||
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
CropOptions,
|
||||
|
|
|
@ -23,19 +23,19 @@ import "@polymer/paper-item/paper-item";
|
|||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { guard } from "lit/directives/guard";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { LocalStorage } from "../common/decorators/local-storage";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
|
@ -47,10 +47,10 @@ import {
|
|||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import { updateCanInstall, UpdateEntity } from "../data/update";
|
||||
import { UpdateEntity, updateCanInstall } from "../data/update";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { loadSortable, SortableInstance } from "../resources/sortable.ondemand";
|
||||
import { SortableInstance, loadSortable } from "../resources/sortable.ondemand";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
import "./ha-icon";
|
||||
|
@ -214,15 +214,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||
|
||||
private sortableStyleLoaded = false;
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("sidebarPanelOrder", true, {
|
||||
attribute: false,
|
||||
@storage({
|
||||
key: "sidebarPanelOrder",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
})
|
||||
private _panelOrder: string[] = [];
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("sidebarHiddenPanels", true, {
|
||||
attribute: false,
|
||||
@storage({
|
||||
key: "sidebarHiddenPanels",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
})
|
||||
private _hiddenPanels: string[] = [];
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
MediaPlayerBrowseAction,
|
||||
|
@ -43,7 +43,12 @@ class BrowseMediaTTS extends LitElement {
|
|||
|
||||
@state() private _provider?: TTSEngine;
|
||||
|
||||
@LocalStorage("TtsMessage", true, false) private _message!: string;
|
||||
@storage({
|
||||
key: "TtsMessage",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _message!: string;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-card>
|
||||
|
|
|
@ -1,54 +1,15 @@
|
|||
import { HomeAssistant } from "../types";
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
interface Image {
|
||||
filesize: number;
|
||||
name: string;
|
||||
uploaded_at: string; // isoformat date
|
||||
content_type: string;
|
||||
id: string;
|
||||
interface ImageEntityAttributes extends HassEntityAttributeBase {
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
export interface ImageMutableParams {
|
||||
name: string;
|
||||
export interface ImageEntity extends HassEntityBase {
|
||||
attributes: ImageEntityAttributes;
|
||||
}
|
||||
|
||||
export const generateImageThumbnailUrl = (mediaId: string, size: number) =>
|
||||
`/api/image/serve/${mediaId}/${size}x${size}`;
|
||||
|
||||
export const fetchImages = (hass: HomeAssistant) =>
|
||||
hass.callWS<Image[]>({ type: "image/list" });
|
||||
|
||||
export const createImage = async (
|
||||
hass: HomeAssistant,
|
||||
file: File
|
||||
): Promise<Image> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const resp = await hass.fetchWithAuth("/api/image/upload", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
if (resp.status === 413) {
|
||||
throw new Error(`Uploaded image is too large (${file.name})`);
|
||||
} else if (resp.status !== 200) {
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
return resp.json();
|
||||
};
|
||||
|
||||
export const updateImage = (
|
||||
hass: HomeAssistant,
|
||||
id: string,
|
||||
updates: Partial<ImageMutableParams>
|
||||
) =>
|
||||
hass.callWS<Image>({
|
||||
type: "image/update",
|
||||
media_id: id,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteImage = (hass: HomeAssistant, id: string) =>
|
||||
hass.callWS({
|
||||
type: "image/delete",
|
||||
media_id: id,
|
||||
});
|
||||
export const computeImageUrl = (entity: ImageEntity): string =>
|
||||
`/api/image_proxy/${entity.entity_id}?token=${entity.attributes.access_token}&state=${entity.state}`;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { HomeAssistant } from "../types";
|
||||
|
||||
interface Image {
|
||||
filesize: number;
|
||||
name: string;
|
||||
uploaded_at: string; // isoformat date
|
||||
content_type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ImageMutableParams {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const generateImageThumbnailUrl = (mediaId: string, size: number) =>
|
||||
`/api/image/serve/${mediaId}/${size}x${size}`;
|
||||
|
||||
export const fetchImages = (hass: HomeAssistant) =>
|
||||
hass.callWS<Image[]>({ type: "image/list" });
|
||||
|
||||
export const createImage = async (
|
||||
hass: HomeAssistant,
|
||||
file: File
|
||||
): Promise<Image> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const resp = await hass.fetchWithAuth("/api/image/upload", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
if (resp.status === 413) {
|
||||
throw new Error(`Uploaded image is too large (${file.name})`);
|
||||
} else if (resp.status !== 200) {
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
return resp.json();
|
||||
};
|
||||
|
||||
export const updateImage = (
|
||||
hass: HomeAssistant,
|
||||
id: string,
|
||||
updates: Partial<ImageMutableParams>
|
||||
) =>
|
||||
hass.callWS<Image>({
|
||||
type: "image/update",
|
||||
media_id: id,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteImage = (hass: HomeAssistant, id: string) =>
|
||||
hass.callWS({
|
||||
type: "image/delete",
|
||||
media_id: id,
|
||||
});
|
|
@ -260,10 +260,10 @@ export const calculateStatisticsSumGrowth = (
|
|||
export const statisticsHaveType = (
|
||||
stats: StatisticValue[],
|
||||
type: StatisticType
|
||||
) => stats.some((stat) => stat[type] !== null);
|
||||
) => stats.some((stat) => stat[type] !== undefined && stat[type] !== null);
|
||||
|
||||
const mean_stat_types: readonly StatisticType[] = ["mean", "min", "max"];
|
||||
const sum_stat_types: readonly StatisticType[] = ["sum"];
|
||||
const sum_stat_types: readonly StatisticType[] = ["sum", "state", "change"];
|
||||
|
||||
export const statisticsMetaHasType = (
|
||||
metadata: StatisticsMetaData,
|
||||
|
|
|
@ -40,6 +40,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
|||
"fan",
|
||||
"group",
|
||||
"humidifier",
|
||||
"image",
|
||||
"input_boolean",
|
||||
"input_datetime",
|
||||
"light",
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-camera-stream";
|
||||
import { computeImageUrl, ImageEntity } from "../../../data/image";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-image")
|
||||
class MoreInfoImage extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: ImageEntity;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<img
|
||||
alt=${this.stateObj.attributes.friendly_name || this.stateObj.entity_id}
|
||||
src=${this.hass.hassUrl(computeImageUrl(this.stateObj))}
|
||||
/> `;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-image": MoreInfoImage;
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
|
|||
fan: () => import("./controls/more-info-fan"),
|
||||
group: () => import("./controls/more-info-group"),
|
||||
humidifier: () => import("./controls/more-info-humidifier"),
|
||||
image: () => import("./controls/more-info-image"),
|
||||
input_boolean: () => import("./controls/more-info-input_boolean"),
|
||||
input_datetime: () => import("./controls/more-info-input_datetime"),
|
||||
light: () => import("./controls/more-info-light"),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { mdiPlayCircleOutline } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
|
@ -25,10 +25,12 @@ export class TTSTryDialog extends LitElement {
|
|||
|
||||
@query("#message") private _messageInput?: HaTextArea;
|
||||
|
||||
@LocalStorage("ttsTryMessages", false, false) private _messages?: Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
@storage({
|
||||
key: "ttsTryMessages",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _messages?: Record<string, string>;
|
||||
|
||||
public showDialog(params: TTSTryDialogParams) {
|
||||
this._params = params;
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../../components/ha-button";
|
||||
|
@ -57,7 +57,12 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||
|
||||
@state() private _opened = false;
|
||||
|
||||
@LocalStorage("AssistPipelineId", true, false) private _pipelineId?: string;
|
||||
@storage({
|
||||
key: "AssistPipelineId",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _pipelineId?: string;
|
||||
|
||||
@state() private _pipeline?: AssistPipeline;
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { computeRTL } from "../common/util/compute_rtl";
|
|||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
|
||||
@customElement("hass-subpage")
|
||||
class HassSubpage extends LitElement {
|
||||
|
@ -73,7 +74,9 @@ class HassSubpage extends LitElement {
|
|||
<div class="main-title"><slot name="header">${this.header}</slot></div>
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
<div class="content" @scroll=${this._saveScrollPos}><slot></slot></div>
|
||||
<div class="content ha-scrollbar" @scroll=${this._saveScrollPos}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div id="fab">
|
||||
<slot name="fab"></slot>
|
||||
</div>
|
||||
|
@ -94,88 +97,91 @@ class HassSubpage extends LitElement {
|
|||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: var(--header-height);
|
||||
padding: 8px 12px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: 400;
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
.toolbar {
|
||||
padding: 4px;
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
pointer-events: auto;
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
:host([narrow]) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin: 0 0 0 24px;
|
||||
line-height: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: var(--header-height);
|
||||
padding: 8px 12px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: 400;
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
.toolbar {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 1px - var(--header-height));
|
||||
overflow-y: auto;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
pointer-events: auto;
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
#fab {
|
||||
position: absolute;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
:host([rtl]) #fab {
|
||||
right: auto;
|
||||
left: calc(16px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][is-wide]) #fab {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
right: auto;
|
||||
}
|
||||
`;
|
||||
.main-title {
|
||||
margin: 0 0 0 24px;
|
||||
line-height: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 1px - var(--header-height));
|
||||
overflow-y: auto;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
#fab {
|
||||
position: absolute;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
:host([rtl]) #fab {
|
||||
right: auto;
|
||||
left: calc(16px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][is-wide]) #fab {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
right: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import "../../components/ha-card";
|
||||
|
@ -41,7 +41,10 @@ class PanelCalendar extends LitElement {
|
|||
|
||||
@state() private _error?: string = undefined;
|
||||
|
||||
@LocalStorage("deSelectedCalendars", true)
|
||||
@storage({
|
||||
key: "deSelectedCalendars",
|
||||
state: true,
|
||||
})
|
||||
private _deSelectedCalendars: string[] = [];
|
||||
|
||||
private _start?: Date;
|
||||
|
|
|
@ -20,7 +20,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
|||
import "./action/ha-automation-action";
|
||||
import "./condition/ha-automation-condition";
|
||||
import "./trigger/ha-automation-trigger";
|
||||
import { LocalStorage } from "../../../common/decorators/local-storage";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("manual-automation-editor")
|
||||
export class HaManualAutomationEditor extends LitElement {
|
||||
|
@ -36,7 +36,12 @@ export class HaManualAutomationEditor extends LitElement {
|
|||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@LocalStorage("automationClipboard", true, false, window.sessionStorage)
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
private _clipboard: Clipboard = {};
|
||||
|
||||
protected render() {
|
||||
|
|
|
@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item";
|
|||
import { mdiPlayCircleOutline, mdiRobot } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { LocalStorage } from "../../../../common/decorators/local-storage";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
|
@ -31,9 +31,19 @@ export class DialogTryTts extends LitElement {
|
|||
|
||||
@query("#message") private _messageInput?: HaTextArea;
|
||||
|
||||
@LocalStorage("cloudTtsTryMessage", false, false) private _message!: string;
|
||||
@storage({
|
||||
key: "cloudTtsTryMessage",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _message!: string;
|
||||
|
||||
@LocalStorage("cloudTtsTryTarget", false, false) private _target!: string;
|
||||
@storage({
|
||||
key: "cloudTtsTryTarget",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _target!: string;
|
||||
|
||||
public showDialog(params: TryTtsDialogParams) {
|
||||
this._params = params;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { LocalStorage } from "../../../../../common/decorators/local-storage";
|
||||
import { storage } from "../../../../../common/decorators/storage";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-code-editor";
|
||||
import "../../../../../components/ha-formfield";
|
||||
|
@ -21,19 +21,39 @@ class HaPanelDevMqtt extends LitElement {
|
|||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@LocalStorage("panel-dev-mqtt-topic-ls", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-mqtt-topic-ls",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _topic = "";
|
||||
|
||||
@LocalStorage("panel-dev-mqtt-payload-ls", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-mqtt-payload-ls",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _payload = "";
|
||||
|
||||
@LocalStorage("panel-dev-mqtt-qos-ls", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-mqtt-qos-ls",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _qos = "0";
|
||||
|
||||
@LocalStorage("panel-dev-mqtt-retain-ls", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-mqtt-retain-ls",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _retain = false;
|
||||
|
||||
@LocalStorage("panel-dev-mqtt-allow-template-ls", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-mqtt-allow-template-ls",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _allowTemplate = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { formatTime } from "../../../../../common/datetime/format_time";
|
|||
import { MQTTMessage, subscribeMQTTTopic } from "../../../../../data/mqtt";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { LocalStorage } from "../../../../../common/decorators/local-storage";
|
||||
import { storage } from "../../../../../common/decorators/storage";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-switch";
|
||||
|
||||
|
@ -18,13 +18,25 @@ const qosLevel = ["0", "1", "2"];
|
|||
class MqttSubscribeCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@LocalStorage("panel-dev-mqtt-topic-subscribe", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-mqtt-topic-subscribe",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _topic = "";
|
||||
|
||||
@LocalStorage("panel-dev-mqtt-qos-subscribe", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-mqtt-qos-subscribe",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _qos = "0";
|
||||
|
||||
@LocalStorage("panel-dev-mqtt-json-format", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-mqtt-json-format",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _json_format = false;
|
||||
|
||||
@state() private _subscribed?: () => void;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { mdiHelpCircle } from "@mdi/js";
|
|||
import deepClone from "deep-clone-simple";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { LocalStorage } from "../../../common/decorators/local-storage";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
|
@ -26,7 +26,12 @@ export class HaManualScriptEditor extends LitElement {
|
|||
|
||||
@property({ attribute: false }) public config!: ScriptConfig;
|
||||
|
||||
@LocalStorage("automationClipboard", true, false, window.sessionStorage)
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
private _clipboard: Clipboard = {};
|
||||
|
||||
protected render() {
|
||||
|
|
|
@ -200,7 +200,7 @@ export class AssistPipelineDebug extends LitElement {
|
|||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="row heading">
|
||||
<span>Speech-to-Text</span>
|
||||
<span>Speech-to-text</span>
|
||||
${renderProgress(this.hass, this.pipelineRun, "stt")}
|
||||
</div>
|
||||
${this.pipelineRun.stt
|
||||
|
@ -274,7 +274,7 @@ export class AssistPipelineDebug extends LitElement {
|
|||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="row heading">
|
||||
<span>Text-to-Speech</span>
|
||||
<span>Text-to-speech</span>
|
||||
${renderProgress(this.hass, this.pipelineRun, "tts")}
|
||||
</div>
|
||||
${this.pipelineRun.tts
|
||||
|
|
|
@ -4,7 +4,7 @@ import { load } from "js-yaml";
|
|||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { LocalStorage } from "../../../common/decorators/local-storage";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { hasTemplate } from "../../../common/string/has-template";
|
||||
|
@ -38,10 +38,18 @@ class HaPanelDevService extends LitElement {
|
|||
|
||||
@state() private _uiAvailable = true;
|
||||
|
||||
@LocalStorage("panel-dev-service-state-service-data", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-service-state-service-data",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _serviceData?: ServiceAction = { service: "", target: {}, data: {} };
|
||||
|
||||
@LocalStorage("panel-dev-service-state-yaml-mode", true, false)
|
||||
@storage({
|
||||
key: "panel-dev-service-state-yaml-mode",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _yamlMode = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
|
@ -58,7 +58,11 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
|
|||
|
||||
@state() private _endDate: Date;
|
||||
|
||||
@LocalStorage("historyPickedValue", true, false)
|
||||
@storage({
|
||||
key: "historyPickedValue",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _targetPickerValue?: HassServiceTarget;
|
||||
|
||||
@state() private _isLoading = false;
|
||||
|
|
|
@ -4,14 +4,15 @@ import {
|
|||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
|
@ -27,7 +28,6 @@ import "../../../components/ha-card";
|
|||
import "../../../components/ha-icon";
|
||||
import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
import { LightEntity } from "../../../data/light";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
|
@ -35,21 +35,12 @@ import { findEntities } from "../common/find-entities";
|
|||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
|
||||
import {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceHeaderFooter,
|
||||
} from "../types";
|
||||
import { LovelaceCard, LovelaceHeaderFooter } from "../types";
|
||||
import { HuiErrorCard } from "./hui-error-card";
|
||||
import { EntityCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-entity-card")
|
||||
export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-entity-card-editor");
|
||||
return document.createElement("hui-entity-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
entities: string[],
|
||||
|
@ -70,6 +61,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||
};
|
||||
}
|
||||
|
||||
public static async getConfigForm() {
|
||||
return (await import("../editor/config-elements/hui-entity-card-editor"))
|
||||
.default;
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EntityCardConfig;
|
||||
|
|
|
@ -3,19 +3,22 @@ import {
|
|||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import "../../../components/ha-card";
|
||||
import { computeImageUrl, ImageEntity } from "../../../data/image";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { PictureCardConfig } from "./types";
|
||||
|
||||
|
@ -30,8 +33,6 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
|||
return {
|
||||
type: "picture",
|
||||
image: "https://demo.home-assistant.io/stub_config/t-shirt-promo.png",
|
||||
tap_action: { action: "none" },
|
||||
hold_action: { action: "none" },
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -44,7 +45,7 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
|||
}
|
||||
|
||||
public setConfig(config: PictureCardConfig): void {
|
||||
if (!config || !config.image) {
|
||||
if (!config || (!config.image && !config.image_entity)) {
|
||||
throw new Error("Image required");
|
||||
}
|
||||
|
||||
|
@ -52,10 +53,21 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
|||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size === 1 && changedProps.has("hass")) {
|
||||
return !changedProps.get("hass");
|
||||
if (!this._config || hasConfigChanged(this, changedProps)) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
if (this._config.image_entity && changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (
|
||||
!oldHass ||
|
||||
oldHass.states[this._config.image_entity] !==
|
||||
this.hass!.states[this._config.image_entity]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
|
@ -83,6 +95,17 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
|||
return nothing;
|
||||
}
|
||||
|
||||
let stateObj: ImageEntity | undefined;
|
||||
|
||||
if (this._config.image_entity) {
|
||||
stateObj = this.hass.states[this._config.image_entity] as ImageEntity;
|
||||
if (!stateObj) {
|
||||
return html`<hui-warning>
|
||||
${createEntityNotFoundWarning(this.hass, this._config.image_entity)}
|
||||
</hui-warning>`;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
@action=${this._handleAction}
|
||||
|
@ -91,19 +114,29 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
|||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
tabindex=${ifDefined(
|
||||
hasAction(this._config.tap_action) ? "0" : undefined
|
||||
hasAction(this._config.tap_action) || this._config.image_entity
|
||||
? "0"
|
||||
: undefined
|
||||
)}
|
||||
class=${classMap({
|
||||
clickable: Boolean(
|
||||
this._config.tap_action ||
|
||||
this._config.hold_action ||
|
||||
this._config.double_tap_action
|
||||
(this._config.image_entity && !this._config.tap_action) ||
|
||||
(this._config.tap_action &&
|
||||
this._config.tap_action.action !== "none") ||
|
||||
(this._config.hold_action &&
|
||||
this._config.hold_action.action !== "none") ||
|
||||
(this._config.double_tap_action &&
|
||||
this._config.double_tap_action.action !== "none")
|
||||
),
|
||||
})}
|
||||
>
|
||||
<img
|
||||
alt=${this._config.alt_text}
|
||||
src=${this.hass.hassUrl(this._config.image)}
|
||||
alt=${ifDefined(
|
||||
this._config.alt_text || stateObj?.attributes.friendly_name
|
||||
)}
|
||||
src=${this.hass.hassUrl(
|
||||
stateObj ? computeImageUrl(stateObj) : this._config.image
|
||||
)}
|
||||
/>
|
||||
</ha-card>
|
||||
`;
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { customElement, property, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import "../../../components/ha-card";
|
||||
import { ImageEntity, computeImageUrl } from "../../../data/image";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||
|
@ -62,7 +63,12 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
|||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
} else if (
|
||||
!(config.image || config.camera_image || config.state_image) ||
|
||||
!(
|
||||
config.image ||
|
||||
config.image_entity ||
|
||||
config.camera_image ||
|
||||
config.state_image
|
||||
) ||
|
||||
(config.state_image && !config.entity)
|
||||
) {
|
||||
throw new Error("Image required");
|
||||
|
@ -115,12 +121,17 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
|||
return nothing;
|
||||
}
|
||||
|
||||
let stateObj: ImageEntity | undefined;
|
||||
if (this._config.image_entity) {
|
||||
stateObj = this.hass.states[this._config.image_entity] as ImageEntity;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card .header=${this._config.title}>
|
||||
<div id="root">
|
||||
<hui-image
|
||||
.hass=${this.hass}
|
||||
.image=${this._config.image}
|
||||
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
|
||||
.stateImage=${this._config.state_image}
|
||||
.stateFilter=${this._config.state_filter}
|
||||
.cameraImage=${this._config.camera_image}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
|||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/ha-card";
|
||||
import { computeImageUrl, ImageEntity } from "../../../data/image";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
|
@ -68,7 +69,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
|||
}
|
||||
|
||||
if (
|
||||
computeDomain(config.entity) !== "camera" &&
|
||||
!["camera", "image"].includes(computeDomain(config.entity)) &&
|
||||
!config.image &&
|
||||
!config.state_image &&
|
||||
!config.camera_image
|
||||
|
@ -141,14 +142,18 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
|||
footer = html`<div class="footer single">${entityState}</div>`;
|
||||
}
|
||||
|
||||
const domain = computeDomain(this._config.entity);
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<hui-image
|
||||
.hass=${this.hass}
|
||||
.image=${this._config.image}
|
||||
.image=${domain === "image"
|
||||
? computeImageUrl(stateObj as ImageEntity)
|
||||
: this._config.image}
|
||||
.stateImage=${this._config.state_image}
|
||||
.stateFilter=${this._config.state_filter}
|
||||
.cameraImage=${computeDomain(this._config.entity) === "camera"
|
||||
.cameraImage=${domain === "camera"
|
||||
? this._config.entity
|
||||
: this._config.camera_image}
|
||||
.cameraView=${this._config.camera_view}
|
||||
|
|
|
@ -3,9 +3,9 @@ import {
|
|||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
@ -18,6 +18,7 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
|
|||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-state-icon";
|
||||
import { computeImageUrl, ImageEntity } from "../../../data/image";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
|
@ -63,7 +64,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: PictureGlanceCardConfig;
|
||||
|
||||
|
@ -80,7 +81,12 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||
!config ||
|
||||
!config.entities ||
|
||||
!Array.isArray(config.entities) ||
|
||||
!(config.image || config.camera_image || config.state_image) ||
|
||||
!(
|
||||
config.image ||
|
||||
config.image_entity ||
|
||||
config.camera_image ||
|
||||
config.state_image
|
||||
) ||
|
||||
(config.state_image && !config.entity)
|
||||
) {
|
||||
throw new Error("Invalid configuration");
|
||||
|
@ -108,25 +114,35 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (hasConfigOrEntityChanged(this, changedProps)) {
|
||||
if (!this._config || hasConfigOrEntityChanged(this, changedProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
oldHass.themes !== this.hass!.themes ||
|
||||
oldHass.locale !== this.hass!.locale
|
||||
oldHass.themes !== this.hass.themes ||
|
||||
oldHass.locale !== this.hass.locale
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this._config.image_entity &&
|
||||
oldHass.states[this._config.image_entity] !==
|
||||
this.hass.states[this._config.image_entity]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._entitiesDialog) {
|
||||
for (const entity of this._entitiesDialog) {
|
||||
if (
|
||||
oldHass!.states[entity.entity] !== this.hass!.states[entity.entity]
|
||||
) {
|
||||
if (oldHass.states[entity.entity] !== this.hass.states[entity.entity]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -134,9 +150,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||
|
||||
if (this._entitiesToggle) {
|
||||
for (const entity of this._entitiesToggle) {
|
||||
if (
|
||||
oldHass!.states[entity.entity] !== this.hass!.states[entity.entity]
|
||||
) {
|
||||
if (oldHass.states[entity.entity] !== this.hass.states[entity.entity]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +184,11 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||
return nothing;
|
||||
}
|
||||
|
||||
let stateObj: ImageEntity | undefined;
|
||||
if (this._config.image_entity) {
|
||||
stateObj = this.hass.states[this._config.image_entity] as ImageEntity;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<hui-image
|
||||
|
@ -177,7 +196,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||
clickable: Boolean(
|
||||
this._config.tap_action ||
|
||||
this._config.hold_action ||
|
||||
this._config.camera_image
|
||||
this._config.camera_image ||
|
||||
this._config.image_entity
|
||||
),
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
|
@ -190,7 +210,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||
)}
|
||||
.config=${this._config}
|
||||
.hass=${this.hass}
|
||||
.image=${this._config.image}
|
||||
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
|
||||
.stateImage=${this._config.state_image}
|
||||
.stateFilter=${this._config.state_filter}
|
||||
.cameraImage=${this._config.camera_image}
|
||||
|
@ -200,7 +220,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
|||
></hui-image>
|
||||
<div class="box">
|
||||
${this._config.title
|
||||
? html` <div class="title">${this._config.title}</div> `
|
||||
? html`<div class="title">${this._config.title}</div>`
|
||||
: ""}
|
||||
<div class="row">
|
||||
${this._entitiesDialog!.map((entityConf) =>
|
||||
|
|
|
@ -335,6 +335,7 @@ export interface StatisticCardConfig extends LovelaceCardConfig {
|
|||
|
||||
export interface PictureCardConfig extends LovelaceCardConfig {
|
||||
image?: string;
|
||||
image_entity?: string;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
|
@ -345,6 +346,7 @@ export interface PictureCardConfig extends LovelaceCardConfig {
|
|||
export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
image?: string;
|
||||
image_entity?: string;
|
||||
camera_image?: string;
|
||||
camera_view?: HuiImage["cameraView"];
|
||||
state_image?: Record<string, unknown>;
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
AlarmPanelCardConfig,
|
||||
EntitiesCardConfig,
|
||||
HumidifierCardConfig,
|
||||
PictureCardConfig,
|
||||
PictureEntityCardConfig,
|
||||
ThermostatCardConfig,
|
||||
} from "../cards/types";
|
||||
|
@ -125,6 +126,12 @@ export const computeCards = (
|
|||
entity: entityId,
|
||||
};
|
||||
cards.push(cardConfig);
|
||||
} else if (domain === "image") {
|
||||
const cardConfig: PictureCardConfig = {
|
||||
type: "picture",
|
||||
image_entity: entityId,
|
||||
};
|
||||
cards.push(cardConfig);
|
||||
} else if (domain === "climate") {
|
||||
const cardConfig: ThermostatCardConfig = {
|
||||
type: "thermostat",
|
||||
|
|
|
@ -18,6 +18,7 @@ declare global {
|
|||
export type ActionConfigParams = {
|
||||
entity?: string;
|
||||
camera_image?: string;
|
||||
image_entity?: string;
|
||||
hold_action?: ActionConfig;
|
||||
tap_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
|
@ -87,9 +88,11 @@ export const handleAction = async (
|
|||
|
||||
switch (actionConfig.action) {
|
||||
case "more-info": {
|
||||
if (config.entity || config.camera_image) {
|
||||
if (config.entity || config.camera_image || config.image_entity) {
|
||||
fireEvent(node, "hass-more-info", {
|
||||
entityId: config.entity ? config.entity : config.camera_image!,
|
||||
entityId: (config.entity ||
|
||||
config.camera_image ||
|
||||
config.image_entity)!,
|
||||
});
|
||||
} else {
|
||||
showToast(node, {
|
||||
|
|
|
@ -10,12 +10,14 @@ import { customElement, property, state } from "lit/decorators";
|
|||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { STATES_OFF } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
import "../../../components/ha-camera-stream";
|
||||
import type { HaCameraStream } from "../../../components/ha-camera-stream";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { CameraEntity, fetchThumbnailUrlWithCache } from "../../../data/camera";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { computeImageUrl, ImageEntity } from "../../../data/image";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
const UPDATE_INTERVAL = 10000;
|
||||
|
@ -164,6 +166,8 @@ export class HuiImage extends LitElement {
|
|||
}
|
||||
} else if (this.darkModeImage && this.hass.themes.darkMode) {
|
||||
imageSrc = this.darkModeImage;
|
||||
} else if (stateObj && computeDomain(stateObj.entity_id) === "image") {
|
||||
imageSrc = computeImageUrl(stateObj as ImageEntity);
|
||||
} else {
|
||||
imageSrc = this.image;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { customElement } from "lit/decorators";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace";
|
||||
import { getCardElementClass } from "../../create-element/create-card-element";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
|
||||
@customElement("hui-card-element-editor")
|
||||
|
@ -16,6 +16,17 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
|||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async getConfigForm(): Promise<LovelaceConfigForm | undefined> {
|
||||
const elClass = await getCardElementClass(this.configElementType!);
|
||||
|
||||
// Check if a schema exists
|
||||
if (elClass && elClass.getConfigForm) {
|
||||
return elClass.getConfigForm();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { EntityCardConfig } from "../../cards/types";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { EntityCardConfig } from "../../cards/types";
|
||||
import { headerFooterConfigStructs } from "../../header-footer/structs";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { LovelaceConfigForm } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
const struct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
entity: optional(string()),
|
||||
|
@ -54,67 +50,19 @@ const SCHEMA = [
|
|||
{ name: "state_color", selector: { boolean: {} } },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
@customElement("hui-entity-card-editor")
|
||||
export class HuiEntityCardEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EntityCardConfig;
|
||||
|
||||
public setConfig(config: EntityCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const config = ev.detail.value;
|
||||
Object.keys(config).forEach((k) => config[k] === "" && delete config[k]);
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
if (schema.name === "entity") {
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.entity"
|
||||
);
|
||||
}
|
||||
] as HaFormSchema[];
|
||||
|
||||
const entityCardConfigForm: LovelaceConfigForm = {
|
||||
schema: SCHEMA,
|
||||
assertConfig: (config: EntityCardConfig) => assert(config, struct),
|
||||
computeLabel: (schema: HaFormSchema, localize: LocalizeFunc) => {
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
return `${localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
)} (${localize("ui.panel.lovelace.editor.card.config.optional")})`;
|
||||
}
|
||||
return localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
|
||||
},
|
||||
};
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entity-card-editor": HuiEntityCardEditor;
|
||||
}
|
||||
}
|
||||
export default entityCardConfigForm;
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceGenericElementEditor } from "../../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
|
||||
@customElement("hui-form-editor")
|
||||
export class HuiFormEditor
|
||||
extends LitElement
|
||||
implements LovelaceGenericElementEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public schema!: HaFormSchema[];
|
||||
|
||||
@state() private _config?: LovelaceCardConfig;
|
||||
|
||||
public assertConfig(_config: LovelaceCardConfig): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceCardConfig): void {
|
||||
this.assertConfig(config);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${this.schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
public computeLabel = (
|
||||
_schema: HaFormSchema,
|
||||
_localize: LocalizeFunc
|
||||
): string | undefined => undefined;
|
||||
|
||||
public computeHelper = (
|
||||
_schema: HaFormSchema,
|
||||
_localize: LocalizeFunc
|
||||
): string | undefined => undefined;
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema) =>
|
||||
this.computeLabel(schema, this.hass.localize) ||
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
) ||
|
||||
capitalizeFirstLetter(schema.name.split("_").join(" "));
|
||||
|
||||
private _computeHelperCallback = (schema: HaFormSchema) =>
|
||||
this.computeHelper(schema, this.hass.localize);
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const config = ev.detail.value;
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = configElementStyle;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-form-editor": HuiFormEditor;
|
||||
}
|
||||
}
|
|
@ -1,22 +1,21 @@
|
|||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-theme-picker";
|
||||
import { ActionConfig } from "../../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { PictureCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-action-editor";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { EditorTarget } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
image: optional(string()),
|
||||
image_entity: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
theme: optional(string()),
|
||||
|
@ -24,6 +23,21 @@ const cardConfigStruct = assign(
|
|||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "image", selector: { text: {} } },
|
||||
{ name: "image_entity", selector: { entity: { domain: "image" } } },
|
||||
{ name: "alt_text", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: { ui_action: {} },
|
||||
},
|
||||
{
|
||||
name: "hold_action",
|
||||
selector: { ui_action: {} },
|
||||
},
|
||||
] as const;
|
||||
|
||||
@customElement("hui-picture-card-editor")
|
||||
export class HuiPictureCardEditor
|
||||
extends LitElement
|
||||
|
@ -38,129 +52,45 @@ export class HuiPictureCardEditor
|
|||
this._config = config;
|
||||
}
|
||||
|
||||
get _image(): string {
|
||||
return this._config!.image || "";
|
||||
}
|
||||
|
||||
get _tap_action(): ActionConfig {
|
||||
return this._config!.tap_action || { action: "none" };
|
||||
}
|
||||
|
||||
get _hold_action(): ActionConfig {
|
||||
return this._config!.hold_action || { action: "none" };
|
||||
}
|
||||
|
||||
get _theme(): string {
|
||||
return this._config!.theme || "";
|
||||
}
|
||||
|
||||
get _alt_text(): string {
|
||||
return this._config!.alt_text || "";
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const actions = ["navigate", "url", "call-service", "none"];
|
||||
|
||||
return html`
|
||||
<div class="card-config">
|
||||
<ha-textfield
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.image"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.required"
|
||||
)})"
|
||||
.value=${this._image}
|
||||
.configValue=${"image"}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.alt_text"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value=${this._alt_text}
|
||||
.configValue=${"alt_text"}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-theme-picker>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.tap_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._tap_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"tap_action"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-action-editor>
|
||||
<hui-action-editor
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.hold_action"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.config=${this._hold_action}
|
||||
.actions=${actions}
|
||||
.configValue=${"hold_action"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-action-editor>
|
||||
</div>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
const target = ev.target! as EditorTarget;
|
||||
const value = ev.detail?.value ?? target.value;
|
||||
|
||||
if (this[`_${target.configValue}`] === value) {
|
||||
return;
|
||||
}
|
||||
if (target.configValue) {
|
||||
if (value !== false && !value) {
|
||||
this._config = { ...this._config };
|
||||
delete this._config[target.configValue!];
|
||||
} else {
|
||||
this._config = {
|
||||
...this._config,
|
||||
[target.configValue!]: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
switch (schema.name) {
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
default:
|
||||
return (
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.picture-card.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -22,6 +22,7 @@ const cardConfigStruct = assign(
|
|||
title: optional(string()),
|
||||
entity: optional(string()),
|
||||
image: optional(string()),
|
||||
image_entity: optional(string()),
|
||||
camera_image: optional(string()),
|
||||
camera_view: optional(string()),
|
||||
aspect_ratio: optional(string()),
|
||||
|
@ -35,6 +36,7 @@ const cardConfigStruct = assign(
|
|||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{ name: "image", selector: { text: {} } },
|
||||
{ name: "image_entity", selector: { entity: { domain: "image" } } },
|
||||
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
|
||||
{
|
||||
name: "",
|
||||
|
|
|
@ -187,7 +187,7 @@ export class HuiStatisticsGraphCardEditor
|
|||
),
|
||||
disabled:
|
||||
!metaDatas ||
|
||||
!metaDatas.every((metaData) =>
|
||||
!metaDatas.some((metaData) =>
|
||||
statisticsMetaHasType(
|
||||
metaData,
|
||||
supportedStatTypeMap[stat_type]
|
||||
|
@ -246,12 +246,10 @@ export class HuiStatisticsGraphCardEditor
|
|||
);
|
||||
const configured_stat_types = this._config!.stat_types
|
||||
? ensureArray(this._config.stat_types)
|
||||
: stat_types.filter(
|
||||
(stat_type) =>
|
||||
stat_type !== "change" &&
|
||||
this._metaDatas?.every((metaData) =>
|
||||
statisticsMetaHasType(metaData, stat_type)
|
||||
)
|
||||
: stat_types.filter((stat_type) =>
|
||||
this._metaDatas?.some((metaData) =>
|
||||
statisticsMetaHasType(metaData, stat_type)
|
||||
)
|
||||
);
|
||||
const data = {
|
||||
chart_type: "line",
|
||||
|
@ -320,9 +318,7 @@ export class HuiStatisticsGraphCardEditor
|
|||
: undefined;
|
||||
if (config.stat_types && config.entities.length) {
|
||||
config.stat_types = ensureArray(config.stat_types).filter((stat_type) =>
|
||||
metadata!.every((metaData) =>
|
||||
statisticsMetaHasType(metaData, stat_type)
|
||||
)
|
||||
metadata!.some((metaData) => statisticsMetaHasType(metaData, stat_type))
|
||||
);
|
||||
if (!config.stat_types.length) {
|
||||
delete config.stat_types;
|
||||
|
|
|
@ -8,13 +8,13 @@ import {
|
|||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { property, state, query } from "lit/decorators";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../common/structs/handle-errors";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-code-editor";
|
||||
import "../../../components/ha-alert";
|
||||
import type { HaCodeEditor } from "../../../components/ha-code-editor";
|
||||
import type {
|
||||
LovelaceCardConfig,
|
||||
|
@ -23,11 +23,15 @@ import type {
|
|||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceRowConfig } from "../entity-rows/types";
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import type { LovelaceGenericElementEditor } from "../types";
|
||||
import { LovelaceTileFeatureConfig } from "../tile-features/types";
|
||||
import type {
|
||||
LovelaceConfigForm,
|
||||
LovelaceGenericElementEditor,
|
||||
} from "../types";
|
||||
import type { HuiFormEditor } from "./config-elements/hui-form-editor";
|
||||
import "./config-elements/hui-generic-entity-row-editor";
|
||||
import { GUISupportError } from "./gui-support-error";
|
||||
import { EditSubElementEvent, GUIModeChangedEvent } from "./types";
|
||||
import { LovelaceTileFeatureConfig } from "../tile-features/types";
|
||||
|
||||
export interface ConfigChangedEvent {
|
||||
config:
|
||||
|
@ -182,6 +186,10 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
protected async getConfigForm(): Promise<LovelaceConfigForm | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected get configElementType(): string | undefined {
|
||||
return this.value ? (this.value as any).type : undefined;
|
||||
}
|
||||
|
@ -328,6 +336,25 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
|||
this._loading = true;
|
||||
configElement = await this.getConfigElement();
|
||||
|
||||
if (!configElement) {
|
||||
const form = await this.getConfigForm();
|
||||
if (form) {
|
||||
await import("./config-elements/hui-form-editor");
|
||||
configElement = document.createElement("hui-form-editor");
|
||||
const { schema, assertConfig, computeLabel, computeHelper } = form;
|
||||
(configElement as HuiFormEditor).schema = schema;
|
||||
if (computeLabel) {
|
||||
(configElement as HuiFormEditor).computeLabel = computeLabel;
|
||||
}
|
||||
if (computeHelper) {
|
||||
(configElement as HuiFormEditor).computeHelper = computeHelper;
|
||||
}
|
||||
if (assertConfig) {
|
||||
(configElement as HuiFormEditor).assertConfig = assertConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (configElement) {
|
||||
configElement.hass = this.hass;
|
||||
if ("lovelace" in configElement) {
|
||||
|
|
|
@ -4,7 +4,10 @@ import {
|
|||
LovelaceTileFeatureConfig,
|
||||
LovelaceTileFeatureContext,
|
||||
} from "../../tile-features/types";
|
||||
import type { LovelaceTileFeatureEditor } from "../../types";
|
||||
import type {
|
||||
LovelaceConfigForm,
|
||||
LovelaceTileFeatureEditor,
|
||||
} from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
|
||||
@customElement("hui-tile-feature-element-editor")
|
||||
|
@ -24,6 +27,17 @@ export class HuiTileFeatureElementEditor extends HuiElementEditor<
|
|||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async getConfigForm(): Promise<LovelaceConfigForm | undefined> {
|
||||
const elClass = await getTileFeatureElementClass(this.configElementType!);
|
||||
|
||||
// Check if a schema exists
|
||||
if (elClass && elClass.getConfigForm) {
|
||||
return elClass.getConfigForm();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { ImageEntity, computeImageUrl } from "../../../data/image";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { computeTooltip } from "../common/compute-tooltip";
|
||||
|
@ -34,12 +35,16 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
|
|||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
let stateObj: ImageEntity | undefined;
|
||||
if (this._config.image_entity) {
|
||||
stateObj = this.hass.states[this._config.image_entity] as ImageEntity;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hui-image
|
||||
.hass=${this.hass}
|
||||
.entity=${this._config.entity}
|
||||
.image=${this._config.image}
|
||||
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
|
||||
.stateImage=${this._config.state_image}
|
||||
.cameraImage=${this._config.camera_image}
|
||||
.cameraView=${this._config.camera_view}
|
||||
|
|
|
@ -42,6 +42,7 @@ export interface ImageElementConfig extends LovelaceElementConfigBase {
|
|||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
image?: string;
|
||||
image_entity?: string;
|
||||
state_image?: string;
|
||||
camera_image?: string;
|
||||
camera_view?: HuiImage["cameraView"];
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { HaFormSchema } from "../../components/ha-form/types";
|
||||
import {
|
||||
LovelaceBadgeConfig,
|
||||
LovelaceCardConfig,
|
||||
|
@ -45,6 +47,19 @@ export interface LovelaceCard extends HTMLElement {
|
|||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
||||
export interface LovelaceConfigForm {
|
||||
schema: HaFormSchema[];
|
||||
assertConfig?: (config: LovelaceCardConfig) => void;
|
||||
computeLabel?: (
|
||||
schema: HaFormSchema,
|
||||
localize: LocalizeFunc
|
||||
) => string | undefined;
|
||||
computeHelper?: (
|
||||
schema: HaFormSchema,
|
||||
localize: LocalizeFunc
|
||||
) => string | undefined;
|
||||
}
|
||||
|
||||
export interface LovelaceCardConstructor extends Constructor<LovelaceCard> {
|
||||
getStubConfig?: (
|
||||
hass: HomeAssistant,
|
||||
|
@ -52,6 +67,7 @@ export interface LovelaceCardConstructor extends Constructor<LovelaceCard> {
|
|||
entitiesFallback: string[]
|
||||
) => LovelaceCardConfig;
|
||||
getConfigElement?: () => LovelaceCardEditor;
|
||||
getConfigForm?: () => LovelaceConfigForm;
|
||||
}
|
||||
|
||||
export interface LovelaceHeaderFooterConstructor
|
||||
|
@ -104,11 +120,15 @@ export interface LovelaceTileFeature extends HTMLElement {
|
|||
|
||||
export interface LovelaceTileFeatureConstructor
|
||||
extends Constructor<LovelaceTileFeature> {
|
||||
getConfigElement?: () => LovelaceTileFeatureEditor;
|
||||
getStubConfig?: (
|
||||
hass: HomeAssistant,
|
||||
stateObj?: HassEntity
|
||||
) => LovelaceTileFeatureConfig;
|
||||
getConfigElement?: () => LovelaceTileFeatureEditor;
|
||||
getConfigForm?: () => {
|
||||
schema: HaFormSchema[];
|
||||
assertConfig?: (config: LovelaceCardConfig) => void;
|
||||
};
|
||||
isSupported?: (stateObj?: HassEntity) => boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-menu-button";
|
||||
|
@ -71,7 +71,11 @@ class PanelMediaBrowser extends LitElement {
|
|||
},
|
||||
];
|
||||
|
||||
@LocalStorage("mediaBrowseEntityId", true, false)
|
||||
@storage({
|
||||
key: "mediaBrowseEntityId",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _entityId = BROWSER_PLAYER;
|
||||
|
||||
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
type Vector = [number, number];
|
||||
type Matrix = [Vector, Vector];
|
||||
|
||||
const rotateVector = ([[a, b], [c, d]]: Matrix, [x, y]: Vector): Vector => [
|
||||
a * x + b * y,
|
||||
c * x + d * y,
|
||||
];
|
||||
const createRotateMatrix = (x: number): Matrix => [
|
||||
[Math.cos(x), -Math.sin(x)],
|
||||
[Math.sin(x), Math.cos(x)],
|
||||
];
|
||||
const addVector = ([a1, a2]: Vector, [b1, b2]: Vector): Vector => [
|
||||
a1 + b1,
|
||||
a2 + b2,
|
||||
];
|
||||
|
||||
export const toRadian = (angle: number) => (angle / 180) * Math.PI;
|
||||
|
||||
type ArcOptions = {
|
||||
x: number;
|
||||
y: number;
|
||||
r: number;
|
||||
start: number;
|
||||
end: number;
|
||||
rotate?: number;
|
||||
};
|
||||
|
||||
export const arc = (options: ArcOptions) => {
|
||||
const { x, y, r, start, end, rotate = 0 } = options;
|
||||
const cx = x;
|
||||
const cy = y;
|
||||
const rx = r;
|
||||
const ry = r;
|
||||
const t1 = toRadian(start);
|
||||
const t2 = toRadian(end);
|
||||
const delta = (t2 - t1) % (2 * Math.PI);
|
||||
const phi = toRadian(rotate);
|
||||
|
||||
const rotMatrix = createRotateMatrix(phi);
|
||||
const [sX, sY] = addVector(
|
||||
rotateVector(rotMatrix, [rx * Math.cos(t1), ry * Math.sin(t1)]),
|
||||
[cx, cy]
|
||||
);
|
||||
const [eX, eY] = addVector(
|
||||
rotateVector(rotMatrix, [
|
||||
rx * Math.cos(t1 + delta),
|
||||
ry * Math.sin(t1 + delta),
|
||||
]),
|
||||
[cx, cy]
|
||||
);
|
||||
const fA = delta > Math.PI ? 1 : 0;
|
||||
const fS = delta > 0 ? 1 : 0;
|
||||
|
||||
return [
|
||||
"M",
|
||||
sX,
|
||||
sY,
|
||||
"A",
|
||||
rx,
|
||||
ry,
|
||||
(phi / (2 * Math.PI)) * 360,
|
||||
fA,
|
||||
fS,
|
||||
eX,
|
||||
eY,
|
||||
].join(" ");
|
||||
};
|
|
@ -3,8 +3,10 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
|||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import "../components/entity/state-info";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
import { LockEntityFeature } from "../data/lock";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
|
@ -19,10 +21,19 @@ class StateCardLock extends LocalizeMixin(PolymerElement) {
|
|||
height: 37px;
|
||||
margin-right: -0.57em;
|
||||
}
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="horizontal justified layout">
|
||||
${this.stateInfoTemplate}
|
||||
<mwc-button
|
||||
on-click="_callService"
|
||||
data-service="open"
|
||||
hidden$="[[!supportsOpen]]"
|
||||
>[[localize('ui.card.lock.open')]]</mwc-button
|
||||
>
|
||||
<mwc-button
|
||||
on-click="_callService"
|
||||
data-service="unlock"
|
||||
|
@ -61,12 +72,14 @@ class StateCardLock extends LocalizeMixin(PolymerElement) {
|
|||
value: false,
|
||||
},
|
||||
isLocked: Boolean,
|
||||
supportsOpen: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
_stateObjChanged(newVal) {
|
||||
if (newVal) {
|
||||
this.isLocked = newVal.state === "locked";
|
||||
this.supportsOpen = supportsFeature(newVal, LockEntityFeature.OPEN);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -147,7 +147,8 @@
|
|||
"lock": {
|
||||
"code": "[%key:ui::card::alarm_control_panel::code%]",
|
||||
"lock": "Lock",
|
||||
"unlock": "Unlock"
|
||||
"unlock": "Unlock",
|
||||
"open": "Open"
|
||||
},
|
||||
"media_player": {
|
||||
"source": "Source",
|
||||
|
@ -4666,6 +4667,7 @@
|
|||
"aspect_ratio": "Aspect Ratio",
|
||||
"attribute": "Attribute",
|
||||
"camera_image": "Camera Entity",
|
||||
"image_entity": "Image Entity",
|
||||
"camera_view": "Camera View",
|
||||
"double_tap_action": "Double Tap Action",
|
||||
"entities": "Entities",
|
||||
|
|
Loading…
Reference in New Issue