From 31c56eb8b535e0d8d40b1d77804e775eda1c8ff7 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 12 Jun 2025 17:31:34 +0200 Subject: [PATCH] Refactor icon loading --- src/components/ha-icon.ts | 147 ++++---------------------------------- src/data/load_icon.ts | 142 ++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 133 deletions(-) create mode 100644 src/data/load_icon.ts diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index 5016e38c6a..82584de556 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -2,34 +2,9 @@ import type { PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; -import { debounce } from "../common/util/debounce"; -import type { CustomIcon } from "../data/custom_icons"; -import { customIcons } from "../data/custom_icons"; -import type { Chunks, Icons } from "../data/iconsets"; -import { - MDI_PREFIXES, - findIconChunk, - getIcon, - writeCache, -} from "../data/iconsets"; +import { loadIcon } from "../data/load_icon"; import "./ha-svg-icon"; -type DeprecatedIcon = Record< - string, - { - removeIn: string; - newName?: string; - } ->; - -const mdiDeprecatedIcons: DeprecatedIcon = {}; - -const chunks: Chunks = {}; - -const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); - -const cachedIcons: Record = {}; - @customElement("ha-icon") export class HaIcon extends LitElement { @property() public icon?: string; @@ -71,118 +46,24 @@ export class HaIcon extends LitElement { if (!this.icon) { return; } - const requestedIcon = this.icon; - const [iconPrefix, origIconName] = this.icon.split(":", 2); + const result = await loadIcon(this.icon, this._handleWarning); - let iconName = origIconName; - - if (!iconPrefix || !iconName) { + if (result.icon !== this.icon) { + // The icon was changed while we were loading it, so we don't update the state return; } - - if (!MDI_PREFIXES.includes(iconPrefix)) { - const customIcon = customIcons[iconPrefix]; - if (customIcon) { - if (customIcon && typeof customIcon.getIcon === "function") { - this._setCustomPath(customIcon.getIcon(iconName), requestedIcon); - } - return; - } - this._legacy = true; - return; - } - - this._legacy = false; - - if (iconName in mdiDeprecatedIcons) { - const deprecatedIcon = mdiDeprecatedIcons[iconName]; - let message: string; - - if (deprecatedIcon.newName) { - message = `Icon ${iconPrefix}:${iconName} was renamed to ${iconPrefix}:${deprecatedIcon.newName}, please change your config, it will be removed in version ${deprecatedIcon.removeIn}.`; - iconName = deprecatedIcon.newName!; - } else { - message = `Icon ${iconPrefix}:${iconName} was removed from MDI, please replace this icon with an other icon in your config, it will be removed in version ${deprecatedIcon.removeIn}.`; - } - // eslint-disable-next-line no-console - console.warn(message); - fireEvent(this, "write_log", { - level: "warning", - message, - }); - } - - if (iconName in cachedIcons) { - this._path = cachedIcons[iconName]; - return; - } - - if (iconName === "home-assistant") { - const icon = (await import("../resources/home-assistant-logo-svg")) - .mdiHomeAssistant; - - if (this.icon === requestedIcon) { - this._path = icon; - } - cachedIcons[iconName] = icon; - return; - } - - let databaseIcon: string | undefined; - try { - databaseIcon = await getIcon(iconName); - } catch (_err) { - // Firefox in private mode doesn't support IDB - // iOS Safari sometimes doesn't open the DB - databaseIcon = undefined; - } - - if (databaseIcon) { - if (this.icon === requestedIcon) { - this._path = databaseIcon; - } - cachedIcons[iconName] = databaseIcon; - return; - } - const chunk = findIconChunk(iconName); - - if (chunk in chunks) { - this._setPath(chunks[chunk], iconName, requestedIcon); - return; - } - - const iconPromise = fetch(`/static/mdi/${chunk}.json`).then((response) => - response.json() - ); - chunks[chunk] = iconPromise; - this._setPath(iconPromise, iconName, requestedIcon); - debouncedWriteCache(); + this._legacy = result.legacy || false; + this._path = result.path; + this._secondaryPath = result.secondaryPath; + this._viewBox = result.viewBox; } - private async _setCustomPath( - promise: Promise, - requestedIcon: string - ) { - const icon = await promise; - if (this.icon !== requestedIcon) { - return; - } - this._path = icon.path; - this._secondaryPath = icon.secondaryPath; - this._viewBox = icon.viewBox; - } - - private async _setPath( - promise: Promise, - iconName: string, - requestedIcon: string - ) { - const iconPack = await promise; - if (this.icon === requestedIcon) { - this._path = iconPack[iconName]; - } - cachedIcons[iconName] = iconPack[iconName]; - } + private _handleWarning = (message: string) => { + fireEvent(this, "write_log", { + level: "warning", + message, + }); + }; static styles = css` :host { diff --git a/src/data/load_icon.ts b/src/data/load_icon.ts new file mode 100644 index 0000000000..7670a1c4a4 --- /dev/null +++ b/src/data/load_icon.ts @@ -0,0 +1,142 @@ +import { debounce } from "../common/util/debounce"; +import { customIcons } from "./custom_icons"; +import { + findIconChunk, + getIcon, + MDI_PREFIXES, + writeCache, + type Chunks, +} from "./iconsets"; + +interface IconLoadResult { + icon: string; + legacy?: boolean; + path?: string; + secondaryPath?: string; + viewBox?: string; +} + +type DeprecatedIcon = Record< + string, + { + removeIn: string; + newName?: string; + } +>; + +const chunks: Chunks = {}; + +const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); + +const cachedIcons: Record = {}; + +const mdiDeprecatedIcons: DeprecatedIcon = {}; + +export const loadIcon = async ( + icon: string, + warningCallback?: (message) => void +): Promise => { + const [iconPrefix, origIconName] = icon.split(":", 2); + + let iconName = origIconName; + + if (!iconPrefix || !iconName) { + return { + icon, + }; + } + + if (!MDI_PREFIXES.includes(iconPrefix)) { + const customIcon = customIcons[iconPrefix]; + if (customIcon) { + if (customIcon && typeof customIcon.getIcon === "function") { + const custom = await customIcon.getIcon(iconName); + return { + icon, + path: custom.path, + secondaryPath: custom.secondaryPath, + viewBox: custom.viewBox, + }; + } + return { + icon, + }; + } + return { + icon, + legacy: true, + }; + } + + if (iconName in mdiDeprecatedIcons) { + const deprecatedIcon = mdiDeprecatedIcons[iconName]; + let message: string; + + if (deprecatedIcon.newName) { + message = `Icon ${iconPrefix}:${iconName} was renamed to ${iconPrefix}:${deprecatedIcon.newName}, please change your config, it will be removed in version ${deprecatedIcon.removeIn}.`; + iconName = deprecatedIcon.newName!; + } else { + message = `Icon ${iconPrefix}:${iconName} was removed from MDI, please replace this icon with an other icon in your config, it will be removed in version ${deprecatedIcon.removeIn}.`; + } + // eslint-disable-next-line no-console + console.warn(message); + if (warningCallback) { + warningCallback(message); + } + } + + if (iconName in cachedIcons) { + return { + icon, + path: cachedIcons[iconName], + }; + } + + if (iconName === "home-assistant") { + const ha = (await import("../resources/home-assistant-logo-svg")) + .mdiHomeAssistant; + + cachedIcons[iconName] = ha; + return { + icon, + path: ha, + }; + } + + let databaseIcon: string | undefined; + try { + databaseIcon = await getIcon(iconName); + } catch (_err) { + // Firefox in private mode doesn't support IDB + // iOS Safari sometimes doesn't open the DB + databaseIcon = undefined; + } + + if (databaseIcon) { + cachedIcons[iconName] = databaseIcon; + return { + icon, + path: databaseIcon, + }; + } + const chunk = findIconChunk(iconName); + + if (chunk in chunks) { + const iconPack = await chunks[chunk]; + return { + icon, + path: iconPack[iconName], + }; + } + + const iconPromise = fetch(`/static/mdi/${chunk}.json`).then((response) => + response.json() + ); + chunks[chunk] = iconPromise; + debouncedWriteCache(); + const iconPack = await iconPromise; + return { + icon, + path: iconPack[iconName], + }; +};