Allow custom icon sets (#5794)

This commit is contained in:
Bram Kragten 2020-05-08 21:56:25 +02:00 committed by GitHub
parent a7ba1977b4
commit 0c8cd680c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 142 additions and 88 deletions

View File

@ -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];

View File

@ -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>

View 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
View 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)
);
}
});
};

View File

@ -339,7 +339,6 @@ class DataEntryFlowDialog extends LitElement {
ha-icon-button {
display: inline-block;
padding: 8px;
margin: 16px 16px 0 0;
float: right;
}
`,

View File

@ -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;