Compare commits

..

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
67d3a05a3d Simplify action header logic - use describeAction directly for title
Co-authored-by: MindFreeze <5219205+MindFreeze@users.noreply.github.com>
2025-10-15 10:15:02 +00:00
copilot-swe-agent[bot]
5a2aed3bb2 Use describeAction for action sidebar headers instead of generic type labels
Co-authored-by: MindFreeze <5219205+MindFreeze@users.noreply.github.com>
2025-10-15 09:35:28 +00:00
copilot-swe-agent[bot]
9da8f8c205 Initial plan 2025-10-15 09:22:18 +00:00
Paul Bottein
9cd74fbff8 Make custom text more discoverable in entity name picker (#27505)
* Make custom text more discoverable in entity name picker

* Fix custom option selection

* Rename label
2025-10-15 10:07:40 +02:00
karwosts
33a7aacd83 Use media selector in picture-glance and picture-elements (#27506) 2025-10-15 08:31:19 +03:00
karwosts
39546615bb Missing translation on back button (#27510) 2025-10-15 06:09:30 +02:00
J. Nick Koston
be51cbc944 Add support for next_flow on abort (#27491) 2025-10-14 11:45:11 -10:00
17 changed files with 363 additions and 251 deletions

View File

@@ -25,6 +25,7 @@ import "../ha-sortable";
interface EntityNameOption {
primary: string;
secondary?: string;
field_label: string;
value: string;
}
@@ -41,6 +42,23 @@ const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]);
const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
const formatOptionValue = (item: EntityNameItem) => {
if (item.type === "text" && item.text) {
return item.text;
}
return `___${item.type}___`;
};
const parseOptionValue = (value: string): EntityNameItem => {
if (value.startsWith("___") && value.endsWith("___")) {
const type = value.slice(3, -3);
if (KNOWN_TYPES.has(type)) {
return { type: type as EntityNameType };
}
}
return { type: "text", text: value };
};
@customElement("ha-entity-name-picker")
export class HaEntityNamePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -121,13 +139,23 @@ export class HaEntityNamePicker extends LitElement {
return {
primary,
secondary,
value: name,
field_label: primary,
value: formatOptionValue({ type: name }),
};
});
return items;
});
private _customNameOption = memoizeOne((text: string) => ({
primary: this.hass.localize(
"ui.components.entity.entity-name-picker.custom_name"
),
secondary: `"${text}"`,
field_label: text,
value: formatOptionValue({ type: "text", text }),
}));
private _formatItem = (item: EntityNameItem) => {
if (item.type === "text") {
return `"${item.text}"`;
@@ -214,7 +242,7 @@ export class HaEntityNamePicker extends LitElement {
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="primary"
item-label-path="field_label"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@@ -286,14 +314,13 @@ export class HaEntityNamePicker extends LitElement {
const initialItem =
this._editIndex != null ? this._value[this._editIndex] : undefined;
const initialValue = initialItem
? initialItem.type === "text"
? initialItem.text
: initialItem.type
: "";
const initialValue = initialItem ? formatOptionValue(initialItem) : "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
if (initialItem && initialItem.type === "text" && initialItem.text) {
filteredItems.push(this._customNameOption(initialItem.text));
}
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
@@ -326,11 +353,7 @@ export class HaEntityNamePicker extends LitElement {
const currentItem =
this._editIndex != null ? this._value[this._editIndex] : undefined;
const currentValue = currentItem
? currentItem.type === "text"
? currentItem.text
: currentItem.type
: "";
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
this._comboBox.filteredItems = this._filterSelectedOptions(
options,
@@ -352,6 +375,7 @@ export class HaEntityNamePicker extends LitElement {
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
const filteredItems = fuse.search(filter).map((result) => result.item);
filteredItems.push(this._customNameOption(input));
this._comboBox.filteredItems = filteredItems;
}
@@ -385,9 +409,7 @@ export class HaEntityNamePicker extends LitElement {
return;
}
const item: EntityNameItem = KNOWN_TYPES.has(value as any)
? { type: value as EntityNameType }
: { type: "text", text: value };
const item: EntityNameItem = parseOptionValue(value);
const newValue = [...this._value];

View File

@@ -107,14 +107,15 @@ export class HaMediaSelector extends LitElement {
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
if (this.selector.media?.image_upload && !this.value) {
return html`<ha-picture-upload
.hass=${this.hass}
.value=${null}
.contentIdHelper=${this.selector.media?.content_id_helper}
select-media
full-media
@media-picked=${this._pictureUploadMediaPicked}
></ha-picture-upload>`;
return html`${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-picture-upload
.hass=${this.hass}
.value=${null}
.contentIdHelper=${this.selector.media?.content_id_helper}
select-media
full-media
@media-picked=${this._pictureUploadMediaPicked}
></ha-picture-upload>`;
}
return html`
@@ -141,6 +142,7 @@ export class HaMediaSelector extends LitElement {
`}
${!supportsBrowse
? html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-alert>
${this.hass.localize(
"ui.components.selectors.media.browse_not_supported"
@@ -154,7 +156,8 @@ export class HaMediaSelector extends LitElement {
.computeHelper=${this._computeHelperCallback}
></ha-form>
`
: html`<ha-card
: html`${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-card
outlined
tabindex="0"
role="button"

View File

@@ -79,6 +79,7 @@ export interface DataEntryFlowStepAbort {
reason: string;
description_placeholders?: Record<string, string>;
translation_domain?: string;
next_flow?: [FlowType, string]; // [flow_type, flow_id]
}
export interface DataEntryFlowStepProgress {

View File

@@ -472,7 +472,10 @@ class DataEntryFlowDialog extends LitElement {
this._step = undefined;
await this.updateComplete;
this._step = _step;
if (_step.type === "create_entry" && _step.next_flow) {
if (
(_step.type === "create_entry" || _step.type === "abort") &&
_step.next_flow
) {
// skip device rename if there is a chained flow
this._step = undefined;
this._handler = undefined;
@@ -486,32 +489,36 @@ class DataEntryFlowDialog extends LitElement {
carryOverDevices: this._devices(
this._params!.flowConfig.showDevices,
Object.values(this.hass.devices),
_step.result?.entry_id,
_step.type === "create_entry" ? _step.result?.entry_id : undefined,
this._params!.carryOverDevices
).map((device) => device.id),
dialogClosedCallback: this._params!.dialogClosedCallback,
});
} else if (_step.next_flow[0] === "options_flow") {
showOptionsFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
{
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
}
);
if (_step.type === "create_entry") {
showOptionsFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
{
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
}
);
}
} else if (_step.next_flow[0] === "config_subentries_flow") {
showSubConfigFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
_step.next_flow[0],
{
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
}
);
if (_step.type === "create_entry") {
showSubConfigFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
_step.next_flow[0],
{
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
}
);
}
} else {
this.closeDialog();
showAlertDialog(this._params!.dialogParentElement!, {

View File

@@ -1,16 +1,17 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { mdiClose } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-bottom-sheet";
import "../../../../components/ha-button";
import {
getMobileOpenFromBottomAnimation,
getMobileCloseToBottomAnimation,
} from "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-toggle";
import "../../../../components/ha-wa-dialog";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { LightColor, LightEntity } from "../../../../data/light";
import {
@@ -40,11 +41,7 @@ class DialogLightColorFavorite extends LitElement {
@state() private _modes: LightPickerMode[] = [];
@state() private _open = false;
@state() private _narrow = false;
private _mediaQuery?: MediaQueryList;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
dialogParams: LightColorFavoriteDialogParams
@@ -53,22 +50,12 @@ class DialogLightColorFavorite extends LitElement {
this._dialogParams = dialogParams;
this._color = dialogParams.initialColor ?? this._computeCurrentColor();
this._updateModes();
this._mediaQuery = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
);
this._narrow = this._mediaQuery.matches;
this._mediaQuery.addEventListener("change", this._handleMediaChange);
this._open = true;
}
public closeDialog(): void {
this._open = false;
this._dialog?.close();
}
private _handleMediaChange = (ev: MediaQueryListEvent) => {
this._narrow = ev.matches;
};
private _updateModes() {
const supportsTemp = lightSupportsColorMode(
this.stateObj!,
@@ -133,16 +120,16 @@ class DialogLightColorFavorite extends LitElement {
);
}
private _cancel() {
private async _cancel() {
this._dialogParams?.cancel?.();
}
private _cancelDialog() {
this._cancel();
this.closeDialog();
}
private _dialogClosed(): void {
if (this._mediaQuery) {
this._mediaQuery.removeEventListener("change", this._handleMediaChange);
this._mediaQuery = undefined;
}
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
@@ -151,6 +138,7 @@ class DialogLightColorFavorite extends LitElement {
private async _save() {
if (!this._color) {
this._cancel();
return;
}
this._dialogParams?.submit?.(this._color);
@@ -165,126 +153,89 @@ class DialogLightColorFavorite extends LitElement {
this._mode = newMode;
}
private _renderModes() {
if (this._modes.length <= 1) {
return nothing;
}
return html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span class="wheel ${classMap({ [value]: true })}"></span>
</ha-icon-button-toggle>
`
)}
</div>
`;
}
private _renderColorPicker() {
return html`
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
`;
}
private _renderButtons() {
return html`
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._cancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
appearance="accent"
@click=${this._save}
.disabled=${!this._color}
>
${this.hass.localize("ui.common.save")}
</ha-button>
`;
}
protected render() {
if (!this._entry || !this.stateObj) {
return nothing;
}
if (this._narrow) {
return html`
<ha-bottom-sheet .open=${this._open} @closed=${this._dialogClosed}>
<div class="bottom-sheet-container">
<ha-dialog-header>
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">${this._dialogParams?.title}</span>
</ha-dialog-header>
<div class="header">${this._renderModes()}</div>
<div class="content">${this._renderColorPicker()}</div>
<div class="buttons">
<ha-button appearance="plain" @click=${this._cancel}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
appearance="accent"
@click=${this._save}
.disabled=${!this._color}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</div>
</div>
</ha-bottom-sheet>
`;
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
.headerTitle=${this._dialogParams?.title ?? ""}
<ha-md-dialog
open
@cancel=${this._cancel}
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<div class="header">${this._renderModes()}</div>
<div class="content">${this._renderColorPicker()}</div>
<ha-dialog-footer slot="footer"
>${this._renderButtons()}</ha-dialog-footer
>
</ha-wa-dialog>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"
>${this._dialogParams?.title}</span
>
</ha-dialog-header>
<div slot="content">
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
</div>
<div slot="actions">
<ha-button appearance="plain" @click=${this._cancelDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._save} .disabled=${!this._color}
>${this.hass.localize("ui.common.save")}</ha-button
>
</div>
</ha-md-dialog>
`;
}
@@ -292,39 +243,24 @@ class DialogLightColorFavorite extends LitElement {
return [
haStyleDialog,
css`
ha-wa-dialog {
--ha-dialog-width-md: 420px;
--dialog-content-padding: 0;
--dialog-surface-position: fixed;
ha-md-dialog {
min-width: 420px; /* prevent width jumps when switching modes */
max-height: min(
600px,
100% - 48px
); /* prevent scrolling on desktop */
}
ha-bottom-sheet {
--ha-bottom-sheet-max-width: 560px;
--ha-bottom-sheet-padding: 0;
--ha-bottom-sheet-surface-background: var(--card-background-color);
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
min-width: 100%;
min-height: auto;
max-height: calc(100% - 100px);
margin-bottom: 0;
.bottom-sheet-container {
display: flex;
flex-direction: column;
height: 100%;
}
.bottom-sheet-container .header {
padding: 0 24px;
}
.bottom-sheet-container .content {
flex: 1;
overflow-y: auto;
}
.buttons {
display: flex;
justify-content: flex-end;
gap: var(--ha-space-2);
padding: var(--ha-space-4) var(--ha-space-6);
padding-bottom: max(var(--ha-space-4), env(safe-area-inset-bottom));
--md-dialog-container-shape-start-start: 28px;
--md-dialog-container-shape-start-end: 28px;
}
}
.content {
@@ -332,14 +268,14 @@ class DialogLightColorFavorite extends LitElement {
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--ha-space-6);
padding: 24px;
flex: 1;
}
.modes {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 0 var(--ha-space-6);
padding: 0 24px;
}
.wheel {
width: 30px;

View File

@@ -1,3 +1,4 @@
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiContentCopy,
@@ -13,6 +14,7 @@ import {
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { LocalizeKeys } from "../../../../common/translations/localize";
@@ -20,7 +22,16 @@ import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
import type { ActionSidebarConfig } from "../../../../data/automation";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { FloorRegistryEntry } from "../../../../data/floor_registry";
import type { LabelRegistryEntry } from "../../../../data/label_registry";
import type { RepeatAction } from "../../../../data/script";
import { describeAction } from "../../../../data/script_i18n";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
@@ -48,6 +59,18 @@ export default class HaAutomationSidebarAction extends LitElement {
@state() private _warnings?: string[];
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>;
@query(".sidebar-editor")
public editor?: HaAutomationConditionEditor;
@@ -78,15 +101,20 @@ export default class HaAutomationSidebarAction extends LitElement {
const isBuildingBlock = ACTION_BUILDING_BLOCKS.includes(type || "");
const title = capitalizeFirstLetter(
describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
actionConfig
)
);
const subtitle = this.hass.localize(
"ui.panel.config.automation.editor.actions.action"
);
const title =
this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys
) || type;
const description = isBuildingBlock
? this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys

View File

@@ -126,7 +126,16 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
return nothing;
}
let image: string | undefined = this._config.image;
let image: string | undefined =
(typeof this._config?.image === "object" &&
this._config.image.media_content_id) ||
(this._config.image as string | undefined);
const darkModeImage: string | undefined =
(typeof this._config?.dark_mode_image === "object" &&
this._config.dark_mode_image.media_content_id) ||
(this._config.dark_mode_image as string | undefined);
if (this._config.image_entity) {
const stateObj: ImageEntity | PersonEntity | undefined =
this.hass.states[this._config.image_entity];
@@ -156,7 +165,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
.entity=${this._config.entity}
.aspectRatio=${this._config.aspect_ratio}
.darkModeFilter=${this._config.dark_mode_filter}
.darkModeImage=${this._config.dark_mode_image}
.darkModeImage=${darkModeImage}
></hui-image>
${this._elements}
</div>

View File

@@ -179,7 +179,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
return nothing;
}
let image: string | undefined = this._config.image;
let image: string | undefined =
(typeof this._config?.image === "object" &&
this._config.image.media_content_id) ||
(this._config.image as string | undefined);
if (this._config.image_entity) {
const stateObj: ImageEntity | PersonEntity | undefined =
this.hass.states[this._config.image_entity];

View File

@@ -459,7 +459,7 @@ export interface PictureCardConfig extends LovelaceCardConfig {
export interface PictureElementsCardConfig extends LovelaceCardConfig {
title?: string;
image?: string;
image?: string | MediaSelectorValue;
image_entity?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
@@ -469,7 +469,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
entity?: string;
elements: LovelaceElementConfig[];
theme?: string;
dark_mode_image?: string;
dark_mode_image?: string | MediaSelectorValue;
dark_mode_filter?: string;
}
@@ -494,7 +494,7 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig {
export interface PictureGlanceCardConfig extends LovelaceCardConfig {
entities: (string | PictureGlanceEntityConfig)[];
title?: string;
image?: string;
image?: string | MediaSelectorValue;
image_entity?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];

View File

@@ -2,7 +2,15 @@ import memoizeOne from "memoize-one";
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import {
any,
assert,
literal,
object,
optional,
string,
union,
} from "superstruct";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
@@ -15,7 +23,7 @@ import { actionConfigStruct } from "../../structs/action-struct";
const imageElementConfigStruct = object({
type: literal("image"),
entity: optional(string()),
image: optional(string()),
image: optional(union([string(), object()])),
style: optional(any()),
title: optional(string()),
tap_action: optional(actionConfigStruct),
@@ -87,7 +95,20 @@ export class HuiImageElementEditor
},
],
},
{ name: "image", selector: { image: {} } },
{
name: "image",
selector: {
media: {
accept: ["image/*"] as string[],
clearable: true,
image_upload: true,
hide_content_type: true,
content_id_helper: localize(
"ui.panel.lovelace.editor.card.picture.content_id_helper"
),
},
},
},
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "camera_view",
@@ -119,7 +140,7 @@ export class HuiImageElementEditor
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.data=${this._processData(this._config)}
.schema=${this._schema(this.hass.localize)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
@@ -127,6 +148,13 @@ export class HuiImageElementEditor
`;
}
private _processData = memoizeOne((config: ImageElementConfig) => ({
...config,
...(typeof config.image === "string"
? { image: { media_content_id: config.image } }
: {}),
}));
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}

View File

@@ -11,6 +11,7 @@ import {
optional,
string,
type,
union,
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -37,14 +38,14 @@ const genericElementConfigStruct = type({
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
image: optional(string()),
image: optional(union([string(), object()])),
camera_image: optional(string()),
camera_view: optional(string()),
elements: array(genericElementConfigStruct),
title: optional(string()),
state_filter: optional(any()),
theme: optional(string()),
dark_mode_image: optional(string()),
dark_mode_image: optional(union([string(), object()])),
dark_mode_filter: optional(any()),
})
);
@@ -76,8 +77,34 @@ export class HuiPictureElementsCardEditor
),
schema: [
{ name: "title", selector: { text: {} } },
{ name: "image", selector: { image: {} } },
{ name: "dark_mode_image", selector: { image: {} } },
{
name: "image",
selector: {
media: {
accept: ["image/*"] as string[],
clearable: true,
image_upload: true,
hide_content_type: true,
content_id_helper: localize(
"ui.panel.lovelace.editor.card.picture.content_id_helper"
),
},
},
},
{
name: "dark_mode_image",
selector: {
media: {
accept: ["image/*"] as string[],
clearable: true,
image_upload: true,
hide_content_type: true,
content_id_helper: localize(
"ui.panel.lovelace.editor.card.picture.content_id_helper"
),
},
},
},
{
name: "camera_image",
selector: { entity: { domain: "camera" } },
@@ -124,7 +151,7 @@ export class HuiPictureElementsCardEditor
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.data=${this._processData(this._config)}
.schema=${this._schema(this.hass.localize)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._formChanged}
@@ -138,6 +165,16 @@ export class HuiPictureElementsCardEditor
`;
}
private _processData = memoizeOne((config: PictureElementsCardConfig) => ({
...config,
...(typeof config.image === "string"
? { image: { media_content_id: config.image } }
: {}),
...(typeof config.dark_mode_image === "string"
? { dark_mode_image: { media_content_id: config.dark_mode_image } }
: {}),
}));
private _formChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {

View File

@@ -11,6 +11,7 @@ import {
object,
optional,
string,
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
@@ -40,7 +41,7 @@ const cardConfigStruct = assign(
object({
title: optional(string()),
entity: optional(string()),
image: optional(string()),
image: optional(union([string(), object()])),
image_entity: optional(string()),
camera_image: optional(string()),
camera_view: optional(enums(["auto", "live"])),
@@ -71,7 +72,20 @@ export class HuiPictureGlanceCardEditor
(localize: LocalizeFunc) =>
[
{ name: "title", selector: { text: {} } },
{ name: "image", selector: { image: {} } },
{
name: "image",
selector: {
media: {
accept: ["image/*"] as string[],
clearable: true,
image_upload: true,
hide_content_type: true,
content_id_helper: localize(
"ui.panel.lovelace.editor.card.picture.content_id_helper"
),
},
},
},
{
name: "image_entity",
selector: { entity: { domain: ["image", "person"] } },
@@ -232,12 +246,10 @@ export class HuiPictureGlanceCardEditor
`;
}
const data = { camera_view: "auto", fit_mode: "cover", ...this._config };
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.data=${this._processData(this._config)}
.schema=${this._schema(this.hass.localize)}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@@ -255,6 +267,15 @@ export class HuiPictureGlanceCardEditor
`;
}
private _processData = memoizeOne((config: PictureGlanceCardConfig) => ({
camera_view: "auto",
fit_mode: "cover",
...config,
...(typeof config.image === "string"
? { image: { media_content_id: config.image } }
: {}),
}));
private _goBack(): void {
this._subElementEditorConfig = undefined;
}

View File

@@ -153,13 +153,21 @@ export class HuiPictureElementsCardRowEditor extends LitElement {
(element as ServiceButtonElementConfig).service ??
""
);
case "image":
return (
element.title ??
(element as ImageElementConfig).image ??
(element as ImageElementConfig).camera_image ??
""
);
case "image": {
if (element.title) {
return element.title;
}
const config = element as ImageElementConfig;
if (config.image) {
if (typeof config.image === "string") {
return config.image;
}
return (
config.image.metadata?.title || config.image.media_content_id || ""
);
}
return config.camera_image || "";
}
case "conditional":
return (
element.title ??

View File

@@ -50,6 +50,12 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
stateObj = this.hass.states[this._config.image_entity] as ImageEntity;
}
const image = stateObj
? computeImageUrl(stateObj)
: (typeof this._config?.image === "object" &&
this._config.image.media_content_id) ||
(this._config.image as string | undefined);
return html`
<div
@action=${this._handleAction}
@@ -67,7 +73,7 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
<hui-image
.hass=${this.hass}
.entity=${this._config.entity}
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
.image=${image}
.stateImage=${this._config.state_image}
.cameraImage=${this._config.camera_image}
.cameraView=${this._config.camera_view}

View File

@@ -3,6 +3,7 @@ import type { ActionConfig } from "../../../data/lovelace/config/action";
import type { HomeAssistant } from "../../../types";
import type { Condition } from "../common/validate-condition";
import type { HuiImage } from "../components/hui-image";
import type { MediaSelectorValue } from "../../../data/selector";
interface LovelaceElementConfigBase {
type: string;
@@ -45,7 +46,7 @@ export interface ImageElementConfig extends LovelaceElementConfigBase {
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
image?: string;
image?: string | MediaSelectorValue;
image_entity?: string;
state_image?: string;
camera_image?: string;

View File

@@ -518,6 +518,7 @@ class HUIRoot extends LitElement {
${isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>

View File

@@ -667,7 +667,8 @@
"floor_missing": "No floor assigned",
"device_missing": "No related device"
},
"add": "Add"
"add": "Add",
"custom_name": "Custom name"
},
"entity-attribute-picker": {
"attribute": "Attribute",