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}`
+        );
       }
     }
   }