mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-13 15:09:41 +00:00
Compare commits
39 Commits
sidebar-re
...
recorded_e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aacffb18ea | ||
![]() |
6dbc2ef3ff | ||
![]() |
90fbbe8e9e | ||
![]() |
f4b1de2af5 | ||
![]() |
e70af1b2f8 | ||
![]() |
88d710496f | ||
![]() |
0f9109a1b7 | ||
![]() |
8c49be013c | ||
![]() |
c3f11784dc | ||
![]() |
9a5767c61d | ||
![]() |
7044d57cae | ||
![]() |
21c003d7f8 | ||
![]() |
73908ea650 | ||
![]() |
f7e4261dc9 | ||
![]() |
16272bc4ae | ||
![]() |
7989db8f54 | ||
![]() |
911720a983 | ||
![]() |
c87f50e336 | ||
![]() |
a0778e3407 | ||
![]() |
e46e502db3 | ||
![]() |
1e573bb358 | ||
![]() |
282b9596ea | ||
![]() |
0e33349d7d | ||
![]() |
dd4111b570 | ||
![]() |
d1e515a5d1 | ||
![]() |
38ee5f2c81 | ||
![]() |
7afaa1bd1b | ||
![]() |
37f4d7ac98 | ||
![]() |
a438f5f0f9 | ||
![]() |
0be739e7d9 | ||
![]() |
bb7c9ced96 | ||
![]() |
d429997938 | ||
![]() |
3ca9e3cc54 | ||
![]() |
ab360614e8 | ||
![]() |
bd9fa9a7c6 | ||
![]() |
fcdea9abee | ||
![]() |
101076cc7c | ||
![]() |
c4e64c3a2e | ||
![]() |
3f4d735d42 |
@@ -1,62 +1,262 @@
|
||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||
const ANIMATION_DURATION_MS = 300;
|
||||
|
||||
/**
|
||||
* A bottom sheet component that slides up from the bottom of the screen.
|
||||
*
|
||||
* The bottom sheet provides a draggable interface that allows users to resize
|
||||
* the sheet by dragging the handle at the top. It supports both mouse and touch
|
||||
* interactions and automatically closes when dragged below a 20% of screen height.
|
||||
*
|
||||
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
|
||||
*
|
||||
* @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
|
||||
*/
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends LitElement {
|
||||
@property({ type: Boolean }) public open = false;
|
||||
@query("dialog") private _dialog!: HTMLDialogElement;
|
||||
|
||||
@state() private _drawerOpen = false;
|
||||
private _dragging = false;
|
||||
|
||||
private _handleAfterHide() {
|
||||
this.open = false;
|
||||
const ev = new Event("closed", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
this.dispatchEvent(ev);
|
||||
private _dragStartY = 0;
|
||||
|
||||
private _initialSize = 0;
|
||||
|
||||
@state() private _dialogMaxViewpointHeight = 70;
|
||||
|
||||
@state() private _dialogMinViewpointHeight = 55;
|
||||
|
||||
@state() private _dialogViewportHeight?: number;
|
||||
|
||||
render() {
|
||||
return html`<dialog
|
||||
open
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
style=${styleMap({
|
||||
height: this._dialogViewportHeight
|
||||
? `${this._dialogViewportHeight}vh`
|
||||
: "auto",
|
||||
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
|
||||
minHeight: `${this._dialogMinViewpointHeight}vh`,
|
||||
})}
|
||||
>
|
||||
<div class="handle-wrapper">
|
||||
<div
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleTouchStart}
|
||||
class="handle"
|
||||
></div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</dialog>`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("open")) {
|
||||
this._drawerOpen = this.open;
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._openSheet();
|
||||
}
|
||||
|
||||
private _openSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
// trigger opening animation
|
||||
this._dialog.classList.add("show");
|
||||
});
|
||||
}
|
||||
|
||||
public closeSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
this._dialog.classList.remove("show");
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTransitionEnd() {
|
||||
if (this._dialog.classList.contains("show")) {
|
||||
// after show animation is done
|
||||
// - set the height to the natural height, to prevent content shift when switch content
|
||||
// - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
|
||||
this._dialogViewportHeight =
|
||||
(this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
this._dialogMaxViewpointHeight = 90;
|
||||
this._dialogMinViewpointHeight = 20;
|
||||
} else {
|
||||
// after close animation is done close dialog element and fire closed event
|
||||
this._dialog.close();
|
||||
fireEvent(this, "bottom-sheet-closed");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<wa-drawer
|
||||
placement="bottom"
|
||||
.open=${this._drawerOpen}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
without-header
|
||||
>
|
||||
<slot></slot>
|
||||
</wa-drawer>
|
||||
`;
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// register event listeners for drag handling
|
||||
document.addEventListener("mousemove", this._handleMouseMove);
|
||||
document.addEventListener("mouseup", this._handleMouseUp);
|
||||
document.addEventListener("touchmove", this._handleTouchMove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("touchend", this._handleTouchEnd);
|
||||
document.addEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// unregister event listeners for drag handling
|
||||
document.removeEventListener("mousemove", this._handleMouseMove);
|
||||
document.removeEventListener("mouseup", this._handleMouseUp);
|
||||
document.removeEventListener("touchmove", this._handleTouchMove);
|
||||
document.removeEventListener("touchend", this._handleTouchEnd);
|
||||
document.removeEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
private _handleMouseDown = (ev: MouseEvent) => {
|
||||
this._startDrag(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
// Prevent the browser from interpreting this as a scroll/PTR gesture.
|
||||
ev.preventDefault();
|
||||
this._startDrag(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _startDrag(clientY: number) {
|
||||
this._dragging = true;
|
||||
this._dragStartY = clientY;
|
||||
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
document.body.style.setProperty("cursor", "grabbing");
|
||||
}
|
||||
|
||||
private _handleMouseMove = (ev: MouseEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._updateSize(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchMove = (ev: TouchEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault(); // Prevent scrolling
|
||||
this._updateSize(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _updateSize(clientY: number) {
|
||||
const deltaY = this._dragStartY - clientY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const deltaVh = (deltaY / viewportHeight) * 100;
|
||||
|
||||
// Calculate new size and clamp between 10vh and 90vh
|
||||
let newSize = this._initialSize + deltaVh;
|
||||
newSize = Math.max(10, Math.min(90, newSize));
|
||||
|
||||
// on drag down and below 20vh
|
||||
if (newSize < 20 && deltaY < 0) {
|
||||
this._endDrag();
|
||||
this.closeSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
this._dialogViewportHeight = newSize;
|
||||
}
|
||||
|
||||
private _handleMouseUp = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _endDrag() {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._dragging = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
wa-drawer {
|
||||
--wa-color-surface-raised: var(
|
||||
.handle-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.handle-wrapper .handle {
|
||||
height: 20px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 7;
|
||||
padding-bottom: 76px;
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: 8px;
|
||||
height: 4px;
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
width: 80px;
|
||||
}
|
||||
.handle-wrapper .handle:active::after {
|
||||
cursor: grabbing;
|
||||
}
|
||||
dialog {
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
min-height: 30vh;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
--spacing: 0;
|
||||
--size: auto;
|
||||
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
|
||||
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
position: fixed;
|
||||
width: calc(100% - 4px);
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
top: auto;
|
||||
inset-inline-end: auto;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
border-top-left-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
border-top-right-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
transform: translateY(100%);
|
||||
transition: transform ${ANIMATION_DURATION_MS}ms ease;
|
||||
border-top-width: var(--ha-bottom-sheet-border-width);
|
||||
border-right-width: var(--ha-bottom-sheet-border-width);
|
||||
border-left-width: var(--ha-bottom-sheet-border-width);
|
||||
border-bottom-width: 0;
|
||||
border-style: var(--ha-bottom-sheet-border-style);
|
||||
border-color: var(--ha-bottom-sheet-border-color);
|
||||
}
|
||||
wa-drawer::part(dialog) {
|
||||
border-top-left-radius: var(--ha-border-radius-lg);
|
||||
border-top-right-radius: var(--ha-border-radius-lg);
|
||||
max-height: 90vh;
|
||||
}
|
||||
wa-drawer::part(body) {
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
|
||||
dialog.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -65,4 +265,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bottom-sheet": HaBottomSheet;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"bottom-sheet-closed": undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -1,271 +0,0 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { BOTTOM_SHEET_ANIMATION_DURATION_MS } from "./ha-bottom-sheet";
|
||||
|
||||
/**
|
||||
* A bottom sheet component that slides up from the bottom of the screen.
|
||||
*
|
||||
* The bottom sheet provides a draggable interface that allows users to resize
|
||||
* the sheet by dragging the handle at the top. It supports both mouse and touch
|
||||
* interactions and automatically closes when dragged below a 20% of screen height.
|
||||
*
|
||||
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
|
||||
*
|
||||
* @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
|
||||
*/
|
||||
@customElement("ha-resizable-bottom-sheet")
|
||||
export class HaResizableBottomSheet extends LitElement {
|
||||
@query("dialog") private _dialog!: HTMLDialogElement;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragStartY = 0;
|
||||
|
||||
private _initialSize = 0;
|
||||
|
||||
@state() private _dialogMaxViewpointHeight = 70;
|
||||
|
||||
@state() private _dialogMinViewpointHeight = 55;
|
||||
|
||||
@state() private _dialogViewportHeight?: number;
|
||||
|
||||
render() {
|
||||
return html`<dialog
|
||||
open
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
style=${styleMap({
|
||||
height: this._dialogViewportHeight
|
||||
? `${this._dialogViewportHeight}vh`
|
||||
: "auto",
|
||||
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
|
||||
minHeight: `${this._dialogMinViewpointHeight}vh`,
|
||||
})}
|
||||
>
|
||||
<div class="handle-wrapper">
|
||||
<div
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleTouchStart}
|
||||
class="handle"
|
||||
></div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</dialog>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._openSheet();
|
||||
}
|
||||
|
||||
private _openSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
// trigger opening animation
|
||||
this._dialog.classList.add("show");
|
||||
});
|
||||
}
|
||||
|
||||
public closeSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
this._dialog.classList.remove("show");
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTransitionEnd() {
|
||||
if (this._dialog.classList.contains("show")) {
|
||||
// after show animation is done
|
||||
// - set the height to the natural height, to prevent content shift when switch content
|
||||
// - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
|
||||
this._dialogViewportHeight =
|
||||
(this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
this._dialogMaxViewpointHeight = 90;
|
||||
this._dialogMinViewpointHeight = 20;
|
||||
} else {
|
||||
// after close animation is done close dialog element and fire closed event
|
||||
this._dialog.close();
|
||||
fireEvent(this, "bottom-sheet-closed");
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// register event listeners for drag handling
|
||||
document.addEventListener("mousemove", this._handleMouseMove);
|
||||
document.addEventListener("mouseup", this._handleMouseUp);
|
||||
document.addEventListener("touchmove", this._handleTouchMove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("touchend", this._handleTouchEnd);
|
||||
document.addEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// unregister event listeners for drag handling
|
||||
document.removeEventListener("mousemove", this._handleMouseMove);
|
||||
document.removeEventListener("mouseup", this._handleMouseUp);
|
||||
document.removeEventListener("touchmove", this._handleTouchMove);
|
||||
document.removeEventListener("touchend", this._handleTouchEnd);
|
||||
document.removeEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
private _handleMouseDown = (ev: MouseEvent) => {
|
||||
this._startDrag(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
// Prevent the browser from interpreting this as a scroll/PTR gesture.
|
||||
ev.preventDefault();
|
||||
this._startDrag(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _startDrag(clientY: number) {
|
||||
this._dragging = true;
|
||||
this._dragStartY = clientY;
|
||||
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
document.body.style.setProperty("cursor", "grabbing");
|
||||
}
|
||||
|
||||
private _handleMouseMove = (ev: MouseEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._updateSize(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchMove = (ev: TouchEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault(); // Prevent scrolling
|
||||
this._updateSize(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _updateSize(clientY: number) {
|
||||
const deltaY = this._dragStartY - clientY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const deltaVh = (deltaY / viewportHeight) * 100;
|
||||
|
||||
// Calculate new size and clamp between 10vh and 90vh
|
||||
let newSize = this._initialSize + deltaVh;
|
||||
newSize = Math.max(10, Math.min(90, newSize));
|
||||
|
||||
// on drag down and below 20vh
|
||||
if (newSize < 20 && deltaY < 0) {
|
||||
this._endDrag();
|
||||
this.closeSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
this._dialogViewportHeight = newSize;
|
||||
}
|
||||
|
||||
private _handleMouseUp = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _endDrag() {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._dragging = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.handle-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.handle-wrapper .handle {
|
||||
height: 20px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 7;
|
||||
padding-bottom: 76px;
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: 8px;
|
||||
height: 4px;
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
width: 80px;
|
||||
}
|
||||
.handle-wrapper .handle:active::after {
|
||||
cursor: grabbing;
|
||||
}
|
||||
dialog {
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
min-height: 30vh;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
position: fixed;
|
||||
width: calc(100% - 4px);
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
top: auto;
|
||||
inset-inline-end: auto;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
border-top-left-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
border-top-right-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
transform: translateY(100%);
|
||||
transition: transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease;
|
||||
border-top-width: var(--ha-bottom-sheet-border-width);
|
||||
border-right-width: var(--ha-bottom-sheet-border-width);
|
||||
border-left-width: var(--ha-bottom-sheet-border-width);
|
||||
border-bottom-width: 0;
|
||||
border-style: var(--ha-bottom-sheet-border-style);
|
||||
border-color: var(--ha-bottom-sheet-border-color);
|
||||
}
|
||||
|
||||
dialog.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-resizable-bottom-sheet": HaResizableBottomSheet;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"bottom-sheet-closed": undefined;
|
||||
}
|
||||
}
|
@@ -116,6 +116,10 @@ export interface SwitchAsXEntityOptions {
|
||||
invert: boolean;
|
||||
}
|
||||
|
||||
export interface RecorderEntityOptions {
|
||||
recording_disabled_by?: string | null;
|
||||
}
|
||||
|
||||
export interface EntityRegistryOptions {
|
||||
number?: NumberEntityOptions;
|
||||
sensor?: SensorEntityOptions;
|
||||
@@ -124,6 +128,7 @@ export interface EntityRegistryOptions {
|
||||
weather?: WeatherEntityOptions;
|
||||
light?: LightEntityOptions;
|
||||
switch_as_x?: SwitchAsXEntityOptions;
|
||||
recorder?: RecorderEntityOptions;
|
||||
conversation?: Record<string, unknown>;
|
||||
"cloud.alexa"?: Record<string, unknown>;
|
||||
"cloud.google_assistant"?: Record<string, unknown>;
|
||||
|
@@ -365,3 +365,36 @@ export const isExternalStatistic = (statisticsId: string): boolean =>
|
||||
|
||||
export const updateStatisticsIssues = (hass: HomeAssistant) =>
|
||||
hass.callWS<undefined>({ type: "recorder/update_statistics_issues" });
|
||||
|
||||
export interface EntityRecordingSettings {
|
||||
recording_disabled_by: string | null;
|
||||
}
|
||||
|
||||
export type EntityRecordingList = Record<string, EntityRecordingSettings>;
|
||||
|
||||
export const getEntityRecordingList = (hass: HomeAssistant) =>
|
||||
hass
|
||||
.callWS<{ recorded_entities: EntityRecordingList }>({
|
||||
type: "homeassistant/record_entity/list",
|
||||
})
|
||||
.then((response) => response.recorded_entities);
|
||||
|
||||
export const getEntityRecordingSettings = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string
|
||||
) =>
|
||||
hass.callWS<EntityRecordingSettings>({
|
||||
type: "homeassistant/record_entity/get",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const setEntityRecordingOptions = (
|
||||
hass: HomeAssistant,
|
||||
entity_ids: string[],
|
||||
recording_disabled_by: string | null
|
||||
) =>
|
||||
hass.callWS<undefined>({
|
||||
type: "homeassistant/record_entity/set_options",
|
||||
entity_ids,
|
||||
recording_disabled_by,
|
||||
});
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-bottom-sheet";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-md-list";
|
||||
@@ -41,54 +40,6 @@ export class ListItemsDialog
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const content = html`
|
||||
<div class="container">
|
||||
<ha-md-list>
|
||||
${this._params.items.map(
|
||||
(item) => html`
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
@click=${this._itemClicked}
|
||||
.item=${item}
|
||||
>
|
||||
${item.iconPath
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="start"
|
||||
class="item-icon"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: item.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
icon=${item.icon}
|
||||
slot="start"
|
||||
class="item-icon"
|
||||
></ha-icon>
|
||||
`
|
||||
: nothing}
|
||||
<span class="headline">${item.label}</span>
|
||||
${item.description
|
||||
? html`
|
||||
<span class="supporting-text">${item.description}</span>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (this._params.mode === "bottom-sheet") {
|
||||
return html`
|
||||
<ha-bottom-sheet placement="bottom" open @closed=${this._dialogClosed}>
|
||||
${content}
|
||||
</ha-bottom-sheet>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -96,7 +47,43 @@ export class ListItemsDialog
|
||||
@closed=${this._dialogClosed}
|
||||
hideActions
|
||||
>
|
||||
${content}
|
||||
<div class="container">
|
||||
<ha-md-list>
|
||||
${this._params.items.map(
|
||||
(item) => html`
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
@click=${this._itemClicked}
|
||||
.item=${item}
|
||||
>
|
||||
${item.iconPath
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="start"
|
||||
class="item-icon"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: item.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
icon=${item.icon}
|
||||
slot="start"
|
||||
class="item-icon"
|
||||
></ha-icon>
|
||||
`
|
||||
: nothing}
|
||||
<span class="headline">${item.label}</span>
|
||||
${item.description
|
||||
? html`
|
||||
<span class="supporting-text">${item.description}</span>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@ interface ListItem {
|
||||
export interface ListItemsDialogParams {
|
||||
title?: string;
|
||||
items: ListItem[];
|
||||
mode?: "dialog" | "bottom-sheet";
|
||||
}
|
||||
|
||||
export const showListItemsDialog = (
|
||||
|
@@ -9,6 +9,7 @@ import type {
|
||||
} from "../../data/entity_registry";
|
||||
import { PLATFORMS_WITH_SETTINGS_TAB } from "../../panels/config/entities/const";
|
||||
import "../../panels/config/entities/entity-registry-settings";
|
||||
import "../../panels/config/entities/entity-settings-without-unique-id";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
|
||||
@@ -28,7 +29,7 @@ export class HaMoreInfoSettings extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// No unique ID
|
||||
// No unique ID - show limited settings
|
||||
if (this.entry === null) {
|
||||
return html`
|
||||
<div class="content">
|
||||
@@ -43,6 +44,10 @@ export class HaMoreInfoSettings extends LitElement {
|
||||
>`,
|
||||
})}
|
||||
</ha-alert>
|
||||
<entity-settings-without-unique-id
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
></entity-settings-without-unique-id>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-resizable-bottom-sheet";
|
||||
import type { HaResizableBottomSheet } from "../../../components/ha-resizable-bottom-sheet";
|
||||
import "../../../components/ha-bottom-sheet";
|
||||
import type { HaBottomSheet } from "../../../components/ha-bottom-sheet";
|
||||
import {
|
||||
isCondition,
|
||||
isScriptField,
|
||||
@@ -38,38 +37,7 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@query("ha-resizable-bottom-sheet")
|
||||
private _bottomSheetElement?: HaResizableBottomSheet;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragStartX = 0;
|
||||
|
||||
private _initialSize = 0;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// register event listeners for drag handling
|
||||
document.addEventListener("mousemove", this._handleMouseMove);
|
||||
document.addEventListener("mouseup", this._handleMouseUp);
|
||||
document.addEventListener("touchmove", this._handleTouchMove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("touchend", this._handleTouchEnd);
|
||||
document.addEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// unregister event listeners for drag handling
|
||||
document.removeEventListener("mousemove", this._handleMouseMove);
|
||||
document.removeEventListener("mouseup", this._handleMouseUp);
|
||||
document.removeEventListener("touchmove", this._handleTouchMove);
|
||||
document.removeEventListener("touchend", this._handleTouchEnd);
|
||||
document.removeEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
@query("ha-bottom-sheet") private _bottomSheetElement?: HaBottomSheet;
|
||||
|
||||
private _renderContent() {
|
||||
// get config type
|
||||
@@ -179,84 +147,13 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
if (this.narrow) {
|
||||
return html`
|
||||
<ha-resizable-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
|
||||
<ha-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
|
||||
${this._renderContent()}
|
||||
</ha-resizable-bottom-sheet>
|
||||
</ha-bottom-sheet>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="handle"
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleTouchStart}
|
||||
></div>
|
||||
${this._renderContent()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleMouseDown = (ev: MouseEvent) => {
|
||||
// Prevent the browser from interpreting this as a scroll/PTR gesture.
|
||||
ev.preventDefault();
|
||||
this._startDrag(ev.clientX);
|
||||
};
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
// Prevent the browser from interpreting this as a scroll/PTR gesture.
|
||||
ev.preventDefault();
|
||||
this._startDrag(ev.touches[0].clientX);
|
||||
};
|
||||
|
||||
private _startDrag(clientX: number) {
|
||||
this._dragging = true;
|
||||
this._dragStartX = clientX;
|
||||
this._initialSize = (this.offsetWidth / window.innerWidth) * 100;
|
||||
document.body.style.setProperty("cursor", "grabbing");
|
||||
}
|
||||
|
||||
private _handleMouseMove = (ev: MouseEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._updateSize(ev.clientX);
|
||||
};
|
||||
|
||||
private _handleTouchMove = (ev: TouchEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault(); // Prevent scrolling
|
||||
this._updateSize(ev.touches[0].clientX);
|
||||
};
|
||||
|
||||
private _updateSize(clientX: number) {
|
||||
const deltaX = this._dragStartX - clientX;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const deltaVw = (deltaX / viewportWidth) * 100;
|
||||
|
||||
// Calculate new size and clamp between 30vh and 70vh
|
||||
let newSize = this._initialSize + deltaVw;
|
||||
newSize = Math.max(30, Math.min(70, newSize));
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
fireEvent(this, "sidebar-width-changed", { width: newSize });
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMouseUp = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _endDrag() {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._dragging = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
return this._renderContent();
|
||||
}
|
||||
|
||||
private _getType() {
|
||||
@@ -329,15 +226,6 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
left: -4;
|
||||
height: 100%;
|
||||
width: 8px;
|
||||
z-index: 7;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -351,8 +239,5 @@ declare global {
|
||||
"yaml-changed": {
|
||||
value: unknown;
|
||||
};
|
||||
"sidebar-width-changed": {
|
||||
width: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,6 @@ import {
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
@@ -102,13 +101,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@state() private _sidebarKey?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-sidebar-width-percentage",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _sidebarWidth? = 30;
|
||||
|
||||
@query("ha-automation-sidebar") private _sidebarElement?: HaAutomationSidebar;
|
||||
|
||||
@queryAll("ha-automation-action, ha-automation-condition")
|
||||
@@ -314,7 +306,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
.disabled=${this.disabled}
|
||||
.sidebarKey=${this._sidebarKey}
|
||||
@sidebar-width-changed=${this._resizeSidebar}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,11 +314,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this.style.setProperty(
|
||||
"--sidebar-dynamic-width",
|
||||
`${this._sidebarWidth}vw`
|
||||
);
|
||||
|
||||
const expanded = extractSearchParam("expanded");
|
||||
if (expanded === "1") {
|
||||
this._clearParam("expanded");
|
||||
@@ -656,14 +642,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _resizeSidebar(ev) {
|
||||
ev.stopPropagation();
|
||||
const width = ev.detail.width as number;
|
||||
|
||||
this.style.setProperty("--sidebar-dynamic-width", `${width}vw`);
|
||||
this._sidebarWidth = width;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
saveFabStyles,
|
||||
|
@@ -109,8 +109,7 @@ export const manualEditorStyles = css`
|
||||
}
|
||||
|
||||
.has-sidebar {
|
||||
--sidebar-width: min(var(--sidebar-dynamic-width), 1078px);
|
||||
/* 1540 * 0.7 = 1078px */
|
||||
--sidebar-width: min(35vw, 500px);
|
||||
--sidebar-gap: 16px;
|
||||
}
|
||||
|
||||
|
@@ -65,6 +65,7 @@ import {
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { entityIcon, entryIcon } from "../../../data/icons";
|
||||
import { handleRecordingChange } from "./recorder-util";
|
||||
import {
|
||||
domainToName,
|
||||
fetchIntegrationManifest,
|
||||
@@ -197,6 +198,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
@state() private _noDeviceArea?: boolean;
|
||||
|
||||
@state() private _recordingDisabled?: boolean;
|
||||
|
||||
private _origEntityId!: string;
|
||||
|
||||
private _deviceClassOptions?: string[][];
|
||||
@@ -227,6 +230,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
|
||||
// Fetch recording settings
|
||||
this._fetchRecordingSettings();
|
||||
|
||||
if (domain === "camera" && isComponentLoaded(this.hass, "stream")) {
|
||||
const stateObj: HassEntity | undefined =
|
||||
this.hass.states[this.entry.entity_id];
|
||||
@@ -972,6 +978,27 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
|
||||
${isComponentLoaded(this.hass, "recorder")
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.record_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.record_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${!this._recordingDisabled}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._recordingChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
${this.entry.device_id
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading"
|
||||
@@ -1399,6 +1426,15 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._labels = ev.detail.value;
|
||||
}
|
||||
|
||||
private _fetchRecordingSettings() {
|
||||
if (!isComponentLoaded(this.hass, "recorder")) {
|
||||
return;
|
||||
}
|
||||
// Get recording settings from entity registry entry options
|
||||
const recorderOptions = this.entry.options?.recorder;
|
||||
this._recordingDisabled = recorderOptions?.recording_disabled_by !== null;
|
||||
}
|
||||
|
||||
private async _fetchCameraPrefs() {
|
||||
const capabilities = await fetchCameraCapabilities(
|
||||
this.hass,
|
||||
@@ -1462,6 +1498,19 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _recordingChanged(ev: CustomEvent): Promise<void> {
|
||||
const checkbox = ev.currentTarget as HaSwitch;
|
||||
|
||||
await handleRecordingChange({
|
||||
hass: this.hass,
|
||||
entityId: this.entry.entity_id,
|
||||
checkbox,
|
||||
onSuccess: (recordingDisabled) => {
|
||||
this._recordingDisabled = recordingDisabled;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _openDeviceSettings() {
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device: this._device!,
|
||||
|
126
src/panels/config/entities/entity-settings-without-unique-id.ts
Normal file
126
src/panels/config/entities/entity-settings-without-unique-id.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import { getEntityRecordingSettings } from "../../../data/recorder";
|
||||
import { handleRecordingChange } from "./recorder-util";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("entity-settings-without-unique-id")
|
||||
export class EntitySettingsWithoutUniqueId extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "entity-id" }) public entityId!: string;
|
||||
|
||||
@state() private _recordingDisabled?: boolean;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._fetchRecordingSettings();
|
||||
}
|
||||
|
||||
private async _fetchRecordingSettings() {
|
||||
if (!isComponentLoaded(this.hass, "recorder")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const settings = await getEntityRecordingSettings(
|
||||
this.hass,
|
||||
this.entityId
|
||||
);
|
||||
this._recordingDisabled = settings?.recording_disabled_by !== null;
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching recording settings:", err);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
const name = stateObj?.attributes?.friendly_name || this.entityId;
|
||||
|
||||
return html`
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize("ui.dialogs.entity_registry.editor.name")}
|
||||
.value=${name}
|
||||
disabled
|
||||
readonly
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.entity_id"
|
||||
)}
|
||||
.value=${this.entityId}
|
||||
disabled
|
||||
readonly
|
||||
></ha-textfield>
|
||||
${isComponentLoaded(this.hass, "recorder")
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.record_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.record_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._recordingDisabled !== true}
|
||||
@change=${this._recordingChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private async _recordingChanged(ev: CustomEvent): Promise<void> {
|
||||
const checkbox = ev.currentTarget as HaSwitch;
|
||||
|
||||
await handleRecordingChange({
|
||||
hass: this.hass,
|
||||
entityId: this.entityId,
|
||||
checkbox,
|
||||
onSuccess: (recordingDisabled) => {
|
||||
this._recordingDisabled = recordingDisabled;
|
||||
// Fire event to notify entities table to refresh recording data
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("entity-recording-updated", {
|
||||
detail: { entityId: this.entityId },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"entity-settings-without-unique-id": EntitySettingsWithoutUniqueId;
|
||||
}
|
||||
}
|
@@ -23,6 +23,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoize from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
@@ -42,7 +43,10 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
@@ -97,6 +101,12 @@ import {
|
||||
subscribeLabelRegistry,
|
||||
} from "../../../data/label_registry";
|
||||
import { regenerateEntityIds } from "../../../data/regenerate_entity_ids";
|
||||
import {
|
||||
getEntityRecordingList,
|
||||
getEntityRecordingSettings,
|
||||
setEntityRecordingOptions,
|
||||
type EntityRecordingList,
|
||||
} from "../../../data/recorder";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -138,6 +148,7 @@ export interface EntityRow extends StateEntity {
|
||||
enabled: string;
|
||||
visible: string;
|
||||
available: string;
|
||||
recorded?: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-config-entities")
|
||||
@@ -193,6 +204,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _recordingEntities?: EntityRecordingList;
|
||||
|
||||
@storage({ key: "entities-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@@ -227,12 +240,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
window.addEventListener("popstate", this._popState);
|
||||
window.addEventListener(
|
||||
"entity-recording-updated",
|
||||
this._handleEntityRecordingUpdated
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("location-changed", this._locationChanged);
|
||||
window.removeEventListener("popstate", this._popState);
|
||||
window.removeEventListener(
|
||||
"entity-recording-updated",
|
||||
this._handleEntityRecordingUpdated
|
||||
);
|
||||
}
|
||||
|
||||
private _locationChanged = () => {
|
||||
@@ -249,6 +270,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
private _handleEntityRecordingUpdated = async (ev: Event) => {
|
||||
const customEvent = ev as CustomEvent<{ entityId: string }>;
|
||||
// Update recording data for the specific entity that changed
|
||||
if (this._activeHiddenColumns?.includes("recorded")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = await getEntityRecordingSettings(
|
||||
this.hass,
|
||||
customEvent.detail.entityId
|
||||
);
|
||||
// Update the recording data for this specific entity
|
||||
this._recordingEntities = {
|
||||
...this._recordingEntities,
|
||||
[customEvent.detail.entityId]: settings,
|
||||
};
|
||||
} catch (_err) {
|
||||
// Entity might not have recording settings yet, treat as enabled
|
||||
this._recordingEntities = {
|
||||
...this._recordingEntities,
|
||||
[customEvent.detail.entityId]: { recording_disabled_by: null },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
private _states = memoize((localize: LocalizeFunc) => [
|
||||
{
|
||||
value: "available",
|
||||
@@ -490,9 +537,75 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
template: (entry) =>
|
||||
entry.label_entries.map((lbl) => lbl.name).join(" "),
|
||||
},
|
||||
recorded: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.recorded"),
|
||||
type: "icon",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
defaultHidden: true,
|
||||
showNarrow: true,
|
||||
minWidth: "86px",
|
||||
maxWidth: "86px",
|
||||
template: (entry) =>
|
||||
entry.recorded === undefined
|
||||
? "—"
|
||||
: entry.recorded
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.id="recording-icon-${slugify(entry.entity_id)}"
|
||||
.path=${mdiToggleSwitch}
|
||||
style="color: var(--success-color)"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
.for="recording-icon-${slugify(entry.entity_id)}"
|
||||
placement="left"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.recorded.enabled"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.id="recording-icon-${slugify(entry.entity_id)}"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
style="color: var(--secondary-text-color)"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
.for="recording-icon-${slugify(entry.entity_id)}"
|
||||
placement="left"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.recorded.disabled"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
private _hasNonUniqueIdEntities = memoize(
|
||||
(selected: string[], filteredEntities: EntityRow[]) => {
|
||||
// Create a Set of readonly entity IDs for O(1) lookup
|
||||
const readonlyEntityIds = new Set(
|
||||
filteredEntities
|
||||
.filter((e) => e.readonly === true)
|
||||
.map((e) => e.entity_id)
|
||||
);
|
||||
return selected.some((entityId) => readonlyEntityIds.has(entityId));
|
||||
}
|
||||
);
|
||||
|
||||
private _filteredEntitiesAndDomains = memoize(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
@@ -503,7 +616,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
filters: DataTableFiltersValues,
|
||||
filteredItems: DataTableFiltersItems,
|
||||
entries?: ConfigEntry[],
|
||||
labelReg?: LabelRegistryEntry[]
|
||||
labelReg?: LabelRegistryEntry[],
|
||||
recordingEntities?: EntityRecordingList
|
||||
) => {
|
||||
const result: EntityRow[] = [];
|
||||
|
||||
@@ -694,6 +808,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
: deviceName
|
||||
: undefined;
|
||||
|
||||
// Determine recording status
|
||||
const recorded: boolean | undefined = recordingEntities
|
||||
? recordingEntities[entry.entity_id]
|
||||
? recordingEntities[entry.entity_id].recording_disabled_by === null
|
||||
: entry.options?.recorder
|
||||
? entry.options.recorder.recording_disabled_by === null
|
||||
: true
|
||||
: undefined;
|
||||
|
||||
result.push({
|
||||
...entry,
|
||||
entity,
|
||||
@@ -730,6 +853,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
visible: hidden
|
||||
? localize("ui.panel.config.entities.picker.status.hidden")
|
||||
: localize("ui.panel.config.entities.picker.status.visible"),
|
||||
recorded,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -760,7 +884,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
this._filters,
|
||||
this._filteredItems,
|
||||
this._entries,
|
||||
this._labels
|
||||
this._labels,
|
||||
this._recordingEntities
|
||||
);
|
||||
|
||||
const includeAddDeviceFab =
|
||||
@@ -769,6 +894,54 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
[...filteredDomains][0]
|
||||
);
|
||||
|
||||
// Check if any selected entities are without unique IDs (memoized for performance)
|
||||
const hasNonUniqueIdEntities = this._hasNonUniqueIdEntities(
|
||||
this._selected,
|
||||
filteredEntities
|
||||
);
|
||||
|
||||
// Helper to render menu items that can be disabled for non-unique ID entities
|
||||
const renderDisableableMenuItem = (
|
||||
action: () => void,
|
||||
icon: string,
|
||||
labelKey: LocalizeKeys,
|
||||
itemId: string,
|
||||
warning = false
|
||||
) => {
|
||||
if (hasNonUniqueIdEntities) {
|
||||
return html`
|
||||
<div
|
||||
id=${itemId}
|
||||
style="position: relative; cursor: not-allowed;"
|
||||
tabindex="0"
|
||||
>
|
||||
<ha-md-menu-item
|
||||
.disabled=${true}
|
||||
class=${warning ? "warning" : ""}
|
||||
style="pointer-events: none; opacity: 0.5;"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${icon}></ha-svg-icon>
|
||||
<div slot="headline">${this.hass.localize(labelKey)}</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-tooltip .for=${itemId} placement="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.non_unique_id_selected"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${action}
|
||||
class=${warning ? "warning" : ""}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${icon}></ha-svg-icon>
|
||||
<div slot="headline">${this.hass.localize(labelKey)}</div>
|
||||
</ha-md-menu-item>
|
||||
`;
|
||||
};
|
||||
|
||||
const labelItems = html` ${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
@@ -915,77 +1088,79 @@ ${
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._enableSelected}>
|
||||
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._disableSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
${renderDisableableMenuItem(
|
||||
this._enableSelected,
|
||||
mdiToggleSwitch,
|
||||
"ui.panel.config.entities.picker.enable_selected.button",
|
||||
"enable-selected-disabled"
|
||||
)}
|
||||
${renderDisableableMenuItem(
|
||||
this._disableSelected,
|
||||
mdiToggleSwitchOffOutline,
|
||||
"ui.panel.config.entities.picker.disable_selected.button",
|
||||
"disable-selected-disabled"
|
||||
)}
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._unhideSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiEye}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.unhide_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._hideSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiEyeOff}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
${renderDisableableMenuItem(
|
||||
this._unhideSelected,
|
||||
mdiEye,
|
||||
"ui.panel.config.entities.picker.unhide_selected.button",
|
||||
"unhide-selected-disabled"
|
||||
)}
|
||||
${renderDisableableMenuItem(
|
||||
this._hideSelected,
|
||||
mdiEyeOff,
|
||||
"ui.panel.config.entities.picker.hide_selected.button",
|
||||
"hide-selected-disabled"
|
||||
)}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._restoreEntityIdSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiRestore}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
${
|
||||
isComponentLoaded(this.hass, "recorder")
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._enableRecordingSelected}>
|
||||
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_recording_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._disableRecordingSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_recording_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${renderDisableableMenuItem(
|
||||
this._restoreEntityIdSelected,
|
||||
mdiRestore,
|
||||
"ui.panel.config.entities.picker.restore_entity_id_selected.button",
|
||||
"restore-selected-disabled"
|
||||
)}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._removeSelected} class="warning">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.delete_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
${renderDisableableMenuItem(
|
||||
this._removeSelected,
|
||||
mdiDelete,
|
||||
"ui.panel.config.entities.picker.delete_selected.button",
|
||||
"delete-selected-disabled",
|
||||
true // warning style
|
||||
)}
|
||||
|
||||
</ha-md-button-menu>
|
||||
${
|
||||
@@ -1107,6 +1282,12 @@ ${
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
|
||||
// Fetch recording data if the column is visible
|
||||
if (!this._activeHiddenColumns?.includes("recorded")) {
|
||||
this._fetchRecordingData();
|
||||
}
|
||||
|
||||
if (Object.keys(this._filters).length) {
|
||||
return;
|
||||
}
|
||||
@@ -1162,6 +1343,14 @@ ${
|
||||
changedProps.has("_entities") ||
|
||||
changedProps.has("_entitySources")
|
||||
) {
|
||||
// Re-fetch recording data when entities change
|
||||
if (
|
||||
changedProps.has("_entities") &&
|
||||
this._recordingEntities &&
|
||||
!this._activeHiddenColumns?.includes("recorded")
|
||||
) {
|
||||
this._fetchRecordingData();
|
||||
}
|
||||
const stateEntities: StateEntity[] = [];
|
||||
const regEntityIds = new Set(
|
||||
this._entities.map((entity) => entity.entity_id)
|
||||
@@ -1189,7 +1378,7 @@ ${
|
||||
device_id: null,
|
||||
icon: null,
|
||||
readonly: true,
|
||||
selectable: false,
|
||||
selectable: true,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
options: null,
|
||||
@@ -1339,6 +1528,53 @@ ${
|
||||
this._clearSelection();
|
||||
};
|
||||
|
||||
private _setRecordingSelected = async (enable: boolean) => {
|
||||
if (!isComponentLoaded(this.hass, "recorder")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = enable ? "enable" : "disable";
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.entities.picker.${action}_recording_selected.confirm_title`,
|
||||
{ number: this._selected.length }
|
||||
),
|
||||
text: this.hass.localize(
|
||||
`ui.panel.config.entities.picker.${action}_recording_selected.confirm_text`
|
||||
),
|
||||
confirmText: this.hass.localize(`ui.common.${action}`),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirm: async () => {
|
||||
try {
|
||||
await setEntityRecordingOptions(
|
||||
this.hass,
|
||||
this._selected,
|
||||
enable ? null : "user" // null = enabled, "user" = disabled by user
|
||||
);
|
||||
|
||||
// Re-fetch recording data to update the table
|
||||
if (this._recordingEntities) {
|
||||
await this._fetchRecordingData();
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.common.multiselect.failed",
|
||||
{
|
||||
number: this._selected.length,
|
||||
}
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _enableRecordingSelected = () => this._setRecordingSelected(true);
|
||||
|
||||
private _disableRecordingSelected = () => this._setRecordingSelected(false);
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
@@ -1497,7 +1733,8 @@ ${rejected
|
||||
this._filters,
|
||||
this._filteredItems,
|
||||
this._entries,
|
||||
this._labels
|
||||
this._labels,
|
||||
this._recordingEntities
|
||||
);
|
||||
if (
|
||||
filteredDomains.size === 1 &&
|
||||
@@ -1527,9 +1764,30 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _fetchRecordingData() {
|
||||
if (!isComponentLoaded(this.hass, "recorder")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._recordingEntities = await getEntityRecordingList(this.hass);
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to fetch recording data:", err);
|
||||
this._recordingEntities = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
|
||||
if (
|
||||
!this._activeHiddenColumns?.includes("recorded") &&
|
||||
!this._recordingEntities
|
||||
) {
|
||||
this._fetchRecordingData();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
38
src/panels/config/entities/recorder-util.ts
Normal file
38
src/panels/config/entities/recorder-util.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import { setEntityRecordingOptions } from "../../../data/recorder";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
export interface RecordingChangeParams {
|
||||
hass: HomeAssistant;
|
||||
entityId: string;
|
||||
checkbox: HaSwitch;
|
||||
onSuccess?: (recordingDisabled: boolean) => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export const handleRecordingChange = async (params: RecordingChangeParams) => {
|
||||
const { hass, entityId, checkbox, onSuccess, onError } = params;
|
||||
const newRecordingDisabled = !checkbox.checked;
|
||||
|
||||
try {
|
||||
await setEntityRecordingOptions(
|
||||
hass,
|
||||
[entityId],
|
||||
newRecordingDisabled ? "user" : null
|
||||
);
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(newRecordingDisabled);
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(checkbox, {
|
||||
text: err.message,
|
||||
});
|
||||
checkbox.checked = !checkbox.checked;
|
||||
|
||||
if (onError) {
|
||||
onError();
|
||||
}
|
||||
}
|
||||
};
|
@@ -230,10 +230,10 @@ class HUIRoot extends LitElement {
|
||||
},
|
||||
{
|
||||
icon: mdiSofa,
|
||||
key: "ui.panel.lovelace.menu.create_area",
|
||||
key: "ui.panel.lovelace.menu.add_area",
|
||||
visible: true,
|
||||
action: this._createArea,
|
||||
overflowAction: this._handleCreateArea,
|
||||
action: this._addArea,
|
||||
overflowAction: this._handleAddArea,
|
||||
},
|
||||
{
|
||||
icon: mdiAccount,
|
||||
@@ -366,7 +366,6 @@ class HUIRoot extends LitElement {
|
||||
}
|
||||
showListItemsDialog(this, {
|
||||
title: title,
|
||||
mode: this.narrow ? "bottom-sheet" : "dialog",
|
||||
items: i.subItems!.map((si) => ({
|
||||
iconPath: si.icon,
|
||||
label: this.hass!.localize(si.key),
|
||||
@@ -838,14 +837,14 @@ class HUIRoot extends LitElement {
|
||||
showNewAutomationDialog(this, { mode: "automation" });
|
||||
};
|
||||
|
||||
private _handleCreateArea(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
private _handleAddArea(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this._createArea();
|
||||
this._addArea();
|
||||
}
|
||||
|
||||
private _createArea = async () => {
|
||||
private _addArea = async () => {
|
||||
await this.hass.loadFragmentTranslation("config");
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
createEntry: async (values) => {
|
||||
@@ -855,15 +854,13 @@ class HUIRoot extends LitElement {
|
||||
}
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.lovelace.menu.create_area_success"
|
||||
"ui.panel.lovelace.menu.add_area_success"
|
||||
),
|
||||
action: {
|
||||
action: () => {
|
||||
navigate(`/config/areas/area/${area.area_id}`);
|
||||
},
|
||||
text: this.hass.localize(
|
||||
"ui.panel.lovelace.menu.create_area_action"
|
||||
),
|
||||
text: this.hass.localize("ui.panel.lovelace.menu.add_area_action"),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@@ -1512,7 +1512,7 @@
|
||||
"settings": "Settings",
|
||||
"control": "Control",
|
||||
"related": "Related",
|
||||
"no_unique_id": "This entity (''{entity_id}'') does not have a unique ID, therefore its settings cannot be managed from the UI. See the {faq_link} for more detail.",
|
||||
"no_unique_id": "This entity (''{entity_id}'') does not have a unique ID, therefore only limited settings can be managed from the UI. See the {faq_link} for more detail.",
|
||||
"faq": "documentation",
|
||||
"editor": {
|
||||
"name": "Name",
|
||||
@@ -1605,6 +1605,10 @@
|
||||
"enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds",
|
||||
"enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities",
|
||||
"hidden_explanation": "Hidden entities will not be included in auto-populated dashboards or when their area, device or label is referenced. Their history is still tracked and you can still interact with them with actions.",
|
||||
"record_label": "Record",
|
||||
"record_description": "Control whether this entity's state history is recorded by the Home Assistant Recorder.",
|
||||
"error_loading_recording_settings": "Error loading recording settings",
|
||||
"error_updating_recording_settings": "Error updating recording settings",
|
||||
"delete": "Delete",
|
||||
"confirm_delete": "Are you sure you want to delete this entity?",
|
||||
"update": "Update",
|
||||
@@ -5306,7 +5310,12 @@
|
||||
"domain": "Domain",
|
||||
"availability": "Availability",
|
||||
"visibility": "Visibility",
|
||||
"enabled": "Enabled"
|
||||
"enabled": "Enabled",
|
||||
"recorded": "Recorded"
|
||||
},
|
||||
"recorded": {
|
||||
"enabled": "Recording enabled",
|
||||
"disabled": "Recording disabled"
|
||||
},
|
||||
"selected": "{number} selected",
|
||||
"enable_selected": {
|
||||
@@ -5331,6 +5340,16 @@
|
||||
"confirm_text": "Are you sure you want to delete the entities?\n\nRemove them from your dashboard and automations if they include these entities.",
|
||||
"confirm_partly_text": "You can only delete {deletable} of the {selected} entities. The others require the integration to stop providing them, and sometimes a Home Assistant restart is needed. Are you sure you want to delete the deletable entities?\n\nRemove them from your dashboard and automations if they include these entities."
|
||||
},
|
||||
"enable_recording_selected": {
|
||||
"button": "Enable recording for selected",
|
||||
"confirm_title": "Do you want to enable recording for {number} {number, plural,\n one {entity}\n other {entities}\n}?",
|
||||
"confirm_text": "This will resume recording their state history in the recorder."
|
||||
},
|
||||
"disable_recording_selected": {
|
||||
"button": "Disable recording for selected",
|
||||
"confirm_title": "Do you want to disable recording for {number} {number, plural,\n one {entity}\n other {entities}\n}?",
|
||||
"confirm_text": "This will stop recording their state history in the recorder."
|
||||
},
|
||||
"hide_selected": {
|
||||
"button": "Hide selected",
|
||||
"confirm_title": "Do you want to hide {number} {number, plural,\n one {entity}\n other {entities}\n}?",
|
||||
@@ -5338,7 +5357,8 @@
|
||||
},
|
||||
"unhide_selected": {
|
||||
"button": "Unhide selected"
|
||||
}
|
||||
},
|
||||
"non_unique_id_selected": "Select only entities with unique IDs to enable this operation"
|
||||
}
|
||||
},
|
||||
"person": {
|
||||
@@ -7059,9 +7079,9 @@
|
||||
"add": "Add to Home Assistant",
|
||||
"add_device": "Add device",
|
||||
"create_automation": "Create automation",
|
||||
"create_area": "Create area",
|
||||
"create_area_success": "Area created",
|
||||
"create_area_action": "View area",
|
||||
"add_area": "Add area",
|
||||
"add_area_success": "Area added",
|
||||
"add_area_action": "View area",
|
||||
"add_person_success": "Person added",
|
||||
"add_person_action": "View persons",
|
||||
"add_person": "Add person"
|
||||
|
Reference in New Issue
Block a user