From 0c8cd680c2ba80fb12de3493fb2a7b10f2f2a5a1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 May 2020 21:56:25 +0200 Subject: [PATCH] Allow custom icon sets (#5794) --- src/components/ha-icon.ts | 123 ++++++------------ src/components/ha-svg-icon.ts | 4 +- src/data/custom_iconsets.ts | 15 +++ src/data/iconsets.ts | 79 +++++++++++ .../config-flow/dialog-data-entry-flow.ts | 1 - .../ha-voice-command-dialog.ts | 8 +- 6 files changed, 142 insertions(+), 88 deletions(-) create mode 100644 src/data/custom_iconsets.ts create mode 100644 src/data/iconsets.ts diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index 3d665756c0..4eb0a1d1a1 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -1,5 +1,4 @@ import "@polymer/iron-icon/iron-icon"; -import { get, set, clear, Store } from "idb-keyval"; import { customElement, LitElement, @@ -11,84 +10,23 @@ import { CSSResult, } from "lit-element"; import "./ha-svg-icon"; +import { customIconsets, CustomIcons } from "../data/custom_iconsets"; +import { + Chunks, + MDI_PREFIXES, + getIcon, + findIconChunk, + Icons, + checkCacheVersion, + writeCache, +} from "../data/iconsets"; import { debounce } from "../common/util/debounce"; -import { iconMetadata } from "../resources/icon-metadata"; -import { IconMeta } from "../types"; - -interface Icons { - [key: string]: string; -} - -interface Chunks { - [key: string]: Promise; -} - -const iconStore = new Store("hass-icon-db", "mdi-icon-store"); - -get("_version", iconStore).then((version) => { - if (!version) { - set("_version", iconMetadata.version, iconStore); - } else if (version !== iconMetadata.version) { - clear(iconStore).then(() => - set("_version", iconMetadata.version, iconStore) - ); - } -}); const chunks: Chunks = {}; -const MDI_PREFIXES = ["mdi", "hass", "hassio", "hademo"]; -let toRead: Array<[string, (string) => void]> = []; +checkCacheVersion(); -// Queue up as many icon fetches in 1 transaction -const getIcon = (iconName: string) => - new Promise((resolve) => { - toRead.push([iconName, resolve]); - - if (toRead.length > 1) { - return; - } - - const results: Array<[(string) => void, IDBRequest]> = []; - - iconStore - ._withIDBStore("readonly", (store) => { - for (const [iconName_, resolve_] of toRead) { - results.push([resolve_, store.get(iconName_)]); - } - toRead = []; - }) - .then(() => { - for (const [resolve_, request] of results) { - resolve_(request.result); - } - }); - }); - -const findIconChunk = (icon): string => { - let lastChunk: IconMeta; - for (const chunk of iconMetadata.parts) { - if (chunk.start !== undefined && icon < chunk.start) { - break; - } - lastChunk = chunk; - } - return lastChunk!.file; -}; - -const debouncedWriteCache = debounce(async () => { - const keys = Object.keys(chunks); - const iconsSets: Icons[] = await Promise.all(Object.values(chunks)); - // We do a batch opening the store just once, for (considerable) performance - iconStore._withIDBStore("readwrite", (store) => { - iconsSets.forEach((icons, idx) => { - Object.entries(icons).forEach(([name, path]) => { - store.put(path, name); - }); - delete chunks[keys[idx]]; - }); - }); -}, 2000); +const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); @customElement("ha-icon") export class HaIcon extends LitElement { @@ -96,11 +34,14 @@ export class HaIcon extends LitElement { @property() private _path?: string; - @property() private _noMdi = false; + @property() private _viewBox?; + + @property() private _legacy = false; protected updated(changedProps: PropertyValues) { if (changedProps.has("icon")) { this._path = undefined; + this._viewBox = undefined; this._loadIcon(); } } @@ -109,25 +50,34 @@ export class HaIcon extends LitElement { if (!this.icon) { return html``; } - if (this._noMdi) { + if (this._legacy) { return html``; } - return html``; + return html``; } private async _loadIcon() { if (!this.icon) { return; } - const icon = this.icon.split(":", 2); - if (!MDI_PREFIXES.includes(icon[0])) { - this._noMdi = true; + const [iconPrefix, iconName] = this.icon.split(":", 2); + if (!MDI_PREFIXES.includes(iconPrefix)) { + if (iconPrefix in customIconsets) { + const customIconset = customIconsets[iconPrefix]; + if (customIconset) { + this._setCustomPath(customIconset(iconName), iconName); + } + return; + } + this._legacy = true; return; } - this._noMdi = false; + this._legacy = false; - const iconName = icon[1]; const cachedPath: string = await getIcon(iconName); if (cachedPath) { this._path = cachedPath; @@ -147,6 +97,15 @@ export class HaIcon extends LitElement { debouncedWriteCache(); } + private async _setCustomPath( + promise: Promise, + iconName: string + ) { + const iconPack = await promise; + this._path = iconPack[iconName].path; + this._viewBox = iconPack[iconName].viewBox; + } + private async _setPath(promise: Promise, iconName: string) { const iconPack = await promise; this._path = iconPack[iconName]; diff --git a/src/components/ha-svg-icon.ts b/src/components/ha-svg-icon.ts index 07b0b66cca..6e58ea5378 100644 --- a/src/components/ha-svg-icon.ts +++ b/src/components/ha-svg-icon.ts @@ -12,10 +12,12 @@ import { export class HaSvgIcon extends LitElement { @property() public path?: string; + @property() public viewBox?: string; + protected render(): SVGTemplateResult { return svg` diff --git a/src/data/custom_iconsets.ts b/src/data/custom_iconsets.ts new file mode 100644 index 0000000000..73b2ed4e61 --- /dev/null +++ b/src/data/custom_iconsets.ts @@ -0,0 +1,15 @@ +export interface CustomIcons { + [key: string]: { path: string; viewBox?: string }; +} + +export interface CustomIconsetsWindow { + customIconsets?: { [key: string]: (name: string) => Promise }; +} + +const customIconsetsWindow = window as CustomIconsetsWindow; + +if (!("customIconsets" in customIconsetsWindow)) { + customIconsetsWindow.customIconsets = {}; +} + +export const customIconsets = customIconsetsWindow.customIconsets!; diff --git a/src/data/iconsets.ts b/src/data/iconsets.ts new file mode 100644 index 0000000000..1f28e7dd80 --- /dev/null +++ b/src/data/iconsets.ts @@ -0,0 +1,79 @@ +import { iconMetadata } from "../resources/icon-metadata"; +import { IconMeta } from "../types"; +import { get, set, clear, Store } from "idb-keyval"; + +export interface Icons { + [key: string]: string; +} + +export interface Chunks { + [key: string]: Promise; +} + +export const iconStore = new Store("hass-icon-db", "mdi-icon-store"); + +export const MDI_PREFIXES = ["mdi", "hass", "hassio", "hademo"]; + +let toRead: Array<[string, (string) => void]> = []; + +// Queue up as many icon fetches in 1 transaction +export const getIcon = (iconName: string) => + new Promise((resolve) => { + toRead.push([iconName, resolve]); + + if (toRead.length > 1) { + return; + } + + const results: Array<[(string) => void, IDBRequest]> = []; + + iconStore + ._withIDBStore("readonly", (store) => { + for (const [iconName_, resolve_] of toRead) { + results.push([resolve_, store.get(iconName_)]); + } + toRead = []; + }) + .then(() => { + for (const [resolve_, request] of results) { + resolve_(request.result); + } + }); + }); + +export const findIconChunk = (icon): string => { + let lastChunk: IconMeta; + for (const chunk of iconMetadata.parts) { + if (chunk.start !== undefined && icon < chunk.start) { + break; + } + lastChunk = chunk; + } + return lastChunk!.file; +}; + +export const writeCache = async (chunks: Chunks) => { + const keys = Object.keys(chunks); + const iconsSets: Icons[] = await Promise.all(Object.values(chunks)); + // We do a batch opening the store just once, for (considerable) performance + iconStore._withIDBStore("readwrite", (store) => { + iconsSets.forEach((icons, idx) => { + Object.entries(icons).forEach(([name, path]) => { + store.put(path, name); + }); + delete chunks[keys[idx]]; + }); + }); +}; + +export const checkCacheVersion = () => { + get("_version", iconStore).then((version) => { + if (!version) { + set("_version", iconMetadata.version, iconStore); + } else if (version !== iconMetadata.version) { + clear(iconStore).then(() => + set("_version", iconMetadata.version, iconStore) + ); + } + }); +}; diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 763b7304d2..3ece18035b 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -339,7 +339,6 @@ class DataEntryFlowDialog extends LitElement { ha-icon-button { display: inline-block; padding: 8px; - margin: 16px 16px 0 0; float: right; } `, diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index 686e54b8fa..2ccef81407 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -456,15 +456,15 @@ export class HaVoiceCommandDialog extends LitElement { } .bouncer { - width: 40px; - height: 40px; + width: 48px; + height: 48px; position: absolute; top: 0; } .double-bounce1, .double-bounce2 { - width: 40px; - height: 40px; + width: 48px; + height: 48px; border-radius: 50%; background-color: var(--primary-color); opacity: 0.2;