Refactor icon loading

ha-icon-pattern
Paul Bottein 2025-06-12 17:31:34 +02:00
parent 01d2ef13c6
commit 31c56eb8b5
No known key found for this signature in database
2 changed files with 156 additions and 133 deletions

View File

@ -2,34 +2,9 @@ import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, 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 { debounce } from "../common/util/debounce"; import { loadIcon } from "../data/load_icon";
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 "./ha-svg-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<string, string> = {};
@customElement("ha-icon") @customElement("ha-icon")
export class HaIcon extends LitElement { export class HaIcon extends LitElement {
@property() public icon?: string; @property() public icon?: string;
@ -71,118 +46,24 @@ export class HaIcon extends LitElement {
if (!this.icon) { if (!this.icon) {
return; return;
} }
const requestedIcon = this.icon; const result = await loadIcon(this.icon, this._handleWarning);
const [iconPrefix, origIconName] = this.icon.split(":", 2);
let iconName = origIconName; if (result.icon !== this.icon) {
// The icon was changed while we were loading it, so we don't update the state
if (!iconPrefix || !iconName) {
return; return;
} }
this._legacy = result.legacy || false;
if (!MDI_PREFIXES.includes(iconPrefix)) { this._path = result.path;
const customIcon = customIcons[iconPrefix]; this._secondaryPath = result.secondaryPath;
if (customIcon) { this._viewBox = result.viewBox;
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();
} }
private async _setCustomPath( private _handleWarning = (message: string) => {
promise: Promise<CustomIcon>, fireEvent(this, "write_log", {
requestedIcon: string level: "warning",
) { message,
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<Icons>,
iconName: string,
requestedIcon: string
) {
const iconPack = await promise;
if (this.icon === requestedIcon) {
this._path = iconPack[iconName];
}
cachedIcons[iconName] = iconPack[iconName];
}
static styles = css` static styles = css`
:host { :host {

142
src/data/load_icon.ts Normal file
View File

@ -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<string, string> = {};
const mdiDeprecatedIcons: DeprecatedIcon = {};
export const loadIcon = async (
icon: string,
warningCallback?: (message) => void
): Promise<IconLoadResult> => {
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],
};
};