-
`;
}
@@ -401,14 +372,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
- this._panelOrder,
- this._hiddenPanels,
+ this.panelOrder,
+ this.hiddenPanels,
this.hass.locale
);
// prettier-ignore
return html`
-
- ${this.editMode
- ? this._renderPanelsEdit(beforeSpacer, selectedPanel)
- : this._renderPanels(beforeSpacer, selectedPanel)}
+ ${this._renderPanels(beforeSpacer, selectedPanel)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer, selectedPanel)}
${this._renderExternalConfiguration()}
-
`;
}
- private _renderPanels(
- panels: PanelInfo[],
- selectedPanel: string,
- sortable = false
- ) {
+ private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
@@ -444,36 +407,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
- selectedPanel,
- sortable
+ selectedPanel
)
);
}
- private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
- return html`
- ${this._renderPanels(beforeSpacer, selectedPanel, true)}
- ${this._renderSpacer()}${this._renderHiddenPanels()}
- `;
- }
-
private _renderPanel(
urlPath: string,
title: string | null,
icon: string | null | undefined,
iconPath: string | null | undefined,
- selectedPanel: string,
- sortable = false
+ selectedPanel: string
) {
return urlPath === "config"
? this._renderConfiguration(title, selectedPanel)
: html`
`
: html``}
${title}
- ${this.editMode
- ? html``
- : nothing}
`;
}
- private _panelMoved(ev: CustomEvent) {
- ev.stopPropagation();
- const { oldIndex, newIndex } = ev.detail;
-
- const [beforeSpacer] = computePanels(
- this.hass.panels,
- this.hass.defaultPanel,
- this._panelOrder,
- this._hiddenPanels,
- this.hass.locale
- );
-
- const panelOrder = beforeSpacer.map((panel) => panel.url_path);
- const panel = panelOrder.splice(oldIndex, 1)[0];
- panelOrder.splice(newIndex, 0, panel);
-
- this._panelOrder = panelOrder;
- }
-
- private _renderHiddenPanels() {
- return html`${this._hiddenPanels.length
- ? html`${this._hiddenPanels.map((url) => {
- const panel = this.hass.panels[url];
- if (!panel) {
- return "";
- }
- return html`
- ${panel.url_path === this.hass.defaultPanel && !panel.icon
- ? html``
- : panel.url_path in PANEL_ICONS
- ? html``
- : html``}
- ${panel.url_path === this.hass.defaultPanel
- ? this.hass.localize("panel.states")
- : this.hass.localize(`panel.${panel.title}`) ||
- panel.title}
-
- `;
- })}
- ${this._renderSpacer()}`
- : ""}`;
- }
-
private _renderDivider() {
return html`
`;
}
@@ -677,48 +559,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return;
}
- fireEvent(this, "hass-edit-sidebar", { editMode: true });
+ showEditSidebarDialog(this, {
+ saveCallback: this._saveSidebar,
+ });
}
- private async _editModeActivated() {
- await this._loadEditStyle();
- }
-
- private async _loadEditStyle() {
- if (this._editStyleLoaded) return;
-
- const editStylesImport = await import("../resources/ha-sidebar-edit-style");
-
- const style = document.createElement("style");
- style.innerHTML = (editStylesImport.sidebarEditStyle as CSSResult).cssText;
- this.shadowRoot!.appendChild(style);
-
- await this.updateComplete;
- }
-
- private _closeEditMode() {
- fireEvent(this, "hass-edit-sidebar", { editMode: false });
- }
-
- private async _hidePanel(ev: Event) {
- ev.preventDefault();
- const panel = (ev.currentTarget as any).panel;
- if (this._hiddenPanels.includes(panel)) {
- return;
- }
- // Make a copy for Memoize
- this._hiddenPanels = [...this._hiddenPanels, panel];
- // Remove it from the panel order
- this._panelOrder = this._panelOrder.filter((order) => order !== panel);
- }
-
- private async _unhidePanel(ev: Event) {
- ev.preventDefault();
- const panel = (ev.currentTarget as any).panel;
- this._hiddenPanels = this._hiddenPanels.filter(
- (hidden) => hidden !== panel
- );
- }
+ private _saveSidebar = (order: string[], hidden: string[]) => {
+ fireEvent(this, "hass-edit-sidebar", {
+ order,
+ hidden,
+ });
+ };
private _itemMouseEnter(ev: MouseEvent) {
// On keypresses on the listbox, we're going to ignore mouse enter events
@@ -851,12 +702,12 @@ class HaSidebar extends SubscribeMixin(LitElement) {
);
font-size: var(--ha-font-size-xl);
align-items: center;
- padding-left: calc(4px + env(safe-area-inset-left));
- padding-inline-start: calc(4px + env(safe-area-inset-left));
+ padding-left: calc(4px + var(--safe-area-inset-left));
+ padding-inline-start: calc(4px + var(--safe-area-inset-left));
padding-inline-end: initial;
}
:host([expanded]) .menu {
- width: calc(256px + env(safe-area-inset-left));
+ width: calc(256px + var(--safe-area-inset-left));
}
.menu ha-icon-button {
color: var(--sidebar-icon-color);
@@ -875,12 +726,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
:host([expanded]) .title {
display: initial;
}
- :host([expanded]) .menu mwc-button {
- margin: 0 8px;
- }
- .menu mwc-button {
- width: 100%;
- }
.hidden-panel {
display: none;
}
@@ -890,11 +735,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
box-sizing: border-box;
height: calc(100% - var(--header-height) - 132px);
height: calc(
- 100% - var(--header-height) - 132px - env(safe-area-inset-bottom)
+ 100% - var(--header-height) - 132px - var(--safe-area-inset-bottom)
);
overflow-x: hidden;
background: none;
- margin-left: env(safe-area-inset-left);
+ margin-left: var(--safe-area-inset-left);
}
ha-md-list-item {
@@ -914,7 +759,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
:host([expanded]) ha-md-list-item {
width: 248px;
- width: calc(248px - env(safe-area-inset-left));
+ width: calc(248px - var(--safe-area-inset-left));
}
ha-md-list-item.selected {
@@ -949,7 +794,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
ha-md-list-item .item-text {
- font-family: var(--ha-font-family-body);
display: none;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts
index 20fd210484..e7a98128b7 100644
--- a/src/components/ha-target-picker.ts
+++ b/src/components/ha-target-picker.ts
@@ -419,7 +419,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass}
id="input"
.type=${"device_id"}
- .label=${this.hass.localize(
+ .placeholder=${this.hass.localize(
+ "ui.components.target-picker.add_device_id"
+ )}
+ .searchLabel=${this.hass.localize(
"ui.components.target-picker.add_device_id"
)}
.deviceFilter=${this.deviceFilter}
@@ -438,7 +441,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass}
id="input"
.type=${"label_id"}
- .label=${this.hass.localize(
+ .placeholder=${this.hass.localize(
+ "ui.components.target-picker.add_label_id"
+ )}
+ .searchLabel=${this.hass.localize(
"ui.components.target-picker.add_label_id"
)}
no-add
diff --git a/src/components/ha-time-input.ts b/src/components/ha-time-input.ts
index 7d55082960..a6e380accd 100644
--- a/src/components/ha-time-input.ts
+++ b/src/components/ha-time-input.ts
@@ -28,22 +28,30 @@ export class HaTimeInput extends LitElement {
protected render() {
const useAMPM = useAmPm(this.locale);
- const parts = this.value?.split(":") || [];
- let hours = parts[0];
- const numberHours = Number(parts[0]);
- if (numberHours && useAMPM && numberHours > 12 && numberHours < 24) {
- hours = String(numberHours - 12).padStart(2, "0");
- }
- if (useAMPM && numberHours === 0) {
- hours = "12";
+ let hours = NaN;
+ let minutes = NaN;
+ let seconds = NaN;
+ let numberHours = 0;
+ if (this.value) {
+ const parts = this.value?.split(":") || [];
+ minutes = parts[1] ? Number(parts[1]) : 0;
+ seconds = parts[2] ? Number(parts[2]) : 0;
+ hours = parts[0] ? Number(parts[0]) : 0;
+ numberHours = hours;
+ if (numberHours && useAMPM && numberHours > 12 && numberHours < 24) {
+ hours = numberHours - 12;
+ }
+ if (useAMPM && numberHours === 0) {
+ hours = 12;
+ }
}
return html`
= 12 ? "PM" : "AM"}
.disabled=${this.disabled}
@@ -52,6 +60,11 @@ export class HaTimeInput extends LitElement {
.required=${this.required}
.clearable=${this.clearable && this.value !== undefined}
.helper=${this.helper}
+ day-label="dd"
+ hour-label="hh"
+ min-label="mm"
+ sec-label="ss"
+ ms-label="ms"
>
`;
}
diff --git a/src/components/ha-toast.ts b/src/components/ha-toast.ts
index 8946aee00c..4f6be82b24 100644
--- a/src/components/ha-toast.ts
+++ b/src/components/ha-toast.ts
@@ -14,9 +14,9 @@ export class HaToast extends Snackbar {
.mdc-snackbar {
margin: 8px;
- right: calc(8px + env(safe-area-inset-right));
- bottom: calc(8px + env(safe-area-inset-bottom));
- left: calc(8px + env(safe-area-inset-left));
+ right: calc(8px + var(--safe-area-inset-right));
+ bottom: calc(8px + var(--safe-area-inset-bottom));
+ left: calc(8px + var(--safe-area-inset-left));
}
.mdc-snackbar__surface {
@@ -37,9 +37,9 @@ export class HaToast extends Snackbar {
@media all and (max-width: 450px), all and (max-height: 500px) {
.mdc-snackbar {
- right: env(safe-area-inset-right);
- bottom: env(safe-area-inset-bottom);
- left: env(safe-area-inset-left);
+ right: var(--safe-area-inset-right);
+ bottom: var(--safe-area-inset-bottom);
+ left: var(--safe-area-inset-left);
}
.mdc-snackbar__surface {
min-width: 100%;
diff --git a/src/components/media-player/ha-browse-media-tts.ts b/src/components/media-player/ha-browse-media-tts.ts
index 1924af09a4..c6b327975c 100644
--- a/src/components/media-player/ha-browse-media-tts.ts
+++ b/src/components/media-player/ha-browse-media-tts.ts
@@ -214,6 +214,7 @@ class BrowseMediaTTS extends LitElement {
item.media_content_id = `${
item.media_content_id.split("?")[0]
}?${query.toString()}`;
+ item.media_content_type = "audio/mp3";
item.can_play = true;
item.title = message;
fireEvent(this, "tts-picked", { item });
diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts
index d16e68cf1d..2959a37d89 100644
--- a/src/components/user/ha-user-picker.ts
+++ b/src/components/user/ha-user-picker.ts
@@ -1,21 +1,30 @@
+import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { TemplateResult } from "lit";
-import { css, html, LitElement } from "lit";
-import { property } from "lit/decorators";
+import { html, LitElement, nothing } from "lit";
+import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
-import { stringCompare } from "../../common/string/compare";
import type { User } from "../../data/user";
import { fetchUsers } from "../../data/user";
import type { HomeAssistant } from "../../types";
-import "../ha-select";
+import "../ha-combo-box-item";
+import "../ha-generic-picker";
+import type { PickerComboBoxItem } from "../ha-picker-combo-box";
+import type { PickerValueRenderer } from "../ha-picker-field";
import "./ha-user-badge";
-import "../ha-list-item";
+interface UserComboBoxItem extends PickerComboBoxItem {
+ user?: User;
+}
+
+@customElement("ha-user-picker")
class HaUserPicker extends LitElement {
- public hass?: HomeAssistant;
+ @property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
+ @property() public placeholder?: string;
+
@property({ attribute: false }) public noUserLabel?: string;
@property() public value = "";
@@ -24,78 +33,124 @@ class HaUserPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
- private _sortedUsers = memoizeOne((users?: User[]) => {
+ protected firstUpdated(changedProps) {
+ super.firstUpdated(changedProps);
+ if (!this.users) {
+ this._fetchUsers();
+ }
+ }
+
+ private async _fetchUsers() {
+ this.users = await fetchUsers(this.hass);
+ }
+
+ private usersMap = memoizeOne((users?: User[]): Map
=> {
+ if (!users) {
+ return new Map();
+ }
+ return new Map(users.map((user) => [user.id, user]));
+ });
+
+ private _valueRenderer: PickerValueRenderer = (value) => {
+ const user = this.usersMap(this.users).get(value);
+ if (!user) {
+ return html` ${value} `;
+ }
+
+ return html`
+
+ ${user.name}
+ `;
+ };
+
+ private _rowRenderer: ComboBoxLitRenderer = (item) => {
+ const user = item.user;
+ if (!user) {
+ return html`
+ ${item.icon
+ ? html``
+ : item.icon_path
+ ? html``
+ : nothing}
+ ${item.primary}
+ ${item.secondary
+ ? html`${item.secondary}`
+ : nothing}
+ `;
+ }
+
+ return html`
+
+
+ ${item.primary}
+
+ `;
+ };
+
+ private _getUsers = memoizeOne((users?: User[]) => {
if (!users) {
return [];
}
return users
.filter((user) => !user.system_generated)
- .sort((a, b) =>
- stringCompare(a.name, b.name, this.hass!.locale.language)
- );
+ .map((user) => ({
+ id: user.id,
+ primary: user.name,
+ domain_name: user.name,
+ search_labels: [user.name, user.id, user.username].filter(
+ Boolean
+ ) as string[],
+ sorting_label: user.name,
+ user,
+ }));
});
+ private _getItems = () => this._getUsers(this.users);
+
protected render(): TemplateResult {
+ const placeholder =
+ this.placeholder ?? this.hass.localize("ui.components.user-picker.user");
+
return html`
-
- ${this.users?.length === 0
- ? html`
- ${this.noUserLabel ||
- this.hass?.localize("ui.components.user-picker.no_user")}
- `
- : ""}
- ${this._sortedUsers(this.users).map(
- (user) => html`
-
-
- ${user.name}
-
- `
+ .notFoundLabel=${this.hass.localize(
+ "ui.components.user-picker.no_match"
)}
-
+ .placeholder=${placeholder}
+ .value=${this.value}
+ .getItems=${this._getItems}
+ .valueRenderer=${this._valueRenderer}
+ .rowRenderer=${this._rowRenderer}
+ @value-changed=${this._valueChanged}
+ >
+
`;
}
- protected firstUpdated(changedProps) {
- super.firstUpdated(changedProps);
- if (this.users === undefined) {
- fetchUsers(this.hass!).then((users) => {
- this.users = users;
- });
- }
+ private _valueChanged(ev) {
+ const value = ev.detail.value;
+
+ this.value = value;
+ fireEvent(this, "value-changed", { value });
+ fireEvent(this, "change");
}
-
- private _userChanged(ev) {
- const newValue = ev.target.value;
-
- if (newValue !== this.value) {
- this.value = newValue;
- setTimeout(() => {
- fireEvent(this, "value-changed", { value: newValue });
- fireEvent(this, "change");
- }, 0);
- }
- }
-
- static styles = css`
- :host {
- display: inline-block;
- }
- `;
}
-customElements.define("ha-user-picker", HaUserPicker);
-
declare global {
interface HTMLElementTagNameMap {
"ha-user-picker": HaUserPicker;
diff --git a/src/components/user/ha-users-picker.ts b/src/components/user/ha-users-picker.ts
index d3160be467..a6c01df6fe 100644
--- a/src/components/user/ha-users-picker.ts
+++ b/src/components/user/ha-users-picker.ts
@@ -1,4 +1,3 @@
-import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { guard } from "lit/directives/guard";
@@ -6,13 +5,15 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { User } from "../../data/user";
import { fetchUsers } from "../../data/user";
-import type { ValueChangedEvent, HomeAssistant } from "../../types";
+import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-icon-button";
import "./ha-user-picker";
@customElement("ha-users-picker")
-class HaUsersPickerLight extends LitElement {
- @property({ attribute: false }) public hass?: HomeAssistant;
+class HaUsersPicker extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public label?: string;
@property({ attribute: false }) public value?: string[];
@@ -29,13 +30,15 @@ class HaUsersPickerLight extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
- if (this.users === undefined) {
- fetchUsers(this.hass!).then((users) => {
- this.users = users;
- });
+ if (!this.users) {
+ this._fetchUsers();
}
}
+ private async _fetchUsers() {
+ this.users = await fetchUsers(this.hass);
+ }
+
protected render() {
if (!this.hass || !this.users) {
return nothing;
@@ -43,15 +46,13 @@ class HaUsersPickerLight extends LitElement {
const notSelectedUsers = this._notSelectedUsers(this.users, this.value);
return html`
+ ${this.label ? html`` : nothing}
${guard([notSelectedUsers], () =>
this.value?.map(
(user_id, idx) => html`
-
- >
`
)
)}
-
+
+
+
`;
}
@@ -120,12 +113,12 @@ class HaUsersPickerLight extends LitElement {
});
}
- private _userChanged(event: ValueChangedEvent) {
- event.stopPropagation();
- const index = (event.currentTarget as any).index;
- const newValue = event.detail.value;
+ private _userChanged(ev: ValueChangedEvent) {
+ ev.stopPropagation();
+ const index = (ev.currentTarget as any).index;
+ const newValue = ev.detail.value;
const newUsers = [...this._currentUsers];
- if (newValue === "") {
+ if (!newValue) {
newUsers.splice(index, 1);
} else {
newUsers.splice(index, 1, newValue);
@@ -148,24 +141,15 @@ class HaUsersPickerLight extends LitElement {
this._updateUsers([...currentUsers, toAdd]);
}
- private _removeUser(event) {
- const userId = (event.currentTarget as any).userId;
- this._updateUsers(this._currentUsers.filter((user) => user !== userId));
- }
-
- static styles = css`
- :host {
- display: block;
- }
+ static override styles = css`
div {
- display: flex;
- align-items: center;
+ margin-top: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
- "ha-users-picker": HaUsersPickerLight;
+ "ha-users-picker": HaUsersPicker;
}
}
diff --git a/src/data/assist_pipeline.ts b/src/data/assist_pipeline.ts
index 72da3c5e9a..8c6d10949a 100644
--- a/src/data/assist_pipeline.ts
+++ b/src/data/assist_pipeline.ts
@@ -1,6 +1,5 @@
import type { HomeAssistant } from "../types";
import type { ConversationResult } from "./conversation";
-import type { ResolvedMediaSource } from "./media_source";
import type { SpeechMetadata } from "./stt";
export interface AssistPipeline {
@@ -53,10 +52,16 @@ interface PipelineRunStartEvent extends PipelineEventBase {
data: {
pipeline: string;
language: string;
+ conversation_id: string;
runner_data: {
stt_binary_handler_id: number | null;
timeout: number;
};
+ tts_output?: {
+ token: string;
+ url: string;
+ mime_type: string;
+ };
};
}
interface PipelineRunEndEvent extends PipelineEventBase {
@@ -109,7 +114,7 @@ interface PipelineIntentStartEvent extends PipelineEventBase {
};
}
-interface ConversationChatLogAssistantDelta {
+export interface ConversationChatLogAssistantDelta {
role: "assistant";
content: string;
tool_calls: {
@@ -119,7 +124,7 @@ interface ConversationChatLogAssistantDelta {
}[];
}
-interface ConversationChatLogToolResultDelta {
+export interface ConversationChatLogToolResultDelta {
role: "tool_result";
agent_id: string;
tool_call_id: string;
@@ -156,7 +161,12 @@ interface PipelineTTSStartEvent extends PipelineEventBase {
interface PipelineTTSEndEvent extends PipelineEventBase {
type: "tts-end";
data: {
- tts_output: ResolvedMediaSource;
+ tts_output: {
+ media_id: string;
+ token: string;
+ url: string;
+ mime_type: string;
+ };
};
}
diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts
index f303233e47..4bb768c1c7 100644
--- a/src/dialogs/more-info/controls/more-info-update.ts
+++ b/src/dialogs/more-info/controls/more-info-update.ts
@@ -471,8 +471,8 @@ class MoreInfoUpdate extends LitElement {
position: sticky;
bottom: 0;
margin: 0 -24px 0 -24px;
- margin-bottom: calc(-1 * max(env(safe-area-inset-bottom), 24px));
- padding-bottom: env(safe-area-inset-bottom);
+ margin-bottom: calc(-1 * max(var(--safe-area-inset-bottom), 24px));
+ padding-bottom: var(--safe-area-inset-bottom);
box-sizing: border-box;
display: flex;
flex-direction: column;
diff --git a/src/dialogs/more-info/ha-more-info-info.ts b/src/dialogs/more-info/ha-more-info-info.ts
index 0517dd206c..5662726d2d 100644
--- a/src/dialogs/more-info/ha-more-info-info.ts
+++ b/src/dialogs/more-info/ha-more-info-info.ts
@@ -128,7 +128,7 @@ export class MoreInfoInfo extends LitElement {
flex-direction: column;
flex: 1;
padding: 24px;
- padding-bottom: max(env(safe-area-inset-bottom), 24px);
+ padding-bottom: max(var(--safe-area-inset-bottom), 24px);
}
[data-domain="camera"] .content {
diff --git a/src/dialogs/notifications/notification-drawer.ts b/src/dialogs/notifications/notification-drawer.ts
index a3c83bcd69..bbb41039eb 100644
--- a/src/dialogs/notifications/notification-drawer.ts
+++ b/src/dialogs/notifications/notification-drawer.ts
@@ -159,11 +159,11 @@ export class HuiNotificationDrawer extends LitElement {
.notifications {
overflow-y: auto;
padding-top: 16px;
- padding-left: env(safe-area-inset-left);
- padding-right: env(safe-area-inset-right);
- padding-inline-start: env(safe-area-inset-left);
- padding-inline-end: env(safe-area-inset-right);
- padding-bottom: env(safe-area-inset-bottom);
+ padding-left: var(--safe-area-inset-left);
+ padding-right: var(--safe-area-inset-right);
+ padding-inline-start: var(--safe-area-inset-left);
+ padding-inline-end: var(--safe-area-inset-right);
+ padding-bottom: var(--safe-area-inset-bottom);
height: calc(100% - 1px - var(--header-height));
box-sizing: border-box;
background-color: var(--primary-background-color);
diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts
index f27ff5b428..68b2fa2a5f 100644
--- a/src/dialogs/quick-bar/ha-quick-bar.ts
+++ b/src/dialogs/quick-bar/ha-quick-bar.ts
@@ -28,6 +28,7 @@ import {
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
+import { getDeviceContext } from "../../common/entity/context/get_device_context";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { navigate } from "../../common/navigate";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
@@ -41,6 +42,7 @@ import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
import "../../components/ha-textfield";
import "../../components/ha-tip";
+import { getConfigEntries } from "../../data/config_entries";
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
import { domainToName } from "../../data/integration";
import { getPanelNameTranslationKey } from "../../data/panel";
@@ -50,6 +52,7 @@ import { HaFuse } from "../../resources/fuse";
import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
+import { brandsUrl } from "../../util/brands-url";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
@@ -75,6 +78,8 @@ interface EntityItem extends QuickBarItem {
interface DeviceItem extends QuickBarItem {
deviceId: string;
+ domain?: string;
+ translatedDomain?: string;
area?: string;
}
@@ -297,7 +302,8 @@ export class QuickBar extends LitElement {
this._commandItems =
this._commandItems || (await this._generateCommandItems());
} else if (this._mode === QuickBarMode.Device) {
- this._deviceItems = this._deviceItems || this._generateDeviceItems();
+ this._deviceItems =
+ this._deviceItems || (await this._generateDeviceItems());
} else {
this._entityItems =
this._entityItems || (await this._generateEntityItems());
@@ -344,10 +350,28 @@ export class QuickBar extends LitElement {
tabindex="0"
type="button"
>
+ ${item.domain
+ ? html`
`
+ : nothing}
${item.primaryText}
${item.area
? html` ${item.area} `
: nothing}
+ ${item.translatedDomain
+ ? html`
+ ${item.translatedDomain}
+
`
+ : nothing}
`;
}
@@ -549,23 +573,44 @@ export class QuickBar extends LitElement {
);
}
- private _generateDeviceItems(): DeviceItem[] {
+ private async _generateDeviceItems(): Promise {
+ const configEntries = await getConfigEntries(this.hass);
+ const configEntryLookup = Object.fromEntries(
+ configEntries.map((entry) => [entry.entry_id, entry])
+ );
+
return Object.values(this.hass.devices)
.filter((device) => !device.disabled_by)
.map((device) => {
- const area = device.area_id
- ? this.hass.areas[device.area_id]
- : undefined;
+ const deviceName = computeDeviceNameDisplay(device, this.hass);
+
+ const { area } = getDeviceContext(device, this.hass);
+
+ const areaName = area ? computeAreaName(area) : undefined;
+
const deviceItem = {
- primaryText: computeDeviceNameDisplay(device, this.hass),
+ primaryText: deviceName,
deviceId: device.id,
- area: area?.name,
+ area: areaName,
action: () => navigate(`/config/devices/device/${device.id}`),
};
+ const configEntry = device.primary_config_entry
+ ? configEntryLookup[device.primary_config_entry]
+ : undefined;
+
+ const domain = configEntry?.domain;
+ const translatedDomain = domain
+ ? domainToName(this.hass.localize, domain)
+ : undefined;
+
return {
...deviceItem,
- strings: [deviceItem.primaryText],
+ domain,
+ translatedDomain,
+ strings: [deviceName, areaName, domain, domainToName].filter(
+ Boolean
+ ) as string[],
};
})
.sort((a, b) =>
@@ -1036,6 +1081,11 @@ export class QuickBar extends LitElement {
white-space: nowrap;
}
+ ha-md-list-item img {
+ width: 32px;
+ height: 32px;
+ }
+
ha-tip {
padding: 20px;
}
diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts
new file mode 100644
index 0000000000..efc42284cc
--- /dev/null
+++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts
@@ -0,0 +1,159 @@
+import "@material/mwc-linear-progress/mwc-linear-progress";
+import { mdiClose } from "@mdi/js";
+import { css, html, LitElement, nothing } from "lit";
+import { customElement, property, query, state } from "lit/decorators";
+import memoizeOne from "memoize-one";
+import { fireEvent } from "../../common/dom/fire_event";
+import "../../components/ha-dialog-header";
+import "../../components/ha-icon-button";
+import "../../components/ha-items-display-editor";
+import type { DisplayValue } from "../../components/ha-items-display-editor";
+import "../../components/ha-md-dialog";
+import type { HaMdDialog } from "../../components/ha-md-dialog";
+import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
+import type { HomeAssistant } from "../../types";
+import type { EditSidebarDialogParams } from "./show-dialog-edit-sidebar";
+
+@customElement("dialog-edit-sidebar")
+class DialogEditSidebar extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() private _open = false;
+
+ @query("ha-md-dialog") private _dialog?: HaMdDialog;
+
+ @state() private _order: string[] = [];
+
+ @state() private _hidden: string[] = [];
+
+ private _saveCallback?: (order: string[], hidden: string[]) => void;
+
+ public async showDialog(params: EditSidebarDialogParams): Promise {
+ this._open = true;
+
+ const storedOrder = localStorage.getItem("sidebarPanelOrder");
+ const storedHidden = localStorage.getItem("sidebarHiddenPanels");
+
+ this._order = storedOrder ? JSON.parse(storedOrder) : this._order;
+ this._hidden = storedHidden ? JSON.parse(storedHidden) : this._hidden;
+ this._saveCallback = params.saveCallback;
+ }
+
+ private _dialogClosed(): void {
+ this._open = false;
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ }
+
+ public closeDialog(): void {
+ this._dialog?.close();
+ }
+
+ private _panels = memoizeOne((panels: HomeAssistant["panels"]) =>
+ panels ? Object.values(panels) : []
+ );
+
+ protected render() {
+ if (!this._open) {
+ return nothing;
+ }
+
+ const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar");
+
+ const panels = this._panels(this.hass.panels);
+
+ const [beforeSpacer, afterSpacer] = computePanels(
+ this.hass.panels,
+ this.hass.defaultPanel,
+ this._order,
+ this._hidden,
+ this.hass.locale
+ );
+
+ const items = [
+ ...beforeSpacer,
+ ...panels.filter((panel) => this._hidden.includes(panel.url_path)),
+ ...afterSpacer.filter((panel) => panel.url_path !== "config"),
+ ].map((panel) => ({
+ value: panel.url_path,
+ label:
+ panel.url_path === this.hass.defaultPanel
+ ? panel.title || this.hass.localize("panel.states")
+ : this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
+ icon: panel.icon || undefined,
+ iconPath:
+ panel.url_path === this.hass.defaultPanel && !panel.icon
+ ? PANEL_ICONS.lovelace
+ : panel.url_path in PANEL_ICONS
+ ? PANEL_ICONS[panel.url_path]
+ : undefined,
+ disableSorting: panel.url_path === "developer-tools",
+ }));
+
+ return html`
+
+
+
+ ${dialogTitle}
+
+
+
+
+
+
+
+ ${this.hass.localize("ui.common.cancel")}
+
+
+ ${this.hass.localize("ui.common.save")}
+
+
+
+ `;
+ }
+
+ private _changed(ev: CustomEvent<{ value: DisplayValue }>): void {
+ const { order = [], hidden = [] } = ev.detail.value;
+ this._order = [...order];
+ this._hidden = [...hidden];
+ }
+
+ private _save(): void {
+ this._saveCallback?.(this._order ?? [], this._hidden ?? []);
+ this.closeDialog();
+ }
+
+ static styles = css`
+ ha-md-dialog {
+ min-width: 600px;
+ max-height: 90%;
+ }
+
+ @media all and (max-width: 600px), all and (max-height: 500px) {
+ ha-md-dialog {
+ --md-dialog-container-shape: 0;
+ min-width: 100%;
+ min-height: 100%;
+ }
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "dialog-edit-sidebar": DialogEditSidebar;
+ }
+}
diff --git a/src/dialogs/sidebar/show-dialog-edit-sidebar.ts b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts
new file mode 100644
index 0000000000..4a88bafd6b
--- /dev/null
+++ b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts
@@ -0,0 +1,18 @@
+import { fireEvent } from "../../common/dom/fire_event";
+
+export interface EditSidebarDialogParams {
+ saveCallback: (order: string[], hidden: string[]) => void;
+}
+
+export const loadEditSidebarDialog = () => import("./dialog-edit-sidebar");
+
+export const showEditSidebarDialog = (
+ element: HTMLElement,
+ dialogParams: EditSidebarDialogParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "dialog-edit-sidebar",
+ dialogImport: loadEditSidebarDialog,
+ dialogParams,
+ });
+};
diff --git a/src/external_app/external_app_entrypoint.ts b/src/external_app/external_app_entrypoint.ts
index e5aac787a8..66edfdb504 100644
--- a/src/external_app/external_app_entrypoint.ts
+++ b/src/external_app/external_app_entrypoint.ts
@@ -7,6 +7,7 @@ This is the entry point for providing external app stuff from app entrypoint.
import { fireEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
+import { navigate } from "../common/navigate";
import { showAutomationEditor } from "../data/automation";
import type { HomeAssistantMain } from "../layouts/home-assistant-main";
import type {
@@ -50,7 +51,7 @@ export const addExternalBarCodeListener = (
};
};
-const handleExternalMessage = (
+export const handleExternalMessage = (
hassMainEl: HomeAssistantMain,
msg: EMIncomingMessageCommands
): boolean => {
@@ -64,6 +65,14 @@ const handleExternalMessage = (
success: true,
result: null,
});
+ } else if (msg.command === "navigate") {
+ navigate(msg.payload.path, msg.payload.options);
+ bus.fireMessage({
+ id: msg.id,
+ type: "result",
+ success: true,
+ result: null,
+ });
} else if (msg.command === "notifications/show") {
fireEvent(hassMainEl, "hass-show-notifications");
bus.fireMessage({
diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts
index 773182c248..be25a5f223 100644
--- a/src/external_app/external_messaging.ts
+++ b/src/external_app/external_messaging.ts
@@ -1,3 +1,4 @@
+import type { NavigateOptions } from "../common/navigate";
import type { AutomationConfig } from "../data/automation";
const CALLBACK_EXTERNAL_BUS = "externalBus";
@@ -178,31 +179,40 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice;
-interface EMIncomingMessageRestart {
+export interface EMIncomingMessageRestart {
id: number;
type: "command";
command: "restart";
}
+export interface EMIncomingMessageNavigate {
+ id: number;
+ type: "command";
+ command: "navigate";
+ payload: {
+ path: string;
+ options?: NavigateOptions;
+ };
+}
-interface EMIncomingMessageShowNotifications {
+export interface EMIncomingMessageShowNotifications {
id: number;
type: "command";
command: "notifications/show";
}
-interface EMIncomingMessageToggleSidebar {
+export interface EMIncomingMessageToggleSidebar {
id: number;
type: "command";
command: "sidebar/toggle";
}
-interface EMIncomingMessageShowSidebar {
+export interface EMIncomingMessageShowSidebar {
id: number;
type: "command";
command: "sidebar/show";
}
-interface EMIncomingMessageShowAutomationEditor {
+export interface EMIncomingMessageShowAutomationEditor {
id: number;
type: "command";
command: "automation/editor/show";
@@ -250,14 +260,14 @@ export interface ImprovDiscoveredDevice {
name: string;
}
-interface EMIncomingMessageImprovDeviceDiscovered extends EMMessage {
+export interface EMIncomingMessageImprovDeviceDiscovered extends EMMessage {
id: number;
type: "command";
command: "improv/discovered_device";
payload: ImprovDiscoveredDevice;
}
-interface EMIncomingMessageImprovDeviceSetupDone extends EMMessage {
+export interface EMIncomingMessageImprovDeviceSetupDone extends EMMessage {
id: number;
type: "command";
command: "improv/device_setup_done";
@@ -265,6 +275,7 @@ interface EMIncomingMessageImprovDeviceSetupDone extends EMMessage {
export type EMIncomingMessageCommands =
| EMIncomingMessageRestart
+ | EMIncomingMessageNavigate
| EMIncomingMessageShowNotifications
| EMIncomingMessageToggleSidebar
| EMIncomingMessageShowSidebar
diff --git a/src/html/_preload_roboto.html.template b/src/html/_preload_roboto.html.template
index 382beadfc0..1ab7c56001 100644
--- a/src/html/_preload_roboto.html.template
+++ b/src/html/_preload_roboto.html.template
@@ -1,6 +1,5 @@