20231026.0 (#18431)

This commit is contained in:
Paul Bottein 2023-10-26 15:57:36 +02:00 committed by GitHub
commit 4e6e924a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 312 additions and 109 deletions

View File

@ -45,8 +45,8 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-app", "copy-static-app",
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
// Don't compress running tests // Don't compress running tests
...(env.isTestBuild() ? [] : ["compress-app"]), ...(env.isTestBuild() ? [] : ["compress-app"])
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod")
) )
); );

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20231025.1" version = "20231026.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -223,7 +223,7 @@ export class StateHistoryCharts extends LitElement {
); );
} else { } else {
this._computedStartTime = new Date( this._computedStartTime = new Date(
this.historyData.timeline.reduce( (this.historyData?.timeline ?? []).reduce(
(minTime, stateInfo) => (minTime, stateInfo) =>
Math.min( Math.min(
minTime, minTime,

View File

@ -10,7 +10,8 @@ export class HaSlider extends MdSlider {
:host { :host {
--md-sys-color-primary: var(--primary-color); --md-sys-color-primary: var(--primary-color);
--md-sys-color-outline: var(--outline-color); --md-sys-color-outline: var(--outline-color);
--md-slider-handle-width: 14px;
--md-slider-handle-height: 14px;
min-width: 100px; min-width: 100px;
min-inline-size: 100px; min-inline-size: 100px;
width: 200px; width: 200px;

View File

@ -136,6 +136,11 @@ export class HaTextField extends TextFieldBase {
text-align: var(--text-field-text-align, start); text-align: var(--text-field-text-align, start);
} }
/* Edge, hide reveal password icon */
::-ms-reveal {
display: none;
}
/* Chrome, Safari, Edge, Opera */ /* Chrome, Safari, Edge, Opera */
:host([no-spinner]) input::-webkit-outer-spin-button, :host([no-spinner]) input::-webkit-outer-spin-button,
:host([no-spinner]) input::-webkit-inner-spin-button { :host([no-spinner]) input::-webkit-inner-spin-button {

View File

@ -348,11 +348,6 @@ export const getLegacyLovelaceCollection = (conn: Connection) =>
) )
); );
export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise<LovelaceConfig>;
llResProm?: Promise<LovelaceResource[]>;
}
export interface ActionHandlerOptions { export interface ActionHandlerOptions {
hasHold?: boolean; hasHold?: boolean;
hasDoubleClick?: boolean; hasDoubleClick?: boolean;

8
src/data/preloads.ts Normal file
View File

@ -0,0 +1,8 @@
import { LovelaceConfig, LovelaceResource } from "./lovelace";
import { RecorderInfo } from "./recorder";
export interface WindowWithPreloads extends Window {
llConfProm?: Promise<LovelaceConfig>;
llResProm?: Promise<LovelaceResource[]>;
recorderInfoProm?: Promise<RecorderInfo>;
}

View File

@ -1,3 +1,4 @@
import { Connection } from "home-assistant-js-websocket";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { HaDurationData } from "../components/ha-duration-input"; import { HaDurationData } from "../components/ha-duration-input";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@ -115,8 +116,8 @@ export interface StatisticsValidationResults {
[statisticId: string]: StatisticsValidationResult[]; [statisticId: string]: StatisticsValidationResult[];
} }
export const getRecorderInfo = (hass: HomeAssistant) => export const getRecorderInfo = (conn: Connection) =>
hass.callWS<RecorderInfo>({ conn.sendMessagePromise<RecorderInfo>({
type: "recorder/info", type: "recorder/info",
}); });

View File

@ -15,7 +15,7 @@ export const enum TodoItemStatus {
} }
export interface TodoItem { export interface TodoItem {
uid?: string; uid: string;
summary: string; summary: string;
status: TodoItemStatus; status: TodoItemStatus;
} }
@ -95,11 +95,11 @@ export const moveItem = (
hass: HomeAssistant, hass: HomeAssistant,
entity_id: string, entity_id: string,
uid: string, uid: string,
pos: number previous_uid: string | undefined
): Promise<void> => ): Promise<void> =>
hass.callWS({ hass.callWS({
type: "todo/item/move", type: "todo/item/move",
entity_id, entity_id,
uid, uid,
pos, previous_uid,
}); });

View File

@ -13,12 +13,9 @@ import {
import { loadTokens, saveTokens } from "../common/auth/token_storage"; import { loadTokens, saveTokens } from "../common/auth/token_storage";
import { hassUrl } from "../data/auth"; import { hassUrl } from "../data/auth";
import { isExternal } from "../data/external"; import { isExternal } from "../data/external";
import { getRecorderInfo } from "../data/recorder";
import { subscribeFrontendUserData } from "../data/frontend"; import { subscribeFrontendUserData } from "../data/frontend";
import { import { fetchConfig, fetchResources } from "../data/lovelace";
fetchConfig,
fetchResources,
WindowWithLovelaceProm,
} from "../data/lovelace";
import { subscribePanels } from "../data/ws-panels"; import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes"; import { subscribeThemes } from "../data/ws-themes";
import { subscribeRepairsIssueRegistry } from "../data/repairs"; import { subscribeRepairsIssueRegistry } from "../data/repairs";
@ -27,6 +24,7 @@ import type { ExternalAuth } from "../external_app/external_auth";
import "../resources/array.flat.polyfill"; import "../resources/array.flat.polyfill";
import "../resources/safari-14-attachshadow-patch"; import "../resources/safari-14-attachshadow-patch";
import { MAIN_WINDOW_NAME } from "../data/main_window"; import { MAIN_WINDOW_NAME } from "../data/main_window";
import { WindowWithPreloads } from "../data/preloads";
window.name = MAIN_WINDOW_NAME; window.name = MAIN_WINDOW_NAME;
(window as any).frontendVersion = __VERSION__; (window as any).frontendVersion = __VERSION__;
@ -124,12 +122,14 @@ window.hassConnection.then(({ conn }) => {
subscribeFrontendUserData(conn, "core", noop); subscribeFrontendUserData(conn, "core", noop);
subscribeRepairsIssueRegistry(conn, noop); subscribeRepairsIssueRegistry(conn, noop);
const preloadWindow = window as WindowWithPreloads;
preloadWindow.recorderInfoProm = getRecorderInfo(conn);
if (location.pathname === "/" || location.pathname.startsWith("/lovelace/")) { if (location.pathname === "/" || location.pathname.startsWith("/lovelace/")) {
const llWindow = window as WindowWithLovelaceProm; preloadWindow.llConfProm = fetchConfig(conn, null, false);
llWindow.llConfProm = fetchConfig(conn, null, false); preloadWindow.llConfProm.catch(() => {
llWindow.llConfProm.catch(() => {
// Ignore it, it is handled by Lovelace panel. // Ignore it, it is handled by Lovelace panel.
}); });
llWindow.llResProm = fetchResources(conn); preloadWindow.llResProm = fetchResources(conn);
} }
}); });

View File

@ -3,11 +3,12 @@ import { customElement, state } from "lit/decorators";
import { isNavigationClick } from "../common/dom/is-navigation-click"; import { isNavigationClick } from "../common/dom/is-navigation-click";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { getStorageDefaultPanelUrlPath } from "../data/panel"; import { getStorageDefaultPanelUrlPath } from "../data/panel";
import { getRecorderInfo } from "../data/recorder"; import { getRecorderInfo, RecorderInfo } from "../data/recorder";
import "../resources/custom-card-support"; import "../resources/custom-card-support";
import { HassElement } from "../state/hass-element"; import { HassElement } from "../state/hass-element";
import QuickBarMixin from "../state/quick-bar-mixin"; import QuickBarMixin from "../state/quick-bar-mixin";
import { HomeAssistant, Route } from "../types"; import { HomeAssistant, Route } from "../types";
import { WindowWithPreloads } from "../data/preloads";
import { storeState } from "../util/ha-pref-storage"; import { storeState } from "../util/ha-pref-storage";
import { import {
renderLaunchScreenInfoBox, renderLaunchScreenInfoBox,
@ -204,7 +205,15 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
protected async checkDataBaseMigration() { protected async checkDataBaseMigration() {
if (this.hass?.config?.components.includes("recorder")) { if (this.hass?.config?.components.includes("recorder")) {
const info = await getRecorderInfo(this.hass); let recorderInfoProm: Promise<RecorderInfo> | undefined;
const preloadWindow = window as WindowWithPreloads;
// On first load, we speed up loading page by having recorderInfoProm ready
if (preloadWindow.recorderInfoProm) {
recorderInfoProm = preloadWindow.recorderInfoProm;
preloadWindow.recorderInfoProm = undefined;
}
const info = await (recorderInfoProm ||
getRecorderInfo(this.hass.connection));
this._databaseMigration = this._databaseMigration =
info.migration_in_progress && !info.migration_is_live; info.migration_in_progress && !info.migration_is_live;
if (this._databaseMigration) { if (this._databaseMigration) {

View File

@ -93,6 +93,20 @@ class HaScheduleForm extends LitElement {
} }
} }
public disconnectedCallback(): void {
super.disconnectedCallback();
this.calendar?.destroy();
this.calendar = undefined;
this.renderRoot.querySelector("style[data-fullcalendar]")?.remove();
}
public connectedCallback(): void {
super.connectedCallback();
if (this.hasUpdated && !this.calendar) {
this.setupCalendar();
}
}
public focus() { public focus() {
this.updateComplete.then( this.updateComplete.then(
() => () =>
@ -165,6 +179,10 @@ class HaScheduleForm extends LitElement {
} }
protected firstUpdated(): void { protected firstUpdated(): void {
this.setupCalendar();
}
private setupCalendar(): void {
const config: CalendarOptions = { const config: CalendarOptions = {
...defaultFullCalendarConfig, ...defaultFullCalendarConfig,
locale: this.hass.language, locale: this.hass.language,

View File

@ -21,6 +21,7 @@ import "../../../components/ha-checkbox";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import "../../../components/ha-select"; import "../../../components/ha-select";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield";
import { import {
@ -37,8 +38,10 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { SortableInstance } from "../../../resources/sortable"; import type { SortableInstance } from "../../../resources/sortable";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types"; import { LovelaceCard, LovelaceCardEditor } from "../types";
import { TodoListCardConfig } from "./types"; import { TodoListCardConfig } from "./types";
import { isUnavailableState } from "../../../data/entity";
@customElement("hui-todo-list-card") @customElement("hui-todo-list-card")
export class HuiTodoListCard export class HuiTodoListCard
@ -74,7 +77,7 @@ export class HuiTodoListCard
@state() private _entityId?: string; @state() private _entityId?: string;
@state() private _items?: Record<string, TodoItem>; @state() private _items?: TodoItem[];
@state() private _reordering = false; @state() private _reordering = false;
@ -104,22 +107,16 @@ export class HuiTodoListCard
return undefined; return undefined;
} }
private _getCheckedItems = memoizeOne( private _getCheckedItems = memoizeOne((items?: TodoItem[]): TodoItem[] =>
(items?: Record<string, TodoItem>): TodoItem[] => items
items ? items.filter((item) => item.status === TodoItemStatus.Completed)
? Object.values(items).filter( : []
(item) => item.status === TodoItemStatus.Completed
)
: []
); );
private _getUncheckedItems = memoizeOne( private _getUncheckedItems = memoizeOne((items?: TodoItem[]): TodoItem[] =>
(items?: Record<string, TodoItem>): TodoItem[] => items
items ? items.filter((item) => item.status === TodoItemStatus.NeedsAction)
? Object.values(items).filter( : []
(item) => item.status === TodoItemStatus.NeedsAction
)
: []
); );
public willUpdate( public willUpdate(
@ -169,6 +166,18 @@ export class HuiTodoListCard
return nothing; return nothing;
} }
const stateObj = this.hass.states[this._entityId];
if (!stateObj) {
return html`
<hui-warning>
${createEntityNotFoundWarning(this.hass, this._entityId)}
</hui-warning>
`;
}
const unavailable = isUnavailableState(stateObj.state);
const checkedItems = this._getCheckedItems(this._items); const checkedItems = this._getCheckedItems(this._items);
const uncheckedItems = this._getUncheckedItems(this._items); const uncheckedItems = this._getUncheckedItems(this._items);
@ -182,39 +191,44 @@ export class HuiTodoListCard
<div class="addRow"> <div class="addRow">
${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM) ${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM)
? html` ? html`
<ha-svg-icon <ha-icon-button
class="addButton" class="addButton"
.path=${mdiPlus} .path=${mdiPlus}
.title=${this.hass!.localize( .title=${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.add_item" "ui.panel.lovelace.cards.todo-list.add_item"
)} )}
.disabled=${unavailable}
@click=${this._addItem} @click=${this._addItem}
> >
</ha-svg-icon> </ha-icon-button>
<ha-textfield <ha-textfield
class="addBox" class="addBox"
.placeholder=${this.hass!.localize( .placeholder=${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.add_item" "ui.panel.lovelace.cards.todo-list.add_item"
)} )}
@keydown=${this._addKeyPress} @keydown=${this._addKeyPress}
.disabled=${unavailable}
></ha-textfield> ></ha-textfield>
` `
: nothing} : nothing}
${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM) ${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM)
? html` ? html`
<ha-svg-icon <ha-icon-button
class="reorderButton" class="reorderButton"
.path=${mdiSort} .path=${mdiSort}
.title=${this.hass!.localize( .title=${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.reorder_items" "ui.panel.lovelace.cards.todo-list.reorder_items"
)} )}
@click=${this._toggleReorder} @click=${this._toggleReorder}
.disabled=${unavailable}
> >
</ha-svg-icon> </ha-icon-button>
` `
: nothing} : nothing}
</div> </div>
<div id="unchecked">${this._renderItems(uncheckedItems)}</div> <div id="unchecked">
${this._renderItems(uncheckedItems, unavailable)}
</div>
${checkedItems.length ${checkedItems.length
? html` ? html`
<div class="divider"></div> <div class="divider"></div>
@ -235,6 +249,7 @@ export class HuiTodoListCard
"ui.panel.lovelace.cards.todo-list.clear_items" "ui.panel.lovelace.cards.todo-list.clear_items"
)} )}
@click=${this._clearCompletedItems} @click=${this._clearCompletedItems}
.disabled=${unavailable}
> >
</ha-svg-icon>` </ha-svg-icon>`
: nothing} : nothing}
@ -247,16 +262,18 @@ export class HuiTodoListCard
${this.todoListSupportsFeature( ${this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM TodoListEntityFeature.UPDATE_TODO_ITEM
) )
? html` <ha-checkbox ? html`<ha-checkbox
tabindex="0" tabindex="0"
.checked=${item.status === TodoItemStatus.Completed} .checked=${item.status === TodoItemStatus.Completed}
.itemId=${item.uid} .itemId=${item.uid}
@change=${this._completeItem} @change=${this._completeItem}
.disabled=${unavailable}
></ha-checkbox>` ></ha-checkbox>`
: nothing} : nothing}
<ha-textfield <ha-textfield
class="item" class="item"
.disabled=${!this.todoListSupportsFeature( .disabled=${unavailable ||
!this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM TodoListEntityFeature.UPDATE_TODO_ITEM
)} )}
.value=${item.summary} .value=${item.summary}
@ -272,7 +289,7 @@ export class HuiTodoListCard
`; `;
} }
private _renderItems(items: TodoItem[]) { private _renderItems(items: TodoItem[], unavailable = false) {
return html` return html`
${repeat( ${repeat(
items, items,
@ -282,16 +299,18 @@ export class HuiTodoListCard
${this.todoListSupportsFeature( ${this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM TodoListEntityFeature.UPDATE_TODO_ITEM
) )
? html` <ha-checkbox ? html`<ha-checkbox
tabindex="0" tabindex="0"
.checked=${item.status === TodoItemStatus.Completed} .checked=${item.status === TodoItemStatus.Completed}
.itemId=${item.uid} .itemId=${item.uid}
.disabled=${unavailable}
@change=${this._completeItem} @change=${this._completeItem}
></ha-checkbox>` ></ha-checkbox>`
: nothing} : nothing}
<ha-textfield <ha-textfield
class="item" class="item"
.disabled=${!this.todoListSupportsFeature( .disabled=${unavailable ||
!this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM TodoListEntityFeature.UPDATE_TODO_ITEM
)} )}
.value=${item.summary} .value=${item.summary}
@ -325,16 +344,21 @@ export class HuiTodoListCard
if (!this.hass || !this._entityId) { if (!this.hass || !this._entityId) {
return; return;
} }
const items = await fetchItems(this.hass!, this._entityId!); if (!(this._entityId in this.hass.states)) {
const records: Record<string, TodoItem> = {}; return;
items.forEach((item) => { }
records[item.uid!] = item; this._items = await fetchItems(this.hass!, this._entityId!);
}); }
this._items = records;
private _getItem(itemId: string) {
return this._items?.find((item) => item.uid === itemId);
} }
private _completeItem(ev): void { private _completeItem(ev): void {
const item = this._items![ev.target.itemId]; const item = this._getItem(ev.target.itemId);
if (!item) {
return;
}
updateItem(this.hass!, this._entityId!, { updateItem(this.hass!, this._entityId!, {
...item, ...item,
status: ev.target.checked status: ev.target.checked
@ -346,7 +370,10 @@ export class HuiTodoListCard
private _saveEdit(ev): void { private _saveEdit(ev): void {
// If name is not empty, update the item otherwise remove it // If name is not empty, update the item otherwise remove it
if (ev.target.value) { if (ev.target.value) {
const item = this._items![ev.target.itemId]; const item = this._getItem(ev.target.itemId);
if (!item) {
return;
}
updateItem(this.hass!, this._entityId!, { updateItem(this.hass!, this._entityId!, {
...item, ...item,
summary: ev.target.value, summary: ev.target.value,
@ -368,7 +395,7 @@ export class HuiTodoListCard
} }
const deleteActions: Array<Promise<any>> = []; const deleteActions: Array<Promise<any>> = [];
this._getCheckedItems(this._items).forEach((item: TodoItem) => { this._getCheckedItems(this._items).forEach((item: TodoItem) => {
deleteActions.push(deleteItem(this.hass!, this._entityId!, item.uid!)); deleteActions.push(deleteItem(this.hass!, this._entityId!, item.uid));
}); });
await Promise.all(deleteActions).finally(() => this._fetchData()); await Promise.all(deleteActions).finally(() => this._fetchData());
} }
@ -438,11 +465,37 @@ export class HuiTodoListCard
}); });
} }
private async _moveItem(oldIndex, newIndex) { private async _moveItem(oldIndex: number, newIndex: number) {
const item = this._getUncheckedItems(this._items)[oldIndex]; const uncheckedItems = this._getUncheckedItems(this._items);
await moveItem(this.hass!, this._entityId!, item.uid!, newIndex).finally( const item = uncheckedItems[oldIndex];
() => this._fetchData() let prevItem: TodoItem | undefined;
); if (newIndex > 0) {
if (newIndex < oldIndex) {
prevItem = uncheckedItems[newIndex - 1];
} else {
prevItem = uncheckedItems[newIndex];
}
}
// Optimistic change
const itemIndex = this._items!.findIndex((itm) => itm.uid === item.uid);
this._items!.splice(itemIndex, 1);
if (newIndex === 0) {
this._items!.unshift(item);
} else {
const prevIndex = this._items!.findIndex(
(itm) => itm.uid === prevItem!.uid
);
this._items!.splice(prevIndex + 1, 0, item);
}
this._items = [...this._items!];
await moveItem(
this.hass!,
this._entityId!,
item.uid,
prevItem?.uid
).finally(() => this._fetchData());
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@ -470,16 +523,14 @@ export class HuiTodoListCard
} }
.addButton { .addButton {
padding-right: 16px; margin-left: -12px;
padding-inline-end: 16px; margin-inline-start: -12px;
cursor: pointer;
direction: var(--direction); direction: var(--direction);
} }
.reorderButton { .reorderButton {
padding-left: 16px; margin-right: -12px;
padding-inline-start: 16px; margin-inline-end: -12px;
cursor: pointer;
direction: var(--direction); direction: var(--direction);
} }

View File

@ -11,7 +11,8 @@ export const loadLovelaceResources = (
hass: HomeAssistant hass: HomeAssistant
) => { ) => {
// Don't load ressources on safe mode // Don't load ressources on safe mode
if (hass.config.safe_mode) { // Sometimes, hass.config is null but it should not.
if (hass.config?.safe_mode) {
return; return;
} }
resources.forEach((resource) => { resources.forEach((resource) => {

View File

@ -48,6 +48,7 @@ export class HaCardConditionNumericState extends LitElement {
name: "above", name: "above",
selector: { selector: {
number: { number: {
step: "any",
mode: "box", mode: "box",
unit_of_measurement: stateObj?.attributes.unit_of_measurement, unit_of_measurement: stateObj?.attributes.unit_of_measurement,
}, },
@ -57,6 +58,7 @@ export class HaCardConditionNumericState extends LitElement {
name: "below", name: "below",
selector: { selector: {
number: { number: {
step: "any",
mode: "box", mode: "box",
unit_of_measurement: stateObj?.attributes.unit_of_measurement, unit_of_measurement: stateObj?.attributes.unit_of_measurement,
}, },

View File

@ -186,11 +186,15 @@ export class HuiTileCardEditor
multiple: true, multiple: true,
options: [ options: [
{ {
label: "State", label: localize(
`ui.panel.lovelace.editor.card.tile.state_content_options.state`
),
value: "state", value: "state",
}, },
{ {
label: "Last changed", label: localize(
`ui.panel.lovelace.editor.card.tile.state_content_options.last-changed`
),
value: "last-changed", value: "last-changed",
}, },
...Object.keys(stateObj?.attributes ?? {}) ...Object.keys(stateObj?.attributes ?? {})

View File

@ -15,8 +15,8 @@ import {
LovelaceConfig, LovelaceConfig,
saveConfig, saveConfig,
subscribeLovelaceUpdates, subscribeLovelaceUpdates,
WindowWithLovelaceProm,
} from "../../data/lovelace"; } from "../../data/lovelace";
import { WindowWithPreloads } from "../../data/preloads";
import "../../layouts/hass-error-screen"; import "../../layouts/hass-error-screen";
import "../../layouts/hass-loading-screen"; import "../../layouts/hass-loading-screen";
import { HomeAssistant, PanelInfo, Route } from "../../types"; import { HomeAssistant, PanelInfo, Route } from "../../types";
@ -220,18 +220,18 @@ export class LovelacePanel extends LitElement {
let rawConf: LovelaceConfig | undefined; let rawConf: LovelaceConfig | undefined;
let confMode: Lovelace["mode"] = this.panel!.config.mode; let confMode: Lovelace["mode"] = this.panel!.config.mode;
let confProm: Promise<LovelaceConfig> | undefined; let confProm: Promise<LovelaceConfig> | undefined;
const llWindow = window as WindowWithLovelaceProm; const preloadWindow = window as WindowWithPreloads;
// On first load, we speed up loading page by having LL promise ready // On first load, we speed up loading page by having LL promise ready
if (llWindow.llConfProm) { if (preloadWindow.llConfProm) {
confProm = llWindow.llConfProm; confProm = preloadWindow.llConfProm;
llWindow.llConfProm = undefined; preloadWindow.llConfProm = undefined;
} }
if (!resourcesLoaded) { if (!resourcesLoaded) {
resourcesLoaded = true; resourcesLoaded = true;
const resources = await (llWindow.llResProm || (preloadWindow.llResProm || fetchResources(this.hass!.connection)).then(
fetchResources(this.hass!.connection)); (resources) => loadLovelaceResources(resources, this.hass!)
loadLovelaceResources(resources, this.hass!); );
} }
if (this.urlPath !== null || !confProm) { if (this.urlPath !== null || !confProm) {

View File

@ -56,6 +56,7 @@ import {
BrowserMediaPlayer, BrowserMediaPlayer,
ERR_UNSUPPORTED_MEDIA, ERR_UNSUPPORTED_MEDIA,
} from "./browser-media-player"; } from "./browser-media-player";
import { debounce } from "../../common/util/debounce";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -118,8 +119,14 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
public showResolvingNewMediaPicked() { public showResolvingNewMediaPicked() {
this._tearDownBrowserPlayer(); this._tearDownBrowserPlayer();
this._newMediaExpected = true; this._newMediaExpected = true;
// Sometimes the state does not update when playing media, like with TTS, so we wait max 2 secs and then stop waiting
this._debouncedResetMediaExpected();
} }
private _debouncedResetMediaExpected = debounce(() => {
this._newMediaExpected = false;
}, 2000);
public hideResolvingNewMediaPicked() { public hideResolvingNewMediaPicked() {
this._newMediaExpected = false; this._newMediaExpected = false;
} }
@ -154,7 +161,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
protected render() { protected render() {
if (this._newMediaExpected) { if (this._newMediaExpected) {
return html` return html`
<div class="controls-progress"> <div class="controls-progress buffering">
${until( ${until(
// Only show spinner after 500ms // Only show spinner after 500ms
new Promise((resolve) => { new Promise((resolve) => {
@ -240,9 +247,13 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
</span> </span>
</div> </div>
</div> </div>
<div class="controls-progress"> <div
class="controls-progress ${stateObj.state === "buffering"
? "buffering"
: ""}"
>
${stateObj.state === "buffering" ${stateObj.state === "buffering"
? html` <ha-circular-progress active></ha-circular-progress> ` ? html`<ha-circular-progress active></ha-circular-progress>`
: html` : html`
<div class="controls"> <div class="controls">
${controls === undefined ${controls === undefined
@ -541,7 +552,8 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
return css` return css`
:host { :host {
display: flex; display: flex;
min-height: 100px; height: 100px;
box-sizing: border-box;
background: var( background: var(
--ha-card-background, --ha-card-background,
var(--card-background-color, white) var(--card-background-color, white)
@ -627,12 +639,11 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
} }
img { img {
max-height: 100px; height: 100%;
max-width: 100px;
} }
.app img { .app img {
max-height: 68px; height: 68px;
margin: 16px 0 16px 16px; margin: 16px 0 16px 16px;
} }
@ -641,8 +652,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
} }
:host([narrow]) { :host([narrow]) {
min-height: 56px; height: 57px;
max-height: 56px;
} }
:host([narrow]) .controls-progress { :host([narrow]) .controls-progress {
@ -650,6 +660,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
min-width: 48px; min-width: 48px;
} }
:host([narrow]) .controls-progress.buffering {
flex: 1;
}
:host([narrow]) .media-info { :host([narrow]) .media-info {
padding-left: 8px; padding-left: 8px;
} }
@ -672,16 +686,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
justify-content: flex-end; justify-content: flex-end;
} }
:host([narrow]) img {
max-height: 56px;
max-width: 56px;
}
:host([narrow]) .blank-image {
height: 56px;
width: 56px;
}
:host([narrow]) mwc-linear-progress { :host([narrow]) mwc-linear-progress {
padding: 0; padding: 0;
position: absolute; position: absolute;

View File

@ -286,7 +286,7 @@ class PanelMediaBrowser extends LitElement {
} }
:host([narrow]) ha-media-player-browse { :host([narrow]) ha-media-player-browse {
height: calc(100vh - (80px + var(--header-height))); height: calc(100vh - (57px + var(--header-height)));
} }
ha-bar-media-player { ha-bar-media-player {

View File

@ -2,7 +2,9 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
import "@material/mwc-list"; import "@material/mwc-list";
import { import {
mdiChevronDown, mdiChevronDown,
mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiInformationOutline,
mdiMicrophone, mdiMicrophone,
mdiPlus, mdiPlus,
} from "@mdi/js"; } from "@mdi/js";
@ -19,6 +21,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { storage } from "../../common/decorators/storage"; import { storage } from "../../common/decorators/storage";
import { fireEvent } from "../../common/dom/fire_event";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import "../../components/ha-button"; import "../../components/ha-button";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
@ -27,15 +30,21 @@ import "../../components/ha-menu-button";
import "../../components/ha-state-icon"; import "../../components/ha-state-icon";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import "../../components/ha-two-pane-top-app-bar-fixed"; import "../../components/ha-two-pane-top-app-bar-fixed";
import { deleteConfigEntry } from "../../data/config_entries";
import { getExtendedEntityRegistryEntry } from "../../data/entity_registry";
import { fetchIntegrationManifest } from "../../data/integration";
import { getTodoLists } from "../../data/todo"; import { getTodoLists } from "../../data/todo";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../dialogs/generic/show-dialog-box";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { HuiErrorCard } from "../lovelace/cards/hui-error-card"; import { HuiErrorCard } from "../lovelace/cards/hui-error-card";
import { createCardElement } from "../lovelace/create-element/create-card-element"; import { createCardElement } from "../lovelace/create-element/create-card-element";
import { LovelaceCard } from "../lovelace/types"; import { LovelaceCard } from "../lovelace/types";
import { fetchIntegrationManifest } from "../../data/integration";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
@customElement("ha-panel-todo") @customElement("ha-panel-todo")
class PanelTodo extends LitElement { class PanelTodo extends LitElement {
@ -92,6 +101,10 @@ class PanelTodo extends LitElement {
protected willUpdate(changedProperties: PropertyValues): void { protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties); super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
if (!this.hasUpdated && !this._entityId) { if (!this.hasUpdated && !this._entityId) {
this._entityId = getTodoLists(this.hass)[0]?.entity_id; this._entityId = getTodoLists(this.hass)[0]?.entity_id;
} else if (!this.hasUpdated) { } else if (!this.hasUpdated) {
@ -124,6 +137,9 @@ class PanelTodo extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const entityRegistryEntry = this._entityId
? this.hass.entities[this._entityId]
: undefined;
const showPane = this._showPaneController.value ?? !this.narrow; const showPane = this._showPaneController.value ?? !this.narrow;
const listItems = getTodoLists(this.hass).map( const listItems = getTodoLists(this.hass).map(
(list) => (list) =>
@ -158,7 +174,9 @@ class PanelTodo extends LitElement {
> >
<ha-button slot="trigger"> <ha-button slot="trigger">
${this._entityId ${this._entityId
? computeStateName(this.hass.states[this._entityId]) ? this._entityId in this.hass.states
? computeStateName(this.hass.states[this._entityId])
: this._entityId
: ""} : ""}
<ha-svg-icon <ha-svg-icon
slot="trailingIcon" slot="trailingIcon"
@ -188,13 +206,36 @@ class PanelTodo extends LitElement {
${this._conversation(this.hass.config.components) ${this._conversation(this.hass.config.components)
? html`<ha-list-item ? html`<ha-list-item
graphic="icon" graphic="icon"
@click=${this._showVoiceCommandDialog} @click=${this._showMoreInfoDialog}
.disabled=${!this._entityId}
> >
<ha-svg-icon .path=${mdiMicrophone} slot="graphic"> <ha-svg-icon .path=${mdiInformationOutline} slot="graphic">
</ha-svg-icon> </ha-svg-icon>
${this.hass.localize("ui.panel.todo.start_conversation")} ${this.hass.localize("ui.panel.todo.information")}
</ha-list-item>` </ha-list-item>`
: nothing} : nothing}
<li divider role="separator"></li>
<ha-list-item graphic="icon" @click=${this._showVoiceCommandDialog}>
<ha-svg-icon .path=${mdiMicrophone} slot="graphic"> </ha-svg-icon>
${this.hass.localize("ui.panel.todo.start_conversation")}
</ha-list-item>
${entityRegistryEntry?.platform === "local_todo"
? html` <li divider role="separator"></li>
<ha-list-item
graphic="icon"
@click=${this._deleteList}
class="warning"
.disabled=${!this._entityId}
>
<ha-svg-icon
.path=${mdiDelete}
slot="graphic"
class="warning"
>
</ha-svg-icon>
${this.hass.localize("ui.panel.todo.delete_list")}
</ha-list-item>`
: nothing}
</ha-button-menu> </ha-button-menu>
<div id="columns"> <div id="columns">
<div class="column">${this._card}</div> <div class="column">${this._card}</div>
@ -215,6 +256,60 @@ class PanelTodo extends LitElement {
}); });
} }
private _showMoreInfoDialog(): void {
if (!this._entityId) {
return;
}
fireEvent(this, "hass-more-info", { entityId: this._entityId });
}
private async _deleteList(): Promise<void> {
if (!this._entityId) {
return;
}
const entityRegistryEntry = await getExtendedEntityRegistryEntry(
this.hass,
this._entityId
);
if (entityRegistryEntry.platform !== "local_todo") {
return;
}
const entryId = entityRegistryEntry.config_entry_id;
if (!entryId) {
return;
}
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.todo.delete_confirm_title", {
name:
this._entityId in this.hass.states
? computeStateName(this.hass.states[this._entityId])
: this._entityId,
}),
text: this.hass.localize("ui.panel.todo.delete_confirm_text"),
confirmText: this.hass!.localize("ui.common.delete"),
dismissText: this.hass!.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {
return;
}
const result = await deleteConfigEntry(this.hass, entryId);
this._entityId = getTodoLists(this.hass)[0]?.entity_id;
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize("ui.panel.todo.restart_confirm"),
});
}
}
private _showVoiceCommandDialog(): void { private _showVoiceCommandDialog(): void {
showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" }); showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" });
} }

View File

@ -5114,6 +5114,10 @@
"vertical": "Vertical", "vertical": "Vertical",
"hide_state": "Hide state", "hide_state": "Hide state",
"state_content": "State content", "state_content": "State content",
"state_content_options": {
"state": "State",
"last-changed": "Last changed"
},
"features": { "features": {
"name": "Features", "name": "Features",
"not_compatible": "Not compatible", "not_compatible": "Not compatible",
@ -5512,7 +5516,12 @@
}, },
"todo": { "todo": {
"start_conversation": "Start conversation", "start_conversation": "Start conversation",
"create_list": "Create list" "create_list": "Create list",
"delete_list": "Delete list",
"information": "Information",
"delete_confirm_title": "Remove {name}?",
"delete_confirm_text": "Are you sure you want to remove this list and all of its items?",
"restart_confirm": "Restart Home Assistant to finish removing this to-do list"
}, },
"page-authorize": { "page-authorize": {
"initializing": "Initializing", "initializing": "Initializing",