mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-09 18:36:35 +00:00
Allow custom icon sets (#5794)
This commit is contained in:
parent
a7ba1977b4
commit
0c8cd680c2
@ -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<Icons>;
|
||||
}
|
||||
|
||||
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<string>((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`<iron-icon .icon=${this.icon}></iron-icon>`;
|
||||
}
|
||||
return html`<ha-svg-icon .path=${this._path}></ha-svg-icon>`;
|
||||
return html`<ha-svg-icon
|
||||
.path=${this._path}
|
||||
.viewBox=${this._viewBox}
|
||||
></ha-svg-icon>`;
|
||||
}
|
||||
|
||||
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<CustomIcons>,
|
||||
iconName: string
|
||||
) {
|
||||
const iconPack = await promise;
|
||||
this._path = iconPack[iconName].path;
|
||||
this._viewBox = iconPack[iconName].viewBox;
|
||||
}
|
||||
|
||||
private async _setPath(promise: Promise<Icons>, iconName: string) {
|
||||
const iconPack = await promise;
|
||||
this._path = iconPack[iconName];
|
||||
|
@ -12,10 +12,12 @@ import {
|
||||
export class HaSvgIcon extends LitElement {
|
||||
@property() public path?: string;
|
||||
|
||||
@property() public viewBox?: string;
|
||||
|
||||
protected render(): SVGTemplateResult {
|
||||
return svg`
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
viewBox=${this.viewBox || "0 0 24 24"}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false">
|
||||
<g>
|
||||
|
15
src/data/custom_iconsets.ts
Normal file
15
src/data/custom_iconsets.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface CustomIcons {
|
||||
[key: string]: { path: string; viewBox?: string };
|
||||
}
|
||||
|
||||
export interface CustomIconsetsWindow {
|
||||
customIconsets?: { [key: string]: (name: string) => Promise<CustomIcons> };
|
||||
}
|
||||
|
||||
const customIconsetsWindow = window as CustomIconsetsWindow;
|
||||
|
||||
if (!("customIconsets" in customIconsetsWindow)) {
|
||||
customIconsetsWindow.customIconsets = {};
|
||||
}
|
||||
|
||||
export const customIconsets = customIconsetsWindow.customIconsets!;
|
79
src/data/iconsets.ts
Normal file
79
src/data/iconsets.ts
Normal file
@ -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<Icons>;
|
||||
}
|
||||
|
||||
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<string>((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)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
@ -339,7 +339,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
ha-icon-button {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
margin: 16px 16px 0 0;
|
||||
float: right;
|
||||
}
|
||||
`,
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user