diff --git a/package.json b/package.json index f9b6125480..b9ab3f2d60 100644 --- a/package.json +++ b/package.json @@ -155,14 +155,14 @@ "@babel/helper-define-polyfill-provider": "0.6.4", "@babel/plugin-transform-runtime": "7.27.1", "@babel/preset-env": "7.27.2", - "@bundle-stats/plugin-webpack-filter": "4.20.0", + "@bundle-stats/plugin-webpack-filter": "4.20.1", "@custom-elements-manifest/analyzer": "0.10.4", "@custom-elements-manifest/to-markdown": "0.1.0", - "@lokalise/node-api": "14.6.0", + "@lokalise/node-api": "14.7.0", "@octokit/auth-oauth-device": "7.1.5", "@octokit/plugin-retry": "7.2.1", "@octokit/rest": "21.1.1", - "@rsdoctor/rspack-plugin": "1.1.1", + "@rsdoctor/rspack-plugin": "1.1.2", "@rspack/cli": "1.3.9", "@rspack/core": "1.3.9", "@types/babel__plugin-transform-runtime": "7.9.5", diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index e2a4723a8d..5c227dfb9f 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -164,6 +164,8 @@ export class HaDataTable extends LitElement { @state() private _collapsedGroups: string[] = []; + @state() private _lastSelectedRowId: string | null = null; + private _checkableRowsCount?: number; private _checkedRows: string[] = []; @@ -187,6 +189,7 @@ export class HaDataTable extends LitElement { public clearSelection(): void { this._checkedRows = []; + this._lastSelectedRowId = null; this._checkedRowsChanged(); } @@ -194,6 +197,7 @@ export class HaDataTable extends LitElement { this._checkedRows = this._filteredData .filter((data) => data.selectable !== false) .map((data) => data[this.id]); + this._lastSelectedRowId = null; this._checkedRowsChanged(); } @@ -207,6 +211,7 @@ export class HaDataTable extends LitElement { this._checkedRows.push(id); } }); + this._lastSelectedRowId = null; this._checkedRowsChanged(); } @@ -217,6 +222,7 @@ export class HaDataTable extends LitElement { this._checkedRows.splice(index, 1); } }); + this._lastSelectedRowId = null; this._checkedRowsChanged(); } @@ -261,6 +267,7 @@ export class HaDataTable extends LitElement { if (this.columns[columnId].direction) { this.sortDirection = this.columns[columnId].direction!; this.sortColumn = columnId; + this._lastSelectedRowId = null; fireEvent(this, "sorting-changed", { column: columnId, @@ -286,6 +293,7 @@ export class HaDataTable extends LitElement { if (properties.has("filter")) { this._debounceSearch(this.filter); + this._lastSelectedRowId = null; } if (properties.has("data")) { @@ -296,9 +304,11 @@ export class HaDataTable extends LitElement { if (!this.hasUpdated && this.initialCollapsedGroups) { this._collapsedGroups = this.initialCollapsedGroups; + this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); } else if (properties.has("groupColumn")) { this._collapsedGroups = []; + this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); } @@ -312,6 +322,14 @@ export class HaDataTable extends LitElement { this._sortFilterData(); } + if ( + properties.has("_filter") || + properties.has("sortColumn") || + properties.has("sortDirection") + ) { + this._lastSelectedRowId = null; + } + if (properties.has("selectable") || properties.has("hiddenColumns")) { this._filteredData = [...this._filteredData]; } @@ -542,7 +560,7 @@ export class HaDataTable extends LitElement { > { groupedItems.push({ append: true, + selectable: false, content: html`
{ + private _handleRowCheckboxClicked = (ev: Event) => { const checkbox = ev.currentTarget as HaCheckbox; const rowId = (checkbox as any).rowId; - if (checkbox.checked) { - if (this._checkedRows.includes(rowId)) { - return; + const groupedData = this._groupData( + this._filteredData, + this.localizeFunc || this.hass.localize, + this.appendRow, + this.hasFab, + this.groupColumn, + this.groupOrder, + this._collapsedGroups + ); + + if ( + groupedData.find((data) => data[this.id] === rowId)?.selectable === false + ) { + return; + } + + const rowIndex = groupedData.findIndex((data) => data[this.id] === rowId); + + if ( + ev instanceof MouseEvent && + ev.shiftKey && + this._lastSelectedRowId !== null + ) { + const lastSelectedRowIndex = groupedData.findIndex( + (data) => data[this.id] === this._lastSelectedRowId + ); + + if (lastSelectedRowIndex > -1 && rowIndex > -1) { + this._checkedRows = [ + ...this._checkedRows, + ...this._selectRange(groupedData, lastSelectedRowIndex, rowIndex), + ]; + } + } else if (!checkbox.checked) { + if (!this._checkedRows.includes(rowId)) { + this._checkedRows = [...this._checkedRows, rowId]; } - this._checkedRows = [...this._checkedRows, rowId]; } else { this._checkedRows = this._checkedRows.filter((row) => row !== rowId); } + + if (rowIndex > -1) { + this._lastSelectedRowId = rowId; + } this._checkedRowsChanged(); }; + private _selectRange( + groupedData: DataTableRowData[], + startIndex: number, + endIndex: number + ) { + const start = Math.min(startIndex, endIndex); + const end = Math.max(startIndex, endIndex); + + const checkedRows: string[] = []; + + for (let i = start; i <= end; i++) { + const row = groupedData[i]; + if ( + row && + row.selectable !== false && + !this._checkedRows.includes(row[this.id]) + ) { + checkedRows.push(row[this.id]); + } + } + + return checkedRows; + } + private _handleRowClick = (ev: Event) => { if ( ev @@ -858,6 +938,7 @@ export class HaDataTable extends LitElement { if (this.filter) { return; } + this._lastSelectedRowId = null; this._debounceSearch(ev.detail.value); } @@ -894,11 +975,13 @@ export class HaDataTable extends LitElement { } else { this._collapsedGroups = [...this._collapsedGroups, groupName]; } + this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); }; public expandAllGroups() { this._collapsedGroups = []; + this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); } @@ -916,6 +999,7 @@ export class HaDataTable extends LitElement { delete grouped.undefined; } this._collapsedGroups = Object.keys(grouped); + this._lastSelectedRowId = null; fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); } diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 515a5a58b4..40e448bae2 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -1,6 +1,13 @@ import { mdiClose, mdiMenuDown, mdiShape } from "@mdi/js"; import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light"; -import { css, html, LitElement, nothing, type CSSResultGroup } from "lit"; +import { + css, + html, + LitElement, + nothing, + type CSSResultGroup, + type PropertyValues, +} from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { stopPropagation } from "../../common/dom/stop_propagation"; @@ -106,6 +113,12 @@ export class HaEntityPicker extends LitElement { @state() private _opened = false; + protected firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + // Load title translations so it is available when the combo-box opens + this.hass.loadBackendTranslation("title"); + } + private _renderContent() { const entityId = this.value || ""; diff --git a/src/components/ha-web-rtc-player.ts b/src/components/ha-web-rtc-player.ts index e11cf5794e..7d9528f38a 100644 --- a/src/components/ha-web-rtc-player.ts +++ b/src/components/ha-web-rtc-player.ts @@ -201,25 +201,6 @@ class HaWebRtcPlayer extends LitElement { let candidates = ""; - if (this._clientConfig?.getCandidatesUpfront) { - await new Promise((resolve) => { - this._peerConnection!.onicegatheringstatechange = (ev: Event) => { - const iceGatheringState = (ev.target as RTCPeerConnection) - .iceGatheringState; - if (iceGatheringState === "complete") { - this._peerConnection!.onicegatheringstatechange = null; - resolve(); - } - - this._logEvent("Ice gathering state changed", iceGatheringState); - }; - }); - - if (!this._peerConnection || !this.entityid) { - return; - } - } - while (this._candidatesList.length) { const candidate = this._candidatesList.pop(); if (candidate) { diff --git a/src/data/camera.ts b/src/data/camera.ts index b8bf02d5de..397d9365fb 100644 --- a/src/data/camera.ts +++ b/src/data/camera.ts @@ -190,7 +190,6 @@ export const fetchCameraCapabilities = async ( export interface WebRTCClientConfiguration { configuration: RTCConfiguration; dataChannel?: string; - getCandidatesUpfront: boolean; } export const fetchWebRtcClientConfiguration = async ( diff --git a/src/data/frontend.ts b/src/data/frontend.ts index 555919c0ed..7e46217cca 100644 --- a/src/data/frontend.ts +++ b/src/data/frontend.ts @@ -1,5 +1,4 @@ import type { Connection } from "home-assistant-js-websocket"; -import { getOptimisticCollection } from "./collection"; export interface CoreFrontendUserData { showAdvanced?: boolean; @@ -42,30 +41,15 @@ export const saveFrontendUserData = async < value, }); -export const getOptimisticFrontendUserDataCollection = < - UserDataKey extends ValidUserDataKey, ->( - conn: Connection, - userDataKey: UserDataKey -) => - getOptimisticCollection( - (_conn, data) => - saveFrontendUserData( - conn, - userDataKey, - // @ts-ignore - data - ), - conn, - `_frontendUserData-${userDataKey}`, - () => fetchFrontendUserData(conn, userDataKey) - ); - export const subscribeFrontendUserData = ( conn: Connection, userDataKey: UserDataKey, - onChange: (state: FrontendUserData[UserDataKey] | null) => void + onChange: (data: { value: FrontendUserData[UserDataKey] | null }) => void ) => - getOptimisticFrontendUserDataCollection(conn, userDataKey).subscribe( - onChange + conn.subscribeMessage<{ value: FrontendUserData[UserDataKey] | null }>( + onChange, + { + type: "frontend/subscribe_user_data", + key: userDataKey, + } ); diff --git a/src/data/translation.ts b/src/data/translation.ts index c3b10f4286..260294e506 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -1,5 +1,9 @@ import type { HomeAssistant } from "../types"; -import { fetchFrontendUserData, saveFrontendUserData } from "./frontend"; +import { + fetchFrontendUserData, + saveFrontendUserData, + subscribeFrontendUserData, +} from "./frontend"; export enum NumberFormat { language = "language", @@ -77,6 +81,11 @@ export type TranslationCategory = export const fetchTranslationPreferences = (hass: HomeAssistant) => fetchFrontendUserData(hass.connection, "language"); +export const subscribeTranslationPreferences = ( + hass: HomeAssistant, + callback: (data: { value: FrontendLocaleData | null }) => void +) => subscribeFrontendUserData(hass.connection, "language", callback); + export const saveTranslationPreferences = ( hass: HomeAssistant, data: FrontendLocaleData diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 726143bdb1..5f141c3572 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,17 +1,20 @@ import { mdiClose, mdiHelpCircle } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; -import type { CSSResultGroup, PropertyValues } from "lit"; +import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import type { HASSDomEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-dialog"; +import "../../components/ha-dialog-header"; import "../../components/ha-icon-button"; import type { DataEntryFlowStep } from "../../data/data_entry_flow"; import { subscribeDataEntryFlowProgress, subscribeDataEntryFlowProgressed, } from "../../data/data_entry_flow"; +import type { DeviceRegistryEntry } from "../../data/device_registry"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; @@ -171,6 +174,92 @@ class DataEntryFlowDialog extends LitElement { fireEvent(this, "dialog-closed", { dialog: this.localName }); } + private _devices = memoizeOne( + ( + showDevices: boolean, + devices: DeviceRegistryEntry[], + entry_id?: string + ) => + showDevices && entry_id + ? devices.filter((device) => device.config_entries.includes(entry_id)) + : [] + ); + + private _getDialogTitle(): string { + if (this._loading || !this._step || !this._params) { + return ""; + } + + switch (this._step.type) { + case "form": + return this._params.flowConfig.renderShowFormStepHeader( + this.hass, + this._step + ); + case "abort": + return this._params.flowConfig.renderAbortHeader + ? this._params.flowConfig.renderAbortHeader(this.hass, this._step) + : this.hass.localize( + `component.${this._params.domain ?? this._step.handler}.title` + ); + case "progress": + return this._params.flowConfig.renderShowFormProgressHeader( + this.hass, + this._step + ); + case "menu": + return this._params.flowConfig.renderMenuHeader(this.hass, this._step); + case "create_entry": { + const devicesLength = this._devices( + this._params.flowConfig.showDevices, + Object.values(this.hass.devices), + this._step.result?.entry_id + ).length; + return this.hass.localize( + `ui.panel.config.integrations.config_flow.${ + devicesLength ? "device_created" : "success" + }`, + { + number: devicesLength, + } + ); + } + default: + return ""; + } + } + + private _getDialogSubtitle(): string | TemplateResult | undefined { + if (this._loading || !this._step || !this._params) { + return ""; + } + + switch (this._step.type) { + case "form": + return this._params.flowConfig.renderShowFormStepSubheader?.( + this.hass, + this._step + ); + case "abort": + return this._params.flowConfig.renderAbortSubheader?.( + this.hass, + this._step + ); + case "progress": + return this._params.flowConfig.renderShowFormProgressSubheader?.( + this.hass, + this._step + ); + case "menu": + return this._params.flowConfig.renderMenuSubheader?.( + this.hass, + this._step + ); + default: + return ""; + } + } + protected render() { if (!this._params) { return nothing; @@ -187,6 +276,9 @@ class DataEntryFlowDialog extends LitElement { this._params.manifest?.is_built_in) || !!this._params.manifest?.documentation; + const dialogTitle = this._getDialogTitle(); + const dialogSubtitle = this._getDialogSubtitle(); + return html` + + + +
+ ${dialogTitle} +
+ + ${dialogSubtitle + ? html`
${dialogSubtitle}
` + : nothing} + ${showDocumentationLink && !this._loading && this._step + ? html` + + + + ` + : nothing} +
${this._loading || this._step === null ? html` @@ -211,40 +346,12 @@ class DataEntryFlowDialog extends LitElement { // to reset the element. nothing : html` -
- ${showDocumentationLink - ? html` - - - - ` - : nothing} - -
${this._step.type === "form" ? html` ` : this._step.type === "external" @@ -253,7 +360,6 @@ class DataEntryFlowDialog extends LitElement { .flowConfig=${this._params.flowConfig} .step=${this._step} .hass=${this.hass} - .increasePaddingEnd=${showDocumentationLink} > ` : this._step.type === "abort" @@ -265,7 +371,6 @@ class DataEntryFlowDialog extends LitElement { .handler=${this._step.handler} .domain=${this._params.domain ?? this._step.handler} - .increasePaddingEnd=${showDocumentationLink} > ` : this._step.type === "progress" @@ -275,7 +380,6 @@ class DataEntryFlowDialog extends LitElement { .step=${this._step} .hass=${this.hass} .progress=${this._progress} - .increasePaddingEnd=${showDocumentationLink} > ` : this._step.type === "menu" @@ -284,7 +388,6 @@ class DataEntryFlowDialog extends LitElement { .flowConfig=${this._params.flowConfig} .step=${this._step} .hass=${this.hass} - .increasePaddingEnd=${showDocumentationLink} > ` : html` @@ -294,7 +397,11 @@ class DataEntryFlowDialog extends LitElement { .hass=${this.hass} .navigateToResult=${this._params .navigateToResult ?? false} - .increasePaddingEnd=${showDocumentationLink} + .devices=${this._devices( + this._params.flowConfig.showDevices, + Object.values(this.hass.devices), + this._step.result?.entry_id + )} > `} `} @@ -384,16 +491,14 @@ class DataEntryFlowDialog extends LitElement { ha-dialog { --dialog-content-padding: 0; } - .dialog-actions { - padding: 16px; - position: absolute; - top: 0; - right: 0; - inset-inline-start: initial; - inset-inline-end: 0px; - direction: var(--direction); + .dialog-title { + overflow: hidden; + text-overflow: ellipsis; } - .dialog-actions > * { + .dialog-title.form { + white-space: normal; + } + .help { color: var(--secondary-text-color); } `, diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index add3a2573c..4bdf0c522f 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -31,10 +31,12 @@ export interface FlowConfig { deleteFlow(hass: HomeAssistant, flowId: string): Promise; - renderAbortHeader?( + renderAbortHeader?(hass: HomeAssistant, step: DataEntryFlowStepAbort): string; + + renderAbortSubheader?( hass: HomeAssistant, step: DataEntryFlowStepAbort - ): TemplateResult | string; + ): string | TemplateResult; renderAbortDescription( hass: HomeAssistant, @@ -44,6 +46,11 @@ export interface FlowConfig { renderShowFormStepHeader( hass: HomeAssistant, step: DataEntryFlowStepForm + ): string; + + renderShowFormStepSubheader?( + hass: HomeAssistant, + step: DataEntryFlowStepForm ): string | TemplateResult; renderShowFormStepDescription( @@ -100,6 +107,11 @@ export interface FlowConfig { renderShowFormProgressHeader( hass: HomeAssistant, step: DataEntryFlowStepProgress + ): string; + + renderShowFormProgressSubheader?( + hass: HomeAssistant, + step: DataEntryFlowStepProgress ): string | TemplateResult; renderShowFormProgressDescription( @@ -107,7 +119,9 @@ export interface FlowConfig { step: DataEntryFlowStepProgress ): TemplateResult | ""; - renderMenuHeader( + renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string; + + renderMenuSubheader?( hass: HomeAssistant, step: DataEntryFlowStepMenu ): string | TemplateResult; diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index 9f0ad9abb0..7277e0b0f8 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -22,9 +22,6 @@ class StepFlowAbort extends LitElement { @property({ attribute: false }) public handler!: string; - @property({ type: Boolean, attribute: "increase-padding-end" }) - public increasePaddingEnd = false; - protected firstUpdated(changed: PropertyValues) { super.firstUpdated(changed); if (this.step.reason === "missing_credentials") { @@ -37,11 +34,6 @@ class StepFlowAbort extends LitElement { return nothing; } return html` -

- ${this.params.flowConfig.renderAbortHeader - ? this.params.flowConfig.renderAbortHeader(this.hass, this.step) - : this.hass.localize(`component.${this.domain}.title`)} -

${this.params.flowConfig.renderAbortDescription(this.hass, this.step)}
diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index eac44ccff9..5a609fac8f 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -36,8 +36,7 @@ class StepFlowCreateEntry extends LitElement { @property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry; - @property({ type: Boolean, attribute: "increase-padding-end" }) - public increasePaddingEnd = false; + @property({ attribute: false }) public devices!: DeviceRegistryEntry[]; public navigateToResult = false; @@ -46,17 +45,6 @@ class StepFlowCreateEntry extends LitElement { { name?: string; area?: string } > = {}; - private _devices = memoizeOne( - ( - showDevices: boolean, - devices: DeviceRegistryEntry[], - entry_id?: string - ) => - showDevices && entry_id - ? devices.filter((device) => device.config_entries.includes(entry_id)) - : [] - ); - private _deviceEntities = memoizeOne( ( deviceId: string, @@ -75,22 +63,16 @@ class StepFlowCreateEntry extends LitElement { return; } - const devices = this._devices( - this.flowConfig.showDevices, - Object.values(this.hass.devices), - this.step.result?.entry_id - ); - if ( - devices.length !== 1 || - devices[0].primary_config_entry !== this.step.result?.entry_id || + this.devices.length !== 1 || + this.devices[0].primary_config_entry !== this.step.result?.entry_id || this.step.result.domain === "voip" ) { return; } const assistSatellites = this._deviceEntities( - devices[0].id, + this.devices[0].id, Object.values(this.hass.entities), "assist_satellite" ); @@ -103,26 +85,14 @@ class StepFlowCreateEntry extends LitElement { this.navigateToResult = false; this._flowDone(); showVoiceAssistantSetupDialog(this, { - deviceId: devices[0].id, + deviceId: this.devices[0].id, }); } } protected render(): TemplateResult { const localize = this.hass.localize; - const devices = this._devices( - this.flowConfig.showDevices, - Object.values(this.hass.devices), - this.step.result?.entry_id - ); return html` -

- ${devices.length - ? localize("ui.panel.config.integrations.config_flow.assign_area", { - number: devices.length, - }) - : `${localize("ui.panel.config.integrations.config_flow.success")}!`} -

${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)} ${this.step.result?.state === "not_loaded" @@ -132,10 +102,10 @@ class StepFlowCreateEntry extends LitElement { )}` : nothing} - ${devices.length === 0 && + ${this.devices.length === 0 && ["options_flow", "repair_flow"].includes(this.flowConfig.flowType) ? nothing - : devices.length === 0 + : this.devices.length === 0 ? html`

${localize( "ui.panel.config.integrations.config_flow.created_config", @@ -144,7 +114,7 @@ class StepFlowCreateEntry extends LitElement {

` : html`
- ${devices.map( + ${this.devices.map( (device) => html`
@@ -203,7 +173,7 @@ class StepFlowCreateEntry extends LitElement {
${localize( - `ui.panel.config.integrations.config_flow.${!devices.length || Object.keys(this._deviceUpdate).length ? "finish" : "finish_skip"}` + `ui.panel.config.integrations.config_flow.${!this.devices.length || Object.keys(this._deviceUpdate).length ? "finish" : "finish_skip"}` )}
diff --git a/src/dialogs/config-flow/step-flow-external.ts b/src/dialogs/config-flow/step-flow-external.ts index 98d98c61ef..7907dd2901 100644 --- a/src/dialogs/config-flow/step-flow-external.ts +++ b/src/dialogs/config-flow/step-flow-external.ts @@ -15,16 +15,10 @@ class StepFlowExternal extends LitElement { @property({ attribute: false }) public step!: DataEntryFlowStepExternal; - @property({ type: Boolean, attribute: "increase-padding-end" }) - public increasePaddingEnd = false; - protected render(): TemplateResult { const localize = this.hass.localize; return html` -

- ${this.flowConfig.renderExternalStepHeader(this.hass, this.step)} -

${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
@@ -56,9 +50,6 @@ class StepFlowExternal extends LitElement { .open-button a { text-decoration: none; } - h2.end-space { - padding-inline-end: 72px; - } `, ]; } diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 06ec17dc0d..aa012c2163 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -6,18 +6,18 @@ import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../common/dom/fire_event"; import { isNavigationClick } from "../../common/dom/is-navigation-click"; import "../../components/ha-alert"; -import "../../components/ha-spinner"; import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data"; import "../../components/ha-form/ha-form"; import type { HaFormSchema } from "../../components/ha-form/types"; import "../../components/ha-markdown"; +import "../../components/ha-spinner"; import { autocompleteLoginFields } from "../../data/auth"; import type { DataEntryFlowStepForm } from "../../data/data_entry_flow"; +import { previewModule } from "../../data/preview"; +import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import type { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; -import { haStyle } from "../../resources/styles"; -import { previewModule } from "../../data/preview"; @customElement("step-flow-form") class StepFlowForm extends LitElement { @@ -27,9 +27,6 @@ class StepFlowForm extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean, attribute: "increase-padding-end" }) - public increasePaddingEnd = false; - @state() private _loading = false; @state() private _stepData?: Record; @@ -46,9 +43,6 @@ class StepFlowForm extends LitElement { const stepData = this._stepDataProcessed; return html` -

- ${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)} -

${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)} ${this._errorMsg @@ -281,9 +275,6 @@ class StepFlowForm extends LitElement { margin-top: 24px; display: block; } - h2 { - word-break: break-word; - } `, ]; } diff --git a/src/dialogs/config-flow/step-flow-menu.ts b/src/dialogs/config-flow/step-flow-menu.ts index b0fcd3ba64..ede8a2998e 100644 --- a/src/dialogs/config-flow/step-flow-menu.ts +++ b/src/dialogs/config-flow/step-flow-menu.ts @@ -17,9 +17,6 @@ class StepFlowMenu extends LitElement { @property({ attribute: false }) public step!: DataEntryFlowStepMenu; - @property({ type: Boolean, attribute: "increase-padding-end" }) - public increasePaddingEnd = false; - protected render(): TemplateResult { let options: string[]; let translations: Record; @@ -45,9 +42,6 @@ class StepFlowMenu extends LitElement { ); return html` -

- ${this.flowConfig.renderMenuHeader(this.hass, this.step)} -

${description ? html`
${description}
` : ""}
${options.map( diff --git a/src/dialogs/config-flow/step-flow-progress.ts b/src/dialogs/config-flow/step-flow-progress.ts index c71efcb98c..f92d355920 100644 --- a/src/dialogs/config-flow/step-flow-progress.ts +++ b/src/dialogs/config-flow/step-flow-progress.ts @@ -2,13 +2,13 @@ import "@material/mwc-button"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import { blankBeforePercent } from "../../common/translations/blank_before_percent"; import "../../components/ha-progress-ring"; import "../../components/ha-spinner"; import type { DataEntryFlowStepProgress } from "../../data/data_entry_flow"; import type { HomeAssistant } from "../../types"; import type { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; -import { blankBeforePercent } from "../../common/translations/blank_before_percent"; @customElement("step-flow-progress") class StepFlowProgress extends LitElement { @@ -24,14 +24,8 @@ class StepFlowProgress extends LitElement { @property({ type: Number }) public progress?: number; - @property({ type: Boolean, attribute: "increase-padding-end" }) - public increasePaddingEnd = false; - protected render(): TemplateResult { return html` -

- ${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)} -

${this.progress ? html` diff --git a/src/dialogs/config-flow/styles.ts b/src/dialogs/config-flow/styles.ts index b82dc29bf1..dce0b9c326 100644 --- a/src/dialogs/config-flow/styles.ts +++ b/src/dialogs/config-flow/styles.ts @@ -25,9 +25,6 @@ export const configFlowContentStyles = css` text-transform: var(--mdc-typography-headline6-text-transform, inherit); box-sizing: border-box; } - h2.end-space { - padding-inline-end: 72px; - } .content, .preview { diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 21b1215e06..f27ff5b428 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -9,50 +9,50 @@ import { mdiReload, mdiServerNetwork, } from "@mdi/js"; +import Fuse from "fuse.js"; import type { TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; -import Fuse from "fuse.js"; import { canShowPage } from "../../common/config/can_show_page"; import { componentsWithService } from "../../common/config/components_with_service"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; +import { computeAreaName } from "../../common/entity/compute_area_name"; import { computeDeviceName, computeDeviceNameDisplay, } from "../../common/entity/compute_device_name"; +import { computeDomain } from "../../common/entity/compute_domain"; +import { computeEntityName } from "../../common/entity/compute_entity_name"; +import { computeStateName } from "../../common/entity/compute_state_name"; +import { getEntityContext } from "../../common/entity/context/get_entity_context"; import { navigate } from "../../common/navigate"; import { caseInsensitiveStringCompare } from "../../common/string/compare"; import type { ScorableTextItem } from "../../common/string/filter/sequence-matching"; +import { computeRTL } from "../../common/util/compute_rtl"; import { debounce } from "../../common/util/debounce"; import "../../components/ha-icon-button"; import "../../components/ha-label"; import "../../components/ha-list"; +import "../../components/ha-md-list-item"; import "../../components/ha-spinner"; import "../../components/ha-textfield"; import "../../components/ha-tip"; -import "../../components/ha-md-list-item"; import { fetchHassioAddonsInfo } from "../../data/hassio/addon"; import { domainToName } from "../../data/integration"; import { getPanelNameTranslationKey } from "../../data/panel"; import type { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { configSections } from "../../panels/config/ha-panel-config"; +import { HaFuse } from "../../resources/fuse"; import { haStyleDialog, haStyleScrollbar } from "../../resources/styles"; import { loadVirtualizer } from "../../resources/virtualizer"; import type { HomeAssistant } from "../../types"; import { showConfirmationDialog } from "../generic/show-dialog-box"; import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog"; import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar"; -import { getEntityContext } from "../../common/entity/context/get_entity_context"; -import { computeEntityName } from "../../common/entity/compute_entity_name"; -import { computeAreaName } from "../../common/entity/compute_area_name"; -import { computeRTL } from "../../common/util/compute_rtl"; -import { computeDomain } from "../../common/entity/compute_domain"; -import { computeStateName } from "../../common/entity/compute_state_name"; -import { HaFuse } from "../../resources/fuse"; interface QuickBarItem extends ScorableTextItem { primaryText: string; @@ -152,11 +152,6 @@ export class QuickBar extends LitElement { } } - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - this.hass.loadBackendTranslation("title"); - } - private _getItems = memoizeOne( ( mode: QuickBarMode, @@ -304,7 +299,8 @@ export class QuickBar extends LitElement { } else if (this._mode === QuickBarMode.Device) { this._deviceItems = this._deviceItems || this._generateDeviceItems(); } else { - this._entityItems = this._entityItems || this._generateEntityItems(); + this._entityItems = + this._entityItems || (await this._generateEntityItems()); } } @@ -581,9 +577,11 @@ export class QuickBar extends LitElement { ); } - private _generateEntityItems(): EntityItem[] { + private async _generateEntityItems(): Promise { const isRTL = computeRTL(this.hass); + await this.hass.loadBackendTranslation("title"); + return Object.keys(this.hass.states) .map((entityId) => { const stateObj = this.hass.states[entityId]; diff --git a/src/mixins/wakelock-mixin.ts b/src/mixins/wakelock-mixin.ts new file mode 100644 index 0000000000..20a7233f6d --- /dev/null +++ b/src/mixins/wakelock-mixin.ts @@ -0,0 +1,21 @@ +import type { ReactiveElement } from "lit"; +import type { Constructor } from "../types"; + +export const WakeLockMixin = >( + superClass: T +) => + class WakeLockClass extends superClass { + private _wakeLock?: Promise; + + public connectedCallback() { + super.connectedCallback(); + if ("wakeLock" in navigator) { + this._wakeLock = navigator.wakeLock.request(); + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._wakeLock?.then((wakeLock) => wakeLock.release()); + } + }; diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index d9b5fbdff4..e9cd570a80 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -107,6 +107,8 @@ export interface DeviceAlert { text: string; } +const DEVICE_ALERTS_INTERVAL = 30000; + @customElement("ha-config-device-page") export class HaConfigDevicePage extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -134,6 +136,8 @@ export class HaConfigDevicePage extends LitElement { @state() private _deviceAlerts?: DeviceAlert[]; + private _deviceAlertsTimeout?: number; + @state() @consume({ context: fullEntitiesContext, subscribe: true }) _entityReg!: EntityRegistryEntry[]; @@ -280,6 +284,7 @@ export class HaConfigDevicePage extends LitElement { this._getDiagnosticButtons(this._diagnosticDownloadLinks); this._getDeleteActions(); this._getDeviceActions(); + clearTimeout(this._deviceAlertsTimeout); this._getDeviceAlerts(); } @@ -295,6 +300,11 @@ export class HaConfigDevicePage extends LitElement { } } + public disconnectedCallback() { + super.disconnectedCallback(); + clearTimeout(this._deviceAlertsTimeout); + } + protected render() { if (!this.hass || !this.deviceId) { return nothing; @@ -1153,6 +1163,10 @@ export class HaConfigDevicePage extends LitElement { if (deviceAlerts.length) { this._deviceAlerts = deviceAlerts; + this._deviceAlertsTimeout = window.setTimeout( + () => this._getDeviceAlerts(), + DEVICE_ALERTS_INTERVAL + ); } } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts index 146c5b97a7..02e5444ac2 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts @@ -5,9 +5,11 @@ import { mdiNetwork, mdiPlus, mdiPencil, + mdiCheckCircle, + mdiAlertCircle, } from "@mdi/js"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import type { ConfigEntry } from "../../../../../data/config_entries"; import { getConfigEntries } from "../../../../../data/config_entries"; @@ -25,6 +27,8 @@ import "../../../ha-config-section"; import "../../../../../components/ha-form/ha-form"; import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/ha-settings-row"; +import "../../../../../components/ha-svg-icon"; +import "../../../../../components/ha-alert"; import { showZHAChangeChannelDialog } from "./show-dialog-zha-change-channel"; import type { ZHAConfiguration, @@ -36,6 +40,7 @@ import { updateZHAConfiguration, fetchZHANetworkSettings, createZHANetworkBackup, + fetchDevices, } from "../../../../../data/zha"; import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; @@ -75,18 +80,27 @@ class ZHAConfigDashboard extends LitElement { @state() private _networkSettings?: ZHANetworkSettings; + @state() private _totalDevices = 0; + + @state() private _offlineDevices = 0; + + @state() private _error?: string; + @state() private _generatingBackup = false; - protected firstUpdated(changedProperties: PropertyValues): void { + protected firstUpdated(changedProperties: PropertyValues) { super.firstUpdated(changedProperties); if (this.hass) { this.hass.loadBackendTranslation("config_panel", "zha", false); this._fetchConfiguration(); this._fetchSettings(); + this._fetchDevicesAndUpdateStatus(); } } protected render(): TemplateResult { + const deviceOnline = + this._offlineDevices < this._totalDevices || this._totalDevices === 0; return html` - + + ${this._error + ? html`${this._error}` + : nothing} +
+
+
+ +
+
+ ZHA + ${this.hass.localize( + "ui.panel.config.zha.configuration_page.status_title" + )}: + ${this.hass.localize( + `ui.panel.config.zha.configuration_page.status_${deviceOnline ? "online" : "offline"}` + )}
+ + ${this.hass.localize( + "ui.panel.config.zha.configuration_page.devices", + { count: this._totalDevices } + )} + + + ${this._offlineDevices > 0 + ? html`(${this.hass.localize( + "ui.panel.config.zha.configuration_page.devices_offline", + { count: this._offlineDevices } + )})` + : nothing} + +
+
+
${this.configEntryId ? html`
{ + try { + const devices = await fetchDevices(this.hass); + this._totalDevices = devices.length; + this._offlineDevices = + this._totalDevices - devices.filter((d) => d.available).length; + } catch (err: any) { + this._error = err.message || err; + } + } + private async _showChannelMigrationDialog(): Promise { if (this._networkSettings!.device.path === MULTIPROTOCOL_ADDON_URL) { showAlertDialog(this, { @@ -388,6 +445,43 @@ class ZHAConfigDashboard extends LitElement { margin-top: -16px; margin-bottom: -16px; } + + .content { + margin-top: 24px; + } + + .network-status div.heading { + display: flex; + align-items: center; + } + + .network-status div.heading .icon { + margin-inline-end: 16px; + } + + .network-status div.heading ha-svg-icon { + --mdc-icon-size: 48px; + } + + .network-status div.heading .details { + font-size: var(--ha-font-size-xl); + } + + .network-status small { + font-size: var(--ha-font-size-m); + } + + .network-status small.offline { + color: var(--secondary-text-color); + } + + .network-status .online { + color: var(--state-on-color, var(--success-color)); + } + + .network-status .offline { + color: var(--error-color, var(--error-color)); + } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts index 203f5b9cc6..245a7ba841 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/dialog-zwave_js-add-node.ts @@ -48,7 +48,6 @@ import "../../../../../../components/ha-dialog-header"; import "../../../../../../components/ha-fade-in"; import "../../../../../../components/ha-icon-button"; import "../../../../../../components/ha-qr-scanner"; -import "../../../../../../components/ha-spinner"; import { computeStateName } from "../../../../../../common/entity/compute_state_name"; import { navigate } from "../../../../../../common/navigate"; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-loading.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-loading.ts index bae3acb94b..ba319058f7 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-loading.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-loading.ts @@ -3,9 +3,10 @@ import { css, html, LitElement, nothing } from "lit"; import "../../../../../../components/ha-fade-in"; import "../../../../../../components/ha-spinner"; +import { WakeLockMixin } from "../../../../../../mixins/wakelock-mixin"; @customElement("zwave-js-add-node-loading") -export class ZWaveJsAddNodeLoading extends LitElement { +export class ZWaveJsAddNodeLoading extends WakeLockMixin(LitElement) { @property() public description?: string; @property({ type: Number }) public delay = 0; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-searching-devices.ts b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-searching-devices.ts index d966009c42..fcde665c93 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-searching-devices.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/add-node/zwave-js-add-node-searching-devices.ts @@ -10,9 +10,10 @@ import { InclusionStrategy } from "../../../../../../data/zwave_js"; import "../../../../../../components/ha-spinner"; import "../../../../../../components/ha-button"; import "../../../../../../components/ha-alert"; +import { WakeLockMixin } from "../../../../../../mixins/wakelock-mixin"; @customElement("zwave-js-add-node-searching-devices") -export class ZWaveJsAddNodeSearchingDevices extends LitElement { +export class ZWaveJsAddNodeSearchingDevices extends WakeLockMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean, attribute: "smart-start" }) diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts index 3e0d3038ea..7aac93f4cf 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts @@ -468,7 +468,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { .value=${this._backupProgress} > ${this.hass.localize( - "ui.panel.config.zwave_js.dashboard.nvm_backup.downloading" + "ui.panel.config.zwave_js.dashboard.nvm_backup.creating" )} ${this._backupProgress}%` : this._restoreProgress !== undefined diff --git a/src/panels/config/repairs/show-dialog-repair-flow.ts b/src/panels/config/repairs/show-dialog-repair-flow.ts index 3ea9e79353..6f006b2e39 100644 --- a/src/panels/config/repairs/show-dialog-repair-flow.ts +++ b/src/panels/config/repairs/show-dialog-repair-flow.ts @@ -1,7 +1,6 @@ import { html, nothing } from "lit"; import type { DataEntryFlowStep } from "../../../data/data_entry_flow"; import { domainToName } from "../../../data/integration"; -import "./dialog-repairs-issue-subtitle"; import type { RepairsIssue } from "../../../data/repairs"; import { createRepairsFlow, @@ -14,6 +13,7 @@ import { showFlowDialog, } from "../../../dialogs/config-flow/show-dialog-data-entry-flow"; import type { HomeAssistant } from "../../../types"; +import "./dialog-repairs-issue-subtitle"; const mergePlaceholders = (issue: RepairsIssue, step: DataEntryFlowStep) => step.description_placeholders && issue.translation_placeholders @@ -68,8 +68,11 @@ export const showRepairsFlowDialog = ( deleteFlow: deleteRepairsFlow, renderAbortHeader(hass) { + return hass.localize("ui.dialogs.repair_flow.form.header"); + }, + + renderAbortSubheader(hass) { return html` - ${hass.localize("ui.dialogs.repair_flow.form.header")} (this._config.entities) : []; this._mapEntities = this._getMapEntities(); + this._clusterMarkers = this._config.cluster ?? true; } public getCardSize(): number { @@ -215,17 +216,21 @@ class HuiMapCard extends LitElement implements LovelaceCard { render-passive >
- + ${this._mapEntities.length > 1 + ? html` + + ` + : nothing} ${this._error}` + : nothing} ${this.hass.localize("ui.panel.profile.advanced_mode.title")} @@ -41,16 +47,24 @@ class AdvancedModeRow extends LitElement { } private async _advancedToggled(ev) { - getOptimisticFrontendUserDataCollection(this.hass.connection, "core").save({ - ...this.coreUserData, - showAdvanced: ev.currentTarget.checked, - }); + try { + saveFrontendUserData(this.hass.connection, "core", { + ...this.coreUserData, + showAdvanced: ev.currentTarget.checked, + }); + } catch (err: any) { + this._error = err.message || err; + } } static styles = css` a { color: var(--primary-color); } + ha-alert { + margin: 0 16px; + display: block; + } `; } diff --git a/src/panels/profile/ha-entity-id-picker-row.ts b/src/panels/profile/ha-entity-id-picker-row.ts index a6b5567f18..6a651debbf 100644 --- a/src/panels/profile/ha-entity-id-picker-row.ts +++ b/src/panels/profile/ha-entity-id-picker-row.ts @@ -1,11 +1,12 @@ import type { TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../components/ha-alert"; import "../../components/ha-card"; import "../../components/ha-settings-row"; import "../../components/ha-switch"; import type { CoreFrontendUserData } from "../../data/frontend"; -import { getOptimisticFrontendUserDataCollection } from "../../data/frontend"; +import { saveFrontendUserData } from "../../data/frontend"; import type { HomeAssistant } from "../../types"; @customElement("ha-entity-id-picker-row") @@ -16,8 +17,13 @@ class EntityIdPickerRow extends LitElement { @property({ attribute: false }) public coreUserData?: CoreFrontendUserData; + @state() private _error?: string; + protected render(): TemplateResult { return html` + ${this._error + ? html`${this._error}` + : nothing} ${this.hass.localize("ui.panel.profile.entity_id_picker.title")}; private _getCoreData() { - this._unsubCoreData = getOptimisticFrontendUserDataCollection( + this._unsubCoreData = subscribeFrontendUserData( this.hass.connection, - "core" - ).subscribe((coreUserData) => { - this._coreUserData = coreUserData; - }); + "core", + ({ value }) => { + this._coreUserData = value; + } + ); } public connectedCallback() { @@ -70,7 +71,7 @@ class HaProfileSectionGeneral extends LitElement { public disconnectedCallback() { super.disconnectedCallback(); if (this._unsubCoreData) { - this._unsubCoreData(); + this._unsubCoreData.then((unsub) => unsub()); this._unsubCoreData = undefined; } } diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts index 725201f6cd..82fc5f3caf 100644 --- a/src/state/connection-mixin.ts +++ b/src/state/connection-mixin.ts @@ -8,6 +8,7 @@ import { subscribeServices, } from "home-assistant-js-websocket"; import { fireEvent } from "../common/dom/fire_event"; +import { promiseTimeout } from "../common/util/promise-timeout"; import { subscribeAreaRegistry } from "../data/area_registry"; import { broadcastConnectionStatus } from "../data/connection-status"; import { subscribeDeviceRegistry } from "../data/device_registry"; @@ -22,6 +23,8 @@ import { TimeFormat, TimeZone, } from "../data/translation"; +import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display"; +import { subscribeFloorRegistry } from "../data/ws-floor_registry"; import { subscribePanels } from "../data/ws-panels"; import { translationMetadata } from "../resources/translations-metadata"; import type { Constructor, HomeAssistant, ServiceCallResponse } from "../types"; @@ -30,9 +33,6 @@ import { fetchWithAuth } from "../util/fetch-with-auth"; import { getState } from "../util/ha-pref-storage"; import hassCallApi, { hassCallApiRaw } from "../util/hass-call-api"; import type { HassBaseEl } from "./hass-base-mixin"; -import { promiseTimeout } from "../common/util/promise-timeout"; -import { subscribeFloorRegistry } from "../data/ws-floor_registry"; -import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display"; export const connectionMixin = >( superClass: T @@ -280,9 +280,9 @@ export const connectionMixin = >( subscribeConfig(conn, (config) => this._updateHass({ config })); subscribeServices(conn, (services) => this._updateHass({ services })); subscribePanels(conn, (panels) => this._updateHass({ panels })); - subscribeFrontendUserData(conn, "core", (userData) => - this._updateHass({ userData }) - ); + subscribeFrontendUserData(conn, "core", ({ value: userData }) => { + this._updateHass({ userData }); + }); clearInterval(this.__backendPingInterval); this.__backendPingInterval = setInterval(() => { diff --git a/src/state/translations-mixin.ts b/src/state/translations-mixin.ts index f7df357768..2b587acadb 100644 --- a/src/state/translations-mixin.ts +++ b/src/state/translations-mixin.ts @@ -8,17 +8,18 @@ import { } from "../common/util/compute_rtl"; import { debounce } from "../common/util/debounce"; import type { + DateFormat, FirstWeekday, NumberFormat, TimeFormat, - DateFormat, - TranslationCategory, TimeZone, + TranslationCategory, } from "../data/translation"; import { getHassTranslations, getHassTranslationsPre109, saveTranslationPreferences, + subscribeTranslationPreferences, } from "../data/translation"; import { translationMetadata } from "../resources/translations-metadata"; import type { Constructor, HomeAssistant } from "../types"; @@ -119,7 +120,10 @@ export default >(superClass: T) => protected hassConnected() { super.hassConnected(); - getUserLocale(this.hass!).then((locale) => { + + subscribeTranslationPreferences(this.hass!, async ({ value }) => { + const locale = await getUserLocale(value); + if (locale?.language && this.hass!.language !== locale.language) { // We just got language from backend, no need to save back this._selectLanguage(locale.language, false); diff --git a/src/translations/en.json b/src/translations/en.json index 5c4a557729..f8166bdb31 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4725,7 +4725,7 @@ "check_your_email": "Check your email for instructions on how to reset your password." }, "register": { - "title": "Register Account", + "title": "Register account", "headline": "Start your free trial", "information": "Create an account to start your free one month trial with Home Assistant Cloud. No payment information necessary.", "information2": "The trial will give you access to all the benefits of Home Assistant Cloud, including:", @@ -5340,7 +5340,7 @@ }, "config_flow": { "success": "Success", - "assign_area": "Assign {number, plural,\n one {device}\n other {devices}\n} to area", + "device_created": "{number, plural,\n one {Device}\n other {Devices}\n} created", "device_name": "Device name", "aborted": "Aborted", "close": "Close", @@ -5610,7 +5610,11 @@ "value": "Value" }, "configuration_page": { - "shortcuts_title": "Shortcuts", + "status_title": "status", + "status_online": "online", + "status_offline": "offline", + "devices": "{count} {count, plural,\n one {device}\n other {devices}\n}", + "devices_offline": "{count} offline", "update_button": "Update configuration", "download_backup": "Download backup", "migrate_radio": "Migrate radio", @@ -5761,7 +5765,7 @@ "backup_failed": "Failed to download backup", "restore_complete": "Backup restored", "restore_failed": "Failed to restore backup", - "downloading": "Downloading backup", + "creating": "Creating backup", "restoring": "Restoring backup", "migrate": "Migrate controller" }, @@ -6419,7 +6423,7 @@ "zeroconf_info": "Show services discovered using mDNS. Does not include services unknown to Home Assistant." }, "network_adapter": "Network adapter", - "network_adapter_info": "Configure which network adapters integrations will use. Currently this setting only affects multicast traffic. A restart is required for these settings to apply.", + "network_adapter_info": "Configure which network adapters integrations will use. A restart is required for these settings to apply.", "ip_information": "IP Information", "adapter": { "auto_configure": "Auto configure", diff --git a/src/util/common-translation.ts b/src/util/common-translation.ts index 0b79f2e747..b043653684 100644 --- a/src/util/common-translation.ts +++ b/src/util/common-translation.ts @@ -1,7 +1,5 @@ import type { FrontendLocaleData } from "../data/translation"; -import { fetchTranslationPreferences } from "../data/translation"; import { translationMetadata } from "../resources/translations-metadata"; -import type { HomeAssistant } from "../types"; const BASE_URL = `${__STATIC_PATH__}translations`; const STORAGE = window.localStorage || {}; @@ -68,15 +66,14 @@ export function findAvailableLanguage(language: string) { * Get user selected locale data from backend */ export async function getUserLocale( - hass: HomeAssistant + data: FrontendLocaleData | null ): Promise> { - const result = await fetchTranslationPreferences(hass); - const language = result?.language; - const number_format = result?.number_format; - const time_format = result?.time_format; - const date_format = result?.date_format; - const time_zone = result?.time_zone; - const first_weekday = result?.first_weekday; + const language = data?.language; + const number_format = data?.number_format; + const time_format = data?.time_format; + const date_format = data?.date_format; + const time_zone = data?.time_zone; + const first_weekday = data?.first_weekday; if (language) { const availableLanguage = findAvailableLanguage(language); if (availableLanguage) { diff --git a/yarn.lock b/yarn.lock index 706cd26a74..772e94598a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1165,14 +1165,14 @@ __metadata: languageName: node linkType: hard -"@bundle-stats/plugin-webpack-filter@npm:4.20.0": - version: 4.20.0 - resolution: "@bundle-stats/plugin-webpack-filter@npm:4.20.0" +"@bundle-stats/plugin-webpack-filter@npm:4.20.1": + version: 4.20.1 + resolution: "@bundle-stats/plugin-webpack-filter@npm:4.20.1" dependencies: tslib: "npm:2.8.1" peerDependencies: core-js: ^3.0.0 - checksum: 10/b20479bb5f5964d8fa3b8be0b31665521681cfa7b0b5c2f3752476d36507332270e454592632a793c2579b98c036d6946eaa54caf984fcd3a649ccd1802492aa + checksum: 10/ba05af0d690010ad4e98cfe3b36f7b6d60f86b0faae4bed4a844f1a0f9ccc57ecee8085abc31dde2f1f290384580565b97086a5f79639553a54e49e712ce4261 languageName: node linkType: hard @@ -2274,10 +2274,10 @@ __metadata: languageName: node linkType: hard -"@lokalise/node-api@npm:14.6.0": - version: 14.6.0 - resolution: "@lokalise/node-api@npm:14.6.0" - checksum: 10/b715a58f1a66cb8a8cfe2f3759b8a1bc25eb4c80a3ea52e800716758939a9430e4e3698958e58591999c2363cdfac4ad75294dd80788e93d6671f30ecb630deb +"@lokalise/node-api@npm:14.7.0": + version: 14.7.0 + resolution: "@lokalise/node-api@npm:14.7.0" + checksum: 10/77a1348c2a95215b924c2489136f5d8fc501f59fc8ff4b2d197716aca544db4d19928b005789a55fde0a0ea40830cda5ed8b7ef0895b97c458ca1fb1aa03d9bd languageName: node linkType: hard @@ -3825,22 +3825,22 @@ __metadata: languageName: node linkType: hard -"@rsdoctor/client@npm:1.1.1": - version: 1.1.1 - resolution: "@rsdoctor/client@npm:1.1.1" - checksum: 10/cd53a42e4c4f987c539e5235f532da4cfdf716aa2f0168c1c58224bf64970714dc90c4155ef4f9b915298d23c0154a07ac17a4f5926af0c60751e1ff2a6f305c +"@rsdoctor/client@npm:1.1.2": + version: 1.1.2 + resolution: "@rsdoctor/client@npm:1.1.2" + checksum: 10/600aadee4fd2fd176a232a0ecbdc62eada7f5aed5aa623983f2652633b487db541ec52fed666b7f7ada69440fb54ec12e7753e7519c8e0ebc9d11ad614fbbbd6 languageName: node linkType: hard -"@rsdoctor/core@npm:1.1.1": - version: 1.1.1 - resolution: "@rsdoctor/core@npm:1.1.1" +"@rsdoctor/core@npm:1.1.2": + version: 1.1.2 + resolution: "@rsdoctor/core@npm:1.1.2" dependencies: "@rsbuild/plugin-check-syntax": "npm:1.3.0" - "@rsdoctor/graph": "npm:1.1.1" - "@rsdoctor/sdk": "npm:1.1.1" - "@rsdoctor/types": "npm:1.1.1" - "@rsdoctor/utils": "npm:1.1.1" + "@rsdoctor/graph": "npm:1.1.2" + "@rsdoctor/sdk": "npm:1.1.2" + "@rsdoctor/types": "npm:1.1.2" + "@rsdoctor/utils": "npm:1.1.2" axios: "npm:^1.8.4" browserslist-load-config: "npm:^1.0.0" enhanced-resolve: "npm:5.12.0" @@ -3851,47 +3851,47 @@ __metadata: semver: "npm:^7.6.3" source-map: "npm:^0.7.4" webpack-bundle-analyzer: "npm:^4.10.2" - checksum: 10/378c50296c482ffa98f906c3a02bf83783f1a524062ca4c5302dfac04e2219216c488d7ab1164ad6dbeb629e33b8e9892a67c755f30fe5caae12a8141a051597 + checksum: 10/f2535e434145f9b267c43acdcec1482ad7747600eb8438ca357700648096b22ee0697005bf58cd2b9180b69a06117a1909cec0923702594336ccc75f3040a3e7 languageName: node linkType: hard -"@rsdoctor/graph@npm:1.1.1": - version: 1.1.1 - resolution: "@rsdoctor/graph@npm:1.1.1" +"@rsdoctor/graph@npm:1.1.2": + version: 1.1.2 + resolution: "@rsdoctor/graph@npm:1.1.2" dependencies: - "@rsdoctor/types": "npm:1.1.1" - "@rsdoctor/utils": "npm:1.1.1" + "@rsdoctor/types": "npm:1.1.2" + "@rsdoctor/utils": "npm:1.1.2" lodash.unionby: "npm:^4.8.0" socket.io: "npm:4.8.1" source-map: "npm:^0.7.4" - checksum: 10/b0bd4e77070bacf7340bf8450eee540c6565d61b7636328fe9ba23ab4ad770eef09a4e7a6a148b006546d1f43feabd014c8fc6e4daac79c23424165f54323efb + checksum: 10/b1cc511602305c36f95e524c6f2e60d32c3cea4a7328f67c068317a98c719ca8d69256e50d8dfad4641f8291cb32d487a041f62e3995d8841f8e5d4b2bb09319 languageName: node linkType: hard -"@rsdoctor/rspack-plugin@npm:1.1.1": - version: 1.1.1 - resolution: "@rsdoctor/rspack-plugin@npm:1.1.1" +"@rsdoctor/rspack-plugin@npm:1.1.2": + version: 1.1.2 + resolution: "@rsdoctor/rspack-plugin@npm:1.1.2" dependencies: - "@rsdoctor/core": "npm:1.1.1" - "@rsdoctor/graph": "npm:1.1.1" - "@rsdoctor/sdk": "npm:1.1.1" - "@rsdoctor/types": "npm:1.1.1" - "@rsdoctor/utils": "npm:1.1.1" + "@rsdoctor/core": "npm:1.1.2" + "@rsdoctor/graph": "npm:1.1.2" + "@rsdoctor/sdk": "npm:1.1.2" + "@rsdoctor/types": "npm:1.1.2" + "@rsdoctor/utils": "npm:1.1.2" lodash: "npm:^4.17.21" peerDependencies: "@rspack/core": "*" - checksum: 10/fb5ece5bfd583e097a4e4caf62e2a2cb30986824cbfe5b9a313a8db8b04f680291319bf4949278269589a5b12aceb26d514003045a5819101c8a6febd05fd226 + checksum: 10/44107db8159c14273356fa5733341b01192e59ecdcca516632354a67321193ed40485c868a20488839dcbee12b6b07f22264c5080e9ddbf06ac6cf1a49b30ce3 languageName: node linkType: hard -"@rsdoctor/sdk@npm:1.1.1": - version: 1.1.1 - resolution: "@rsdoctor/sdk@npm:1.1.1" +"@rsdoctor/sdk@npm:1.1.2": + version: 1.1.2 + resolution: "@rsdoctor/sdk@npm:1.1.2" dependencies: - "@rsdoctor/client": "npm:1.1.1" - "@rsdoctor/graph": "npm:1.1.1" - "@rsdoctor/types": "npm:1.1.1" - "@rsdoctor/utils": "npm:1.1.1" + "@rsdoctor/client": "npm:1.1.2" + "@rsdoctor/graph": "npm:1.1.2" + "@rsdoctor/types": "npm:1.1.2" + "@rsdoctor/utils": "npm:1.1.2" "@types/fs-extra": "npm:^11.0.4" body-parser: "npm:1.20.3" cors: "npm:2.8.5" @@ -3904,13 +3904,13 @@ __metadata: socket.io: "npm:4.8.1" source-map: "npm:^0.7.4" tapable: "npm:2.2.1" - checksum: 10/971a422e301961a74f64da56866b50a9d5773769aa0341d8a64ea14a0c940a16db77dbf8bb0a2c56b868f63d2fa00425709bb5eb28ac630171fb2817fbb2604e + checksum: 10/28baf794ee34fc46548a10fdce99a8ed207df2c74bb47ac50683ea23a81a52e5f0b50d07b7ad9f36348a0b4bfb9bad34613a2bde9f3b4dd988fe250ac7587b28 languageName: node linkType: hard -"@rsdoctor/types@npm:1.1.1": - version: 1.1.1 - resolution: "@rsdoctor/types@npm:1.1.1" +"@rsdoctor/types@npm:1.1.2": + version: 1.1.2 + resolution: "@rsdoctor/types@npm:1.1.2" dependencies: "@types/connect": "npm:3.4.38" "@types/estree": "npm:1.0.5" @@ -3924,16 +3924,16 @@ __metadata: optional: true webpack: optional: true - checksum: 10/578b18139cd66ba1a07d5ff9862050b9ed66a3883987b95d4764ebd392bc7538dfc3a20408048661201e73e3a4d46231d77ce36b4e2f1f56b528d75c8cc8e6f1 + checksum: 10/37254be48d434cdb4663451fdc120cbba31f871072886eea0c84d8913177deab7eee88a22ce457764b2c432c2aaa0902234f6606244812d26f91921591a2b0dc languageName: node linkType: hard -"@rsdoctor/utils@npm:1.1.1": - version: 1.1.1 - resolution: "@rsdoctor/utils@npm:1.1.1" +"@rsdoctor/utils@npm:1.1.2": + version: 1.1.2 + resolution: "@rsdoctor/utils@npm:1.1.2" dependencies: "@babel/code-frame": "npm:7.26.2" - "@rsdoctor/types": "npm:1.1.1" + "@rsdoctor/types": "npm:1.1.2" "@types/estree": "npm:1.0.5" acorn: "npm:^8.10.0" acorn-import-attributes: "npm:^1.9.5" @@ -3949,7 +3949,7 @@ __metadata: picocolors: "npm:^1.1.1" rslog: "npm:^1.2.3" strip-ansi: "npm:^6.0.1" - checksum: 10/30d21c1bb0a389b98a723e5ebc1b897d10ce23edda161edb3ab75f7b2614650c0f48955649a5e768d513236a694ff25f9a38dae2f05de43b4f493808742340ae + checksum: 10/d8435f149e4252c6e6eaca6e4f6e82352b3d3d0642b120b8b12323bc222c185ccb902fd7adc03ee2b5a2c52cb9d7cebbad388c6fdce319f2d3b7fbfc6bfd6287 languageName: node linkType: hard @@ -9628,7 +9628,7 @@ __metadata: "@babel/preset-env": "npm:7.27.2" "@babel/runtime": "npm:7.27.1" "@braintree/sanitize-url": "npm:7.1.1" - "@bundle-stats/plugin-webpack-filter": "npm:4.20.0" + "@bundle-stats/plugin-webpack-filter": "npm:4.20.1" "@codemirror/autocomplete": "npm:6.18.6" "@codemirror/commands": "npm:6.8.1" "@codemirror/language": "npm:6.11.0" @@ -9660,7 +9660,7 @@ __metadata: "@lit-labs/virtualizer": "npm:2.1.0" "@lit/context": "npm:1.1.5" "@lit/reactive-element": "npm:2.1.0" - "@lokalise/node-api": "npm:14.6.0" + "@lokalise/node-api": "npm:14.7.0" "@material/chips": "npm:=14.0.0-canary.53b3cad2f.0" "@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0" "@material/mwc-base": "npm:0.27.0" @@ -9691,7 +9691,7 @@ __metadata: "@octokit/plugin-retry": "npm:7.2.1" "@octokit/rest": "npm:21.1.1" "@replit/codemirror-indentation-markers": "npm:6.5.3" - "@rsdoctor/rspack-plugin": "npm:1.1.1" + "@rsdoctor/rspack-plugin": "npm:1.1.2" "@rspack/cli": "npm:1.3.9" "@rspack/core": "npm:1.3.9" "@shoelace-style/shoelace": "npm:2.20.1"