mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-11 03:16:34 +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 "@polymer/iron-icon/iron-icon";
|
||||||
import { get, set, clear, Store } from "idb-keyval";
|
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
LitElement,
|
LitElement,
|
||||||
@ -11,84 +10,23 @@ import {
|
|||||||
CSSResult,
|
CSSResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import "./ha-svg-icon";
|
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 { 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 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 debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
|
||||||
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);
|
|
||||||
|
|
||||||
@customElement("ha-icon")
|
@customElement("ha-icon")
|
||||||
export class HaIcon extends LitElement {
|
export class HaIcon extends LitElement {
|
||||||
@ -96,11 +34,14 @@ export class HaIcon extends LitElement {
|
|||||||
|
|
||||||
@property() private _path?: string;
|
@property() private _path?: string;
|
||||||
|
|
||||||
@property() private _noMdi = false;
|
@property() private _viewBox?;
|
||||||
|
|
||||||
|
@property() private _legacy = false;
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
if (changedProps.has("icon")) {
|
if (changedProps.has("icon")) {
|
||||||
this._path = undefined;
|
this._path = undefined;
|
||||||
|
this._viewBox = undefined;
|
||||||
this._loadIcon();
|
this._loadIcon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,25 +50,34 @@ export class HaIcon extends LitElement {
|
|||||||
if (!this.icon) {
|
if (!this.icon) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
if (this._noMdi) {
|
if (this._legacy) {
|
||||||
return html`<iron-icon .icon=${this.icon}></iron-icon>`;
|
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() {
|
private async _loadIcon() {
|
||||||
if (!this.icon) {
|
if (!this.icon) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const icon = this.icon.split(":", 2);
|
const [iconPrefix, iconName] = this.icon.split(":", 2);
|
||||||
if (!MDI_PREFIXES.includes(icon[0])) {
|
if (!MDI_PREFIXES.includes(iconPrefix)) {
|
||||||
this._noMdi = true;
|
if (iconPrefix in customIconsets) {
|
||||||
|
const customIconset = customIconsets[iconPrefix];
|
||||||
|
if (customIconset) {
|
||||||
|
this._setCustomPath(customIconset(iconName), iconName);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._legacy = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._noMdi = false;
|
this._legacy = false;
|
||||||
|
|
||||||
const iconName = icon[1];
|
|
||||||
const cachedPath: string = await getIcon(iconName);
|
const cachedPath: string = await getIcon(iconName);
|
||||||
if (cachedPath) {
|
if (cachedPath) {
|
||||||
this._path = cachedPath;
|
this._path = cachedPath;
|
||||||
@ -147,6 +97,15 @@ export class HaIcon extends LitElement {
|
|||||||
debouncedWriteCache();
|
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) {
|
private async _setPath(promise: Promise<Icons>, iconName: string) {
|
||||||
const iconPack = await promise;
|
const iconPack = await promise;
|
||||||
this._path = iconPack[iconName];
|
this._path = iconPack[iconName];
|
||||||
|
@ -12,10 +12,12 @@ import {
|
|||||||
export class HaSvgIcon extends LitElement {
|
export class HaSvgIcon extends LitElement {
|
||||||
@property() public path?: string;
|
@property() public path?: string;
|
||||||
|
|
||||||
|
@property() public viewBox?: string;
|
||||||
|
|
||||||
protected render(): SVGTemplateResult {
|
protected render(): SVGTemplateResult {
|
||||||
return svg`
|
return svg`
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox=${this.viewBox || "0 0 24 24"}
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
focusable="false">
|
focusable="false">
|
||||||
<g>
|
<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 {
|
ha-icon-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 16px 16px 0 0;
|
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@ -456,15 +456,15 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bouncer {
|
.bouncer {
|
||||||
width: 40px;
|
width: 48px;
|
||||||
height: 40px;
|
height: 48px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
.double-bounce1,
|
.double-bounce1,
|
||||||
.double-bounce2 {
|
.double-bounce2 {
|
||||||
width: 40px;
|
width: 48px;
|
||||||
height: 40px;
|
height: 48px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user