mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Add support for service icons (#19507)
* Add service icons * Fix lint --------- Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
parent
c2d71ac789
commit
2925ef3db0
@ -9,8 +9,8 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@ -19,6 +19,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { nestedArrayMove } from "../common/util/array-move";
|
||||
import {
|
||||
fetchIntegrationManifest,
|
||||
IntegrationManifest,
|
||||
@ -31,6 +32,7 @@ import {
|
||||
expandDeviceTarget,
|
||||
Selector,
|
||||
} from "../data/selector";
|
||||
import { ReorderModeMixin } from "../state/reorder-mode-mixin";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
@ -40,8 +42,6 @@ import "./ha-service-picker";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
import { nestedArrayMove } from "../common/util/array-move";
|
||||
import { ReorderModeMixin } from "../state/reorder-mode-mixin";
|
||||
|
||||
const attributeFilter = (values: any[], attribute: any) => {
|
||||
if (typeof attribute === "object") {
|
||||
|
57
src/components/ha-service-icon.ts
Normal file
57
src/components/ha-service-icon.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { mdiRoomService } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { domainIconWithoutDefault } from "../common/entity/domain_icon";
|
||||
import { serviceIcon } from "../data/icons";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-service-icon")
|
||||
export class HaServiceIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public service?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
}
|
||||
|
||||
if (!this.service) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceIcon(this.hass, this.service).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
.path=${domainIconWithoutDefault(computeDomain(this.service!)) ||
|
||||
mdiRoomService}
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-service-icon": HaServiceIcon;
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@ -8,16 +7,9 @@ import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box";
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
|
||||
item
|
||||
) =>
|
||||
html`<mwc-list-item twoline>
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary"
|
||||
>${item.name === item.service ? "" : item.service}</span
|
||||
>
|
||||
</mwc-list-item>`;
|
||||
import "./ha-list-item";
|
||||
import "./ha-service-icon";
|
||||
import { getServiceIcons } from "../data/icons";
|
||||
|
||||
@customElement("ha-service-picker")
|
||||
class HaServicePicker extends LitElement {
|
||||
@ -32,9 +24,24 @@ class HaServicePicker extends LitElement {
|
||||
protected willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadBackendTranslation("services");
|
||||
getServiceIcons(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
|
||||
(item) =>
|
||||
html`<ha-list-item twoline graphic="icon">
|
||||
<ha-service-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.service=${item.service}
|
||||
></ha-service-icon>
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary"
|
||||
>${item.name === item.service ? "" : item.service}</span
|
||||
>
|
||||
</ha-list-item>`;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
@ -47,7 +54,7 @@ class HaServicePicker extends LitElement {
|
||||
)}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.renderer=${rowRenderer}
|
||||
.renderer=${this._rowRenderer}
|
||||
item-value-path="service"
|
||||
item-label-path="name"
|
||||
allow-custom-value
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { HomeAssistant } from "../types";
|
||||
import {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const resources: Record<IconCategory, any> = {
|
||||
entity: {},
|
||||
entity_component: undefined,
|
||||
services: {},
|
||||
};
|
||||
|
||||
interface IconResources {
|
||||
@ -46,7 +48,11 @@ interface ComponentIcons {
|
||||
};
|
||||
}
|
||||
|
||||
export type IconCategory = "entity" | "entity_component";
|
||||
interface ServiceIcons {
|
||||
[domain: string]: Record<string, string>;
|
||||
}
|
||||
|
||||
export type IconCategory = "entity" | "entity_component" | "services";
|
||||
|
||||
export const getHassIcons = async (
|
||||
hass: HomeAssistant,
|
||||
@ -64,7 +70,7 @@ export const getPlatformIcons = async (
|
||||
integration: string,
|
||||
force = false
|
||||
): Promise<PlatformIcons> => {
|
||||
if (!force && integration && integration in resources.entity) {
|
||||
if (!force && integration in resources.entity) {
|
||||
return resources.entity[integration];
|
||||
}
|
||||
const result = getHassIcons(hass, "entity", integration);
|
||||
@ -88,6 +94,37 @@ export const getComponentIcons = async (
|
||||
return resources.entity_component.then((res) => res[domain]);
|
||||
};
|
||||
|
||||
export const getServiceIcons = async (
|
||||
hass: HomeAssistant,
|
||||
domain?: string,
|
||||
force = false
|
||||
): Promise<ServiceIcons> => {
|
||||
if (!domain) {
|
||||
if (!force && resources.services.all) {
|
||||
return resources.services.all;
|
||||
}
|
||||
resources.services.all = getHassIcons(hass, "services", domain).then(
|
||||
(res) => {
|
||||
resources.services = res.resources;
|
||||
return res?.resources;
|
||||
}
|
||||
);
|
||||
return resources.services.all;
|
||||
}
|
||||
if (!force && domain && domain in resources.services) {
|
||||
return resources.services[domain];
|
||||
}
|
||||
if (resources.services.all && !force) {
|
||||
await resources.services.all;
|
||||
if (domain in resources.services) {
|
||||
return resources.services[domain];
|
||||
}
|
||||
}
|
||||
const result = getHassIcons(hass, "services", domain);
|
||||
resources.services[domain] = result.then((res) => res?.resources[domain]);
|
||||
return resources.services[domain];
|
||||
};
|
||||
|
||||
export const entityIcon = async (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
@ -96,7 +133,6 @@ export const entityIcon = async (
|
||||
const entity = hass.entities?.[state.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
|
||||
if (entity?.icon) {
|
||||
return entity.icon;
|
||||
}
|
||||
@ -197,3 +233,13 @@ export const attributeIcon = async (
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const serviceIcon = async (hass: HomeAssistant, service: string) => {
|
||||
const domain = computeDomain(service);
|
||||
const serviceName = computeObjectId(service);
|
||||
const serviceIcons = await getServiceIcons(hass, domain);
|
||||
if (serviceIcons) {
|
||||
return serviceIcons[serviceName];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
@ -29,13 +29,12 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { domainIconWithoutDefault } from "../../../../common/entity/domain_icon";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-service-icon";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
@ -203,16 +202,18 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
: ""}
|
||||
<ha-expansion-panel leftChevron>
|
||||
<h3 slot="header">
|
||||
<ha-svg-icon
|
||||
class="action-icon"
|
||||
.path=${type === "service" &&
|
||||
"service" in this.action &&
|
||||
this.action.service
|
||||
? domainIconWithoutDefault(
|
||||
computeDomain(this.action.service as string)
|
||||
) || ACTION_ICONS[type!]
|
||||
: ACTION_ICONS[type!]}
|
||||
></ha-svg-icon>
|
||||
${type === "service" &&
|
||||
"service" in this.action &&
|
||||
this.action.service
|
||||
? html`<ha-service-icon
|
||||
class="action-icon"
|
||||
.hass=${this.hass}
|
||||
.service=${this.action.service}
|
||||
></ha-service-icon>`
|
||||
: html`<ha-svg-icon
|
||||
class="action-icon"
|
||||
.path=${ACTION_ICONS[type!]}
|
||||
></ha-svg-icon>`}
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
)}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
@ -53,6 +54,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/search-input";
|
||||
import "@material/web/divider/divider";
|
||||
import { getServiceIcons } from "../../../data/icons";
|
||||
|
||||
const TYPES = {
|
||||
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
|
||||
@ -70,7 +72,8 @@ interface ListItem {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
iconPath?: string;
|
||||
icon?: TemplateResult;
|
||||
image?: string;
|
||||
group: boolean;
|
||||
}
|
||||
@ -124,6 +127,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
this.hass.loadBackendTranslation("services");
|
||||
this._fetchManifests();
|
||||
this._calculateUsedDomains();
|
||||
getServiceIcons(this.hass);
|
||||
}
|
||||
this._fullScreen = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@ -174,7 +178,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
options.members ? "groups" : "type"
|
||||
}.${key}.description${options.members ? "" : ".picker"}`
|
||||
),
|
||||
icon: options.icon || TYPES[type].icons[key],
|
||||
iconPath: options.icon || TYPES[type].icons[key],
|
||||
});
|
||||
|
||||
private _getFilteredItems = memoizeOne(
|
||||
@ -317,7 +321,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
const icon = domainIconWithoutDefault(domain);
|
||||
result.push({
|
||||
group: true,
|
||||
icon,
|
||||
iconPath: icon,
|
||||
image: !icon
|
||||
? brandsUrl({
|
||||
domain,
|
||||
@ -358,17 +362,12 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
const services_keys = Object.keys(services[dmn]);
|
||||
|
||||
for (const service of services_keys) {
|
||||
const icon = domainIconWithoutDefault(dmn);
|
||||
result.push({
|
||||
group: false,
|
||||
icon,
|
||||
image: !icon
|
||||
? brandsUrl({
|
||||
domain: dmn,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})
|
||||
: undefined,
|
||||
icon: html`<ha-service-icon
|
||||
.hass=${this.hass}
|
||||
.service=${`${dmn}.${service}`}
|
||||
></ha-service-icon>`,
|
||||
key: `${SERVICE_PREFIX}${dmn}.${service}`,
|
||||
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
|
||||
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
|
||||
@ -573,17 +572,19 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
<div slot="headline">${item.name}</div>
|
||||
<div slot="supporting-text">${item.description}</div>
|
||||
${item.icon
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon}
|
||||
></ha-svg-icon>`
|
||||
: html`<img
|
||||
alt=""
|
||||
slot="start"
|
||||
src=${item.image!}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>`}
|
||||
? html`<span slot="start">${item.icon}</span>`
|
||||
: item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: html`<img
|
||||
alt=""
|
||||
slot="start"
|
||||
src=${item.image!}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>`}
|
||||
${item.group
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: html`<ha-svg-icon
|
||||
|
Loading…
x
Reference in New Issue
Block a user