mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 19:56:42 +00:00
Convert HUI-IMAGE to TypeScript/Lit (#2713)
* Fix gallery demos * Convert HUI-IMAGE to TypeScript/Lit * Clean up
This commit is contained in:
parent
f23258eb8c
commit
310b81de04
@ -2,6 +2,17 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import "../components/demo-cards";
|
import "../components/demo-cards";
|
||||||
|
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||||
|
import { getEntity } from "../../../src/fake_data/entity";
|
||||||
|
|
||||||
|
const ENTITIES = [
|
||||||
|
getEntity("light", "kitchen_lights", "on", {
|
||||||
|
friendly_name: "Kitchen Lights",
|
||||||
|
}),
|
||||||
|
getEntity("light", "bed_light", "off", {
|
||||||
|
friendly_name: "Bed Light",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
const CONFIGS = [
|
const CONFIGS = [
|
||||||
{
|
{
|
||||||
@ -10,6 +21,8 @@ const CONFIGS = [
|
|||||||
- type: picture-entity
|
- type: picture-entity
|
||||||
image: /images/kitchen.png
|
image: /images/kitchen.png
|
||||||
entity: light.kitchen_lights
|
entity: light.kitchen_lights
|
||||||
|
tap_action:
|
||||||
|
action: toggle
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -18,6 +31,8 @@ const CONFIGS = [
|
|||||||
- type: picture-entity
|
- type: picture-entity
|
||||||
image: /images/bed.png
|
image: /images/bed.png
|
||||||
entity: light.bed_light
|
entity: light.bed_light
|
||||||
|
tap_action:
|
||||||
|
action: toggle
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -68,7 +83,7 @@ const CONFIGS = [
|
|||||||
class DemoPicEntity extends PolymerElement {
|
class DemoPicEntity extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<demo-cards configs="[[_configs]]"></demo-cards>
|
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +95,12 @@ class DemoPicEntity extends PolymerElement {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ready() {
|
||||||
|
super.ready();
|
||||||
|
const hass = provideHass(this.$.demos);
|
||||||
|
hass.addEntities(ENTITIES);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("demo-hui-picture-entity-card", DemoPicEntity);
|
customElements.define("demo-hui-picture-entity-card", DemoPicEntity);
|
||||||
|
@ -2,6 +2,25 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import "../components/demo-cards";
|
import "../components/demo-cards";
|
||||||
|
import { getEntity } from "../../../src/fake_data/entity";
|
||||||
|
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
|
const ENTITIES = [
|
||||||
|
getEntity("switch", "decorative_lights", "on", {
|
||||||
|
friendly_name: "Decorative Lights",
|
||||||
|
}),
|
||||||
|
getEntity("light", "ceiling_lights", "on", {
|
||||||
|
friendly_name: "Ceiling Lights",
|
||||||
|
}),
|
||||||
|
getEntity("binary_sensor", "movement_backyard", "on", {
|
||||||
|
friendly_name: "Movement Backyard",
|
||||||
|
device_class: "moving",
|
||||||
|
}),
|
||||||
|
getEntity("binary_sensor", "basement_floor_wet", "off", {
|
||||||
|
friendly_name: "Basement Floor Wet",
|
||||||
|
device_class: "moisture",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
const CONFIGS = [
|
const CONFIGS = [
|
||||||
{
|
{
|
||||||
@ -105,7 +124,7 @@ const CONFIGS = [
|
|||||||
class DemoPicGlance extends PolymerElement {
|
class DemoPicGlance extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<demo-cards configs="[[_configs]]"></demo-cards>
|
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +136,12 @@ class DemoPicGlance extends PolymerElement {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ready() {
|
||||||
|
super.ready();
|
||||||
|
const hass = provideHass(this.$.demos);
|
||||||
|
hass.addEntities(ENTITIES);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("demo-hui-picture-glance-card", DemoPicGlance);
|
customElements.define("demo-hui-picture-glance-card", DemoPicGlance);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const { babelLoaderConfig } = require("../config/babel.js");
|
const { babelLoaderConfig } = require("../config/babel.js");
|
||||||
const webpackBase = require("../config/babel.js");
|
const webpackBase = require("../config/webpack.js");
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === "production";
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||||
|
20
src/common/file/b64-to-blob.ts
Normal file
20
src/common/file/b64-to-blob.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// https://stackoverflow.com/a/16245768
|
||||||
|
export const b64toBlob = (b64Data, contentType = "", sliceSize = 512) => {
|
||||||
|
const byteCharacters = atob(b64Data);
|
||||||
|
const byteArrays: Uint8Array[] = [];
|
||||||
|
|
||||||
|
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||||
|
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||||
|
|
||||||
|
const byteNumbers = new Array(slice.length);
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
byteNumbers[i] = slice.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
|
||||||
|
byteArrays.push(byteArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Blob(byteArrays, { type: contentType });
|
||||||
|
};
|
12
src/data/camera.ts
Normal file
12
src/data/camera.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface CameraThumbnail {
|
||||||
|
content_type: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) =>
|
||||||
|
hass.callWS<CameraThumbnail>({
|
||||||
|
type: "camera_thumbnail",
|
||||||
|
entity_id: entityId,
|
||||||
|
});
|
@ -1,199 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
|
||||||
|
|
||||||
import { STATES_OFF } from "../../../common/const";
|
|
||||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
|
||||||
|
|
||||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 10000;
|
|
||||||
const DEFAULT_FILTER = "grayscale(100%)";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
*/
|
|
||||||
class HuiImage extends LocalizeMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
${this.styleTemplate}
|
|
||||||
<div id="wrapper">
|
|
||||||
<img
|
|
||||||
id="image"
|
|
||||||
src="[[_imageSrc]]"
|
|
||||||
on-error="_onImageError"
|
|
||||||
on-load="_onImageLoad"
|
|
||||||
/>
|
|
||||||
<div id="brokenImage"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styleTemplate() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
img {
|
|
||||||
display: block;
|
|
||||||
height: auto;
|
|
||||||
transition: filter 0.2s linear;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ratio {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ratio img,
|
|
||||||
.ratio div {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#brokenImage {
|
|
||||||
background: grey url("/static/images/image-broken.svg") center/36px
|
|
||||||
no-repeat;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
observer: "_hassChanged",
|
|
||||||
},
|
|
||||||
entity: String,
|
|
||||||
image: String,
|
|
||||||
stateImage: Object,
|
|
||||||
cameraImage: String,
|
|
||||||
aspectRatio: String,
|
|
||||||
filter: String,
|
|
||||||
stateFilter: Object,
|
|
||||||
_imageSrc: String,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get observers() {
|
|
||||||
return ["_configChanged(image, stateImage, cameraImage, aspectRatio)"];
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
if (this.cameraImage) {
|
|
||||||
this.timer = setInterval(
|
|
||||||
() => this._updateCameraImageSrc(),
|
|
||||||
UPDATE_INTERVAL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
clearInterval(this.timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
_configChanged(image, stateImage, cameraImage, aspectRatio) {
|
|
||||||
const ratio = parseAspectRatio(aspectRatio);
|
|
||||||
|
|
||||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
|
||||||
this.$.wrapper.style.paddingBottom = `${(
|
|
||||||
(100 * ratio.h) /
|
|
||||||
ratio.w
|
|
||||||
).toFixed(2)}%`;
|
|
||||||
this.$.wrapper.classList.add("ratio");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cameraImage) {
|
|
||||||
this._updateCameraImageSrc();
|
|
||||||
} else if (image && !stateImage) {
|
|
||||||
this._imageSrc = image;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onImageError() {
|
|
||||||
this._imageSrc = null;
|
|
||||||
this.$.image.classList.add("hidden");
|
|
||||||
if (!this.$.wrapper.classList.contains("ratio")) {
|
|
||||||
this.$.brokenImage.style.setProperty(
|
|
||||||
"height",
|
|
||||||
`${this._lastImageHeight || "100"}px`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.$.brokenImage.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
_onImageLoad() {
|
|
||||||
this.$.image.classList.remove("hidden");
|
|
||||||
this.$.brokenImage.classList.add("hidden");
|
|
||||||
if (!this.$.wrapper.classList.contains("ratio")) {
|
|
||||||
this._lastImageHeight = this.$.image.offsetHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_hassChanged(hass) {
|
|
||||||
if (this.cameraImage || !this.entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateObj = hass.states[this.entity];
|
|
||||||
const newState = !stateObj ? "unavailable" : stateObj.state;
|
|
||||||
|
|
||||||
if (newState === this._currentState) return;
|
|
||||||
this._currentState = newState;
|
|
||||||
|
|
||||||
this._updateStateImage();
|
|
||||||
this._updateStateFilter(stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateStateImage() {
|
|
||||||
if (!this.stateImage) {
|
|
||||||
this._imageFallback = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const stateImg = this.stateImage[this._currentState];
|
|
||||||
this._imageSrc = stateImg || this.image;
|
|
||||||
this._imageFallback = !stateImg;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateStateFilter(stateObj) {
|
|
||||||
let filter;
|
|
||||||
if (!this.stateFilter) {
|
|
||||||
filter = this.filter;
|
|
||||||
} else {
|
|
||||||
filter = this.stateFilter[this._currentState] || this.filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOff = !stateObj || STATES_OFF.includes(stateObj.state);
|
|
||||||
this.$.image.style.filter =
|
|
||||||
filter || (isOff && this._imageFallback && DEFAULT_FILTER) || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async _updateCameraImageSrc() {
|
|
||||||
try {
|
|
||||||
const { content_type: contentType, content } = await this.hass.callWS({
|
|
||||||
type: "camera_thumbnail",
|
|
||||||
entity_id: this.cameraImage,
|
|
||||||
});
|
|
||||||
this._imageSrc = `data:${contentType};base64, ${content}`;
|
|
||||||
this._onImageLoad();
|
|
||||||
} catch (err) {
|
|
||||||
this._onImageError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hui-image", HuiImage);
|
|
226
src/panels/lovelace/components/hui-image.ts
Normal file
226
src/panels/lovelace/components/hui-image.ts
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import "@polymer/paper-toggle-button/paper-toggle-button";
|
||||||
|
|
||||||
|
import { STATES_OFF } from "../../../common/const";
|
||||||
|
|
||||||
|
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
|
PropertyValues,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { styleMap } from "lit-html/directives/style-map";
|
||||||
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
import { b64toBlob } from "../../../common/file/b64-to-blob";
|
||||||
|
import { fetchThumbnail } from "../../../data/camera";
|
||||||
|
|
||||||
|
const UPDATE_INTERVAL = 10000;
|
||||||
|
const DEFAULT_FILTER = "grayscale(100%)";
|
||||||
|
|
||||||
|
export interface StateSpecificConfig {
|
||||||
|
[state: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @appliesMixin LocalizeMixin
|
||||||
|
*/
|
||||||
|
class HuiImage extends LitElement {
|
||||||
|
@property() public hass?: HomeAssistant;
|
||||||
|
@property() public entity?: string;
|
||||||
|
@property() public image?: string;
|
||||||
|
@property() public stateImage?: StateSpecificConfig;
|
||||||
|
@property() public cameraImage?: string;
|
||||||
|
@property() public aspectRatio?: string;
|
||||||
|
@property() public filter?: string;
|
||||||
|
@property() public stateFilter?: StateSpecificConfig;
|
||||||
|
|
||||||
|
@property() private _loadError?: boolean;
|
||||||
|
@property() private _cameraImageSrc?: string;
|
||||||
|
@query("img") private _image!: HTMLImageElement;
|
||||||
|
private _lastImageHeight?: number;
|
||||||
|
private _cameraUpdater?: number;
|
||||||
|
private _attached?: boolean;
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._attached = true;
|
||||||
|
this._startUpdateCameraInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._attached = false;
|
||||||
|
this._stopUpdateCameraInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
const ratio = this.aspectRatio ? parseAspectRatio(this.aspectRatio) : null;
|
||||||
|
const stateObj =
|
||||||
|
this.hass && this.entity ? this.hass.states[this.entity] : undefined;
|
||||||
|
const state = stateObj ? stateObj.state : "unavailable";
|
||||||
|
|
||||||
|
// Figure out image source to use
|
||||||
|
let imageSrc: string | undefined;
|
||||||
|
// Track if we are we using a fallback image, used for filter.
|
||||||
|
let imageFallback = !this.stateImage;
|
||||||
|
|
||||||
|
if (this.cameraImage) {
|
||||||
|
imageSrc = this._cameraImageSrc;
|
||||||
|
} else if (this.stateImage) {
|
||||||
|
const stateImage = this.stateImage[state];
|
||||||
|
|
||||||
|
if (stateImage) {
|
||||||
|
imageSrc = stateImage;
|
||||||
|
} else {
|
||||||
|
imageSrc = this.image;
|
||||||
|
imageFallback = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imageSrc = this.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out filter to use
|
||||||
|
let filter = this.filter || "";
|
||||||
|
|
||||||
|
if (this.stateFilter && this.stateFilter[state]) {
|
||||||
|
filter = this.stateFilter[state];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter && this.entity) {
|
||||||
|
const isOff = !stateObj || STATES_OFF.includes(state);
|
||||||
|
filter = isOff && imageFallback ? DEFAULT_FILTER : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
style=${styleMap({
|
||||||
|
paddingBottom:
|
||||||
|
ratio && ratio.w > 0 && ratio.h > 0
|
||||||
|
? `${((100 * ratio.h) / ratio.w).toFixed(2)}%`
|
||||||
|
: "",
|
||||||
|
})}
|
||||||
|
class=${classMap({
|
||||||
|
ratio: Boolean(ratio && ratio.w > 0 && ratio.h > 0),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
id="image"
|
||||||
|
src=${imageSrc}
|
||||||
|
@error=${this._onImageError}
|
||||||
|
@load=${this._onImageLoad}
|
||||||
|
style=${styleMap({
|
||||||
|
filter,
|
||||||
|
display: this._loadError ? "none" : "block",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
id="brokenImage"
|
||||||
|
style=${styleMap({
|
||||||
|
height: `${this._lastImageHeight || "100"}px`,
|
||||||
|
display: this._loadError ? "block" : "none",
|
||||||
|
})}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has("cameraImage")) {
|
||||||
|
this._updateCameraImageSrc();
|
||||||
|
this._startUpdateCameraInterval();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _startUpdateCameraInterval() {
|
||||||
|
this._stopUpdateCameraInterval();
|
||||||
|
if (this.cameraImage && this._attached) {
|
||||||
|
this._cameraUpdater = window.setInterval(
|
||||||
|
() => this._updateCameraImageSrc(),
|
||||||
|
UPDATE_INTERVAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopUpdateCameraInterval() {
|
||||||
|
if (this._cameraUpdater) {
|
||||||
|
clearInterval(this._cameraUpdater);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onImageError() {
|
||||||
|
this._loadError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _onImageLoad() {
|
||||||
|
this._loadError = false;
|
||||||
|
await this.updateComplete;
|
||||||
|
this._lastImageHeight = this._image.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateCameraImageSrc() {
|
||||||
|
if (!this.hass || !this.cameraImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._cameraImageSrc) {
|
||||||
|
URL.revokeObjectURL(this._cameraImageSrc);
|
||||||
|
this._cameraImageSrc = undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { content_type: contentType, content } = await fetchThumbnail(
|
||||||
|
this.hass,
|
||||||
|
this.cameraImage
|
||||||
|
);
|
||||||
|
this._cameraImageSrc = URL.createObjectURL(
|
||||||
|
b64toBlob(content, contentType)
|
||||||
|
);
|
||||||
|
this._onImageLoad();
|
||||||
|
} catch (err) {
|
||||||
|
this._onImageError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
transition: filter 0.2s linear;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio img,
|
||||||
|
.ratio div {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#brokenImage {
|
||||||
|
background: grey url("/static/images/image-broken.svg") center/36px
|
||||||
|
no-repeat;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-image": HuiImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("hui-image", HuiImage);
|
Loading…
x
Reference in New Issue
Block a user