Compare commits

...
Author SHA1 Message Date
Paul BotteinandGitHub dc5d9f48bb Migrate add-to action list to grouped list style (#53921) 2026-09-01 16:28:30 +02:00
Paul BotteinandGitHub 3d3cdabfe7 Split more-info related and details views (#53920)
* Split more-info related and details views

* Update the related view e2e smoke case
2026-09-01 16:17:37 +02:00
Paul BotteinandGitHub 4b1f12c0ef Add seek support to the browser media player (#53917) 2026-09-01 14:44:21 +02:00
9 changed files with 700 additions and 632 deletions
+267 -313
View File
@@ -1,16 +1,17 @@
import {
mdiAlertCircleOutline,
mdiDevices,
mdiPaletteSwatch,
mdiTextureBox,
mdiTransitConnectionVariant,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { Blueprints } from "../data/blueprint";
import { fetchBlueprints } from "../data/blueprint";
@@ -18,14 +19,15 @@ import type { ConfigEntry } from "../data/config_entries";
import { getConfigEntries } from "../data/config_entries";
import type { ItemType, RelatedResult } from "../data/search";
import { findRelated } from "../data/search";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import "./ha-icon";
import "./ha-icon-next";
import "./ha-list-item";
import "./ha-state-icon";
import "./ha-switch";
import "./ha-list";
import "./ha-svg-icon";
import "./item/ha-list-item-button";
import type { HaListItemButton } from "./item/ha-list-item-button";
import "./list/ha-grouped-list";
@customElement("ha-related-items")
export class HaRelatedItems extends LitElement {
@@ -35,16 +37,16 @@ export class HaRelatedItems extends LitElement {
@property({ attribute: false }) public itemId!: string;
@property({ attribute: false }) public exclude?: (keyof RelatedResult)[];
@property({ type: Boolean, reflect: true }) public empty = true;
@state() private _entries?: ConfigEntry[];
@state() private _blueprints?: Record<"automation" | "script", Blueprints>;
@state() private _related?: RelatedResult;
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
}
private async _fetchConfigEntries() {
if (this._entries) {
return;
@@ -64,6 +66,19 @@ export class HaRelatedItems extends LitElement {
this._blueprints = { automation, script };
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("_related") || changedProps.has("exclude")) {
this.empty =
!this._related ||
!Object.entries(this._related).some(
([section, items]) =>
items?.length &&
!this.exclude?.includes(section as keyof RelatedResult)
);
}
}
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (
@@ -127,25 +142,14 @@ export class HaRelatedItems extends LitElement {
}
);
private _isExcluded(section: keyof RelatedResult) {
return this.exclude?.includes(section) ?? false;
}
protected render() {
if (!this._related) {
return nothing;
}
if (Object.keys(this._related).length === 0) {
return html`
<ha-list>
<ha-list-item hasMeta graphic="icon" noninteractive>
<ha-svg-icon
.path=${mdiAlertCircleOutline}
slot="graphic"
></ha-svg-icon>
${this.hass.localize(
"ui.components.related-items.no_related_found"
)}
</ha-list-item>
</ha-list>
`;
}
const { configEntries, configEntryDomains } = this._getConfigEntries(
this._related.config_entry,
@@ -154,97 +158,77 @@ export class HaRelatedItems extends LitElement {
return html`
${
this._related.entity
this._related.entity && !this._isExcluded("entity")
? html`
<h3>
${this.hass.localize("ui.components.related-items.entity")}
</h3>
<ha-list>
${this._relatedEntities(this._related.entity).map(
(entity) => html`
<ha-list-item
@click=${this._openMoreInfo}
.entityId=${entity.entity_id}
hasMeta
graphic="icon"
>
<ha-state-icon
.stateObj=${entity}
slot="graphic"
></ha-state-icon>
${entity.attributes.friendly_name || entity.entity_id}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
<ha-grouped-list
.header=${this.hass.localize(
"ui.components.related-items.entity"
)}
</ha-list>
>
${this._relatedEntities(this._related.entity).map((entity) =>
this._renderEntityRow(entity)
)}
</ha-grouped-list>
`
: nothing
}
${
this._related.device
? html`<h3>
${this.hass.localize("ui.components.related-items.device")}
</h3>
<ha-list>
this._related.device && !this._isExcluded("device")
? html`
<ha-grouped-list
.header=${this.hass.localize(
"ui.components.related-items.device"
)}
>
${this._related.device.map((relatedDeviceId) => {
const device = this.hass.devices[relatedDeviceId];
if (!device) {
return nothing;
}
return html`
<a href="/config/devices/device/${relatedDeviceId}">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${
device.entry_type === "service"
? mdiTransitConnectionVariant
: mdiDevices
}
slot="graphic"
></ha-svg-icon>
${device.name_by_user || device.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
<ha-list-item-button
.href=${`/config/devices/device/${relatedDeviceId}`}
.headline=${device.name_by_user || device.name || ""}
>
<ha-svg-icon
slot="start"
.path=${
device.entry_type === "service"
? mdiTransitConnectionVariant
: mdiDevices
}
></ha-svg-icon>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`;
})}
</ha-list>`
</ha-grouped-list>
`
: nothing
}
${
configEntries || this._related.integration
? html`<h3>
${this.hass.localize("ui.components.related-items.integration")}
</h3>
<ha-list
>${configEntries?.map((entry) => {
(configEntries || this._related.integration) &&
!this._isExcluded("integration")
? html`
<ha-grouped-list
.header=${this.hass.localize(
"ui.components.related-items.integration"
)}
>
${configEntries?.map((entry) => {
if (!entry) {
return nothing;
}
return html`
<a
href=${`/config/integrations/integration/${entry.domain}#config_entry=${entry.entry_id}`}
<ha-list-item-button
.href=${`/config/integrations/integration/${entry.domain}#config_entry=${entry.entry_id}`}
.headline=${`${this.hass.localize(
`component.${entry.domain}.title`
)}: ${entry.title}`}
>
<ha-list-item hasMeta graphic="icon">
<img
.src=${brandsUrl(
{
domain: entry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${entry.domain}
slot="graphic"
/>
${this.hass.localize(`component.${entry.domain}.title`)}:
${entry.title} <ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
${this._renderBrandIcon(entry.domain)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`;
})}
${this._related.integration
@@ -252,259 +236,216 @@ export class HaRelatedItems extends LitElement {
(integration) => !configEntryDomains.has(integration)
)
.map(
(integration) =>
html`<a
href=${`/config/integrations/integration/${integration}`}
(integration) => html`
<ha-list-item-button
.href=${`/config/integrations/integration/${integration}`}
.headline=${this.hass.localize(
`component.${integration}.title`
)}
>
<ha-list-item hasMeta graphic="icon">
<img
.src=${brandsUrl(
{
domain: integration,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${integration}
slot="graphic"
/>
${this.hass.localize(`component.${integration}.title`)}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`
${this._renderBrandIcon(integration)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`
)}
</ha-list>`
</ha-grouped-list>
`
: nothing
}
${
this._related.area
? html`<h3>
${this.hass.localize("ui.components.related-items.area")}
</h3>
<ha-list
>${this._related.area.map((relatedAreaId) => {
this._related.area && !this._isExcluded("area")
? html`
<ha-grouped-list
.header=${this.hass.localize("ui.components.related-items.area")}
>
${this._related.area.map((relatedAreaId) => {
const area = this.hass.areas[relatedAreaId];
if (!area) {
return nothing;
}
return html`
<a href="/config/areas/area/${relatedAreaId}">
<ha-list-item
hasMeta
.graphic=${area.picture ? "avatar" : "icon"}
>
${
area.picture
? html` <div
<ha-list-item-button
.href=${`/config/areas/area/${relatedAreaId}`}
.headline=${area.name}
>
${
area.picture
? html`
<div
class="avatar"
style=${styleMap({
backgroundImage: `url(${area.picture})`,
})}
slot="graphic"
></div>`
: area.icon
? html`<ha-icon
slot="graphic"
slot="start"
></div>
`
: area.icon
? html`
<ha-icon
slot="start"
.icon=${area.icon}
></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
></ha-icon>
`
: html`
<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`
}
${area.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
></ha-svg-icon>
`
}
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`;
})}
</ha-list>`
</ha-grouped-list>
`
: nothing
}
${
this._related.group
this._related.group && !this._isExcluded("group")
? html`
<h3>
${this.hass.localize("ui.components.related-items.group")}
</h3>
<ha-list>
${this._relatedGroups(this._related.group).map(
(group) => html`
<ha-list-item
@click=${this._openMoreInfo}
.entityId=${group.entity_id}
hasMeta
graphic="icon"
>
<ha-state-icon
.stateObj=${group}
slot="graphic"
></ha-state-icon>
${group.attributes.friendly_name || group.entity_id}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
<ha-grouped-list
.header=${this.hass.localize("ui.components.related-items.group")}
>
${this._relatedGroups(this._related.group).map((group) =>
this._renderEntityRow(group)
)}
</ha-list>
</ha-grouped-list>
`
: nothing
}
${
this._related.scene
this._related.scene && !this._isExcluded("scene")
? html`
<h3>
${this.hass.localize("ui.components.related-items.scene")}
</h3>
<ha-list>
${this._relatedScenes(this._related.scene).map(
(scene) => html`
<ha-list-item
@click=${this._openMoreInfo}
.entityId=${scene.entity_id}
hasMeta
graphic="icon"
>
<ha-state-icon
.stateObj=${scene}
slot="graphic"
></ha-state-icon>
${scene.attributes.friendly_name || scene.entity_id}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
<ha-grouped-list
.header=${this.hass.localize("ui.components.related-items.scene")}
>
${this._relatedScenes(this._related.scene).map((scene) =>
this._renderEntityRow(scene)
)}
</ha-list>
</ha-grouped-list>
`
: nothing
}
${
this._related.automation_blueprint
this._related.automation_blueprint &&
!this._isExcluded("automation_blueprint")
? html`
<h3>
${this.hass.localize("ui.components.related-items.blueprint")}
</h3>
<ha-list>
${this._related.automation_blueprint.map((path) => {
const blueprintMeta = this._blueprints
? this._blueprints.automation[path]
: undefined;
return html`<a href="/config/blueprint/dashboard">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiPaletteSwatch}
slot="graphic"
></ha-svg-icon>
${
!blueprintMeta || "error" in blueprintMeta
? path
: blueprintMeta.metadata.name || path
}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`;
})}
</ha-list>
<ha-grouped-list
.header=${this.hass.localize(
"ui.components.related-items.blueprint"
)}
>
${this._related.automation_blueprint.map((path) =>
this._renderBlueprintRow(path, "automation")
)}
</ha-grouped-list>
`
: nothing
}
${
this._related.automation
this._related.automation && !this._isExcluded("automation")
? html`
<h3>
${this.hass.localize("ui.components.related-items.automation")}
</h3>
<ha-list>
<ha-grouped-list
.header=${this.hass.localize(
"ui.components.related-items.automation"
)}
>
${this._relatedAutomations(this._related.automation).map(
(automation) => html`
<ha-list-item
@click=${this._openMoreInfo}
.entityId=${automation.entity_id}
hasMeta
graphic="icon"
>
<ha-state-icon
.stateObj=${automation}
slot="graphic"
></ha-state-icon>
${
automation.attributes.friendly_name ||
automation.entity_id
}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
(automation) => this._renderEntityRow(automation)
)}
</ha-list>
</ha-grouped-list>
`
: nothing
}
${
this._related.script_blueprint
this._related.script_blueprint && !this._isExcluded("script_blueprint")
? html`
<h3>
${this.hass.localize("ui.components.related-items.blueprint")}
</h3>
<ha-list>
${this._related.script_blueprint.map((path) => {
const blueprintMeta = this._blueprints
? this._blueprints.script[path]
: undefined;
return html`<a href="/config/blueprint/dashboard">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiPaletteSwatch}
slot="graphic"
></ha-svg-icon>
${
!blueprintMeta || "error" in blueprintMeta
? path
: blueprintMeta.metadata.name || path
}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`;
})}
</ha-list>
<ha-grouped-list
.header=${this.hass.localize(
"ui.components.related-items.blueprint"
)}
>
${this._related.script_blueprint.map((path) =>
this._renderBlueprintRow(path, "script")
)}
</ha-grouped-list>
`
: nothing
}
${
this._related.script
this._related.script && !this._isExcluded("script")
? html`
<h3>
${this.hass.localize("ui.components.related-items.script")}
</h3>
<ha-list>
${this._relatedScripts(this._related.script).map(
(script) => html`
<ha-list-item
@click=${this._openMoreInfo}
.entityId=${script.entity_id}
hasMeta
graphic="icon"
>
<ha-state-icon
.stateObj=${script}
slot="graphic"
></ha-state-icon>
${script.attributes.friendly_name || script.entity_id}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
<ha-grouped-list
.header=${this.hass.localize(
"ui.components.related-items.script"
)}
</ha-list>
>
${this._relatedScripts(this._related.script).map((script) =>
this._renderEntityRow(script)
)}
</ha-grouped-list>
`
: nothing
}
`;
}
private _renderEntityRow(entity: HassEntity) {
return html`
<ha-list-item-button
.headline=${entity.attributes.friendly_name || entity.entity_id}
data-entity-id=${entity.entity_id}
@click=${this._openMoreInfo}
>
<ha-state-icon slot="start" .stateObj=${entity}></ha-state-icon>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`;
}
private _renderBrandIcon(domain: string) {
return html`
<img
slot="start"
alt=""
loading="lazy"
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl(
{
domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>
`;
}
private _renderBlueprintRow(path: string, type: "automation" | "script") {
const blueprintMeta = this._blueprints
? this._blueprints[type][path]
: undefined;
return html`
<ha-list-item-button
href="/config/blueprint/dashboard"
.headline=${
!blueprintMeta || "error" in blueprintMeta
? path
: blueprintMeta.metadata.name || path
}
>
<ha-svg-icon slot="start" .path=${mdiPaletteSwatch}></ha-svg-icon>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`;
}
private async _findRelated() {
this._related = await findRelated(this.hass, this.itemType, this.itemId);
if (this._related.config_entry) {
if (this._related.config_entry && !this._isExcluded("integration")) {
this._fetchConfigEntries();
}
if (this._related.script_blueprint || this._related.automation_blueprint) {
@@ -512,36 +453,49 @@ export class HaRelatedItems extends LitElement {
}
}
private _openMoreInfo(ev: CustomEvent) {
const entityId = (ev.target as any).entityId;
private _openMoreInfo(ev: HASSDomCurrentTargetEvent<HaListItemButton>) {
const entityId = ev.currentTarget.dataset.entityId;
if (!entityId) {
return;
}
fireEvent(this, "hass-more-info", { entityId });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
a {
color: var(--primary-color);
text-decoration: none;
}
ha-list-item {
--mdc-list-side-padding: 24px;
}
h3 {
padding: 0 24px;
margin-bottom: -8px;
}
h3:first-child {
margin-top: 0;
}
.avatar {
background-position: center center;
background-size: cover;
}
`,
];
}
static styles: CSSResultGroup = css`
:host {
display: flex;
flex-direction: column;
gap: var(--ha-space-6);
}
:host([empty]) {
display: none;
}
ha-list-item-button {
--ha-row-item-padding-block: var(--ha-space-2);
--ha-row-item-min-height: 40px;
--ha-row-item-gap: var(--ha-space-3);
--mdc-icon-size: 20px;
}
img[slot="start"],
.avatar {
width: 20px;
height: 20px;
object-fit: contain;
}
.avatar {
border-radius: var(--ha-border-radius-circle);
background-position: center center;
background-size: cover;
}
ha-icon-next {
color: var(--secondary-text-color);
}
`;
}
declare global {
+18 -28
View File
@@ -15,7 +15,7 @@ import "../../components/ha-icon";
import "../../components/ha-svg-icon";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import "../../components/list/ha-grouped-list";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
export interface AddToActionListItem {
@@ -76,20 +76,19 @@ class HaAddToActionList extends LitElement {
}
return html`
<h3 class="section-header">
${this._localizeValue(section.title, section.titleKey)}
</h3>
${
section.actions.length
? html`<ha-list-base>
${section.actions.map((action, actionIndex) =>
<ha-grouped-list
.header=${this._localizeValue(section.title, section.titleKey)}
>
${
section.actions.length
? section.actions.map((action, actionIndex) =>
this._renderActionItem(action, sectionIndex, actionIndex)
)}
</ha-list-base>`
: html`<h4 class="empty">
${this._localizeValue(section.empty, section.emptyKey)}
</h4>`
}
)
: html`<div class="empty">
${this._localizeValue(section.empty, section.emptyKey)}
</div>`
}
</ha-grouped-list>
`;
}
@@ -161,28 +160,19 @@ class HaAddToActionList extends LitElement {
static styles: CSSResultGroup = css`
:host {
display: block;
padding: 0 var(--ha-space-6);
}
.section-header {
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
margin: 0;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
color: var(--secondary-text-color);
ha-grouped-list + ha-grouped-list {
margin-top: var(--ha-space-6);
}
.empty {
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
margin: 0;
padding: var(--ha-space-2) var(--ha-space-3);
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
color: var(--secondary-text-color);
}
ha-list-item-button {
--ha-row-item-padding-inline: var(--ha-space-5);
}
ha-icon,
ha-svg-icon {
display: flex;
@@ -194,7 +184,7 @@ class HaAddToActionList extends LitElement {
}
.plus {
color: var(--primary-color);
color: var(--secondary-text-color);
}
ha-list-item-button[disabled] .start-icon,
+34 -216
View File
@@ -1,22 +1,13 @@
import { mdiCheck, mdiContentCopy, mdiDevices, mdiTextureBox } from "@mdi/js";
import { consume } from "@lit/context";
import { mdiCheck, mdiContentCopy } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeFloorName } from "../../common/entity/compute_floor_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import checkValidDate from "../../common/datetime/check_valid_date";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import "../../components/ha-attribute-value";
import "../../components/ha-floor-icon";
import "../../components/ha-icon";
import "../../components/ha-icon-next";
import "../../components/ha-label";
import "../../components/ha-svg-icon";
import "../../components/item/ha-list-item-button";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
@@ -24,9 +15,7 @@ import "../../components/item/ha-list-item-value";
import "../../components/list/ha-grouped-list";
import { copyToClipboard } from "../../common/util/copy-clipboard";
import type { LocalizeKeys } from "../../common/translations/localize";
import { labelsContext } from "../../data/context";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { HomeAssistant } from "../../types";
import "../../components/ha-yaml-editor";
import { computeDomain } from "../../common/entity/compute_domain";
@@ -35,7 +24,6 @@ import { getFeatures } from "../../common/entity/get_domain_features";
import { supportsFeature } from "../../common/entity/supports-feature";
import { titleCase } from "../../common/string/title-case";
import { stringCompare } from "../../common/string/compare";
import { brandsUrl } from "../../util/brands-url";
import { showToast } from "../../util/toast";
interface DetailsViewParams {
@@ -45,9 +33,6 @@ interface DetailsViewParams {
interface DetailEntry {
translationKey: LocalizeKeys;
value: string;
displayValue?: TemplateResult;
href?: string;
icon?: TemplateResult;
copyable?: boolean;
}
@@ -63,10 +48,6 @@ class HaMoreInfoDetails extends LitElement {
@state() private _stateObj?: HassEntity;
@consume({ context: labelsContext, subscribe: true })
@state()
private _labels?: LabelRegistryEntry[];
@state() private _copiedValue?: string;
private _copyFeedbackTimeout?: number;
@@ -80,9 +61,6 @@ class HaMoreInfoDetails extends LitElement {
protected willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
if (changedProps.has("entry") && this.entry) {
this.hass.loadBackendTranslation("title", [this.entry.platform]);
}
if (changedProps.has("params") || changedProps.has("hass")) {
if (this.params?.entityId && this.hass) {
this._stateObj = this.hass.states[this.params.entityId];
@@ -103,123 +81,17 @@ class HaMoreInfoDetails extends LitElement {
this._stateObj,
this.hass.formatEntityAttributeName
);
const { floor, area, device } = getEntityContext(
this._stateObj,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const floorName = floor ? computeFloorName(floor) : undefined;
const areaName = area ? (computeAreaName(area) ?? area.area_id) : undefined;
const deviceName = device
? computeDeviceNameDisplay(device, this.hass.localize, this.hass.states)
: undefined;
const integrationName = this.entry?.platform
? this.hass.localize(`component.${this.entry.platform}.title`) ||
this.entry.platform
: undefined;
const labels =
this.entry?.labels.map((labelId) => ({
id: labelId,
entry: this._labels?.find((label) => label.label_id === labelId),
})) ?? [];
const labelNames = labels.map(({ id, entry }) => entry?.name ?? id);
const contextEntries: DetailEntry[] = [];
if (floor && floorName) {
contextEntries.push({
translationKey: "ui.dialogs.more_info_control.floor",
value: floorName,
href: "/config/areas/dashboard",
icon: html`<ha-floor-icon slot="end" .floor=${floor}></ha-floor-icon>`,
});
}
if (area && areaName) {
contextEntries.push({
translationKey: "ui.components.related-items.area",
value: areaName,
href: `/config/areas/area/${area.area_id}`,
icon: area.icon
? html`<ha-icon slot="end" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon slot="end" .path=${mdiTextureBox}></ha-svg-icon>`,
});
}
if (device && deviceName) {
contextEntries.push({
translationKey: "ui.components.related-items.device",
value: deviceName,
href: `/config/devices/device/${device.id}`,
icon: html`<ha-svg-icon slot="end" .path=${mdiDevices}></ha-svg-icon>`,
});
}
if (this.entry?.platform && integrationName) {
contextEntries.push({
translationKey: "ui.components.related-items.integration",
value: integrationName,
href: this.entry.config_entry_id
? `/config/integrations/integration/${this.entry.platform}#config_entry=${this.entry.config_entry_id}`
: undefined,
icon: html`<img
slot="end"
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl(
{
domain: this.entry.platform,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>`,
});
}
contextEntries.push(
const entityEntries: DetailEntry[] = [
{
translationKey: "ui.dialogs.more_info_control.entity_id",
value: this.params.entityId,
copyable: true,
},
{
translationKey: "ui.dialogs.more_info_control.labels",
value: labelNames.join(", ") || this.hass.localize("ui.common.none"),
displayValue: labels.length
? html`<div class="labels">
${labels.map(
({ id, entry }) => html`
<ha-label
class="text-ellipsis"
.color=${entry?.color ?? undefined}
.description=${entry?.description ?? undefined}
>
${
entry?.icon
? html`<ha-icon
slot="icon"
.icon=${entry.icon}
></ha-icon>`
: nothing
}
${entry?.name ?? id}
</ha-label>
`
)}
</div>`
: undefined,
}
);
];
const yamlData = {
context: {
...(floorName ? { floor: floorName } : {}),
...(areaName ? { area: areaName } : {}),
...(deviceName ? { device: deviceName } : {}),
...(integrationName ? { integration: integrationName } : {}),
entity_id: this.params.entityId,
labels: labelNames,
},
entity_id: this.params.entityId,
...stateYamlData,
};
@@ -234,15 +106,12 @@ class HaMoreInfoDetails extends LitElement {
in-dialog
></ha-yaml-editor>`
: html`
<ha-grouped-list>
${this._renderEntries(contextEntries)}
</ha-grouped-list>
<ha-grouped-list
.header=${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
"ui.dialogs.more_info_control.entity"
)}
>
${this._renderEntries(entityEntries)}
${this._renderEntries(stateEntries)}
</ha-grouped-list>
@@ -269,17 +138,12 @@ class HaMoreInfoDetails extends LitElement {
stateEntries: DetailEntry[];
attributes: { name: string; label: string }[];
yamlData: {
state: {
translated: string;
raw: string;
last_changed: string;
last_updated: string;
};
state: string;
last_changed: string;
last_updated: string;
attributes: Record<string, string>;
};
} => {
const translatedState = this.hass.formatEntityState(stateObj);
const attributes = Object.keys(stateObj.attributes)
.map((a) => ({
name: a,
@@ -292,12 +156,8 @@ class HaMoreInfoDetails extends LitElement {
return {
stateEntries: [
{
translationKey: "ui.dialogs.more_info_control.translated",
value: translatedState,
},
{
translationKey: "ui.dialogs.more_info_control.raw",
value: stateObj.state,
translationKey: "ui.dialogs.more_info_control.state",
value: this.hass.formatEntityState(stateObj),
},
{
translationKey: "ui.dialogs.more_info_control.last_changed",
@@ -310,12 +170,9 @@ class HaMoreInfoDetails extends LitElement {
],
attributes,
yamlData: {
state: {
translated: translatedState,
raw: stateObj.state,
last_changed: stateObj.last_changed,
last_updated: stateObj.last_updated,
},
state: stateObj.state,
last_changed: stateObj.last_changed,
last_updated: stateObj.last_updated,
attributes: stateObj.attributes,
},
};
@@ -334,47 +191,34 @@ class HaMoreInfoDetails extends LitElement {
return entries.map((entry) => {
const label = this.hass.localize(entry.translationKey);
if (!entry.href && !entry.copyable) {
if (!entry.copyable) {
return html`
<ha-list-item-value .label=${label}
>${entry.displayValue ?? entry.value}</ha-list-item-value
>${entry.value}</ha-list-item-value
>
`;
}
if (entry.copyable) {
return html`
<ha-list-item-button
aria-label=${this.hass.localize(
"ui.dialogs.more_info_control.copy_value",
{ label, value: entry.value }
)}
data-value=${entry.value}
@click=${this._copyValue}
>
<div class="link-row" slot="content">
<div class="label">${label}</div>
<div class="value">${entry.value}</div>
</div>
<ha-svg-icon
class=${this._copiedValue === entry.value ? "copy-success" : ""}
slot="end"
.path=${
this._copiedValue === entry.value ? mdiCheck : mdiContentCopy
}
></ha-svg-icon>
</ha-list-item-button>
`;
}
return html`
<ha-list-item-button .href=${entry.href}>
<ha-list-item-button
aria-label=${this.hass.localize(
"ui.dialogs.more_info_control.copy_value",
{ label, value: entry.value }
)}
data-value=${entry.value}
@click=${this._copyValue}
>
<div class="link-row" slot="content">
<div class="label">${label}</div>
<div class="value">${entry.value}</div>
</div>
${entry.icon ?? nothing}
<ha-icon-next slot="end"></ha-icon-next>
<ha-svg-icon
class=${this._copiedValue === entry.value ? "copy-success" : ""}
slot="end"
.path=${
this._copiedValue === entry.value ? mdiCheck : mdiContentCopy
}
></ha-svg-icon>
</ha-list-item-button>
`;
});
@@ -439,7 +283,7 @@ class HaMoreInfoDetails extends LitElement {
.filter(([_key, value]) => typeof value === "number")
.map(([key, value]) =>
supportsFeature(stateObj, value as number)
? titleCase(key.replaceAll("_", "\u00A0").toLowerCase())
? titleCase(key.replaceAll("_", " ").toLowerCase())
: undefined
)
.filter(Boolean)
@@ -470,16 +314,6 @@ class HaMoreInfoDetails extends LitElement {
--mdc-icon-size: 20px;
}
ha-list-item-button::part(end) {
gap: var(--ha-space-2);
}
ha-list-item-button img {
width: 20px;
height: 20px;
object-fit: contain;
}
.link-row {
display: flex;
flex-direction: row;
@@ -499,22 +333,6 @@ class HaMoreInfoDetails extends LitElement {
overflow-wrap: anywhere;
}
.labels {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: var(--ha-space-1);
}
.labels ha-label {
min-width: 0;
max-width: 100%;
}
ha-icon-next {
color: var(--secondary-text-color);
}
ha-svg-icon.copy-success {
color: var(--success-color);
}
+19 -23
View File
@@ -8,8 +8,8 @@ import {
mdiContentDuplicate,
mdiDevices,
mdiDotsVertical,
mdiFormatListBulletedSquare,
mdiInformationOutline,
mdiLinkVariant,
mdiPencil,
mdiPencilOff,
mdiPencilOutline,
@@ -60,7 +60,7 @@ import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-prev";
import "../../components/ha-related-items";
import "./ha-more-info-related";
import type {
EntityRegistryEntry,
ExtEntityRegistryEntry,
@@ -69,8 +69,6 @@ import {
getExtendedEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../data/entity/entity_registry";
import type { ItemType } from "../../data/search";
import { SearchableDomains } from "../../data/search";
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
import type { EntitySettingsState } from "../../panels/config/entities/entity-registry-settings-editor";
import type { Helper } from "../../panels/config/helpers/const";
@@ -598,20 +596,22 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
const breadcrumb = [areaName, deviceName, entityName].filter(
(v): v is string => Boolean(v)
);
const defaultTitle = breadcrumb.pop() || entityId;
const addToTitle = this.hass.localize(
"ui.dialogs.more_info_control.add_to.title",
{ target: defaultTitle }
);
const addToMenuItem = this.hass.localize(
"ui.dialogs.more_info_control.add_to.item"
);
const title =
const viewTitle =
this._currView === "details"
? this.hass.localize("ui.dialogs.more_info_control.details")
: this._currView === "add_to"
? addToTitle
: this._childView?.viewTitle || defaultTitle;
: this._currView === "related"
? this.hass.localize("ui.dialogs.more_info_control.related")
: this._currView === "add_to"
? addToMenuItem
: this._childView?.viewTitle;
const defaultTitle = breadcrumb[breadcrumb.length - 1] || entityId;
if (!viewTitle) {
breadcrumb.pop();
}
const title = viewTitle || defaultTitle;
const favoritesContext =
this._entry && stateObj
@@ -856,7 +856,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
<ha-dropdown-item value="related">
<ha-svg-icon
slot="icon"
.path=${mdiInformationOutline}
.path=${mdiLinkVariant}
></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.related"
@@ -865,7 +865,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
<ha-dropdown-item value="details">
<ha-svg-icon
slot="icon"
.path=${mdiFormatListBulletedSquare}
.path=${mdiInformationOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.details"
@@ -954,15 +954,11 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
`
: this._currView === "related"
? html`
<ha-related-items
<ha-more-info-related
.hass=${this.hass}
.itemId=${entityId}
.itemType=${
SearchableDomains.has(domain)
? (domain as ItemType)
: "entity"
}
></ha-related-items>
.entry=${this._entry}
.params=${{ entityId }}
></ha-more-info-related>
`
: this._currView === "add_to"
? html`
@@ -0,0 +1,304 @@
import { mdiDevices, mdiTextureBox } from "@mdi/js";
import { consume } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeFloorName } from "../../common/entity/compute_floor_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-floor-icon";
import "../../components/ha-icon";
import "../../components/ha-icon-next";
import "../../components/ha-label";
import "../../components/ha-related-items";
import "../../components/ha-svg-icon";
import "../../components/item/ha-list-item-button";
import "../../components/item/ha-list-item-value";
import "../../components/list/ha-grouped-list";
import { labelsContext } from "../../data/context";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { ItemType, RelatedResult } from "../../data/search";
import { SearchableDomains } from "../../data/search";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
interface RelatedViewParams {
entityId: string;
}
interface ContextEntry {
translationKey: LocalizeKeys;
value: string;
displayValue?: TemplateResult;
href?: string;
icon?: TemplateResult;
}
const CONTEXT_SECTIONS: (keyof RelatedResult)[] = [
"device",
"area",
"integration",
"config_entry",
];
@customElement("ha-more-info-related")
class HaMoreInfoRelated extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
@property({ attribute: false }) public params?: RelatedViewParams;
@state() private _stateObj?: HassEntity;
@consume({ context: labelsContext, subscribe: true })
@state()
private _labels?: LabelRegistryEntry[];
protected willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
if (changedProps.has("entry") && this.entry) {
this.hass.loadBackendTranslation("title", [this.entry.platform]);
}
if (changedProps.has("params") || changedProps.has("hass")) {
if (this.params?.entityId && this.hass) {
this._stateObj = this.hass.states[this.params.entityId];
}
}
}
protected render() {
if (!this.params || !this._stateObj) {
return nothing;
}
const domain = computeDomain(this.params.entityId);
const itemType = SearchableDomains.has(domain)
? (domain as ItemType)
: "entity";
const { floor, area, device } = getEntityContext(
this._stateObj,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const floorName = floor ? computeFloorName(floor) : undefined;
const areaName = area ? (computeAreaName(area) ?? area.area_id) : undefined;
const deviceName = device
? computeDeviceNameDisplay(device, this.hass.localize, this.hass.states)
: undefined;
const integrationName = this.entry?.platform
? this.hass.localize(`component.${this.entry.platform}.title`) ||
this.entry.platform
: undefined;
const labels =
this.entry?.labels.map((labelId) => ({
id: labelId,
entry: this._labels?.find((label) => label.label_id === labelId),
})) ?? [];
const contextEntries: ContextEntry[] = [];
if (floor && floorName) {
contextEntries.push({
translationKey: "ui.dialogs.more_info_control.floor",
value: floorName,
href: "/config/areas/dashboard",
icon: html`<ha-floor-icon slot="end" .floor=${floor}></ha-floor-icon>`,
});
}
if (area && areaName) {
contextEntries.push({
translationKey: "ui.components.related-items.area",
value: areaName,
href: `/config/areas/area/${area.area_id}`,
icon: area.icon
? html`<ha-icon slot="end" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon slot="end" .path=${mdiTextureBox}></ha-svg-icon>`,
});
}
if (device && deviceName) {
contextEntries.push({
translationKey: "ui.components.related-items.device",
value: deviceName,
href: `/config/devices/device/${device.id}`,
icon: html`<ha-svg-icon slot="end" .path=${mdiDevices}></ha-svg-icon>`,
});
}
if (this.entry?.platform && integrationName) {
contextEntries.push({
translationKey: "ui.components.related-items.integration",
value: integrationName,
href: this.entry.config_entry_id
? `/config/integrations/integration/${this.entry.platform}#config_entry=${this.entry.config_entry_id}`
: undefined,
icon: html`<img
slot="end"
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl(
{
domain: this.entry.platform,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
/>`,
});
}
contextEntries.push({
translationKey: "ui.dialogs.more_info_control.labels",
value:
labels.map(({ id, entry }) => entry?.name ?? id).join(", ") ||
this.hass.localize("ui.dialogs.more_info_control.no_labels"),
displayValue: labels.length
? html`<div class="labels">
${labels.map(
({ id, entry }) => html`
<ha-label
class="text-ellipsis"
.color=${entry?.color ?? undefined}
.description=${entry?.description ?? undefined}
>
${
entry?.icon
? html`<ha-icon
slot="icon"
.icon=${entry.icon}
></ha-icon>`
: nothing
}
${entry?.name ?? id}
</ha-label>
`
)}
</div>`
: undefined,
});
return html`
<div class="content">
<ha-grouped-list
.header=${this.hass.localize("ui.dialogs.more_info_control.context")}
>
${this._renderEntries(contextEntries)}
</ha-grouped-list>
<ha-related-items
.hass=${this.hass}
.itemId=${this.params.entityId}
.itemType=${itemType}
.exclude=${itemType === "entity" ? CONTEXT_SECTIONS : undefined}
></ha-related-items>
</div>
`;
}
private _renderEntries(entries: ContextEntry[]) {
return entries.map((entry) => {
const label = this.hass.localize(entry.translationKey);
if (!entry.href) {
return html`
<ha-list-item-value .label=${label}
>${entry.displayValue ?? entry.value}</ha-list-item-value
>
`;
}
return html`
<ha-list-item-button .href=${entry.href}>
<div class="link-row" slot="content">
<div class="label">${label}</div>
<div class="value">${entry.value}</div>
</div>
${entry.icon ?? nothing}
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
`;
});
}
static styles: CSSResultGroup = css`
:host {
display: flex;
flex-direction: column;
flex: 1;
}
.content {
padding: var(--ha-space-6);
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
}
ha-related-items {
margin-top: var(--ha-space-6);
}
ha-list-item-button {
--ha-row-item-padding-block: var(--ha-space-2);
--ha-row-item-min-height: 40px;
--ha-row-item-gap: var(--ha-space-3);
--mdc-icon-size: 20px;
}
ha-list-item-button::part(end) {
gap: var(--ha-space-2);
}
ha-list-item-button img {
width: 20px;
height: 20px;
object-fit: contain;
}
.link-row {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--ha-space-3);
}
.link-row .label {
flex: 1;
color: var(--secondary-text-color);
}
.link-row .value {
max-width: 60%;
min-width: 0;
text-align: end;
overflow-wrap: anywhere;
}
.labels {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: var(--ha-space-1);
}
.labels ha-label {
min-width: 0;
max-width: 100%;
}
ha-icon-next {
color: var(--secondary-text-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-related": HaMoreInfoRelated;
}
}
@@ -9,8 +9,6 @@ import {
import type { ResolvedMediaSource } from "../../data/media_source";
import type { HomeAssistant } from "../../types";
export const ERR_UNSUPPORTED_MEDIA = "Unsupported Media";
export class BrowserMediaPlayer {
private player: HTMLAudioElement;
@@ -24,12 +22,10 @@ export class BrowserMediaPlayer {
public item: MediaPlayerItem,
public resolved: ResolvedMediaSource,
volume: number,
private onChange: () => void
private onChange: () => void,
private onError: () => void
) {
const player = new Audio(this.resolved.url);
if (player.canPlayType(resolved.mime_type) === "") {
throw new Error(ERR_UNSUPPORTED_MEDIA);
}
player.autoplay = true;
player.volume = volume;
player.addEventListener("play", this._handleChange);
@@ -40,6 +36,8 @@ export class BrowserMediaPlayer {
player.addEventListener("pause", this._handleChange);
player.addEventListener("ended", this._handleChange);
player.addEventListener("canplaythrough", this._handleChange);
player.addEventListener("seeked", this._handleChange);
player.addEventListener("error", this._handleError);
this.player = player;
}
@@ -49,6 +47,12 @@ export class BrowserMediaPlayer {
}
};
private _handleError = () => {
if (!this._removed) {
this.onError();
}
};
public pause() {
this.buffering = false;
this.player.pause();
@@ -63,6 +67,10 @@ export class BrowserMediaPlayer {
this.onChange();
}
public seek(position: number) {
this.player.currentTime = position;
}
public remove() {
this._removed = true;
// @ts-ignore
@@ -100,7 +108,10 @@ export class BrowserMediaPlayer {
// eslint-disable-next-line no-bitwise
MediaPlayerEntityFeature.PLAY |
MediaPlayerEntityFeature.PAUSE |
MediaPlayerEntityFeature.VOLUME_SET,
MediaPlayerEntityFeature.VOLUME_SET |
(Number.isFinite(this.player.duration)
? MediaPlayerEntityFeature.SEEK
: 0),
};
if (this.player.duration) {
+25 -32
View File
@@ -58,10 +58,7 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-marquee";
import {
BrowserMediaPlayer,
ERR_UNSUPPORTED_MEDIA,
} from "./browser-media-player";
import { BrowserMediaPlayer } from "./browser-media-player";
declare global {
interface HASSDomEvents {
@@ -158,25 +155,21 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
throw Error("Only browser supported");
}
this._tearDownBrowserPlayer();
try {
this._browserPlayer = new BrowserMediaPlayer(
this.hass,
item,
resolved,
this._browserPlayerVolume,
() => this.requestUpdate("_browserPlayer")
);
} catch (err: any) {
if (err.message === ERR_UNSUPPORTED_MEDIA) {
this._browserPlayer = new BrowserMediaPlayer(
this.hass,
item,
resolved,
this._browserPlayerVolume,
() => this.requestUpdate("_browserPlayer"),
() => {
this._tearDownBrowserPlayer();
showAlertDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.media_not_supported"
),
});
} else {
throw err;
}
}
);
this._newMediaExpected = false;
}
@@ -313,13 +306,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
aria-label=${this.hass.localize(
"ui.card.media_player.track_position"
)}
?disabled=${
isBrowser ||
!supportsFeature(
stateObj,
MediaPlayerEntityFeature.SEEK
)
}
?disabled=${!supportsFeature(
stateObj,
MediaPlayerEntityFeature.SEEK
)}
@change=${this._handleMediaSeekChanged}
></ha-slider>`
: html`
@@ -336,13 +326,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
aria-label=${this.hass.localize(
"ui.card.media_player.track_position"
)}
?disabled=${
isBrowser ||
!supportsFeature(
stateObj,
MediaPlayerEntityFeature.SEEK
)
}
?disabled=${!supportsFeature(
stateObj,
MediaPlayerEntityFeature.SEEK
)}
@change=${this._handleMediaSeekChanged}
></ha-slider>
<div>${mediaDuration}</div>
@@ -607,11 +594,17 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
}
private _handleMediaSeekChanged(e: HASSDomTargetEvent<HaSlider>): void {
if (this.entityId === BROWSER_PLAYER || !this._stateObj) {
if (!this._stateObj) {
return;
}
const newValue = e.target.value;
if (this.entityId === BROWSER_PLAYER) {
this._browserPlayer?.seek(newValue);
return;
}
this.hass.callService("media_player", "media_seek", {
entity_id: this._stateObj.entity_id,
seek_position: newValue,
+11 -9
View File
@@ -1180,12 +1180,12 @@
"integration": "Integration",
"device": "Device",
"area": "Area",
"entity": "Related entities",
"group": "Part of the following groups",
"scene": "Part of the following scenes",
"script": "Part of the following scripts",
"automation": "Part of the following automations",
"blueprint": "Using blueprint"
"entity": "Entities",
"group": "Groups",
"scene": "Scenes",
"script": "Scripts",
"automation": "Automations",
"blueprint": "Blueprint"
},
"data-table": {
"search": "Search",
@@ -1687,13 +1687,15 @@
"person": "Edit person"
},
"details": "Details",
"entity": "Entity",
"state": "State",
"context": "Context",
"floor": "Floor",
"entity_id": "Entity ID",
"entity_id": "ID",
"copy_value": "Copy {label}: {value}",
"labels": "Labels",
"no_labels": "None",
"toggle_yaml_mode": "Toggle YAML mode",
"translated": "Translated",
"raw": "Raw",
"back_to_info": "Back to info",
"info": "Information",
"related": "Related",
+4 -4
View File
@@ -67,10 +67,10 @@ export const moreInfoViewElements: ViewElementSmokeCase<MoreInfoView>[] = [
},
{
view: "related",
element: "ha-related-items",
// search/related is mocked to return no relations, so the empty list
// renders.
content: [{ selector: "ha-related-items >> ha-list" }],
element: "ha-more-info-related",
// search/related is mocked to return no relations, so only the context
// group renders.
content: [{ selector: "ha-more-info-related >> ha-grouped-list" }],
},
{
view: "add_to",