From 117b40d7f5411a49ecd42542559eaceb5a893837 Mon Sep 17 00:00:00 2001 From: Bram Kragten <mail@bramkragten.nl> Date: Wed, 27 Dec 2023 16:06:34 +0100 Subject: [PATCH 1/4] Add QR code element --- src/components/ha-markdown-element.ts | 7 ++ src/components/ha-markdown.ts | 5 - src/components/ha-qr-code.ts | 114 ++++++++++++++++++++ src/panels/config/tags/dialog-tag-detail.ts | 69 +++--------- src/resources/markdown-worker.ts | 8 ++ 5 files changed, 144 insertions(+), 59 deletions(-) create mode 100644 src/components/ha-qr-code.ts diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts index e00bfceb9477..3f2c904428f4 100644 --- a/src/components/ha-markdown-element.ts +++ b/src/components/ha-markdown-element.ts @@ -95,6 +95,13 @@ class HaMarkdownElement extends ReactiveElement { } node.firstElementChild!.replaceWith(alertNote); } + } else if ( + node instanceof HTMLElement && + ["ha-alert", "ha-qr-code", "ha-icon", "ha-svg-icon"].includes( + node.localName + ) + ) { + import(`./${node.localName}`); } } } diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index c248a320e819..f81ddd286037 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -2,11 +2,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; 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") export class HaMarkdown extends LitElement { @property() public content?; diff --git a/src/components/ha-qr-code.ts b/src/components/ha-qr-code.ts new file mode 100644 index 000000000000..f6a686d06823 --- /dev/null +++ b/src/components/ha-qr-code.ts @@ -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; + } + `; +} diff --git a/src/panels/config/tags/dialog-tag-detail.ts b/src/panels/config/tags/dialog-tag-detail.ts index 7f402f2d4a4d..62c0e6fe3f33 100644 --- a/src/panels/config/tags/dialog-tag-detail.ts +++ b/src/panels/config/tags/dialog-tag-detail.ts @@ -1,17 +1,11 @@ import "@material/mwc-button"; -import { - css, - CSSResultGroup, - html, - LitElement, - TemplateResult, - nothing, -} from "lit"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-alert"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-formfield"; +import "../../../components/ha-qr-code"; import "../../../components/ha-switch"; import "../../../components/ha-textfield"; import { Tag, UpdateTagParams } from "../../../data/tag"; @@ -20,8 +14,6 @@ import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { TagDetailDialogParams } from "./show-dialog-tag-detail"; -const QR_LOGO_URL = "/static/icons/favicon-192x192.png"; - @customElement("dialog-tag-detail") class DialogTagDetail extends LitElement @@ -39,8 +31,6 @@ class DialogTagDetail @state() private _submitting = false; - @state() private _qrCode?: TemplateResult; - public showDialog(params: TagDetailDialogParams): void { this._params = params; this._error = undefined; @@ -50,13 +40,10 @@ class DialogTagDetail this._id = ""; this._name = ""; } - - this._generateQR(); } public closeDialog(): void { this._params = undefined; - this._qrCode = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -130,9 +117,15 @@ class DialogTagDetail })} </p> </div> - ${this._qrCode - ? html` <div id="qr">${this._qrCode}</div> ` - : ""} + <div id="qr"> + <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> @@ -158,7 +151,7 @@ class DialogTagDetail : this.hass!.localize("ui.panel.config.tag.detail.create")} </mwc-button> ${this._params.openWrite && !this._params.entry - ? html` <mwc-button + ? html`<mwc-button slot="primaryAction" @click=${this._updateWriteEntry} .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 { return [ haStyleDialog, @@ -270,6 +228,9 @@ class DialogTagDetail display: block; margin: 8px 0; } + ::slotted(img) { + height: 100%; + } `, ]; } diff --git a/src/resources/markdown-worker.ts b/src/resources/markdown-worker.ts index a2e2234ecaba..85949e3f2307 100644 --- a/src/resources/markdown-worker.ts +++ b/src/resources/markdown-worker.ts @@ -42,6 +42,14 @@ const renderMarkdown = async ( "ha-icon": ["icon"], "ha-svg-icon": ["path"], "ha-alert": ["alert-type", "title"], + "ha-qr-code": [ + "data", + "scale", + "width", + "margin", + "error-correction-level", + "center-image", + ], }; } From 2555c9fe4905a9f4fb2b492221cb3dc0e9223c16 Mon Sep 17 00:00:00 2001 From: Bram Kragten <mail@bramkragten.nl> Date: Wed, 27 Dec 2023 16:24:49 +0100 Subject: [PATCH 2/4] fixes --- hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts | 1 - package.json | 2 +- src/panels/config/automation/manual-automation-editor.ts | 1 + src/panels/config/storage/dialog-move-datadisk.ts | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts index 8a1c3c670221..4dcfde2ec2b4 100644 --- a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts +++ b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts @@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../src/common/dom/fire_event"; import "../../../../src/components/ha-circular-progress"; -import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-select"; import { extractApiErrorMessage, diff --git a/package.json b/package.json index 5ec1a9f98eb0..db61e11165fc 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "postinstall": "husky install", "prepack": "pinst --disable", "postpack": "pinst --enable", - "test": "instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.cjs \"test/**/*.ts\"" + "test": "gulp gen-icons-json && instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.cjs \"test/**/*.ts\"" }, "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "license": "Apache-2.0", diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 291d30854011..811ea2c3fd0a 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -7,6 +7,7 @@ import { ensureArray } from "../../../common/array/ensure-array"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; +import "../../../components/ha-markdown"; import { Condition, ManualAutomationConfig, diff --git a/src/panels/config/storage/dialog-move-datadisk.ts b/src/panels/config/storage/dialog-move-datadisk.ts index 456c9884e7db..7f5495a8e9dc 100644 --- a/src/panels/config/storage/dialog-move-datadisk.ts +++ b/src/panels/config/storage/dialog-move-datadisk.ts @@ -5,7 +5,6 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import "../../../components/ha-circular-progress"; -import "../../../components/ha-markdown"; import "../../../components/ha-select"; import { extractApiErrorMessage, From 9d4516e7779a9b7e9381b6c4afe01b3709343b3f Mon Sep 17 00:00:00 2001 From: Bram Kragten <mail@bramkragten.nl> Date: Wed, 27 Dec 2023 16:37:37 +0100 Subject: [PATCH 3/4] move to workflow --- .github/workflows/ci.yaml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 49839b27f660..af8a33890396 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -64,7 +64,7 @@ jobs: - name: Install dependencies run: yarn install --immutable - 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 run: yarn run test build: diff --git a/package.json b/package.json index db61e11165fc..5ec1a9f98eb0 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "postinstall": "husky install", "prepack": "pinst --disable", "postpack": "pinst --enable", - "test": "gulp gen-icons-json && instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.cjs \"test/**/*.ts\"" + "test": "instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.cjs \"test/**/*.ts\"" }, "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "license": "Apache-2.0", From 993800444fd0c7bffe7ab1d74d707f60deb2d96b Mon Sep 17 00:00:00 2001 From: Bram Kragten <mail@bramkragten.nl> Date: Wed, 27 Dec 2023 17:11:16 +0100 Subject: [PATCH 4/4] limit webpack imports --- src/components/ha-markdown-element.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts index 3f2c904428f4..0e806b7c7dcf 100644 --- a/src/components/ha-markdown-element.ts +++ b/src/components/ha-markdown-element.ts @@ -101,7 +101,9 @@ class HaMarkdownElement extends ReactiveElement { node.localName ) ) { - import(`./${node.localName}`); + import( + /* webpackInclude: /(ha-alert)|(ha-qr-code)|(ha-icon)|(ha-svg-icon)/ */ `./${node.localName}` + ); } } }