mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Add QR code element (#19155)
* Add QR code element * fixes * move to workflow * limit webpack imports
This commit is contained in:
parent
953a3793c4
commit
caece9d6ad
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -64,7 +64,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --immutable
|
run: yarn install --immutable
|
||||||
- name: Build resources
|
- name: Build resources
|
||||||
run: ./node_modules/.bin/gulp build-translations build-locale-data
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn run test
|
run: yarn run test
|
||||||
build:
|
build:
|
||||||
|
@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import "../../../../src/components/ha-markdown";
|
|
||||||
import "../../../../src/components/ha-select";
|
import "../../../../src/components/ha-select";
|
||||||
import {
|
import {
|
||||||
extractApiErrorMessage,
|
extractApiErrorMessage,
|
||||||
|
@ -95,6 +95,15 @@ class HaMarkdownElement extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
node.firstElementChild!.replaceWith(alertNote);
|
node.firstElementChild!.replaceWith(alertNote);
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
node instanceof HTMLElement &&
|
||||||
|
["ha-alert", "ha-qr-code", "ha-icon", "ha-svg-icon"].includes(
|
||||||
|
node.localName
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
import(
|
||||||
|
/* webpackInclude: /(ha-alert)|(ha-qr-code)|(ha-icon)|(ha-svg-icon)/ */ `./${node.localName}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import "./ha-markdown-element";
|
import "./ha-markdown-element";
|
||||||
|
|
||||||
// Import components that are allwoed to be defined.
|
|
||||||
import "./ha-alert";
|
|
||||||
import "./ha-icon";
|
|
||||||
import "./ha-svg-icon";
|
|
||||||
|
|
||||||
@customElement("ha-markdown")
|
@customElement("ha-markdown")
|
||||||
export class HaMarkdown extends LitElement {
|
export class HaMarkdown extends LitElement {
|
||||||
@property() public content?;
|
@property() public content?;
|
||||||
|
114
src/components/ha-qr-code.ts
Normal file
114
src/components/ha-qr-code.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
@customElement("ha-qr-code")
|
||||||
|
export class HaQrCode extends LitElement {
|
||||||
|
@property() public data?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "error-correction-level" })
|
||||||
|
public errorCorrectionLevel: "low" | "medium" | "quartile" | "high" =
|
||||||
|
"medium";
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public width = 4;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public scale = 4;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public margin = 4;
|
||||||
|
|
||||||
|
@property({ type: Number }) public maskPattern?:
|
||||||
|
| 0
|
||||||
|
| 1
|
||||||
|
| 2
|
||||||
|
| 3
|
||||||
|
| 4
|
||||||
|
| 5
|
||||||
|
| 6
|
||||||
|
| 7;
|
||||||
|
|
||||||
|
@property({ attribute: "center-image" }) public centerImage?: string;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@query("canvas") private _canvas?: HTMLCanvasElement;
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
|
if (
|
||||||
|
(changedProperties.has("data") ||
|
||||||
|
changedProperties.has("scale") ||
|
||||||
|
changedProperties.has("width") ||
|
||||||
|
changedProperties.has("margin") ||
|
||||||
|
changedProperties.has("maskPattern") ||
|
||||||
|
changedProperties.has("errorCorrectionLevel")) &&
|
||||||
|
this._error
|
||||||
|
) {
|
||||||
|
this._error = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProperties: PropertyValues) {
|
||||||
|
const canvas = this._canvas;
|
||||||
|
if (
|
||||||
|
canvas &&
|
||||||
|
this.data &&
|
||||||
|
(changedProperties.has("data") ||
|
||||||
|
changedProperties.has("scale") ||
|
||||||
|
changedProperties.has("width") ||
|
||||||
|
changedProperties.has("margin") ||
|
||||||
|
changedProperties.has("maskPattern") ||
|
||||||
|
changedProperties.has("errorCorrectionLevel") ||
|
||||||
|
changedProperties.has("centerImage"))
|
||||||
|
) {
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
|
||||||
|
QRCode.toCanvas(canvas, this.data, {
|
||||||
|
errorCorrectionLevel: this.errorCorrectionLevel,
|
||||||
|
width: this.width,
|
||||||
|
scale: this.scale,
|
||||||
|
margin: this.margin,
|
||||||
|
maskPattern: this.maskPattern,
|
||||||
|
color: {
|
||||||
|
light: computedStyles.getPropertyValue("--card-background-color"),
|
||||||
|
dark: computedStyles.getPropertyValue("--primary-text-color"),
|
||||||
|
},
|
||||||
|
}).catch((err) => {
|
||||||
|
this._error = err.message;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.centerImage) {
|
||||||
|
const context = this._canvas!.getContext("2d");
|
||||||
|
const imageObj = new Image();
|
||||||
|
imageObj.src = this.centerImage;
|
||||||
|
imageObj.onload = () => {
|
||||||
|
context?.drawImage(
|
||||||
|
imageObj,
|
||||||
|
canvas.width * 0.375,
|
||||||
|
canvas.height * 0.375,
|
||||||
|
canvas.width / 4,
|
||||||
|
canvas.height / 4
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.data) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
if (this._error) {
|
||||||
|
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||||
|
}
|
||||||
|
return html`<canvas></canvas>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
@ -7,6 +7,7 @@ import { ensureArray } from "../../../common/array/ensure-array";
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-markdown";
|
||||||
import {
|
import {
|
||||||
Condition,
|
Condition,
|
||||||
ManualAutomationConfig,
|
ManualAutomationConfig,
|
||||||
|
@ -5,7 +5,6 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||||
import "../../../components/ha-circular-progress";
|
import "../../../components/ha-circular-progress";
|
||||||
import "../../../components/ha-markdown";
|
|
||||||
import "../../../components/ha-select";
|
import "../../../components/ha-select";
|
||||||
import {
|
import {
|
||||||
extractApiErrorMessage,
|
extractApiErrorMessage,
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import {
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
TemplateResult,
|
|
||||||
nothing,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
import "../../../components/ha-formfield";
|
import "../../../components/ha-formfield";
|
||||||
|
import "../../../components/ha-qr-code";
|
||||||
import "../../../components/ha-switch";
|
import "../../../components/ha-switch";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
import { Tag, UpdateTagParams } from "../../../data/tag";
|
import { Tag, UpdateTagParams } from "../../../data/tag";
|
||||||
@ -20,8 +14,6 @@ import { haStyleDialog } from "../../../resources/styles";
|
|||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { TagDetailDialogParams } from "./show-dialog-tag-detail";
|
import { TagDetailDialogParams } from "./show-dialog-tag-detail";
|
||||||
|
|
||||||
const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
|
|
||||||
|
|
||||||
@customElement("dialog-tag-detail")
|
@customElement("dialog-tag-detail")
|
||||||
class DialogTagDetail
|
class DialogTagDetail
|
||||||
extends LitElement
|
extends LitElement
|
||||||
@ -39,8 +31,6 @@ class DialogTagDetail
|
|||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
|
|
||||||
@state() private _qrCode?: TemplateResult;
|
|
||||||
|
|
||||||
public showDialog(params: TagDetailDialogParams): void {
|
public showDialog(params: TagDetailDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
@ -50,13 +40,10 @@ class DialogTagDetail
|
|||||||
this._id = "";
|
this._id = "";
|
||||||
this._name = "";
|
this._name = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
this._generateQR();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._qrCode = undefined;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,9 +117,15 @@ class DialogTagDetail
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
${this._qrCode
|
<div id="qr">
|
||||||
? html` <div id="qr">${this._qrCode}</div> `
|
<ha-qr-code
|
||||||
: ""}
|
.data=${this._params!.entry!.id}
|
||||||
|
center-image="/static/icons/favicon-192x192.png"
|
||||||
|
error-correction-level="quartile"
|
||||||
|
scale="5"
|
||||||
|
>
|
||||||
|
</ha-qr-code>
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
: ``}
|
: ``}
|
||||||
</div>
|
</div>
|
||||||
@ -158,7 +151,7 @@ class DialogTagDetail
|
|||||||
: this.hass!.localize("ui.panel.config.tag.detail.create")}
|
: this.hass!.localize("ui.panel.config.tag.detail.create")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
${this._params.openWrite && !this._params.entry
|
${this._params.openWrite && !this._params.entry
|
||||||
? html` <mwc-button
|
? html`<mwc-button
|
||||||
slot="primaryAction"
|
slot="primaryAction"
|
||||||
@click=${this._updateWriteEntry}
|
@click=${this._updateWriteEntry}
|
||||||
.disabled=${this._submitting || !this._name}
|
.disabled=${this._submitting || !this._name}
|
||||||
@ -221,41 +214,6 @@ class DialogTagDetail
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _generateQR() {
|
|
||||||
const qrcode = await import("qrcode");
|
|
||||||
const canvas = await qrcode.toCanvas(
|
|
||||||
`https://www.home-assistant.io/tag/${this._params!.entry!.id}`,
|
|
||||||
{
|
|
||||||
width: 180,
|
|
||||||
errorCorrectionLevel: "Q",
|
|
||||||
color: {
|
|
||||||
light: "#fff",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
|
|
||||||
const imageObj = new Image();
|
|
||||||
imageObj.src = QR_LOGO_URL;
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
imageObj.onload = resolve;
|
|
||||||
});
|
|
||||||
context?.drawImage(
|
|
||||||
imageObj,
|
|
||||||
canvas.width / 3,
|
|
||||||
canvas.height / 3,
|
|
||||||
canvas.width / 3,
|
|
||||||
canvas.height / 3
|
|
||||||
);
|
|
||||||
|
|
||||||
this._qrCode = html`<img
|
|
||||||
alt=${this.hass.localize("ui.panel.config.tag.qr_code_image", {
|
|
||||||
name: this._name,
|
|
||||||
})}
|
|
||||||
src=${canvas.toDataURL()}
|
|
||||||
></img>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
@ -270,6 +228,9 @@ class DialogTagDetail
|
|||||||
display: block;
|
display: block;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
::slotted(img) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,14 @@ const renderMarkdown = async (
|
|||||||
"ha-icon": ["icon"],
|
"ha-icon": ["icon"],
|
||||||
"ha-svg-icon": ["path"],
|
"ha-svg-icon": ["path"],
|
||||||
"ha-alert": ["alert-type", "title"],
|
"ha-alert": ["alert-type", "title"],
|
||||||
|
"ha-qr-code": [
|
||||||
|
"data",
|
||||||
|
"scale",
|
||||||
|
"width",
|
||||||
|
"margin",
|
||||||
|
"error-correction-level",
|
||||||
|
"center-image",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user