mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-27 22:09:42 +00:00
Compare commits
147 Commits
sec_pypi_p
...
20250925.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bab0391a19 | ||
![]() |
444123c47e | ||
![]() |
f123d34046 | ||
![]() |
1b40f99f68 | ||
![]() |
b314b3ed2b | ||
![]() |
59b8932969 | ||
![]() |
107af753ec | ||
![]() |
1f0acb3046 | ||
![]() |
431e533929 | ||
![]() |
02c845cbc6 | ||
![]() |
628111ed20 | ||
![]() |
e825a9c02f | ||
![]() |
7a35bddf36 | ||
![]() |
ad69270af8 | ||
![]() |
404edf9483 | ||
![]() |
a166b4e9b6 | ||
![]() |
7a285f11db | ||
![]() |
e42c5a3254 | ||
![]() |
df7a6297b0 | ||
![]() |
e4ca478d01 | ||
![]() |
7be2c59295 | ||
![]() |
99d9c67492 | ||
![]() |
8f781e53e3 | ||
![]() |
3c92826e71 | ||
![]() |
151a879e0a | ||
![]() |
f3a8529ed7 | ||
![]() |
d2cc7856d1 | ||
![]() |
d5cb815bbd | ||
![]() |
7f88d863e9 | ||
![]() |
88ac56ac0b | ||
![]() |
3d173ad03e | ||
![]() |
3889d71768 | ||
![]() |
8872adf2ed | ||
![]() |
969e655fff | ||
![]() |
cdc913d878 | ||
![]() |
4ac1215def | ||
![]() |
b2376fba56 | ||
![]() |
f14d9198ac | ||
![]() |
f4e583b302 | ||
![]() |
2c602aecee | ||
![]() |
cbf96898fe | ||
![]() |
6760f4a2ae | ||
![]() |
3481f7e8be | ||
![]() |
95a0fe335f | ||
![]() |
1e2d144d26 | ||
![]() |
6aa89cb532 | ||
![]() |
1b0ed7017f | ||
![]() |
1cc7e387da | ||
![]() |
41bf935f6e | ||
![]() |
b08ea36a1e | ||
![]() |
4f52a46725 | ||
![]() |
f8a82563b0 | ||
![]() |
a1672ccdfb | ||
![]() |
bde851e5a4 | ||
![]() |
d64acca598 | ||
![]() |
59571d03a6 | ||
![]() |
28c515bbac | ||
![]() |
27db5b3b02 | ||
![]() |
1922db0474 | ||
![]() |
c8c74a9744 | ||
![]() |
2c676baa99 | ||
![]() |
3e41474faa | ||
![]() |
5f9c69ac21 | ||
![]() |
8b45ccaaba | ||
![]() |
455925f637 | ||
![]() |
9fba7427f8 | ||
![]() |
21aae02652 | ||
![]() |
24e3fbf622 | ||
![]() |
2cbcf1a689 | ||
![]() |
1c1c0d70c5 | ||
![]() |
a66f5fb573 | ||
![]() |
9affeab755 | ||
![]() |
2bfaf77908 | ||
![]() |
bc4caae796 | ||
![]() |
8746acd329 | ||
![]() |
96ecf16da2 | ||
![]() |
1e95a0f3ef | ||
![]() |
a164d793b1 | ||
![]() |
510fc71b40 | ||
![]() |
2a6a3edb77 | ||
![]() |
c7a8796a47 | ||
![]() |
9d40fa5f2b | ||
![]() |
8f2a023775 | ||
![]() |
989b0b34fe | ||
![]() |
cf94e71215 | ||
![]() |
49896f3fa6 | ||
![]() |
fc4b7674b1 | ||
![]() |
04c9f32539 | ||
![]() |
21e3fc9bb9 | ||
![]() |
4b78eb7656 | ||
![]() |
e6f91aef8e | ||
![]() |
8f99f86c8b | ||
![]() |
b7eff547c7 | ||
![]() |
ceb6b64152 | ||
![]() |
d253041376 | ||
![]() |
cb0aa81f89 | ||
![]() |
42061b2f8c | ||
![]() |
69bfb89a65 | ||
![]() |
e0307f9688 | ||
![]() |
1cf353461f | ||
![]() |
1786235c86 | ||
![]() |
645ba3f9c1 | ||
![]() |
b65f6f46e1 | ||
![]() |
84ad521b3d | ||
![]() |
dfb9c662e7 | ||
![]() |
5ac42e17b0 | ||
![]() |
be2f19637e | ||
![]() |
b7a6ee3792 | ||
![]() |
1fb2f0c989 | ||
![]() |
b4ad411e6f | ||
![]() |
5d76a92f73 | ||
![]() |
beee09491a | ||
![]() |
ee5aabdddf | ||
![]() |
ec80f6a6f1 | ||
![]() |
9845f0b47c | ||
![]() |
cd294ba619 | ||
![]() |
61e27cb1ea | ||
![]() |
8d6295e8e8 | ||
![]() |
b0e95699f7 | ||
![]() |
c8e1e7b8a8 | ||
![]() |
d2cea159af | ||
![]() |
eb5d1c79c8 | ||
![]() |
65ab6848ab | ||
![]() |
7a1d934e8d | ||
![]() |
cbacde12fa | ||
![]() |
4c33618e05 | ||
![]() |
3837b3e630 | ||
![]() |
7c15633f6d | ||
![]() |
f7ec8650eb | ||
![]() |
7674eee0fb | ||
![]() |
f494a6453a | ||
![]() |
37f3682ffa | ||
![]() |
8055286a1f | ||
![]() |
0bdd213761 | ||
![]() |
810b43760e | ||
![]() |
424d71c55a | ||
![]() |
176924241c | ||
![]() |
da08aa7fb0 | ||
![]() |
6047227648 | ||
![]() |
fc71fd6bc3 | ||
![]() |
90a1b135e1 | ||
![]() |
e19413b6ca | ||
![]() |
0dfc10af5f | ||
![]() |
bbbc419bea | ||
![]() |
50ad5e376f | ||
![]() |
a9f2254bbc | ||
![]() |
a8836404d4 |
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.4.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.10.2.cjs
|
||||
|
@@ -5,17 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
media.hlsVideoSegmentFormat =
|
||||
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
|
@@ -40,8 +40,7 @@ const playDummyMedia = (viewTitle?: string) => {
|
||||
loadRequestData.media.contentId =
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||
loadRequestData.media.contentType = "image/jpeg";
|
||||
loadRequestData.media.streamType =
|
||||
"NONE" as framework.messages.StreamType.NONE;
|
||||
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
|
||||
const metadata = new framework.messages.GenericMediaMetadata();
|
||||
metadata.title = viewTitle;
|
||||
loadRequestData.media.metadata = metadata;
|
||||
@@ -90,7 +89,7 @@ const showMediaPlayer = () => {
|
||||
const options = new framework.CastReceiverOptions();
|
||||
options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
[CAST_NS]: "json" as framework.system.MessageType.JSON,
|
||||
[CAST_NS]: framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
castContext.addCustomMessageListener(
|
||||
@@ -98,7 +97,9 @@ castContext.addCustomMessageListener(
|
||||
// @ts-ignore
|
||||
(ev: ReceivedMessage<HassMessage>) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (playerManager.getPlayerState() !== "IDLE") {
|
||||
if (
|
||||
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
@@ -112,7 +113,7 @@ castContext.addCustomMessageListener(
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
@@ -126,23 +127,24 @@ playerManager.setMessageInterceptor(
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
media.hlsVideoSegmentFormat =
|
||||
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
);
|
||||
|
||||
playerManager.addEventListener(
|
||||
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
||||
framework.events.EventType.MEDIA_STATUS,
|
||||
(event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState === "IDLE" &&
|
||||
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !== "INTERRUPTED"
|
||||
event.mediaStatus?.idleReason !==
|
||||
framework.messages.IdleReason.INTERRUPTED
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
|
@@ -17,6 +17,10 @@ export const createMediaPlayerEntities = () => [
|
||||
new Date().getTime() - 23000
|
||||
).toISOString(),
|
||||
volume_level: 0.5,
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
source: "AirPlay",
|
||||
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||
sound_mode: "Music",
|
||||
}),
|
||||
getEntity("media_player", "music_playing", "playing", {
|
||||
friendly_name: "Playing The Music",
|
||||
@@ -24,8 +28,8 @@ export const createMediaPlayerEntities = () => [
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
|
||||
supported_features: 195135,
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media + Grouping
|
||||
supported_features: 784959,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 0,
|
||||
@@ -34,6 +38,9 @@ export const createMediaPlayerEntities = () => [
|
||||
new Date().getTime() - 23000
|
||||
).toISOString(),
|
||||
volume_level: 0.5,
|
||||
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||
sound_mode: "Music",
|
||||
group_members: ["media_player.playing", "media_player.stream_playing"],
|
||||
}),
|
||||
getEntity("media_player", "stream_playing", "playing", {
|
||||
friendly_name: "Playing the Stream",
|
||||
@@ -149,15 +156,18 @@ export const createMediaPlayerEntities = () => [
|
||||
}),
|
||||
getEntity("media_player", "receiver_on", "on", {
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||
volume_level: 0.63,
|
||||
is_volume_muted: false,
|
||||
source: "TV",
|
||||
sound_mode: "Movie",
|
||||
friendly_name: "Receiver (selectable sources)",
|
||||
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
|
||||
supported_features: 84364,
|
||||
}),
|
||||
getEntity("media_player", "receiver_off", "off", {
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||
friendly_name: "Receiver (selectable sources)",
|
||||
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
|
||||
supported_features: 84364,
|
||||
|
37
gallery/src/pages/components/ha-marquee-text.markdown
Normal file
37
gallery/src/pages/components/ha-marquee-text.markdown
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Marquee Text
|
||||
---
|
||||
|
||||
# Marquee Text `<ha-marquee-text>`
|
||||
|
||||
Marquee text component scrolls text horizontally if it overflows its container. It supports pausing on hover and customizable speed and pause duration.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Example Usage
|
||||
|
||||
<ha-marquee-text style="width: 200px;">
|
||||
This is a long text that will scroll horizontally if it overflows the container.
|
||||
</ha-marquee-text>
|
||||
|
||||
```html
|
||||
<ha-marquee-text style="width: 200px;">
|
||||
This is a long text that will scroll horizontally if it overflows the
|
||||
container.
|
||||
</ha-marquee-text>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
**Slots**
|
||||
|
||||
- default slot: The text content to be displayed and scrolled.
|
||||
- no default
|
||||
|
||||
**Properties/Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------------- | ------- | ------- | ---------------------------------------------------------------------------- |
|
||||
| speed | number | `15` | The speed of the scrolling animation. Higher values result in faster scroll. |
|
||||
| pause-on-hover | boolean | `true` | Whether to pause the scrolling animation when |
|
||||
| pause-duration | number | `1000` | The delay in milliseconds before the scrolling animation starts/restarts. |
|
25
gallery/src/pages/components/ha-marquee-text.ts
Normal file
25
gallery/src/pages/components/ha-marquee-text.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-marquee-text";
|
||||
|
||||
@customElement("demo-components-ha-marquee-text")
|
||||
export class DemoHaMarqueeText extends LitElement {
|
||||
static styles = css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-marquee-text": DemoHaMarqueeText;
|
||||
}
|
||||
}
|
@@ -11,7 +11,10 @@ import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button-menu";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
HaFormDataContainer,
|
||||
} from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
@@ -33,6 +36,7 @@ import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import type { ObjectSelector, Selector } from "../../../../src/data/selector";
|
||||
|
||||
const SUPPORTED_UI_TYPES = [
|
||||
"string",
|
||||
@@ -78,77 +82,124 @@ class HassioAddonConfig extends LitElement {
|
||||
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
public computeLabel = (entry: HaFormSchema): string =>
|
||||
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
||||
?.name ||
|
||||
this.addon.translations.en?.configuration?.[entry.name]?.name ||
|
||||
private _getTranslationEntry(
|
||||
language: string,
|
||||
entry: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
) {
|
||||
let parent = this.addon.translations[language]?.configuration;
|
||||
if (!parent) return undefined;
|
||||
if (options?.path) {
|
||||
for (const key of options.path) {
|
||||
parent = parent[key]?.fields;
|
||||
if (!parent) return undefined;
|
||||
}
|
||||
}
|
||||
return parent[entry.name];
|
||||
}
|
||||
|
||||
public computeLabel = (
|
||||
entry: HaFormSchema,
|
||||
_data: HaFormDataContainer,
|
||||
options?: { path?: string[] }
|
||||
): string =>
|
||||
this._getTranslationEntry(this.hass.language, entry, options)?.name ||
|
||||
this._getTranslationEntry("en", entry, options)?.name ||
|
||||
entry.name;
|
||||
|
||||
public computeHelper = (entry: HaFormSchema): string =>
|
||||
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
||||
public computeHelper = (
|
||||
entry: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
): string =>
|
||||
this._getTranslationEntry(this.hass.language, entry, options)
|
||||
?.description ||
|
||||
this.addon.translations.en?.configuration?.[entry.name]?.description ||
|
||||
this._getTranslationEntry("en", entry, options)?.description ||
|
||||
"";
|
||||
|
||||
private _convertSchema = memoizeOne(
|
||||
// Convert supervisor schema to selectors
|
||||
(schema: Record<string, any>): HaFormSchema[] =>
|
||||
schema.map((entry) =>
|
||||
entry.type === "select"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: { select: { options: entry.options } },
|
||||
}
|
||||
: entry.type === "string"
|
||||
? entry.multiple
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
select: { options: [], multiple: true, custom_value: true },
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
text: {
|
||||
type: entry.format
|
||||
? entry.format
|
||||
: MASKED_FIELDS.includes(entry.name)
|
||||
? "password"
|
||||
: "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
: entry.type === "boolean"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: { boolean: {} },
|
||||
}
|
||||
: entry.type === "schema"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: { object: {} },
|
||||
}
|
||||
: entry.type === "float" || entry.type === "integer"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: entry.type === "float" ? "any" : undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
: entry
|
||||
)
|
||||
(schema: readonly HaFormSchema[]): HaFormSchema[] =>
|
||||
this._convertSchemaElements(schema)
|
||||
);
|
||||
|
||||
private _convertSchemaElements(
|
||||
schema: readonly HaFormSchema[]
|
||||
): HaFormSchema[] {
|
||||
return schema.map((entry) => this._convertSchemaElement(entry));
|
||||
}
|
||||
|
||||
private _convertSchemaElement(entry: any): HaFormSchema {
|
||||
if (entry.type === "schema" && !entry.multiple) {
|
||||
return {
|
||||
name: entry.name,
|
||||
type: "expandable",
|
||||
required: entry.required,
|
||||
schema: this._convertSchemaElements(entry.schema),
|
||||
};
|
||||
}
|
||||
const selector = this._convertSchemaElementToSelector(entry, false);
|
||||
if (selector) {
|
||||
return {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector,
|
||||
};
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private _convertSchemaElementToSelector(
|
||||
entry: any,
|
||||
force: boolean
|
||||
): Selector | null {
|
||||
if (entry.type === "select") {
|
||||
return { select: { options: entry.options } };
|
||||
}
|
||||
if (entry.type === "string") {
|
||||
return entry.multiple
|
||||
? { select: { options: [], multiple: true, custom_value: true } }
|
||||
: {
|
||||
text: {
|
||||
type: entry.format
|
||||
? entry.format
|
||||
: MASKED_FIELDS.includes(entry.name)
|
||||
? "password"
|
||||
: "text",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (entry.type === "boolean") {
|
||||
return { boolean: {} };
|
||||
}
|
||||
if (entry.type === "schema") {
|
||||
const fields: NonNullable<ObjectSelector["object"]>["fields"] = {};
|
||||
for (const child_entry of entry.schema) {
|
||||
fields[child_entry.name] = {
|
||||
required: child_entry.required,
|
||||
selector: this._convertSchemaElementToSelector(child_entry, true)!,
|
||||
};
|
||||
}
|
||||
return {
|
||||
object: {
|
||||
multiple: entry.multiple,
|
||||
fields,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (entry.type === "float" || entry.type === "integer") {
|
||||
return {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: entry.type === "float" ? "any" : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (force) {
|
||||
return { object: {} };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _filteredSchema = memoizeOne(
|
||||
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
|
||||
schema.filter((entry) => entry.name in options || entry.required)
|
||||
|
10
package.json
10
package.json
@@ -161,7 +161,7 @@
|
||||
"@rspack/core": "1.5.5",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.24",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
@@ -182,8 +182,8 @@
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.35.0",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.36.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -210,7 +210,7 @@
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.6.2",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"rspack-manifest-plugin": "5.1.0",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.0",
|
||||
"tar": "7.4.3",
|
||||
@@ -235,5 +235,5 @@
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.4"
|
||||
"packageManager": "yarn@4.10.2"
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250903.0"
|
||||
version = "20250925.1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
@@ -84,6 +84,9 @@ export class StateHistoryCharts extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "sync-charts", type: Boolean })
|
||||
public syncCharts = false;
|
||||
|
||||
private _computedStartTime!: Date;
|
||||
|
||||
private _computedEndTime!: Date;
|
||||
@@ -146,7 +149,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
: html`${combinedItems.map((item, index) =>
|
||||
this._renderHistoryItem(item, index)
|
||||
)}`}
|
||||
${this._hasZoomedCharts
|
||||
${this.syncCharts && this._hasZoomedCharts
|
||||
? html`<ha-fab
|
||||
slot="fab"
|
||||
class="reset-button"
|
||||
@@ -190,7 +193,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
.expandLegend=${this.expandLegend}
|
||||
hide-reset-button
|
||||
?hide-reset-button=${this.syncCharts}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
@@ -209,7 +212,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
hide-reset-button
|
||||
?hide-reset-button=${this.syncCharts}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
@@ -302,7 +305,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
private _handleTimelineSync(
|
||||
e: CustomEvent<HASSDomEvents["chart-zoom-with-index"]>
|
||||
) {
|
||||
if (this._isSyncing) {
|
||||
if (!this.syncCharts || this._isSyncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -444,6 +447,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
position: fixed;
|
||||
bottom: calc(24px + var(--safe-area-inset-bottom));
|
||||
right: calc(24px + var(--safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -86,6 +86,10 @@ class HaAlert extends LitElement {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
}
|
||||
.icon {
|
||||
height: var(--ha-alert-icon-size, 24px);
|
||||
width: var(--ha-alert-icon-size, 24px);
|
||||
}
|
||||
.issue-type::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
@@ -73,14 +73,18 @@ export class HaAnalytics extends LitElement {
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
?disabled=${baseEnabled}
|
||||
>
|
||||
</ha-switch>
|
||||
<ha-tooltip .for="switch-${preference}" placement="right">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>
|
||||
${baseEnabled
|
||||
? nothing
|
||||
: html`<ha-tooltip
|
||||
.for="switch-${preference}"
|
||||
placement="right"
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
|
@@ -54,9 +54,9 @@ export class HaBottomSheet extends LitElement {
|
||||
border-top-left-radius: var(--ha-border-radius-lg);
|
||||
border-top-right-radius: var(--ha-border-radius-lg);
|
||||
max-height: 90vh;
|
||||
margin-bottom: var(--safe-area-inset-bottom);
|
||||
margin-left: var(--safe-area-inset-left);
|
||||
margin-right: var(--safe-area-inset-right);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -5,9 +5,16 @@ import type {
|
||||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { undo, undoDepth, redo, redoDepth } from "@codemirror/commands";
|
||||
import type { Extension, TransactionSpec } from "@codemirror/state";
|
||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
|
||||
import {
|
||||
mdiArrowExpand,
|
||||
mdiArrowCollapse,
|
||||
mdiContentCopy,
|
||||
mdiUndo,
|
||||
mdiRedo,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, ReactiveElement, html, render } from "lit";
|
||||
@@ -16,11 +23,14 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { getEntityContext } from "../common/entity/context/get_entity_context";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import "./ha-code-editor-completion-items";
|
||||
import type { CompletionItem } from "./ha-code-editor-completion-items";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-code-editor-completion-items";
|
||||
import "./ha-icon-button-toolbar";
|
||||
import type { HaIconButtonToolbar } from "./ha-icon-button-toolbar";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -68,13 +78,24 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "has-toolbar" })
|
||||
public hasToolbar = true;
|
||||
|
||||
@state() private _value = "";
|
||||
|
||||
@state() private _isFullscreen = false;
|
||||
|
||||
@state() private _canUndo = false;
|
||||
|
||||
@state() private _canRedo = false;
|
||||
|
||||
@state() private _canCopy = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||
|
||||
private _editorToolbar?: HaIconButtonToolbar;
|
||||
|
||||
private _iconList?: Completion[];
|
||||
|
||||
public set value(value: string) {
|
||||
@@ -119,9 +140,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", stopPropagation);
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
if (this._isFullscreen) {
|
||||
this._toggleFullscreen();
|
||||
}
|
||||
this._updateFullscreenState(false);
|
||||
this.updateComplete.then(() => {
|
||||
this.codemirror!.destroy();
|
||||
delete this.codemirror;
|
||||
@@ -157,6 +176,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
||||
),
|
||||
});
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (changedProps.has("linewrap")) {
|
||||
transactions.push({
|
||||
@@ -177,14 +197,25 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
if (transactions.length > 0) {
|
||||
this.codemirror.dispatch(...transactions);
|
||||
}
|
||||
if (changedProps.has("hasToolbar")) {
|
||||
this._updateToolbar();
|
||||
}
|
||||
if (changedProps.has("error")) {
|
||||
this.classList.toggle("error-state", this.error);
|
||||
}
|
||||
if (changedProps.has("_isFullscreen")) {
|
||||
this.classList.toggle("fullscreen", this._isFullscreen);
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (
|
||||
changedProps.has("_canCopy") ||
|
||||
changedProps.has("_canUndo") ||
|
||||
changedProps.has("_canRedo")
|
||||
) {
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (changedProps.has("disableFullscreen")) {
|
||||
this._updateFullscreenButton();
|
||||
this._updateFullscreenState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +284,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Create the code editor
|
||||
this.codemirror = new this._loadedCodeMirror.EditorView({
|
||||
state: this._loadedCodeMirror.EditorState.create({
|
||||
doc: this._value,
|
||||
@@ -260,71 +292,160 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}),
|
||||
parent: this.renderRoot,
|
||||
});
|
||||
this._canCopy = this._value?.length > 0;
|
||||
|
||||
this._updateFullscreenButton();
|
||||
// Update the toolbar. Creating it if required
|
||||
this._updateToolbar();
|
||||
}
|
||||
|
||||
private _updateFullscreenButton() {
|
||||
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
|
||||
private _fullscreenLabel(): string {
|
||||
if (this._isFullscreen)
|
||||
return (
|
||||
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
|
||||
"Exit fullscreen"
|
||||
);
|
||||
return (
|
||||
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
|
||||
"Enter fullscreen"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.disableFullscreen) {
|
||||
// Remove button if it exists and fullscreen is disabled
|
||||
if (existingButton) {
|
||||
existingButton.remove();
|
||||
}
|
||||
// Exit fullscreen if currently in fullscreen mode
|
||||
if (this._isFullscreen) {
|
||||
this._isFullscreen = false;
|
||||
}
|
||||
private _fullscreenIcon(): string {
|
||||
return this._isFullscreen ? mdiArrowCollapse : mdiArrowExpand;
|
||||
}
|
||||
|
||||
private _createEditorToolbar(): HaIconButtonToolbar {
|
||||
// Create the editor toolbar element
|
||||
const editorToolbar = document.createElement("ha-icon-button-toolbar");
|
||||
editorToolbar.classList.add("code-editor-toolbar");
|
||||
editorToolbar.items = [];
|
||||
return editorToolbar;
|
||||
}
|
||||
|
||||
private _updateToolbar() {
|
||||
// Show/Hide the toolbar if we have one.
|
||||
this.classList.toggle("hasToolbar", this.hasToolbar);
|
||||
|
||||
// Update fullscreen state. Handles toolbar and fullscreen mode being disabled.
|
||||
this._updateFullscreenState();
|
||||
|
||||
// If we don't have a toolbar, nothing to update
|
||||
if (!this.hasToolbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create button if it doesn't exist
|
||||
if (!existingButton) {
|
||||
const button = document.createElement("ha-icon-button");
|
||||
(button as any).path = this._isFullscreen
|
||||
? mdiArrowCollapse
|
||||
: mdiArrowExpand;
|
||||
button.setAttribute(
|
||||
"label",
|
||||
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||
);
|
||||
button.classList.add("fullscreen-button");
|
||||
// Use bound method to ensure proper this context
|
||||
button.addEventListener("click", this._handleFullscreenClick);
|
||||
this.renderRoot.appendChild(button);
|
||||
} else {
|
||||
// Update existing button
|
||||
(existingButton as any).path = this._isFullscreen
|
||||
? mdiArrowCollapse
|
||||
: mdiArrowExpand;
|
||||
existingButton.setAttribute(
|
||||
"label",
|
||||
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||
);
|
||||
// If we don't yet have the toolbar, create it.
|
||||
if (!this._editorToolbar) {
|
||||
this._editorToolbar = this._createEditorToolbar();
|
||||
}
|
||||
|
||||
// Ensure all toolbar buttons are correctly configured.
|
||||
this._updateToolbarButtons();
|
||||
|
||||
// Render the toolbar. This must be placed as a child of the code
|
||||
// mirror element to ensure it doesn't affect the positioning and
|
||||
// size of codemirror.
|
||||
this.codemirror?.dom.appendChild(this._editorToolbar);
|
||||
}
|
||||
|
||||
private _updateToolbarButtons() {
|
||||
// Re-render all toolbar items.
|
||||
if (!this._editorToolbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editorToolbar.items = [
|
||||
{
|
||||
id: "undo",
|
||||
disabled: !this._canUndo,
|
||||
label: this.hass?.localize("ui.common.undo") || "Undo",
|
||||
path: mdiUndo,
|
||||
action: (e: Event) => this._handleUndoClick(e),
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
disabled: !this._canRedo,
|
||||
label: this.hass?.localize("ui.common.redo") || "Redo",
|
||||
path: mdiRedo,
|
||||
action: (e: Event) => this._handleRedoClick(e),
|
||||
},
|
||||
{
|
||||
id: "copy",
|
||||
disabled: !this._canCopy,
|
||||
label:
|
||||
this.hass?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
|
||||
"Copy to Clipboard",
|
||||
path: mdiContentCopy,
|
||||
action: (e: Event) => this._handleClipboardClick(e),
|
||||
},
|
||||
{
|
||||
id: "fullscreen",
|
||||
disabled: this.disableFullscreen,
|
||||
label: this._fullscreenLabel(),
|
||||
path: this._fullscreenIcon(),
|
||||
action: (e: Event) => this._handleFullscreenClick(e),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private _updateFullscreenState(
|
||||
fullscreen: boolean = this._isFullscreen
|
||||
): boolean {
|
||||
// Update the current fullscreen state based on selected value. If fullscreen
|
||||
// is disabled, or we have no toolbar, ensure we are not in fullscreen mode.
|
||||
this._isFullscreen =
|
||||
fullscreen && !this.disableFullscreen && this.hasToolbar;
|
||||
// Return whether successfully in requested state
|
||||
return this._isFullscreen === fullscreen;
|
||||
}
|
||||
|
||||
private _handleClipboardClick = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.value) {
|
||||
await copyToClipboard(this.value);
|
||||
showToast(this, {
|
||||
message:
|
||||
this.hass?.localize("ui.common.copied_clipboard") ||
|
||||
"Copied to clipboard",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _handleUndoClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.codemirror) {
|
||||
return;
|
||||
}
|
||||
undo(this.codemirror);
|
||||
};
|
||||
|
||||
private _handleRedoClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.codemirror) {
|
||||
return;
|
||||
}
|
||||
redo(this.codemirror);
|
||||
};
|
||||
|
||||
private _handleFullscreenClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
this._updateFullscreenState(!this._isFullscreen);
|
||||
};
|
||||
|
||||
private _toggleFullscreen() {
|
||||
this._isFullscreen = !this._isFullscreen;
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
|
||||
private _handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (this._isFullscreen && e.key === "Escape") {
|
||||
if (
|
||||
(e.key === "Escape" &&
|
||||
this._isFullscreen &&
|
||||
this._updateFullscreenState(false)) ||
|
||||
(e.key === "F11" && this._updateFullscreenState(true))
|
||||
) {
|
||||
// If we successfully performed the action, stop it propagating further.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
} else if (e.key === "F11" && !this.disableFullscreen) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -592,10 +713,13 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _onUpdate = (update: ViewUpdate): void => {
|
||||
this._canUndo = !this.readOnly && undoDepth(update.state) > 0;
|
||||
this._canRedo = !this.readOnly && redoDepth(update.state) > 0;
|
||||
if (!update.docChanged) {
|
||||
return;
|
||||
}
|
||||
this._value = update.state.doc.toString();
|
||||
this._canCopy = this._value?.length > 0;
|
||||
fireEvent(this, "value-changed", { value: this._value });
|
||||
};
|
||||
|
||||
@@ -614,39 +738,33 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
--code-editor-toolbar-height: 28px;
|
||||
}
|
||||
|
||||
:host(.error-state) .cm-gutters {
|
||||
border-color: var(--error-state-color, red);
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
|
||||
.fullscreen-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
color: var(--secondary-text-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
border-radius: 50%;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s;
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 18px;
|
||||
/* Ensure button is clickable on iOS */
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
:host(.hasToolbar) .cm-gutters {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.fullscreen-button:hover,
|
||||
.fullscreen-button:active {
|
||||
opacity: 1;
|
||||
:host(.hasToolbar) .cm-focused .cm-gutters {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.fullscreen-button {
|
||||
opacity: 0.8;
|
||||
}
|
||||
:host(.error-state) .cm-content {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-content {
|
||||
border: none;
|
||||
border-top: 1px solid var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-focused .cm-content {
|
||||
border-top: 2px solid var(--primary-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
:host(.fullscreen) {
|
||||
@@ -655,7 +773,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: 8px !important;
|
||||
z-index: 9999 !important;
|
||||
z-index: 6;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
@@ -672,15 +790,28 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
:host(.fullscreen) .fullscreen-button {
|
||||
top: calc(var(--safe-area-inset-top, 0px) + 8px);
|
||||
right: calc(var(--safe-area-inset-right, 0px) + 8px);
|
||||
:host(:not(.hasToolbar)) .code-editor-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.code-editor-toolbar {
|
||||
--icon-button-toolbar-height: var(--code-editor-toolbar-height);
|
||||
--icon-button-toolbar-color: var(
|
||||
--code-editor-gutter-color,
|
||||
var(--secondary-background-color, whitesmoke)
|
||||
);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
|
||||
.completion-info {
|
||||
|
@@ -100,8 +100,6 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertical-align-dialog, center);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: 16px 16px 0 16px;
|
||||
@@ -123,11 +121,7 @@ export class HaDialog extends DialogBase {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
top: var(--dialog-surface-top);
|
||||
margin-top: var(--dialog-surface-margin-top);
|
||||
min-width: calc(
|
||||
var(--mdc-dialog-min-width, 100vw) - var(
|
||||
--safe-area-inset-left
|
||||
) - var(--safe-area-inset-right)
|
||||
);
|
||||
min-width: var(--mdc-dialog-min-width, 100vw);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(--ha-dialog-border-radius, 24px);
|
||||
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
@@ -144,18 +138,12 @@ export class HaDialog extends DialogBase {
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
min-height: calc(
|
||||
100vh - var(--safe-area-inset-top, 0px) - var(
|
||||
--safe-area-inset-bottom,
|
||||
0px
|
||||
)
|
||||
);
|
||||
max-height: calc(
|
||||
100vh - var(--safe-area-inset-top, 0px) - var(
|
||||
--safe-area-inset-bottom,
|
||||
0px
|
||||
)
|
||||
);
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
|
126
src/components/ha-icon-button-toolbar.ts
Normal file
126
src/components/ha-icon-button-toolbar.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
import "./ha-icon-button-group";
|
||||
import "./ha-tooltip";
|
||||
|
||||
export interface HaIconButtonToolbarItem {
|
||||
[key: string]: any;
|
||||
path: string;
|
||||
label: string;
|
||||
id?: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
action?: (e: Event) => any;
|
||||
}
|
||||
|
||||
@customElement("ha-icon-button-toolbar")
|
||||
export class HaIconButtonToolbar extends LitElement {
|
||||
@property({ type: Array, attribute: false })
|
||||
public items: (HaIconButtonToolbarItem | string)[] = [];
|
||||
|
||||
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];
|
||||
|
||||
// Returns all toolbar buttons, or undefined if there are none.
|
||||
// Optionally returns only those with matching selector.
|
||||
public findToolbarButtons(selector = ""): HaIconButton[] | undefined {
|
||||
// Search for all toolbar buttons
|
||||
const toolbarButtons = this._buttons?.filter((button) =>
|
||||
button.classList.contains("icon-toolbar-button")
|
||||
);
|
||||
if (!toolbarButtons || !toolbarButtons.length) return undefined;
|
||||
if (!selector.length) return toolbarButtons;
|
||||
// Filter by user class if provided
|
||||
const classButtons = toolbarButtons.filter((button) =>
|
||||
button.querySelector(selector)
|
||||
);
|
||||
return classButtons.length ? classButtons : undefined;
|
||||
}
|
||||
|
||||
// Returns a toolbar button based on the provided id.
|
||||
// Will return undefined if not found.
|
||||
public findToolbarButtonById(id): HaIconButton | undefined {
|
||||
// Find the specified id
|
||||
const element = this.shadowRoot?.getElementById(id);
|
||||
if (!element || element.localName !== "ha-icon-button") return undefined;
|
||||
return element as HaIconButton;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button-group class="icon-toolbar-buttongroup">
|
||||
${this.items.map((item) =>
|
||||
typeof item === "string"
|
||||
? html`<div class="icon-toolbar-divider" role="separator"></div>`
|
||||
: html`<ha-tooltip
|
||||
.disabled=${!item.tooltip}
|
||||
.for=${item.id ?? "icon-button-" + item.label}
|
||||
>${item.tooltip ?? ""}</ha-tooltip
|
||||
>
|
||||
<ha-icon-button
|
||||
class="icon-toolbar-button"
|
||||
.id=${item.id ?? "icon-button-" + item.label}
|
||||
@click=${item.action}
|
||||
.label=${item.label}
|
||||
.path=${item.path}
|
||||
.disabled=${item.disabled ?? false}
|
||||
></ha-icon-button>`
|
||||
)}
|
||||
</ha-icon-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
background-color: var(
|
||||
--icon-button-toolbar-color,
|
||||
var(--secondary-background-color, whitesmoke)
|
||||
);
|
||||
--icon-button-toolbar-height: 32px;
|
||||
--icon-button-toolbar-button: calc(
|
||||
var(--icon-button-toolbar-height) - 4px
|
||||
);
|
||||
--icon-button-toolbar-icon: calc(
|
||||
var(--icon-button-toolbar-height) - 10px
|
||||
);
|
||||
}
|
||||
|
||||
.icon-toolbar-divider {
|
||||
height: var(--icon-button-toolbar-icon);
|
||||
margin: 0px 4px;
|
||||
border: 0.5px solid
|
||||
var(--divider-color, var(--secondary-text-color, transparent));
|
||||
}
|
||||
|
||||
.icon-toolbar-buttongroup {
|
||||
background-color: transparent;
|
||||
padding-right: 4px;
|
||||
height: var(--icon-button-toolbar-height);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-toolbar-button {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-icon-button-size: var(--icon-button-toolbar-button);
|
||||
--mdc-icon-size: var(--icon-button-toolbar-icon);
|
||||
/* Ensure button is clickable on iOS */
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-button-toolbar": HaIconButtonToolbar;
|
||||
}
|
||||
}
|
178
src/components/ha-marquee-text.ts
Normal file
178
src/components/ha-marquee-text.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
type TemplateResult,
|
||||
LitElement,
|
||||
html,
|
||||
css,
|
||||
type PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, eventOptions, property, query } from "lit/decorators";
|
||||
|
||||
@customElement("ha-marquee-text")
|
||||
export class HaMarqueeText extends LitElement {
|
||||
@property({ type: Number }) speed = 15; // pixels per second
|
||||
|
||||
@property({ type: Number, attribute: "pause-duration" }) pauseDuration = 1000; // ms delay at ends
|
||||
|
||||
@property({ type: Boolean, attribute: "pause-on-hover" })
|
||||
pauseOnHover = false;
|
||||
|
||||
private _direction: "left" | "right" = "left";
|
||||
|
||||
private _animationFrame?: number;
|
||||
|
||||
@query(".marquee-container")
|
||||
private _container?: HTMLDivElement;
|
||||
|
||||
@query(".marquee-text")
|
||||
private _textSpan?: HTMLSpanElement;
|
||||
|
||||
private _position = 0;
|
||||
|
||||
private _maxOffset = 0;
|
||||
|
||||
private _pauseTimeout?: number;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._setupAnimation();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("text")) {
|
||||
this._setupAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._animationFrame) {
|
||||
cancelAnimationFrame(this._animationFrame);
|
||||
}
|
||||
if (this._pauseTimeout) {
|
||||
clearTimeout(this._pauseTimeout);
|
||||
this._pauseTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="marquee-container"
|
||||
@mouseenter=${this._handleMouseEnter}
|
||||
@mouseleave=${this._handleMouseLeave}
|
||||
@touchstart=${this._handleMouseEnter}
|
||||
@touchend=${this._handleMouseLeave}
|
||||
>
|
||||
<span class="marquee-text"><slot></slot></span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _setupAnimation() {
|
||||
if (!this._container || !this._textSpan) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._position = 0;
|
||||
this._direction = "left";
|
||||
this._maxOffset = Math.max(
|
||||
0,
|
||||
this._textSpan.offsetWidth - this._container.offsetWidth
|
||||
);
|
||||
this._textSpan.style.transform = `translateX(0px)`;
|
||||
if (this._animationFrame) {
|
||||
cancelAnimationFrame(this._animationFrame);
|
||||
}
|
||||
if (this._pauseTimeout) {
|
||||
clearTimeout(this._pauseTimeout);
|
||||
this._pauseTimeout = undefined;
|
||||
}
|
||||
this._animate();
|
||||
}
|
||||
|
||||
private _animate = () => {
|
||||
if (!this._container || !this._textSpan) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dt = 1 / 60; // ~16ms per frame
|
||||
const pxPerFrame = this.speed * dt;
|
||||
let reachedEnd = false;
|
||||
if (this._direction === "left") {
|
||||
this._position -= pxPerFrame;
|
||||
if (this._position <= -this._maxOffset) {
|
||||
this._position = -this._maxOffset;
|
||||
this._direction = "right";
|
||||
reachedEnd = true;
|
||||
}
|
||||
} else {
|
||||
this._position += pxPerFrame;
|
||||
if (this._position >= 0) {
|
||||
this._position = 0;
|
||||
this._direction = "left";
|
||||
reachedEnd = true;
|
||||
}
|
||||
}
|
||||
this._textSpan.style.transform = `translateX(${this._position}px)`;
|
||||
if (reachedEnd) {
|
||||
this._pauseTimeout = window.setTimeout(() => {
|
||||
this._pauseTimeout = undefined;
|
||||
this._animationFrame = requestAnimationFrame(this._animate);
|
||||
}, this.pauseDuration);
|
||||
} else {
|
||||
this._animationFrame = requestAnimationFrame(this._animate);
|
||||
}
|
||||
};
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleMouseEnter() {
|
||||
if (this.pauseOnHover && this._animationFrame) {
|
||||
cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
if (this.pauseOnHover && this._pauseTimeout) {
|
||||
clearTimeout(this._pauseTimeout);
|
||||
this._pauseTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMouseLeave() {
|
||||
if (this.pauseOnHover && !this._animationFrame && !this._pauseTimeout) {
|
||||
this._animate();
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.marquee-container {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.marquee-text {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
will-change: transform;
|
||||
font-size: 1em;
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-marquee-text": HaMarqueeText;
|
||||
}
|
||||
}
|
@@ -168,16 +168,18 @@ export class HaMdDialog extends Dialog {
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host(:not([type="alert"])) {
|
||||
min-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
max-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
min-width: var(--mdc-dialog-min-width, 100vw);
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
--md-dialog-container-shape: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(ha-dialog-header[slot="headline"]) {
|
||||
|
@@ -16,6 +16,14 @@ export class HaRipple extends Ripple {
|
||||
this.attachableTouchController.attach(control);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
// @ts-ignore
|
||||
this.hovered = false;
|
||||
// @ts-ignore
|
||||
this.pressed = false;
|
||||
}
|
||||
|
||||
detach() {
|
||||
super.detach();
|
||||
this.attachableTouchController.detach();
|
||||
|
@@ -39,22 +39,24 @@ class HaSegmentedBar extends LitElement {
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
<div class="bar">
|
||||
${this.segments.map((segment) => {
|
||||
const bar = html`<div
|
||||
style=${styleMap({
|
||||
width: `${(segment.value / totalValue) * 100}%`,
|
||||
backgroundColor: segment.color,
|
||||
})}
|
||||
></div>`;
|
||||
return this.hideTooltip && !segment.label
|
||||
? bar
|
||||
: html`
|
||||
<ha-tooltip>
|
||||
<span slot="content">${segment.label}</span>
|
||||
${bar}
|
||||
</ha-tooltip>
|
||||
`;
|
||||
})}
|
||||
${this.segments.map(
|
||||
(segment, index) => html`
|
||||
${this.hideTooltip || !segment.label
|
||||
? nothing
|
||||
: html`
|
||||
<ha-tooltip for="segment-${index}" placement="top">
|
||||
${segment.label}
|
||||
</ha-tooltip>
|
||||
`}
|
||||
<div
|
||||
id="segment-${index}"
|
||||
style=${styleMap({
|
||||
width: `${(segment.value / totalValue) * 100}%`,
|
||||
backgroundColor: segment.color,
|
||||
})}
|
||||
></div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${this.hideLegend
|
||||
? nothing
|
||||
|
@@ -18,6 +18,8 @@ export class HaTabGroupTab extends Tab {
|
||||
opacity: 0.8;
|
||||
|
||||
color: inherit;
|
||||
|
||||
--wa-space-l: 16px;
|
||||
}
|
||||
|
||||
:host([active]:not([disabled])) {
|
||||
|
@@ -522,6 +522,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _handleExpand(ev) {
|
||||
const target = ev.currentTarget as any;
|
||||
const id = target.id.replace(/^expand-/, "");
|
||||
const newAreas: string[] = [];
|
||||
const newDevices: string[] = [];
|
||||
const newEntities: string[] = [];
|
||||
@@ -529,7 +530,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
if (target.type === "floor_id") {
|
||||
Object.values(this.hass.areas).forEach((area) => {
|
||||
if (
|
||||
area.floor_id === target.id &&
|
||||
area.floor_id === id &&
|
||||
!this.value!.area_id?.includes(area.area_id) &&
|
||||
this._areaMeetsFilter(area)
|
||||
) {
|
||||
@@ -539,7 +540,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
} else if (target.type === "area_id") {
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
if (
|
||||
device.area_id === target.id &&
|
||||
device.area_id === id &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
this._deviceMeetsFilter(device)
|
||||
) {
|
||||
@@ -548,7 +549,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.area_id === target.id &&
|
||||
entity.area_id === id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
@@ -558,7 +559,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
} else if (target.type === "device_id") {
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.device_id === target.id &&
|
||||
entity.device_id === id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
@@ -568,7 +569,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
} else if (target.type === "label_id") {
|
||||
Object.values(this.hass.areas).forEach((area) => {
|
||||
if (
|
||||
area.labels.includes(target.id) &&
|
||||
area.labels.includes(id) &&
|
||||
!this.value!.area_id?.includes(area.area_id) &&
|
||||
this._areaMeetsFilter(area)
|
||||
) {
|
||||
@@ -577,7 +578,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
if (
|
||||
device.labels.includes(target.id) &&
|
||||
device.labels.includes(id) &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
this._deviceMeetsFilter(device)
|
||||
) {
|
||||
@@ -586,7 +587,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.labels.includes(target.id) &&
|
||||
entity.labels.includes(id) &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity, true)
|
||||
) {
|
||||
@@ -606,14 +607,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
if (newAreas.length) {
|
||||
value = this._addItems(value, "area_id", newAreas);
|
||||
}
|
||||
value = this._removeItem(value, target.type, target.id);
|
||||
value = this._removeItem(value, target.type, id);
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _handleRemove(ev) {
|
||||
const target = ev.currentTarget as any;
|
||||
const id = target.id.replace(/^remove-/, "");
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._removeItem(this.value, target.type, target.id),
|
||||
value: this._removeItem(this.value, target.type, id),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,28 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant tile info component
|
||||
*
|
||||
* @element ha-tile-info
|
||||
*
|
||||
* @summary
|
||||
* A tile info component, used in tile card in Home Assistant to display primary and secondary text.
|
||||
*
|
||||
* @slot primary - The primary text container.
|
||||
* @slot secondary - The secondary text container.
|
||||
*
|
||||
* @cssprop --ha-tile-info-primary-font-size - The font size of the primary text. defaults to `var(--ha-font-size-m)`.
|
||||
* @cssprop --ha-tile-info-primary-font-weight - The font weight of the primary text. defaults to `var(--ha-font-weight-medium)`.
|
||||
* @cssprop --ha-tile-info-primary-line-height - The line height of the primary text. defaults to `var(--ha-line-height-normal)`.
|
||||
* @cssprop --ha-tile-info-primary-letter-spacing - The letter spacing of the primary text. defaults to `0.1px`.
|
||||
* @cssprop --ha-tile-info-primary-color - The color of the primary text. defaults to `var(--primary-text-color)`.
|
||||
* @cssprop --ha-tile-info-secondary-font-size - The font size of the secondary text. defaults to `var(--ha-font-size-s)`.
|
||||
* @cssprop --ha-tile-info-secondary-font-weight - The font weight of the secondary text. defaults to `var(--ha-font-weight-normal)`.
|
||||
* @cssprop --ha-tile-info-secondary-line-height - The line height of the secondary text. defaults to `var(--ha-line-height-condensed)`.
|
||||
* @cssprop --ha-tile-info-secondary-letter-spacing - The letter spacing of the secondary text. defaults to `0.4px`.
|
||||
* @cssprop --ha-tile-info-secondary-color - The color of the secondary text. defaults to `var(--primary-text-color)`.
|
||||
*/
|
||||
@customElement("ha-tile-info")
|
||||
export class HaTileInfo extends LitElement {
|
||||
@property() public primary?: string;
|
||||
@@ -21,6 +43,48 @@ export class HaTileInfo extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--tile-info-primary-font-size: var(
|
||||
--ha-tile-info-primary-font-size,
|
||||
var(--ha-font-size-m)
|
||||
);
|
||||
--tile-info-primary-font-weight: var(
|
||||
--ha-tile-info-primary-font-weight,
|
||||
var(--ha-font-weight-medium)
|
||||
);
|
||||
--tile-info-primary-line-height: var(
|
||||
--ha-tile-info-primary-line-height,
|
||||
var(--ha-line-height-normal)
|
||||
);
|
||||
--tile-info-primary-letter-spacing: var(
|
||||
--ha-tile-info-primary-letter-spacing,
|
||||
0.1px
|
||||
);
|
||||
--tile-info-primary-color: var(
|
||||
--ha-tile-info-primary-color,
|
||||
var(--primary-text-color)
|
||||
);
|
||||
--tile-info-secondary-font-size: var(
|
||||
--ha-tile-info-secondary-font-size,
|
||||
var(--ha-font-size-s)
|
||||
);
|
||||
--tile-info-secondary-font-weight: var(
|
||||
--ha-tile-info-secondary-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
--tile-info-secondary-line-height: var(
|
||||
--ha-tile-info-secondary-line-height,
|
||||
var(--ha-line-height-condensed)
|
||||
);
|
||||
--tile-info-secondary-letter-spacing: var(
|
||||
--ha-tile-info-secondary-letter-spacing,
|
||||
0.4px
|
||||
);
|
||||
--tile-info-secondary-color: var(
|
||||
--ha-tile-info-secondary-color,
|
||||
var(--primary-text-color)
|
||||
);
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -36,18 +100,18 @@ export class HaTileInfo extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
.primary {
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--tile-info-primary-font-size);
|
||||
font-weight: var(--tile-info-primary-font-weight);
|
||||
line-height: var(--tile-info-primary-line-height);
|
||||
letter-spacing: var(--tile-info-primary-letter-spacing);
|
||||
color: var(--tile-info-primary-color);
|
||||
}
|
||||
.secondary {
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
letter-spacing: 0.4px;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--tile-info-secondary-font-size);
|
||||
font-weight: var(--tile-info-secondary-font-weight);
|
||||
line-height: var(--tile-info-secondary-line-height);
|
||||
letter-spacing: var(--tile-info-secondary-letter-spacing);
|
||||
color: var(--tile-info-secondary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -29,9 +29,15 @@ export type AddonState =
|
||||
| null;
|
||||
export type AddonRepository = "core" | "local" | string;
|
||||
|
||||
interface AddonFieldTranslation {
|
||||
name?: string;
|
||||
description?: string;
|
||||
fields?: Record<string, AddonFieldTranslation>;
|
||||
}
|
||||
|
||||
interface AddonTranslations {
|
||||
network?: Record<string, string>;
|
||||
configuration?: Record<string, { name?: string; description?: string }>;
|
||||
configuration?: Record<string, AddonFieldTranslation>;
|
||||
}
|
||||
|
||||
export interface HassioAddonInfo {
|
||||
|
@@ -279,3 +279,5 @@ export const setSupervisorOption = async (
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
export const coreLatestLogsUrl = "/api/hassio/core/logs/latest";
|
||||
|
@@ -4,6 +4,7 @@ export interface LovelaceBadgeConfig {
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
visibility?: Condition[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ensureBadgeConfig = (
|
||||
|
@@ -5,17 +5,20 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { Describe } from "superstruct";
|
||||
import {
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
array,
|
||||
assign,
|
||||
boolean,
|
||||
object,
|
||||
optional,
|
||||
refine,
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { arrayLiteralIncludes } from "../common/array/literal-includes";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
Condition,
|
||||
@@ -26,9 +29,6 @@ import type {
|
||||
} from "./automation";
|
||||
import { migrateAutomationTrigger } from "./automation";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
|
||||
export const MODES = ["single", "restart", "queued", "parallel"] as const;
|
||||
export const MODES_MAX = ["queued", "parallel"] as const;
|
||||
@@ -395,6 +395,37 @@ export const hasScriptFields = (
|
||||
return fields !== undefined && Object.keys(fields).length > 0;
|
||||
};
|
||||
|
||||
export const hasRequiredScriptFields = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): boolean => {
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
return (
|
||||
fields !== undefined &&
|
||||
Object.values(fields).some((field) => field.required)
|
||||
);
|
||||
};
|
||||
|
||||
export const requiredScriptFieldsFilled = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
data?: Record<string, any>
|
||||
): boolean => {
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
if (fields === undefined || Object.keys(fields).length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (data === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Object.entries(fields).every(([key, field]) => {
|
||||
if (field.required) {
|
||||
return data[key] !== undefined;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export const migrateAutomationAction = (
|
||||
action: Action | Action[]
|
||||
): Action | Action[] => {
|
||||
|
@@ -4,7 +4,7 @@ import type { Action } from "./script";
|
||||
export const callExecuteScript = (
|
||||
hass: HomeAssistant,
|
||||
sequence: Action | Action[]
|
||||
): Promise<{ context: Context; response: Record<string, any> }> =>
|
||||
): Promise<{ context: Context; response: Record<string, any> | null }> =>
|
||||
hass.callWS({
|
||||
type: "execute_script",
|
||||
sequence,
|
||||
|
@@ -34,6 +34,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
|
||||
"valve",
|
||||
"water_heater",
|
||||
"weather",
|
||||
"media_player",
|
||||
];
|
||||
/** Domains with full height more info dialog */
|
||||
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
mdiLoginVariant,
|
||||
mdiMusicNote,
|
||||
mdiMusicNoteEighth,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiSpeakerMultiple,
|
||||
mdiVolumeHigh,
|
||||
@@ -8,11 +9,13 @@ import {
|
||||
mdiVolumeOff,
|
||||
mdiVolumePlus,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { formatDurationDigital } from "../../../common/datetime/format_duration";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-select";
|
||||
@@ -27,12 +30,17 @@ import type {
|
||||
MediaPlayerEntity,
|
||||
} from "../../../data/media-player";
|
||||
import {
|
||||
MediaPlayerEntityFeature,
|
||||
computeMediaControls,
|
||||
handleMediaControlClick,
|
||||
MediaPlayerEntityFeature,
|
||||
mediaPlayerPlayMedia,
|
||||
} from "../../../data/media-player";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
|
||||
import "../../../components/ha-md-button-menu";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-marquee-text";
|
||||
|
||||
@customElement("more-info-media_player")
|
||||
class MoreInfoMediaPlayer extends LitElement {
|
||||
@@ -40,259 +48,488 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: MediaPlayerEntity;
|
||||
|
||||
private _formateDuration(duration: number) {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
const seconds = duration % 60;
|
||||
return formatDurationDigital(this.hass.locale, {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
})!;
|
||||
}
|
||||
|
||||
protected _renderVolumeControl() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsMute = supportsFeature(
|
||||
this.stateObj,
|
||||
MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
);
|
||||
const supportsSliding = supportsFeature(
|
||||
this.stateObj,
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
);
|
||||
|
||||
return html`${(supportsFeature(
|
||||
this.stateObj!,
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
) ||
|
||||
supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) &&
|
||||
stateActive(this.stateObj!)
|
||||
? html`
|
||||
<div class="volume">
|
||||
${supportsMute
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${this.stateObj.attributes.is_volume_muted
|
||||
? mdiVolumeOff
|
||||
: mdiVolumeHigh}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${
|
||||
this.stateObj.attributes.is_volume_muted
|
||||
? "media_volume_unmute"
|
||||
: "media_volume_mute"
|
||||
}`
|
||||
)}
|
||||
@click=${this._toggleMute}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(
|
||||
this.stateObj,
|
||||
MediaPlayerEntityFeature.VOLUME_STEP
|
||||
) && !supportsSliding
|
||||
? html`
|
||||
<ha-icon-button
|
||||
action="volume_down"
|
||||
.path=${mdiVolumeMinus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_down"
|
||||
)}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
action="volume_up"
|
||||
.path=${mdiVolumePlus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_up"
|
||||
)}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${supportsSliding
|
||||
? html`
|
||||
${!supportsMute
|
||||
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
|
||||
: nothing}
|
||||
<ha-slider
|
||||
labeled
|
||||
id="input"
|
||||
.value=${Number(this.stateObj.attributes.volume_level) *
|
||||
100}
|
||||
@change=${this._selectedValueChanged}
|
||||
></ha-slider>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}`;
|
||||
}
|
||||
|
||||
protected _renderSourceControl() {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) ||
|
||||
!this.stateObj.attributes.source_list?.length
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-md-button-menu positioning="popover">
|
||||
<ha-button
|
||||
slot="trigger"
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
size="small"
|
||||
title=${this.hass.localize(`ui.card.media_player.source`)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLoginVariant}></ha-svg-icon>
|
||||
</ha-button>
|
||||
${this.stateObj.attributes.source_list!.map(
|
||||
(source) =>
|
||||
html`<ha-md-menu-item
|
||||
data-source=${source}
|
||||
@click=${this._handleSourceClick}
|
||||
@keydown=${this._handleSourceClick}
|
||||
.selected=${source === this.stateObj?.attributes.source}
|
||||
>
|
||||
${source}
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
</ha-md-button-menu>`;
|
||||
}
|
||||
|
||||
protected _renderSoundMode() {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!supportsFeature(
|
||||
this.stateObj,
|
||||
MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
) ||
|
||||
!this.stateObj.attributes.sound_mode_list?.length
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-md-button-menu positioning="popover">
|
||||
<ha-button
|
||||
slot="trigger"
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
size="small"
|
||||
title=${this.hass.localize(`ui.card.media_player.sound_mode`)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiMusicNoteEighth}></ha-svg-icon>
|
||||
</ha-button>
|
||||
${this.stateObj.attributes.sound_mode_list!.map(
|
||||
(soundMode) =>
|
||||
html`<ha-md-menu-item
|
||||
data-sound-mode=${soundMode}
|
||||
@click=${this._handleSoundModeClick}
|
||||
@keydown=${this._handleSoundModeClick}
|
||||
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
|
||||
>
|
||||
${soundMode}
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
</ha-md-button-menu>`;
|
||||
}
|
||||
|
||||
protected _renderGrouping() {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
isUnavailableState(this.stateObj.state) ||
|
||||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.GROUPING)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
const groupMembers = this.stateObj.attributes.group_members;
|
||||
const hasMultipleMembers = groupMembers && groupMembers?.length > 1;
|
||||
|
||||
return html`<ha-button
|
||||
class="grouping"
|
||||
@click=${this._showGroupMediaPlayers}
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
size="small"
|
||||
title=${this.hass.localize("ui.card.media_player.join")}
|
||||
>
|
||||
<div>
|
||||
<ha-svg-icon .path=${mdiSpeakerMultiple}></ha-svg-icon>
|
||||
${hasMultipleMembers
|
||||
? html`<span class="badge"> ${groupMembers?.length || 4} </span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-button>`;
|
||||
}
|
||||
|
||||
protected _renderEmptyCover(title: string, icon?: string) {
|
||||
return html`
|
||||
<div class="cover-container">
|
||||
<div class="cover-image empty-cover" role="img" aria-label=${title}>
|
||||
${icon ? html`<ha-svg-icon .path=${icon}></ha-svg-icon>` : title}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (isUnavailableState(this.stateObj.state)) {
|
||||
return this._renderEmptyCover(this.hass.formatEntityState(this.stateObj));
|
||||
}
|
||||
|
||||
const stateObj = this.stateObj;
|
||||
const controls = computeMediaControls(stateObj, true);
|
||||
const groupMembers = stateObj.attributes.group_members?.length;
|
||||
const coverUrl = stateObj.attributes.entity_picture || "";
|
||||
const playerObj = new HassMediaPlayerEntity(this.hass, this.stateObj);
|
||||
const position = Math.floor(playerObj.currentProgress) || 0;
|
||||
const duration = stateObj.attributes.media_duration || 0;
|
||||
const remaining = duration - position;
|
||||
const durationFormated =
|
||||
remaining > 0 ? this._formateDuration(remaining) : 0;
|
||||
const postionFormated = this._formateDuration(position);
|
||||
const primaryTitle = playerObj.primaryTitle;
|
||||
const secondaryTitle = playerObj.secondaryTitle;
|
||||
const turnOn = controls?.find((c) => c.action === "turn_on");
|
||||
const turnOff = controls?.find((c) => c.action === "turn_off");
|
||||
|
||||
return html`
|
||||
<div class="controls">
|
||||
<div class="basic-controls">
|
||||
${!controls
|
||||
? ""
|
||||
: controls.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
action=${control.action}
|
||||
@click=${this._handleClick}
|
||||
.path=${control.icon}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${!isUnavailableState(stateObj.state) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._showBrowseMedia}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiPlayBoxMultiple}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.card.media_player.browse_media")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
${!isUnavailableState(stateObj.state) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.GROUPING)
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._showGroupMediaPlayers}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiSpeakerMultiple}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${groupMembers && groupMembers > 1
|
||||
? html`<span class="badge">
|
||||
${stateObj.attributes.group_members?.length || 4}
|
||||
</span>`
|
||||
: nothing}
|
||||
${this.hass.localize("ui.card.media_player.join")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${(supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) ||
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)) &&
|
||||
stateActive(stateObj)
|
||||
${coverUrl
|
||||
? html`<div class="cover-container">
|
||||
<img
|
||||
class=${classMap({
|
||||
"cover-image": true,
|
||||
"cover-image--playing": stateObj.state === "playing",
|
||||
})}
|
||||
src=${coverUrl}
|
||||
alt=${ifDefined(primaryTitle)}
|
||||
/>
|
||||
</div>`
|
||||
: this._renderEmptyCover(
|
||||
this.hass.formatEntityState(this.stateObj),
|
||||
mdiMusicNote
|
||||
)}
|
||||
${primaryTitle || secondaryTitle
|
||||
? html`<div class="media-info-row">
|
||||
${primaryTitle
|
||||
? html`<ha-marquee-text
|
||||
class="media-title"
|
||||
speed="30"
|
||||
pause-on-hover
|
||||
>
|
||||
${primaryTitle}
|
||||
</ha-marquee-text>`
|
||||
: nothing}
|
||||
${secondaryTitle
|
||||
? html`<ha-marquee-text
|
||||
class="media-artist"
|
||||
speed="30"
|
||||
pause-on-hover
|
||||
>
|
||||
${secondaryTitle}
|
||||
</ha-marquee-text>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
${duration && duration > 0
|
||||
? html`
|
||||
<div class="volume">
|
||||
${supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_MUTE)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${stateObj.attributes.is_volume_muted
|
||||
? mdiVolumeOff
|
||||
: mdiVolumeHigh}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${
|
||||
stateObj.attributes.is_volume_muted
|
||||
? "media_volume_unmute"
|
||||
: "media_volume_mute"
|
||||
}`
|
||||
)}
|
||||
@click=${this._toggleMute}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(
|
||||
stateObj,
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
) ||
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
action="volume_down"
|
||||
.path=${mdiVolumeMinus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_down"
|
||||
)}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
action="volume_up"
|
||||
.path=${mdiVolumePlus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_up"
|
||||
)}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
|
||||
? html`
|
||||
<ha-slider
|
||||
labeled
|
||||
id="input"
|
||||
.value=${Number(stateObj.attributes.volume_level) * 100}
|
||||
@change=${this._selectedValueChanged}
|
||||
></ha-slider>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${stateActive(stateObj) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) &&
|
||||
stateObj.attributes.source_list?.length
|
||||
? html`
|
||||
<div class="source-input">
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.card.media_player.source")}
|
||||
icon
|
||||
.value=${stateObj.attributes.source!}
|
||||
@selected=${this._handleSourceChanged}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${stateObj.attributes.source_list!.map(
|
||||
(source) => html`
|
||||
<ha-list-item .value=${source}>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"source",
|
||||
source
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
<div class="position-bar">
|
||||
<ha-slider
|
||||
min="0"
|
||||
max=${duration}
|
||||
step="1"
|
||||
.value=${position}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.card.media_player.track_position"
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiLoginVariant} slot="icon"></ha-svg-icon>
|
||||
</ha-select>
|
||||
@change=${this._handleMediaSeekChanged}
|
||||
?disabled=${!stateActive(stateObj) ||
|
||||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
|
||||
></ha-slider>
|
||||
<div class="position-info-row">
|
||||
<span class="position-time">${postionFormated}</span>
|
||||
<span class="duration-time">${durationFormated}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${stateActive(stateObj) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) &&
|
||||
stateObj.attributes.sound_mode_list?.length
|
||||
? html`
|
||||
<div class="sound-input">
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.card.media_player.sound_mode")}
|
||||
.value=${stateObj.attributes.sound_mode!}
|
||||
icon
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleSoundModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${stateObj.attributes.sound_mode_list.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode}>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"sound_mode",
|
||||
mode
|
||||
<div class="bottom-controls">
|
||||
${controls && controls.length > 0
|
||||
? html`<div class="main-controls">
|
||||
${["repeat_set", "media_previous_track"].map((action) => {
|
||||
const control = controls?.find((c) => c.action === action);
|
||||
return control
|
||||
? html`<ha-icon-button
|
||||
action=${action}
|
||||
@click=${this._handleClick}
|
||||
.path=${control.icon}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: html`<span class="spacer"></span>`;
|
||||
})}
|
||||
${["media_play_pause", "media_pause", "media_play"].map(
|
||||
(action) => {
|
||||
const control = controls?.find((c) => c.action === action);
|
||||
return control
|
||||
? html`<ha-button
|
||||
variant="brand"
|
||||
appearance="filled"
|
||||
size="medium"
|
||||
action=${action}
|
||||
@click=${this._handleClick}
|
||||
class="center-control"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${control.icon}
|
||||
aria-label=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
></ha-svg-icon>
|
||||
</ha-button>`
|
||||
: nothing;
|
||||
}
|
||||
)}
|
||||
${["media_next_track", "shuffle_set"].map((action) => {
|
||||
const control = controls?.find((c) => c.action === action);
|
||||
return control
|
||||
? html`<ha-icon-button
|
||||
action=${action}
|
||||
@click=${this._handleClick}
|
||||
.path=${control.icon}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: html`<span class="spacer"></span>`;
|
||||
})}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._renderVolumeControl()}
|
||||
<div class="controls-row">
|
||||
${!isUnavailableState(stateObj.state) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._showBrowseMedia}
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
size="small"
|
||||
title=${this.hass.localize(
|
||||
"ui.card.media_player.browse_media"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlayBoxMultiple}></ha-svg-icon>
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
${this._renderGrouping()} ${this._renderSourceControl()}
|
||||
${this._renderSoundMode()}
|
||||
${turnOn
|
||||
? html`<ha-button
|
||||
action=${turnOn.action}
|
||||
@click=${this._handleClick}
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
size="small"
|
||||
title=${this.hass.localize(
|
||||
`ui.card.media_player.${turnOn.action}`
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiMusicNote} slot="icon"></ha-svg-icon>
|
||||
</ha-select>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
>
|
||||
<ha-svg-icon .path=${turnOn.icon}></ha-svg-icon>
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
${turnOff
|
||||
? html`<ha-button
|
||||
action=${turnOff.action}
|
||||
@click=${this._handleClick}
|
||||
appearance="plain"
|
||||
variant="neutral"
|
||||
size="small"
|
||||
title=${this.hass.localize(
|
||||
`ui.card.media_player.${turnOff.action}`
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${turnOff.icon}></ha-svg-icon>
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-slider {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
ha-icon-button[action="turn_off"],
|
||||
ha-icon-button[action="turn_on"] {
|
||||
margin-right: auto;
|
||||
margin-left: inherit;
|
||||
margin-inline-start: inherit;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
:host {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
--mdc-theme-primary: currentColor;
|
||||
direction: ltr;
|
||||
height: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.basic-controls {
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
.cover-image {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
width 0.3s,
|
||||
height 0.3s;
|
||||
}
|
||||
|
||||
.volume {
|
||||
direction: ltr;
|
||||
.cover-image--playing {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.source-input,
|
||||
.sound-input {
|
||||
direction: var(--direction);
|
||||
.empty-cover {
|
||||
background-color: var(--secondary-background-color);
|
||||
font-size: 1.5em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.volume,
|
||||
.source-input,
|
||||
.sound-input {
|
||||
.main-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.source-input ha-select,
|
||||
.sound-input ha-select {
|
||||
margin-left: 10px;
|
||||
flex-grow: 1;
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
.center-control {
|
||||
--ha-button-height: 56px;
|
||||
}
|
||||
|
||||
.tts {
|
||||
margin-top: 16px;
|
||||
font-style: italic;
|
||||
.spacer {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
ha-button > ha-svg-icon {
|
||||
vertical-align: text-bottom;
|
||||
.volume,
|
||||
.position-bar,
|
||||
.main-controls {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.volume ha-slider,
|
||||
.position-bar ha-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.volume {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.volume ha-svg-icon {
|
||||
padding: 4px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.volume ha-icon-button {
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 24px;
|
||||
top: -10px;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -301,9 +538,68 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
border-radius: 10px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
background-color: var(--accent-color);
|
||||
background-color: var(--primary-color);
|
||||
padding: 0 4px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.position-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.position-info-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
color: var(--secondary-text-color);
|
||||
padding: 0 8px;
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
|
||||
.media-info-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin: 8px 0 8px 8px;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.media-artist {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.controls-row ha-button {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.controls-row ha-svg-icon {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
|
||||
.grouping::part(label) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
align-self: center;
|
||||
width: 320px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -329,29 +625,29 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSourceChanged(e) {
|
||||
const newVal = e.target.value;
|
||||
|
||||
if (!newVal || this.stateObj!.attributes.source === newVal) {
|
||||
private _handleSourceClick(e: Event) {
|
||||
const source = (e.currentTarget as HTMLElement).getAttribute("data-source");
|
||||
if (!source || this.stateObj!.attributes.source === source) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("media_player", "select_source", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
source: newVal,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSoundModeChanged(e) {
|
||||
const newVal = e.target.value;
|
||||
|
||||
if (!newVal || this.stateObj?.attributes.sound_mode === newVal) {
|
||||
private _handleSoundModeClick(e: Event) {
|
||||
const soundMode = (e.currentTarget as HTMLElement).getAttribute(
|
||||
"data-sound-mode"
|
||||
);
|
||||
if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("media_player", "select_sound_mode", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
sound_mode: newVal,
|
||||
sound_mode: soundMode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -374,6 +670,18 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
entityId: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleMediaSeekChanged(e: Event): Promise<void> {
|
||||
if (!this.stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = (e.target as any).value;
|
||||
this.hass.callService("media_player", "media_seek", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
seek_position: newValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -3,20 +3,24 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-service-control";
|
||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import "../../../components/entity/state-info";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/entity/state-info";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { ScriptEntity } from "../../../data/script";
|
||||
import { canRun } from "../../../data/script";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||
import "../components/ha-more-info-state-header";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-service-control";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import type { ScriptEntity } from "../../../data/script";
|
||||
import {
|
||||
canRun,
|
||||
hasRequiredScriptFields,
|
||||
requiredScriptFieldsFilled,
|
||||
} from "../../../data/script";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-more-info-state-header";
|
||||
|
||||
@customElement("more-info-script")
|
||||
class MoreInfoScript extends LitElement {
|
||||
@@ -26,6 +30,8 @@ class MoreInfoScript extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
|
||||
|
||||
@property({ attribute: false }) public data?: Record<string, any>;
|
||||
|
||||
@state() private _scriptData: Record<string, any> = {};
|
||||
|
||||
@state() private narrow = false;
|
||||
@@ -110,7 +116,10 @@ class MoreInfoScript extends LitElement {
|
||||
hide-picker
|
||||
hide-description
|
||||
.hass=${this.hass}
|
||||
.value=${this._scriptData}
|
||||
.value=${{
|
||||
...(this.data ? { data: this.data } : {}),
|
||||
...this._scriptData,
|
||||
}}
|
||||
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||
.narrow=${this.narrow}
|
||||
@value-changed=${this._scriptDataChanged}
|
||||
@@ -198,7 +207,13 @@ class MoreInfoScript extends LitElement {
|
||||
|
||||
private _canRun() {
|
||||
if (
|
||||
canRun(this.stateObj!) ||
|
||||
!hasRequiredScriptFields(this.hass, this.stateObj!.entity_id) ||
|
||||
(requiredScriptFieldsFilled(
|
||||
this.hass,
|
||||
this.stateObj!.entity_id,
|
||||
this._scriptData.data
|
||||
) &&
|
||||
canRun(this.stateObj!)) ||
|
||||
// Restart can also always runs. Just cancels other run.
|
||||
this.stateObj!.attributes.mode === "restart"
|
||||
) {
|
||||
|
@@ -64,6 +64,7 @@ export interface MoreInfoDialogParams {
|
||||
view?: View;
|
||||
/** @deprecated Use `view` instead */
|
||||
tab?: View;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
type View = "info" | "history" | "settings" | "related";
|
||||
@@ -96,6 +97,8 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@state() private _entityId?: string | null;
|
||||
|
||||
@state() private _data?: Record<string, any>;
|
||||
|
||||
@state() private _currView: View = DEFAULT_VIEW;
|
||||
|
||||
@state() private _initialView: View = DEFAULT_VIEW;
|
||||
@@ -116,6 +119,8 @@ export class MoreInfoDialog extends LitElement {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
this._data = params.data;
|
||||
this._currView = params.view || DEFAULT_VIEW;
|
||||
this._initialView = params.view || DEFAULT_VIEW;
|
||||
this._childView = undefined;
|
||||
@@ -570,6 +575,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
.data=${this._data}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "history"
|
||||
|
@@ -59,9 +59,18 @@ export class MoreInfoHistory extends LitElement {
|
||||
|
||||
return html`${isComponentLoaded(this.hass, "history")
|
||||
? html`<div class="header">
|
||||
<h2>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.history")}
|
||||
</h2>
|
||||
<div>
|
||||
<h2>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.history")}
|
||||
</h2>
|
||||
${this._statistics
|
||||
? html`<div class="header-secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.aggregate"
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${__DEMO__
|
||||
? nothing
|
||||
: html`<a href=${this._showMoreHref}
|
||||
@@ -246,6 +255,10 @@ export class MoreInfoHistory extends LitElement {
|
||||
a:visited {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.header-secondary {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
@@ -27,6 +27,8 @@ export class MoreInfoInfo extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public editMode?: boolean;
|
||||
|
||||
@property({ attribute: false }) public data?: Record<string, any>;
|
||||
|
||||
@state() private _sensorNumericDeviceClasses?: string[] = [];
|
||||
|
||||
private async _loadNumericDeviceClasses() {
|
||||
@@ -105,6 +107,7 @@ export class MoreInfoInfo extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.entry=${this.entry}
|
||||
.editMode=${this.editMode}
|
||||
.data=${this.data}
|
||||
></more-info-content>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -6,14 +6,14 @@ import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import "../../components/ha-badge";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
|
||||
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
|
||||
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
|
||||
import type { TileCardConfig } from "../../panels/lovelace/cards/types";
|
||||
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
|
||||
import "../../panels/lovelace/sections/hui-section";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { stateMoreInfoType } from "./state_more_info_control";
|
||||
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
|
||||
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
|
||||
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
|
||||
|
||||
@customElement("more-info-content")
|
||||
class MoreInfoContent extends LitElement {
|
||||
@@ -25,6 +25,8 @@ class MoreInfoContent extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public editMode?: boolean;
|
||||
|
||||
@property({ attribute: false }) public data?: Record<string, any>;
|
||||
|
||||
protected render() {
|
||||
let moreInfoType: string | undefined;
|
||||
|
||||
@@ -48,6 +50,7 @@ class MoreInfoContent extends LitElement {
|
||||
stateObj: this.stateObj,
|
||||
entry: this.entry,
|
||||
editMode: this.editMode,
|
||||
data: this.data,
|
||||
})}
|
||||
${this._showEntityMembers(this.stateObj)
|
||||
? html`
|
||||
|
@@ -104,6 +104,16 @@ const _SHORTCUTS: Section[] = [
|
||||
descriptionTranslationKey:
|
||||
"ui.dialogs.shortcuts.automation_script.save",
|
||||
},
|
||||
{
|
||||
shortcut: [CTRL_CMD, "Z"],
|
||||
descriptionTranslationKey:
|
||||
"ui.dialogs.shortcuts.automation_script.undo",
|
||||
},
|
||||
{
|
||||
shortcut: [CTRL_CMD, "Y"],
|
||||
descriptionTranslationKey:
|
||||
"ui.dialogs.shortcuts.automation_script.redo",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -22,6 +22,7 @@ import { demoPanels } from "./demo_panels";
|
||||
import { demoServices } from "./demo_services";
|
||||
import type { Entity } from "./entity";
|
||||
import { getEntity } from "./entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
|
||||
const ensureArray = <T>(val: T | T[]): T[] =>
|
||||
Array.isArray(val) ? val : [val];
|
||||
@@ -147,6 +148,17 @@ export const provideHass = (
|
||||
} else {
|
||||
updateStates(states);
|
||||
}
|
||||
|
||||
for (const ent of ensureArray(newEntities)) {
|
||||
hass().entities[ent.entityId] = {
|
||||
entity_id: ent.entityId,
|
||||
name: ent.name,
|
||||
icon: ent.icon,
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
} satisfies EntityRegistryDisplayEntry;
|
||||
}
|
||||
|
||||
updateFormatFunctions();
|
||||
}
|
||||
|
||||
|
@@ -882,12 +882,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-max-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
--mdc-dialog-min-height: 100%;
|
||||
--mdc-dialog-max-height: 100%;
|
||||
--vertical-align-dialog: flex-end;
|
||||
|
60
src/mixins/undo-redo-mixin.ts
Normal file
60
src/mixins/undo-redo-mixin.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { LitElement } from "lit";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
export const UndoRedoMixin = <T extends Constructor<LitElement>, ConfigType>(
|
||||
superClass: T
|
||||
) => {
|
||||
class UndoRedoClass extends superClass {
|
||||
private _undoStack: ConfigType[] = [];
|
||||
|
||||
private _redoStack: ConfigType[] = [];
|
||||
|
||||
protected _undoStackLimit = 75;
|
||||
|
||||
protected pushToUndo(config: ConfigType) {
|
||||
if (this._undoStack.length >= this._undoStackLimit) {
|
||||
this._undoStack.shift();
|
||||
}
|
||||
this._undoStack.push({ ...config });
|
||||
this._redoStack = [];
|
||||
}
|
||||
|
||||
public undo() {
|
||||
const currentConfig = this.currentConfig;
|
||||
if (this._undoStack.length === 0 || !currentConfig) {
|
||||
return;
|
||||
}
|
||||
this._redoStack.push({ ...currentConfig });
|
||||
const config = this._undoStack.pop()!;
|
||||
this.applyUndoRedo(config);
|
||||
}
|
||||
|
||||
public redo() {
|
||||
const currentConfig = this.currentConfig;
|
||||
if (this._redoStack.length === 0 || !currentConfig) {
|
||||
return;
|
||||
}
|
||||
this._undoStack.push({ ...currentConfig });
|
||||
const config = this._redoStack.pop()!;
|
||||
this.applyUndoRedo(config);
|
||||
}
|
||||
|
||||
public get canUndo(): boolean {
|
||||
return this._undoStack.length > 0;
|
||||
}
|
||||
|
||||
public get canRedo(): boolean {
|
||||
return this._redoStack.length > 0;
|
||||
}
|
||||
|
||||
protected get currentConfig(): ConfigType | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected applyUndoRedo(_: ConfigType) {
|
||||
throw new Error("applyUndoRedo not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
return UndoRedoClass;
|
||||
};
|
@@ -1,6 +1,7 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircleCheck,
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentCopy,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { dump } from "js-yaml";
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -73,9 +74,10 @@ import {
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { rowStyles } from "../styles";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "./ha-automation-action-editor";
|
||||
import type HaAutomationActionEditor from "./ha-automation-action-editor";
|
||||
import "./types/ha-automation-action-choose";
|
||||
@@ -225,6 +227,20 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _renderOverflowLabel(label: string, shortcut?: TemplateResult) {
|
||||
return html`
|
||||
<div class="overflow-label">
|
||||
${label}
|
||||
${this.optionsInSidebar && !this.narrow
|
||||
? shortcut ||
|
||||
html`<span
|
||||
class="shortcut-placeholder ${isMac ? "mac" : ""}"
|
||||
></span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
@@ -272,136 +288,200 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
${!this.optionsInSidebar
|
||||
? html`<ha-md-button-menu
|
||||
quick
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._runAction}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.run"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlusCircleMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-button-menu
|
||||
quick
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._runAction}>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize("ui.panel.config.automation.editor.actions.run")
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlusCircleMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || !!this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>C</span>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || !!this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>X</span>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this._uiModeAvailable || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${!this.optionsInSidebar
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || !!this.first}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || !!this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this._uiModeAvailable || !!this._warnings}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.action.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${this.action.enabled === false ? "enable" : "disable"}`
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.del"
|
||||
)}</span
|
||||
>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.action.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.action.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>`
|
||||
: nothing}
|
||||
${!this.optionsInSidebar
|
||||
? html`${this._warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
@@ -516,7 +596,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
const enabled = !(this.action.enabled ?? true);
|
||||
const value = { ...this.action, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
}
|
||||
|
||||
if (this._yamlMode && !this.optionsInSidebar) {
|
||||
this._actionEditor?.yamlEditor?.setValue(value);
|
||||
@@ -680,7 +763,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
private _toggleYamlMode = (item?: HTMLElement) => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
@@ -689,6 +772,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
this.expand();
|
||||
} else if (item) {
|
||||
this.openSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -804,7 +889,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._automationRowElement?.focus();
|
||||
}
|
||||
|
||||
static styles = rowStyles;
|
||||
static styles = [rowStyles, overflowStyles];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -111,8 +111,6 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentCopy,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -56,9 +57,10 @@ import {
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { rowStyles } from "../styles";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "./ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
|
||||
import "./types/ha-automation-condition-and";
|
||||
@@ -162,6 +164,20 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
private _renderOverflowLabel(label: string, shortcut?: TemplateResult) {
|
||||
return html`
|
||||
<div class="overflow-label">
|
||||
${label}
|
||||
${this.optionsInSidebar && !this.narrow
|
||||
? shortcut ||
|
||||
html`<span
|
||||
class="shortcut-placeholder ${isMac ? "mac" : ""}"
|
||||
></span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
@@ -177,140 +193,198 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html`<ha-md-button-menu
|
||||
quick
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-md-button-menu
|
||||
quick
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._testCondition}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._testCondition}>
|
||||
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.rename"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlusCircleMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlusCircleMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon
|
||||
>${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>C</span>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon
|
||||
>${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>X</span>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
${!this.optionsInSidebar
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._toggleYamlMode}>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${this._uiSupported(this.condition.condition) ||
|
||||
!!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.condition.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.condition.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.condition.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>`
|
||||
: nothing}
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${this.condition.enabled === false ? "enable" : "disable"}`
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.del"
|
||||
)}</span
|
||||
>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
${!this.optionsInSidebar
|
||||
? html`${this._warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
@@ -447,7 +521,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
const enabled = !(this.condition.enabled ?? true);
|
||||
const value = { ...this.condition, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
}
|
||||
|
||||
if (this._yamlMode && !this.optionsInSidebar) {
|
||||
this.conditionEditor?.yamlEditor?.setValue(value);
|
||||
@@ -490,9 +567,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._testing = true;
|
||||
const condition = this.condition;
|
||||
requestAnimationFrame(() => {
|
||||
// @ts-ignore is supported in all browsers expect firefox
|
||||
// @ts-ignore is supported in all browsers except firefox
|
||||
if (this.scrollIntoViewIfNeeded) {
|
||||
// @ts-ignore is supported in all browsers expect firefox
|
||||
// @ts-ignore is supported in all browsers except firefox
|
||||
this.scrollIntoViewIfNeeded();
|
||||
return;
|
||||
}
|
||||
@@ -626,7 +703,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
private _toggleYamlMode = (item?: HTMLElement) => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
@@ -635,6 +712,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
this.expand();
|
||||
} else if (item) {
|
||||
this.openSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -742,6 +821,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
rowStyles,
|
||||
overflowStyles,
|
||||
css`
|
||||
.testing {
|
||||
position: absolute;
|
||||
|
@@ -11,11 +11,13 @@ import {
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiPlusCircleMultipleOutline,
|
||||
mdiRedo,
|
||||
mdiRenameBox,
|
||||
mdiRobotConfused,
|
||||
mdiStopCircleOutline,
|
||||
mdiTag,
|
||||
mdiTransitConnection,
|
||||
mdiUndo,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
@@ -84,6 +86,7 @@ import {
|
||||
import "./blueprint-automation-editor";
|
||||
import "./manual-automation-editor";
|
||||
import type { HaManualAutomationEditor } from "./manual-automation-editor";
|
||||
import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -106,9 +109,12 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
KeyboardShortcutMixin(LitElement)
|
||||
) {
|
||||
const baseEditorMixins = PreventUnsavedMixin(KeyboardShortcutMixin(LitElement));
|
||||
|
||||
export class HaAutomationEditor extends UndoRedoMixin<
|
||||
typeof baseEditorMixins,
|
||||
AutomationConfig
|
||||
>(baseEditorMixins) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public automationId: string | null = null;
|
||||
@@ -218,6 +224,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.header=${this._config.alias ||
|
||||
this.hass.localize("ui.panel.config.automation.editor.default_name")}
|
||||
>
|
||||
${this._mode === "gui" && !this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.undo")}
|
||||
.path=${mdiUndo}
|
||||
@click=${this.undo}
|
||||
.disabled=${!this.canUndo}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.redo")}
|
||||
.path=${mdiRedo}
|
||||
@click=${this.redo}
|
||||
.disabled=${!this.canRedo}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
${this._config?.id && !this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
@@ -239,6 +263,25 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${this._mode === "gui" && this.narrow
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this.undo}
|
||||
.disabled=${!this.canUndo}
|
||||
>
|
||||
${this.hass.localize("ui.common.undo")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this.redo}
|
||||
.disabled=${!this.canRedo}
|
||||
>
|
||||
${this.hass.localize("ui.common.redo")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${!stateObj}
|
||||
@@ -443,6 +486,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
@undo-paste=${this.undo}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
@@ -549,7 +593,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
copy-clipboard
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
@@ -715,6 +758,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this._config) {
|
||||
this.pushToUndo(this._config);
|
||||
}
|
||||
|
||||
this._config = ev.detail.value;
|
||||
if (this._readOnly) {
|
||||
return;
|
||||
@@ -1123,6 +1170,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
x: () => this._cutSelectedRow(),
|
||||
Delete: () => this._deleteSelectedRow(),
|
||||
Backspace: () => this._deleteSelectedRow(),
|
||||
z: () => this.undo(),
|
||||
y: () => this.redo(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1156,6 +1205,16 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this._manualEditor?.deleteSelectedRow();
|
||||
}
|
||||
|
||||
protected get currentConfig() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
protected applyUndoRedo(config: AutomationConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -171,7 +171,7 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleMouseDown}
|
||||
>
|
||||
${this._resizing ? html`<div class="indicator"></div>` : nothing}
|
||||
<div class="indicator ${this._resizing ? "" : "hidden"}"></div>
|
||||
</div>
|
||||
${this._renderContent()}
|
||||
`;
|
||||
@@ -333,6 +333,15 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
transform: scale3d(1, 1, 1);
|
||||
opacity: 1;
|
||||
transition:
|
||||
transform 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
}
|
||||
.handle .indicator.hidden {
|
||||
transform: scale3d(0, 1, 1);
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -117,8 +117,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
HaAutomationAction | HaAutomationCondition
|
||||
>;
|
||||
|
||||
private _previousConfig?: ManualAutomationConfig;
|
||||
|
||||
private _prevSidebarWidthPx?: number;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -177,7 +175,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
.disabled=${this.disabled || this.saving}
|
||||
.narrow=${this.narrow}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@request-close-sidebar=${this._triggerCloseSidebar}
|
||||
@request-close-sidebar=${this.triggerCloseSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
root
|
||||
sidebar
|
||||
@@ -224,7 +222,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
.disabled=${this.disabled || this.saving}
|
||||
.narrow=${this.narrow}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@request-close-sidebar=${this._triggerCloseSidebar}
|
||||
@request-close-sidebar=${this.triggerCloseSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
root
|
||||
sidebar
|
||||
@@ -266,7 +264,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
.highlightedActions=${this._pastedConfig?.actions}
|
||||
@value-changed=${this._actionChanged}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@request-close-sidebar=${this._triggerCloseSidebar}
|
||||
@request-close-sidebar=${this.triggerCloseSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@@ -370,7 +368,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _triggerCloseSidebar() {
|
||||
public triggerCloseSidebar() {
|
||||
if (this._sidebarConfig) {
|
||||
if (this._sidebarElement) {
|
||||
this._sidebarElement.triggerCloseSidebar();
|
||||
@@ -412,7 +410,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _saveAutomation() {
|
||||
this._triggerCloseSidebar();
|
||||
this.triggerCloseSidebar();
|
||||
fireEvent(this, "save-automation");
|
||||
}
|
||||
|
||||
@@ -526,9 +524,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
["triggers", "conditions", "actions"].includes(keysPresent[0])
|
||||
) {
|
||||
// if only one type of element is pasted, insert under the currently active item
|
||||
const previousConfig = { ...this.config };
|
||||
if (this._tryInsertAfterSelected(normalized[keysPresent[0]])) {
|
||||
this._previousConfig = previousConfig;
|
||||
this._showPastedToastWithUndo();
|
||||
return;
|
||||
}
|
||||
@@ -565,26 +561,27 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
};
|
||||
|
||||
private _appendToExistingConfig(config: ManualAutomationConfig) {
|
||||
// make a copy otherwise we will reference the original config
|
||||
this._previousConfig = { ...this.config } as ManualAutomationConfig;
|
||||
this._pastedConfig = config;
|
||||
// make a copy otherwise we will modify the original config
|
||||
// which breaks the (referenced) config used for storing in undo stack
|
||||
const workingCopy: ManualAutomationConfig = { ...this.config };
|
||||
|
||||
if (!this.config) {
|
||||
if (!workingCopy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("triggers" in config) {
|
||||
this.config.triggers = ensureArray(this.config.triggers || []).concat(
|
||||
workingCopy.triggers = ensureArray(workingCopy.triggers || []).concat(
|
||||
ensureArray(config.triggers)
|
||||
);
|
||||
}
|
||||
if ("conditions" in config) {
|
||||
this.config.conditions = ensureArray(this.config.conditions || []).concat(
|
||||
workingCopy.conditions = ensureArray(workingCopy.conditions || []).concat(
|
||||
ensureArray(config.conditions)
|
||||
);
|
||||
}
|
||||
if ("actions" in config) {
|
||||
this.config.actions = ensureArray(this.config.actions || []).concat(
|
||||
workingCopy.actions = ensureArray(workingCopy.actions || []).concat(
|
||||
ensureArray(config.actions)
|
||||
) as Action[];
|
||||
}
|
||||
@@ -593,22 +590,19 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config!,
|
||||
...workingCopy!,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _replaceExistingConfig(config: ManualAutomationConfig) {
|
||||
// make a copy otherwise we will reference the original config
|
||||
this._previousConfig = { ...this.config } as ManualAutomationConfig;
|
||||
this._pastedConfig = config;
|
||||
this.config = config;
|
||||
|
||||
this._showPastedToastWithUndo();
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
...config,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -622,13 +616,8 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
action: {
|
||||
text: this.hass.localize("ui.common.undo"),
|
||||
action: () => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this._previousConfig!,
|
||||
},
|
||||
});
|
||||
fireEvent(this, "undo-paste");
|
||||
|
||||
this._previousConfig = undefined;
|
||||
this._pastedConfig = undefined;
|
||||
},
|
||||
},
|
||||
@@ -636,12 +625,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
}
|
||||
|
||||
public resetPastedConfig() {
|
||||
if (!this._previousConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pastedConfig = undefined;
|
||||
this._previousConfig = undefined;
|
||||
|
||||
showToast(this, {
|
||||
message: "",
|
||||
@@ -758,5 +742,6 @@ declare global {
|
||||
"open-sidebar": SidebarConfig;
|
||||
"request-close-sidebar": undefined;
|
||||
"close-sidebar": undefined;
|
||||
"undo-paste": undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiDelete,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
mdiPlusCircleMultipleOutline,
|
||||
mdiRenameBox,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -37,11 +38,17 @@ import {
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import "../action/ha-automation-action";
|
||||
import type HaAutomationAction from "../action/ha-automation-action";
|
||||
import "../condition/ha-automation-condition";
|
||||
import type HaAutomationCondition from "../condition/ha-automation-condition";
|
||||
import { editorStyles, indentStyle, rowStyles } from "../styles";
|
||||
import {
|
||||
editorStyles,
|
||||
indentStyle,
|
||||
overflowStyles,
|
||||
rowStyles,
|
||||
} from "../styles";
|
||||
|
||||
@customElement("ha-automation-option-row")
|
||||
export default class HaAutomationOptionRow extends LitElement {
|
||||
@@ -119,6 +126,20 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
return str;
|
||||
}
|
||||
|
||||
private _renderOverflowLabel(label: string, shortcut?: TemplateResult) {
|
||||
return html`
|
||||
<div class="overflow-label">
|
||||
${label}
|
||||
${this.optionsInSidebar && !this.narrow
|
||||
? shortcut ||
|
||||
html`<span
|
||||
class="shortcut-placeholder ${isMac ? "mac" : ""}"
|
||||
></span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
return html`
|
||||
<h3 slot="header">
|
||||
@@ -134,7 +155,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
${this.option && !this.optionsInSidebar
|
||||
${this.option
|
||||
? html`
|
||||
<ha-md-button-menu
|
||||
quick
|
||||
@@ -156,58 +177,92 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
@click=${this._renameOption}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
@click=${this._duplicateOption}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlusCircleMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
@click=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
@click=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${!this.optionsInSidebar
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || !!this.first}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiArrowUp}
|
||||
></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || !!this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiArrowDown}
|
||||
></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item
|
||||
@click=${this._removeOption}
|
||||
class="warning"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.del"
|
||||
)}</span
|
||||
>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
`
|
||||
@@ -309,25 +364,27 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _removeOption = () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete_confirm_text"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: null,
|
||||
});
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
},
|
||||
});
|
||||
if (this.option) {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete_confirm_text"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: null,
|
||||
});
|
||||
if (this._selected) {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _renameOption = async () => {
|
||||
@@ -457,6 +514,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
return [
|
||||
rowStyles,
|
||||
editorStyles,
|
||||
overflowStyles,
|
||||
indentStyle,
|
||||
css`
|
||||
li[role="separator"] {
|
||||
|
@@ -26,7 +26,7 @@ import { isMac } from "../../../../util/is_mac";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import { getAutomationActionType } from "../action/ha-automation-action-row";
|
||||
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
|
||||
import { sidebarEditorStyles } from "../styles";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "../trigger/ha-automation-trigger-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@@ -66,9 +66,8 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
protected render() {
|
||||
const actionConfig = this.config.config.action;
|
||||
|
||||
const disabled =
|
||||
this.disabled ||
|
||||
("enabled" in actionConfig && actionConfig.enabled === false);
|
||||
const rowDisabled =
|
||||
"enabled" in actionConfig && actionConfig.enabled === false;
|
||||
|
||||
const actionType = getAutomationActionType(actionConfig);
|
||||
|
||||
@@ -102,7 +101,13 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<span slot="subtitle"
|
||||
>${subtitle}${rowDisabled
|
||||
? html` (${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disabled"
|
||||
)})`
|
||||
: ""}</span
|
||||
>
|
||||
|
||||
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.run}>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
|
||||
@@ -114,7 +119,7 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${!!disabled}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
@@ -215,14 +220,18 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>
|
||||
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.disable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
.path=${rowDisabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
|
||||
`ui.panel.config.automation.editor.actions.${rowDisabled ? "enable" : "disable"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -315,7 +324,7 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
static styles = sidebarEditorStyles;
|
||||
static styles = [sidebarEditorStyles, overflowStyles];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -111,13 +111,9 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
<div class="card-content" @scroll=${this._onScroll}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
${this.narrow
|
||||
? html`
|
||||
<div
|
||||
class="fade ${this._contentScrollable ? "scrollable" : ""}"
|
||||
></div>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class=${classMap({ fade: true, scrollable: this._contentScrollable })}
|
||||
></div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -131,9 +127,13 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
}
|
||||
|
||||
private _canScrollDown(element: HTMLElement) {
|
||||
const safeAreaInsetBottom =
|
||||
parseFloat(
|
||||
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
|
||||
) || 0;
|
||||
this._contentScrollable =
|
||||
(element.scrollHeight ?? 0) - (element.clientHeight ?? 0) >
|
||||
(element.scrollTop ?? 0);
|
||||
(element.scrollTop ?? 0) + safeAreaInsetBottom + 16;
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
@@ -147,11 +147,13 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
@@ -183,30 +185,43 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
}
|
||||
|
||||
.fade {
|
||||
position: fixed;
|
||||
bottom: -12px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 12px;
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
height: 16px;
|
||||
pointer-events: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
transform: rotate(180deg);
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
.fade.scrollable {
|
||||
box-shadow: var(--bar-box-shadow);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
max-height: calc(100% - 80px);
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
|
||||
}
|
||||
|
||||
@media (min-width: 450px) and (min-height: 500px) {
|
||||
@media all and (max-width: 870px) {
|
||||
.fade {
|
||||
bottom: 0;
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
max-height: calc(100% - 104px);
|
||||
overflow: auto;
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -27,7 +27,7 @@ import { isMac } from "../../../../util/is_mac";
|
||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import "../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor";
|
||||
import { sidebarEditorStyles } from "../styles";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-condition")
|
||||
@@ -72,9 +72,8 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const disabled =
|
||||
this.disabled ||
|
||||
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||
const rowDisabled =
|
||||
"enabled" in this.config.config && this.config.config.enabled === false;
|
||||
|
||||
const type = this.config.config.condition;
|
||||
|
||||
@@ -103,7 +102,11 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<span slot="subtitle"
|
||||
>${subtitle}${rowDisabled
|
||||
? ` (${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")})`
|
||||
: ""}</span
|
||||
>
|
||||
<ha-md-menu-item slot="menu-items" .clickAction=${this._testCondition}>
|
||||
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
@@ -116,7 +119,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${!!disabled}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
@@ -221,14 +224,18 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>
|
||||
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.disable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
.path=${rowDisabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
|
||||
`ui.panel.config.automation.editor.actions.${rowDisabled ? "enable" : "disable"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -357,6 +364,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this._testing = false;
|
||||
this._testingResult = undefined;
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
@@ -395,6 +403,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
|
||||
static styles = [
|
||||
sidebarEditorStyles,
|
||||
overflowStyles,
|
||||
css`
|
||||
ha-automation-sidebar-card {
|
||||
position: relative;
|
||||
|
@@ -10,7 +10,7 @@ import type { OptionSidebarConfig } from "../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import { sidebarEditorStyles } from "../styles";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-option")
|
||||
@@ -127,7 +127,7 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
|
||||
static styles = sidebarEditorStyles;
|
||||
static styles = [sidebarEditorStyles, overflowStyles];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -8,7 +8,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import "../../script/ha-script-field-editor";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import { sidebarEditorStyles } from "../styles";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-script-field")
|
||||
@@ -153,7 +153,7 @@ export default class HaAutomationSidebarScriptField extends LitElement {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
static styles = sidebarEditorStyles;
|
||||
static styles = [sidebarEditorStyles, overflowStyles];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -3,7 +3,6 @@ import {
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiDelete,
|
||||
mdiIdentifier,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiPlusCircleMultipleOutline,
|
||||
@@ -19,7 +18,7 @@ import type { TriggerSidebarConfig } from "../../../../data/automation";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import { sidebarEditorStyles } from "../styles";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "../trigger/ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
@@ -40,8 +39,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
|
||||
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
@@ -49,7 +46,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._requestShowId = false;
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
@@ -61,7 +57,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const disabled =
|
||||
const rowDisabled =
|
||||
this.disabled ||
|
||||
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||
const type = isTriggerList(this.config.config)
|
||||
@@ -85,11 +81,15 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<span slot="subtitle">${subtitle}</span>
|
||||
<span slot="subtitle"
|
||||
>${subtitle}${rowDisabled
|
||||
? ` (${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")})`
|
||||
: ""}</span
|
||||
>
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.rename}
|
||||
.disabled=${disabled || type === "list"}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
@@ -99,23 +99,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
${!this.yamlMode &&
|
||||
!("id" in this.config.config) &&
|
||||
!this._requestShowId
|
||||
? html`<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${disabled || type === "list"}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-md-menu-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-md-divider
|
||||
slot="menu-items"
|
||||
@@ -211,15 +194,15 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
<ha-md-menu-item
|
||||
slot="menu-items"
|
||||
.clickAction=${this.config.disable}
|
||||
.disabled=${type === "list"}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
.path=${rowDisabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
|
||||
`ui.panel.config.automation.editor.actions.${rowDisabled ? "enable" : "disable"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -266,7 +249,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
@yaml-changed=${this._yamlChangedSidebar}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
.showId=${this._requestShowId}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@@ -309,11 +291,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
};
|
||||
|
||||
static styles = sidebarEditorStyles;
|
||||
static styles = [sidebarEditorStyles, overflowStyles];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -238,6 +238,9 @@ export const sidebarEditorStyles = css`
|
||||
.description {
|
||||
padding-top: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const overflowStyles = css`
|
||||
.overflow-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@@ -27,8 +27,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
@@ -37,7 +35,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
|
||||
|
||||
const yamlMode = this.yamlMode || !this.uiSupported;
|
||||
const showId = "id" in this.trigger || this.showId;
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -73,19 +70,23 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
${showId && !isTriggerList(this.trigger)
|
||||
${!isTriggerList(this.trigger)
|
||||
? html`
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
.label=${`${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id"
|
||||
)}
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.optional"
|
||||
)})`}
|
||||
.value=${this.trigger.id || ""}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._idChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id_helper"
|
||||
)}
|
||||
></ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||
hass: this.hass,
|
||||
@@ -144,9 +145,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.card-content.yaml {
|
||||
padding: 0 1px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiIdentifier,
|
||||
mdiInformation,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiPlusCircleMultipleOutline,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -38,6 +39,7 @@ import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Trigger,
|
||||
@@ -55,9 +57,10 @@ import {
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { rowStyles } from "../styles";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "./ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
@@ -133,8 +136,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@state() private _selected = false;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -163,6 +164,20 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
private _triggerUnsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _renderOverflowLabel(label: string, shortcut?: TemplateResult) {
|
||||
return html`
|
||||
<div class="overflow-label">
|
||||
${label}
|
||||
${this.optionsInSidebar && !this.narrow
|
||||
? shortcut ||
|
||||
html`<span
|
||||
class="shortcut-placeholder ${isMac ? "mac" : ""}"
|
||||
></span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const type = this._getType(this.trigger);
|
||||
|
||||
@@ -182,142 +197,192 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
${!this.optionsInSidebar
|
||||
? html`<ha-md-button-menu
|
||||
quick
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameTrigger}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-button-menu
|
||||
quick
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameTrigger}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlusCircleMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlusCircleMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>C</span>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>X</span>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
${!this.optionsInSidebar
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || !!this.first}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || !!this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!supported || !!this._warnings}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!supported || !!this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${"enabled" in this.trigger &&
|
||||
this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
class="warning"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>`
|
||||
: nothing}
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.${"enabled" in this.trigger && this.trigger.enabled === false ? "enable" : "disable"}`
|
||||
)
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
class="warning"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
),
|
||||
html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.del"
|
||||
)}</span
|
||||
>
|
||||
</span>`
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
${!this.optionsInSidebar
|
||||
? html`${this._warnings
|
||||
? html`<ha-automation-editor-warning
|
||||
@@ -331,7 +396,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.trigger=${this.trigger}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.showId=${this._requestShowId}
|
||||
.uiSupported=${supported}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-trigger-editor>`
|
||||
@@ -380,6 +444,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.triggered"
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiInformation}></ha-svg-icon>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -563,7 +628,10 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
const enabled = !(this.trigger.enabled ?? true);
|
||||
const value = { ...this.trigger, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
}
|
||||
|
||||
if (this._yamlMode && !this.optionsInSidebar) {
|
||||
this.triggerEditor?.yamlEditor?.setValue(value);
|
||||
@@ -630,14 +698,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
this.expand();
|
||||
}
|
||||
};
|
||||
|
||||
private _duplicateTrigger = () => {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
@@ -682,7 +742,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
private _toggleYamlMode = (item?: HTMLElement) => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
@@ -691,6 +751,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
this.expand();
|
||||
} else if (item) {
|
||||
this.openSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -716,6 +778,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
rowStyles,
|
||||
overflowStyles,
|
||||
css`
|
||||
.triggered {
|
||||
cursor: pointer;
|
||||
@@ -740,9 +803,20 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
}
|
||||
.triggered ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
.triggered.active {
|
||||
max-height: 100px;
|
||||
padding: 4px;
|
||||
}
|
||||
.triggered:hover {
|
||||
opacity: 0.8;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import { mdiDownload } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -26,6 +27,7 @@ import "./components/ha-energy-solar-settings";
|
||||
import "./components/ha-energy-battery-settings";
|
||||
import "./components/ha-energy-gas-settings";
|
||||
import "./components/ha-energy-water-settings";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
|
||||
const INITIAL_CONFIG: EnergyPreferences = {
|
||||
energy_sources: [],
|
||||
@@ -85,6 +87,14 @@ class HaConfigEnergy extends LitElement {
|
||||
: "/config/lovelace/dashboards"}
|
||||
.header=${this.hass.localize("ui.panel.config.energy.caption")}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiDownload}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.download_diagnostics"
|
||||
)}
|
||||
@click=${this._downloadDiagnostics}
|
||||
></ha-icon-button>
|
||||
<ha-alert>
|
||||
${this.hass.localize("ui.panel.config.energy.new_device_info")}
|
||||
</ha-alert>
|
||||
@@ -185,6 +195,32 @@ class HaConfigEnergy extends LitElement {
|
||||
this._statsMetadata = statsMetadata;
|
||||
}
|
||||
|
||||
private async _downloadDiagnostics() {
|
||||
const data = {
|
||||
version: this.hass.config.version,
|
||||
info: this._info,
|
||||
preferences: this._preferences,
|
||||
metadata: this._statsMetadata,
|
||||
entities: Object.fromEntries(
|
||||
Object.keys(this._statsMetadata || {}).map((key) => [
|
||||
key,
|
||||
this.hass.entities[key],
|
||||
])
|
||||
),
|
||||
states: Object.fromEntries(
|
||||
Object.keys(this._statsMetadata || {}).map((key) => [
|
||||
key,
|
||||
this.hass.states[key],
|
||||
])
|
||||
),
|
||||
};
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
fileDownload(url, "energy_diagnostics.json");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -255,10 +255,10 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
@click=${this._showRestartDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
: nothing}
|
||||
<div class="content">
|
||||
${boardName || isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
@@ -271,7 +271,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>`
|
||||
: ""}
|
||||
: nothing}
|
||||
<div class="board-info">
|
||||
<p class="primary-text">
|
||||
${boardName ||
|
||||
@@ -281,7 +281,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
</p>
|
||||
${boardId
|
||||
? html`<p class="secondary-text">${boardId}</p>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${documentationURL
|
||||
@@ -305,7 +305,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${boardConfigEntries.length ||
|
||||
isComponentLoaded(this.hass, "hassio")
|
||||
? html`<div class="card-actions">
|
||||
@@ -335,10 +335,10 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: nothing}
|
||||
</div>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${dongles?.length
|
||||
? html`<ha-card outlined>
|
||||
${dongles.map((dongle) => {
|
||||
@@ -358,11 +358,11 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.hardware.configure"
|
||||
)}
|
||||
</ha-button>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>`;
|
||||
})}
|
||||
</ha-card>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this._systemStatusData
|
||||
? html`<ha-card outlined>
|
||||
<div class="header">
|
||||
@@ -412,14 +412,15 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
: isComponentLoaded(this.hass, "hardware")
|
||||
? html`<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="value">
|
||||
<ha-alert alert-type="info">
|
||||
<ha-spinner slot="icon"></ha-spinner>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.loading_system_data"
|
||||
)}
|
||||
</div>
|
||||
</ha-alert>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -541,6 +542,14 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
--ha-alert-icon-size: 24px;
|
||||
}
|
||||
|
||||
ha-alert ha-spinner {
|
||||
--ha-spinner-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -17,11 +17,11 @@ import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/search-input";
|
||||
@@ -478,10 +478,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
</search-input-outlined>
|
||||
<div class="filters">
|
||||
${!this._showDisabled && disabledConfigEntries.length
|
||||
? html`<div
|
||||
class="active-filters"
|
||||
@click=${this._preventDefault}
|
||||
>
|
||||
? html`<div class="active-filters">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.disable.disabled_integrations",
|
||||
{ number: disabledConfigEntries.length }
|
||||
@@ -496,7 +493,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
)}
|
||||
</ha-button>
|
||||
</div>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${filterMenu}
|
||||
</div>
|
||||
</div>
|
||||
@@ -668,10 +665,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
`;
|
||||
}
|
||||
|
||||
private _preventDefault(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private async _scanUSBDevices() {
|
||||
if (!isComponentLoaded(this.hass, "usb")) {
|
||||
return;
|
||||
@@ -1036,13 +1029,10 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
}
|
||||
.active-filters {
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-right: 2px;
|
||||
padding-left: 8px;
|
||||
padding: 2px 2px 2px 8px;
|
||||
line-height: 1;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: 2px;
|
||||
font-size: var(--ha-font-size-m);
|
||||
@@ -1050,6 +1040,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
cursor: initial;
|
||||
direction: var(--direction);
|
||||
height: 32px;
|
||||
background-color: var(--ha-color-fill-primary-normal-resting);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
.active-filters ha-button {
|
||||
margin-left: 8px;
|
||||
@@ -1057,17 +1049,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
margin-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.active-filters::before {
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.12;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
}
|
||||
.badge {
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
|
@@ -221,12 +221,8 @@ class DialogMatterAddDevice extends LitElement {
|
||||
--horizontal-padding: 16px;
|
||||
}
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-max-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
}
|
||||
}
|
||||
.loading {
|
||||
|
@@ -1111,12 +1111,8 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
@media all and (max-width: 500px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-max-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
--mdc-dialog-min-height: 100%;
|
||||
--mdc-dialog-max-height: 100%;
|
||||
--vertical-align-dialog: flex-end;
|
||||
|
@@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { atLeastVersion } from "../../../common/config/version";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
@@ -14,6 +15,7 @@ import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import { getErrorLogDownloadUrl } from "../../../data/error_log";
|
||||
import { coreLatestLogsUrl } from "../../../data/hassio/supervisor";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { LoggedError } from "../../../data/system_log";
|
||||
import {
|
||||
@@ -23,7 +25,6 @@ import {
|
||||
} from "../../../data/system_log";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showDownloadLogsDialog } from "./show-dialog-download-logs";
|
||||
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
|
||||
import { formatSystemLogTime } from "./util";
|
||||
|
||||
@@ -229,20 +230,12 @@ export class SystemLogCard extends LitElement {
|
||||
}
|
||||
|
||||
private async _downloadLogs() {
|
||||
// download logs via supervisor
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
showDownloadLogsDialog(this, {
|
||||
header: this.header,
|
||||
provider: "core",
|
||||
defaultLineCount: 100,
|
||||
boot: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// download logs from core
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const downloadUrl = getErrorLogDownloadUrl;
|
||||
const downloadUrl =
|
||||
isComponentLoaded(this.hass, "hassio") &&
|
||||
atLeastVersion(this.hass.config.version, 2025, 10)
|
||||
? coreLatestLogsUrl
|
||||
: getErrorLogDownloadUrl;
|
||||
const logFileName = `home-assistant_${timeString}.log`;
|
||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
|
@@ -260,7 +260,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.common.save")
|
||||
: this.hass!.localize("ui.panel.config.person.detail.create")}
|
||||
: this.hass!.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
|
@@ -66,8 +66,6 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -11,10 +11,12 @@ import {
|
||||
mdiPlay,
|
||||
mdiPlaylistEdit,
|
||||
mdiPlusCircleMultipleOutline,
|
||||
mdiRedo,
|
||||
mdiRenameBox,
|
||||
mdiRobotConfused,
|
||||
mdiTag,
|
||||
mdiTransitConnection,
|
||||
mdiUndo,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -72,10 +74,16 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
import "./blueprint-script-editor";
|
||||
import "./manual-script-editor";
|
||||
import type { HaManualScriptEditor } from "./manual-script-editor";
|
||||
import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin";
|
||||
|
||||
export class HaScriptEditor extends SubscribeMixin(
|
||||
const baseEditorMixins = SubscribeMixin(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
) {
|
||||
);
|
||||
|
||||
export class HaScriptEditor extends UndoRedoMixin<
|
||||
typeof baseEditorMixins,
|
||||
ScriptConfig
|
||||
>(baseEditorMixins) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public scriptId: string | null = null;
|
||||
@@ -169,6 +177,24 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.header=${this._config.alias ||
|
||||
this.hass.localize("ui.panel.config.script.editor.default_name")}
|
||||
>
|
||||
${this._mode === "gui" && !this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.undo")}
|
||||
.path=${mdiUndo}
|
||||
@click=${this.undo}
|
||||
.disabled=${!this.canUndo}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.redo")}
|
||||
.path=${mdiRedo}
|
||||
@click=${this.redo}
|
||||
.disabled=${!this.canRedo}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
${this.scriptId && !this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
@@ -189,6 +215,25 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${this._mode === "gui" && this.narrow
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this.undo}
|
||||
.disabled=${!this.canUndo}
|
||||
>
|
||||
${this.hass.localize("ui.common.undo")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this.redo}
|
||||
.disabled=${!this.canRedo}
|
||||
>
|
||||
${this.hass.localize("ui.common.redo")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${!this.scriptId}
|
||||
@@ -387,6 +432,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
@value-changed=${this._valueChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@save-script=${this._handleSaveScript}
|
||||
@undo-paste=${this.undo}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
@@ -454,7 +500,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
? html`<ha-yaml-editor
|
||||
copy-clipboard
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
@@ -602,6 +647,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
if (this._config) {
|
||||
this.pushToUndo(this._config);
|
||||
}
|
||||
|
||||
this._config = ev.detail.value;
|
||||
this._errors = undefined;
|
||||
this._dirty = true;
|
||||
@@ -694,6 +743,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
if ("fields" in this._config!) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._config) {
|
||||
this.pushToUndo(this._config);
|
||||
}
|
||||
|
||||
this._manualEditor?.addFields();
|
||||
this._dirty = true;
|
||||
}
|
||||
@@ -1025,6 +1079,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
x: () => this._cutSelectedRow(),
|
||||
Delete: () => this._deleteSelectedRow(),
|
||||
Backspace: () => this._deleteSelectedRow(),
|
||||
z: () => this.undo(),
|
||||
y: () => this.redo(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1058,6 +1114,16 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
this._manualEditor?.deleteSelectedRow();
|
||||
}
|
||||
|
||||
protected get currentConfig() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
protected applyUndoRedo(config: ScriptConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -1,19 +1,30 @@
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiPlaylistEdit,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { preventDefaultStopPropagation } from "../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import type { LocalizeKeys } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-automation-row";
|
||||
import type { HaAutomationRow } from "../../../components/ha-automation-row";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-md-button-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { ScriptFieldSidebarConfig } from "../../../data/automation";
|
||||
import type { Field } from "../../../data/script";
|
||||
import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { indentStyle } from "../automation/styles";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { indentStyle, overflowStyles } from "../automation/styles";
|
||||
import "./ha-script-field-selector-editor";
|
||||
import type HaScriptFieldSelectorEditor from "./ha-script-field-selector-editor";
|
||||
|
||||
@@ -66,6 +77,64 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
.highlight=${this.highlight}
|
||||
@delete-row=${this._onDelete}
|
||||
>
|
||||
<ha-md-button-menu
|
||||
quick
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item .clickAction=${this._toggleYamlMode}>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
${!this.narrow
|
||||
? html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.del"
|
||||
)}</span
|
||||
>
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<h3 slot="header">${this.key}</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -97,6 +166,71 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
"ui.panel.config.script.editor.field.selector"
|
||||
)}
|
||||
</h3>
|
||||
<ha-md-button-menu
|
||||
quick
|
||||
slot="icons"
|
||||
@click=${preventDefaultStopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
selector-row
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<span
|
||||
class="shortcut-placeholder ${isMac ? "mac" : ""}"
|
||||
></span>
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
)}
|
||||
${!this.narrow
|
||||
? html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.del"
|
||||
)}</span
|
||||
>
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-automation-row>
|
||||
</ha-card>
|
||||
${typeof this.field.selector === "object" &&
|
||||
@@ -243,8 +377,12 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
private _toggleYamlMode = (item?: HTMLElement) => {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
|
||||
if (item) {
|
||||
this.openSidebar(item.hasAttribute("selector-row"));
|
||||
}
|
||||
};
|
||||
|
||||
private _onDelete = () => {
|
||||
@@ -279,6 +417,7 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
indentStyle,
|
||||
overflowStyles,
|
||||
css`
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
|
@@ -35,7 +35,12 @@ import type {
|
||||
ActionSidebarConfig,
|
||||
SidebarConfig,
|
||||
} from "../../../data/automation";
|
||||
import type { Action, Fields, ScriptConfig } from "../../../data/script";
|
||||
import type {
|
||||
Action,
|
||||
Fields,
|
||||
ManualScriptConfig,
|
||||
ScriptConfig,
|
||||
} from "../../../data/script";
|
||||
import {
|
||||
getActionType,
|
||||
MODES,
|
||||
@@ -103,8 +108,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
HaAutomationAction | HaScriptFields
|
||||
>;
|
||||
|
||||
private _previousConfig?: ScriptConfig;
|
||||
|
||||
private _openFields = false;
|
||||
|
||||
private _prevSidebarWidthPx?: number;
|
||||
@@ -181,7 +184,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@request-close-sidebar=${this._triggerCloseSidebar}
|
||||
@request-close-sidebar=${this.triggerCloseSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
></ha-script-fields>`
|
||||
: nothing
|
||||
@@ -212,7 +215,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
.highlightedActions=${this._pastedConfig?.sequence}
|
||||
@value-changed=${this._sequenceChanged}
|
||||
@open-sidebar=${this._openSidebar}
|
||||
@request-close-sidebar=${this._triggerCloseSidebar}
|
||||
@request-close-sidebar=${this.triggerCloseSidebar}
|
||||
@close-sidebar=${this._handleCloseSidebar}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@@ -402,9 +405,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
if (keysPresent.length === 1 && ["sequence"].includes(keysPresent[0])) {
|
||||
// if only one type of element is pasted, insert under the currently active item
|
||||
const previousConfig = { ...this.config };
|
||||
if (this._tryInsertAfterSelected(normalized[keysPresent[0]])) {
|
||||
this._previousConfig = previousConfig;
|
||||
this._showPastedToastWithUndo();
|
||||
return;
|
||||
}
|
||||
@@ -439,22 +440,23 @@ export class HaManualScriptEditor extends LitElement {
|
||||
};
|
||||
|
||||
private _appendToExistingConfig(config: ScriptConfig) {
|
||||
// make a copy otherwise we will reference the original config
|
||||
this._previousConfig = { ...this.config } as ScriptConfig;
|
||||
this._pastedConfig = config;
|
||||
// make a copy otherwise we will modify the original config
|
||||
// which breaks the (referenced) config used for storing in undo stack
|
||||
const workingCopy: ManualScriptConfig = { ...this.config };
|
||||
|
||||
if (!this.config) {
|
||||
if (!workingCopy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("fields" in config) {
|
||||
this.config.fields = {
|
||||
...this.config.fields,
|
||||
workingCopy.fields = {
|
||||
...workingCopy.fields,
|
||||
...config.fields,
|
||||
};
|
||||
}
|
||||
if ("sequence" in config) {
|
||||
this.config.sequence = ensureArray(this.config.sequence || []).concat(
|
||||
workingCopy.sequence = ensureArray(workingCopy.sequence || []).concat(
|
||||
ensureArray(config.sequence)
|
||||
) as Action[];
|
||||
}
|
||||
@@ -463,22 +465,19 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
...workingCopy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _replaceExistingConfig(config: ScriptConfig) {
|
||||
// make a copy otherwise we will reference the original config
|
||||
this._previousConfig = { ...this.config } as ScriptConfig;
|
||||
this._pastedConfig = config;
|
||||
this.config = config;
|
||||
|
||||
this._showPastedToastWithUndo();
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
...config,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -492,13 +491,8 @@ export class HaManualScriptEditor extends LitElement {
|
||||
action: {
|
||||
text: this.hass.localize("ui.common.undo"),
|
||||
action: () => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this._previousConfig!,
|
||||
},
|
||||
});
|
||||
fireEvent(this, "undo-paste");
|
||||
|
||||
this._previousConfig = undefined;
|
||||
this._pastedConfig = undefined;
|
||||
},
|
||||
},
|
||||
@@ -506,12 +500,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
}
|
||||
|
||||
public resetPastedConfig() {
|
||||
if (!this._previousConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pastedConfig = undefined;
|
||||
this._previousConfig = undefined;
|
||||
|
||||
showToast(this, {
|
||||
message: "",
|
||||
@@ -541,7 +530,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _triggerCloseSidebar() {
|
||||
public triggerCloseSidebar() {
|
||||
if (this._sidebarConfig) {
|
||||
if (this._sidebarElement) {
|
||||
this._sidebarElement.triggerCloseSidebar();
|
||||
@@ -556,7 +545,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _saveScript() {
|
||||
this._triggerCloseSidebar();
|
||||
this.triggerCloseSidebar();
|
||||
fireEvent(this, "save-script");
|
||||
}
|
||||
|
||||
|
@@ -520,6 +520,10 @@ class HaConfigSectionStorage extends LitElement {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
--ha-alert-icon-size: 24px;
|
||||
}
|
||||
|
||||
ha-alert ha-spinner {
|
||||
--ha-spinner-size: 24px;
|
||||
}
|
||||
|
@@ -51,7 +51,7 @@ class HaPanelDevAction extends LitElement {
|
||||
@state() private _response?: {
|
||||
domain: string;
|
||||
service: string;
|
||||
result: Record<string, any>;
|
||||
result: Record<string, any> | null;
|
||||
media?: Promise<TemplateResult | typeof nothing>;
|
||||
};
|
||||
|
||||
@@ -205,7 +205,7 @@ class HaPanelDevAction extends LitElement {
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</div>
|
||||
${this._response
|
||||
${this._response?.result
|
||||
? html`<div class="content response">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
@@ -215,7 +215,6 @@ class HaPanelDevAction extends LitElement {
|
||||
<div class="card-content">
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
copy-clipboard
|
||||
read-only
|
||||
auto-update
|
||||
has-extra-actions
|
||||
@@ -492,7 +491,7 @@ class HaPanelDevAction extends LitElement {
|
||||
service,
|
||||
result,
|
||||
media:
|
||||
"media_source_id" in result
|
||||
result && "media_source_id" in result
|
||||
? resolveMediaSource(this.hass, result.media_source_id).then(
|
||||
(resolved) =>
|
||||
resolved.mime_type.startsWith("image/")
|
||||
|
@@ -802,12 +802,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-max-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
--mdc-dialog-min-height: 100%;
|
||||
--mdc-dialog-max-height: 100%;
|
||||
--vertical-align-dialog: flex-end;
|
||||
|
@@ -199,6 +199,7 @@ class HaPanelHistory extends LitElement {
|
||||
.startTime=${this._startDate}
|
||||
.endTime=${this._endDate}
|
||||
.narrow=${this.narrow}
|
||||
sync-charts
|
||||
>
|
||||
</state-history-charts>
|
||||
`}
|
||||
|
@@ -161,7 +161,7 @@ export class HuiBadge extends ReactiveElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _updateVisibility(forceVisible?: boolean) {
|
||||
private _updateVisibility(ignoreConditions?: boolean) {
|
||||
if (!this._element || !this.hass) {
|
||||
return;
|
||||
}
|
||||
@@ -171,9 +171,18 @@ export class HuiBadge extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.preview) {
|
||||
this._setElementVisibility(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config?.disabled) {
|
||||
this._setElementVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const visible =
|
||||
forceVisible ||
|
||||
this.preview ||
|
||||
ignoreConditions ||
|
||||
!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass);
|
||||
this._setElementVisibility(visible);
|
||||
|
@@ -4,7 +4,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import { hasScriptFields } from "../../../data/script";
|
||||
import {
|
||||
hasRequiredScriptFields,
|
||||
requiredScriptFieldsFilled,
|
||||
} from "../../../data/script";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
@@ -50,15 +53,28 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
|
||||
if (domain === "script") {
|
||||
const entityId = this._stateObj.entity_id;
|
||||
if (hasScriptFields(this.hass!, entityId)) {
|
||||
showMoreInfoDialog(this, { entityId: entityId });
|
||||
if (
|
||||
hasRequiredScriptFields(this.hass!, entityId) &&
|
||||
!requiredScriptFieldsFilled(this.hass!, entityId, this._config?.data)
|
||||
) {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: entityId,
|
||||
data: this._config?.data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.hass.callService(domain, service, {
|
||||
const serviceData = {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
});
|
||||
...(this._config?.data
|
||||
? {
|
||||
variables: this._config.data,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
this.hass.callService(domain, service, serviceData);
|
||||
}
|
||||
|
||||
static getStubConfig(): ButtonCardFeatureConfig {
|
||||
|
@@ -2,9 +2,12 @@ import type { AlarmMode } from "../../../data/alarm_control_panel";
|
||||
import type { HvacMode } from "../../../data/climate";
|
||||
import type { OperationMode } from "../../../data/water_heater";
|
||||
|
||||
export type ButtonCardData = Record<string, any>;
|
||||
|
||||
export interface ButtonCardFeatureConfig {
|
||||
type: "button";
|
||||
action_name?: string;
|
||||
data?: ButtonCardData;
|
||||
}
|
||||
|
||||
export interface CoverOpenCloseCardFeatureConfig {
|
||||
|
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { INTERVAL } from "../hui-clock-card";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
|
||||
function romanize12HourClock(num: number) {
|
||||
@@ -35,13 +34,11 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
|
||||
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
|
||||
|
||||
@state() private _hourDeg?: number;
|
||||
@state() private _hourOffsetSec?: number;
|
||||
|
||||
@state() private _minuteDeg?: number;
|
||||
@state() private _minuteOffsetSec?: number;
|
||||
|
||||
@state() private _secondDeg?: number;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
@state() private _secondOffsetSec?: number;
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
@@ -63,7 +60,7 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._tick();
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -77,27 +74,25 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._startTick();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._stopTick();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
this._tick();
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeOffsets();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _tick() {
|
||||
private _computeOffsets() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
@@ -108,10 +103,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
const hour = hourStr ? parseInt(hourStr, 10) : 0;
|
||||
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
|
||||
const second = secondStr ? parseInt(secondStr, 10) : 0;
|
||||
const ms = new Date().getMilliseconds();
|
||||
const secondsWithMs = second + ms / 1000;
|
||||
|
||||
this._hourDeg = hour * 30 + minute * 0.5; // 30deg per hour + 0.5deg per minute
|
||||
this._minuteDeg = minute * 6 + second * 0.1; // 6deg per minute + 0.1deg per second
|
||||
this._secondDeg = this.config?.show_seconds ? second * 6 : undefined; // 6deg per second
|
||||
const hour12 = hour % 12;
|
||||
|
||||
this._secondOffsetSec = secondsWithMs;
|
||||
this._minuteOffsetSec = minute * 60 + secondsWithMs;
|
||||
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -212,16 +211,24 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
<div class="center-dot"></div>
|
||||
<div
|
||||
class="hand hour"
|
||||
style=${`--hand-rotation: ${this._hourDeg ?? 0}deg;`}
|
||||
style=${`animation-delay: -${this._hourOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
<div
|
||||
class="hand minute"
|
||||
style=${`--hand-rotation: ${this._minuteDeg ?? 0}deg;`}
|
||||
style=${`animation-delay: -${this._minuteOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
${this.config.show_seconds
|
||||
? html`<div
|
||||
class="hand second"
|
||||
style=${`--hand-rotation: ${this._secondDeg ?? 0}deg;`}
|
||||
class=${classMap({
|
||||
hand: true,
|
||||
second: true,
|
||||
step: this.config.seconds_motion === "tick",
|
||||
})}
|
||||
style=${`animation-delay: -${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s;`}
|
||||
></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -350,10 +357,13 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
left: 50%;
|
||||
bottom: 50%;
|
||||
transform-origin: 50% 100%;
|
||||
transform: translate(-50%, 0) rotate(var(--hand-rotation, 0deg));
|
||||
transform: translate(-50%, 0) rotate(0deg);
|
||||
background: var(--primary-text-color);
|
||||
border-radius: 2px;
|
||||
will-change: transform;
|
||||
animation-name: ha-clock-rotate;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.hand.hour {
|
||||
@@ -362,6 +372,7 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
background: var(--primary-text-color);
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
animation-duration: 43200s; /* 12 hours */
|
||||
}
|
||||
|
||||
.hand.minute {
|
||||
@@ -371,6 +382,7 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.9;
|
||||
z-index: 3;
|
||||
animation-duration: 3600s; /* 60 minutes */
|
||||
}
|
||||
|
||||
.hand.second {
|
||||
@@ -380,6 +392,20 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.8;
|
||||
z-index: 2;
|
||||
animation-duration: 60s; /* 60 seconds */
|
||||
}
|
||||
|
||||
.hand.second.step {
|
||||
animation-timing-function: steps(60, end);
|
||||
}
|
||||
|
||||
@keyframes ha-clock-rotate {
|
||||
from {
|
||||
transform: translate(-50%, 0) rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, 0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -3,10 +3,11 @@ import type { PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { INTERVAL } from "../hui-clock-card";
|
||||
import { useAmPm } from "../../../../common/datetime/use_am_pm";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
|
||||
const INTERVAL = 1000;
|
||||
|
||||
@customElement("hui-clock-card-digital")
|
||||
export class HuiClockCardDigital extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
@@ -11,8 +11,6 @@ import type {
|
||||
} from "../types";
|
||||
import type { ClockCardConfig } from "./types";
|
||||
|
||||
export const INTERVAL = 1000;
|
||||
|
||||
@customElement("hui-clock-card")
|
||||
export class HuiClockCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
|
@@ -143,8 +143,7 @@ export class HuiEntityFilterCard
|
||||
this._element.preview = this.preview;
|
||||
this._element.layout = this.layout;
|
||||
}
|
||||
|
||||
if (changedProps.has("_config")) {
|
||||
if (changedProps.has("_config") || changedProps.has("preview")) {
|
||||
return true;
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
@@ -186,7 +185,11 @@ export class HuiEntityFilterCard
|
||||
return true;
|
||||
});
|
||||
|
||||
if (entitiesList.length === 0 && this._config.show_empty === false) {
|
||||
if (
|
||||
entitiesList.length === 0 &&
|
||||
this._config.show_empty === false &&
|
||||
!this.preview
|
||||
) {
|
||||
if (!this.hidden) {
|
||||
this.style.display = "none";
|
||||
this.toggleAttribute("hidden", true);
|
||||
|
@@ -10,8 +10,8 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import type { ClimateEntity } from "../../../data/climate";
|
||||
import "../../../state-control/climate/ha-state-control-climate-temperature";
|
||||
import "../../../state-control/water_heater/ha-state-control-water_heater-temperature";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../card-features/hui-card-features";
|
||||
import type { LovelaceCardFeatureContext } from "../card-features/types";
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import type { ThermostatCardConfig } from "./types";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
|
||||
@customElement("hui-thermostat-card")
|
||||
export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
@@ -69,8 +70,13 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public setConfig(config: ThermostatCardConfig): void {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "climate") {
|
||||
throw new Error("Specify an entity from within the climate domain");
|
||||
if (
|
||||
!config.entity ||
|
||||
!["climate", "water_heater"].includes(config.entity.split(".")[0])
|
||||
) {
|
||||
throw new Error(
|
||||
"Specify an entity from within the climate or water_heater domain"
|
||||
);
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
@@ -115,7 +121,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
@@ -124,6 +130,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
</hui-warning>
|
||||
`;
|
||||
}
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
const name = this._config!.name || computeStateName(stateObj);
|
||||
|
||||
@@ -137,16 +144,26 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
<ha-card>
|
||||
<p class="title">${name}</p>
|
||||
<div class="container">
|
||||
<ha-state-control-climate-temperature
|
||||
style=${styleMap({
|
||||
maxWidth: controlMaxWidth,
|
||||
})}
|
||||
prevent-interaction-on-scroll
|
||||
.showCurrentAsPrimary=${this._config.show_current_as_primary}
|
||||
show-secondary
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-control-climate-temperature>
|
||||
${domain === "water_heater"
|
||||
? html` <ha-state-control-water_heater-temperature
|
||||
style=${styleMap({
|
||||
maxWidth: controlMaxWidth,
|
||||
})}
|
||||
prevent-interaction-on-scroll
|
||||
show-current
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-control-water_heater-temperature>`
|
||||
: html` <ha-state-control-climate-temperature
|
||||
style=${styleMap({
|
||||
maxWidth: controlMaxWidth,
|
||||
})}
|
||||
prevent-interaction-on-scroll
|
||||
.showCurrentAsPrimary=${this._config.show_current_as_primary}
|
||||
show-secondary
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-control-climate-temperature>`}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
class="more-info"
|
||||
|
@@ -375,6 +375,7 @@ export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
clock_style?: "digital" | "analog";
|
||||
clock_size?: "small" | "medium" | "large";
|
||||
show_seconds?: boolean | undefined;
|
||||
seconds_motion?: "continuous" | "tick";
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
|
@@ -1,10 +1,21 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { mdiApplicationVariableOutline } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-service-control";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { hasScriptFields } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { ButtonCardFeatureConfig } from "../../card-features/types";
|
||||
import type {
|
||||
ButtonCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-button-card-feature-editor")
|
||||
@@ -14,6 +25,8 @@ export class HuiButtonCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: ButtonCardFeatureConfig;
|
||||
|
||||
public setConfig(config: ButtonCardFeatureConfig): void {
|
||||
@@ -35,6 +48,27 @@ export class HuiButtonCardFeatureEditor
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let scriptData:
|
||||
| {
|
||||
action: string;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (this.context?.entity_id) {
|
||||
const domain = computeDomain(this.context.entity_id);
|
||||
|
||||
if (
|
||||
domain === "script" &&
|
||||
hasScriptFields(this.hass, this.context.entity_id)
|
||||
) {
|
||||
scriptData = {
|
||||
action: this.context.entity_id,
|
||||
data: this._config.data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
@@ -43,19 +77,68 @@ export class HuiButtonCardFeatureEditor
|
||||
.computeLabel=${this._computeLabel}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
${scriptData
|
||||
? html`<ha-expansion-panel
|
||||
outlined
|
||||
expanded
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.service-control.script_variables"
|
||||
)}
|
||||
.secondary=${this.hass.localize("ui.common.optional")}
|
||||
no-collapse
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
.path=${mdiApplicationVariableOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-service-control
|
||||
hide-picker
|
||||
hide-description
|
||||
.hass=${this.hass}
|
||||
.value=${scriptData}
|
||||
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||
.narrow=${false}
|
||||
@value-changed=${this._scriptFieldVariablesChanged}
|
||||
></ha-service-control
|
||||
></ha-expansion-panel>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabel = () => this.hass.localize("ui.common.name");
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "action_name":
|
||||
return this.hass!.localize("ui.common.name");
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private _scriptFieldVariablesChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", {
|
||||
config: {
|
||||
...(this._config || {}),
|
||||
data: ev.detail.value.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("config-changed", {
|
||||
detail: { config: ev.detail.value },
|
||||
})
|
||||
);
|
||||
fireEvent(this, "config-changed", {
|
||||
config: { ...(this._config || {}), ...ev.detail.value },
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-expansion-panel {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -52,6 +52,12 @@ const cardConfigStruct = assign(
|
||||
literal("hour")
|
||||
)
|
||||
),
|
||||
seconds_motion: optional(
|
||||
defaulted(
|
||||
union([literal("continuous"), literal("tick")]),
|
||||
literal("continuous")
|
||||
)
|
||||
),
|
||||
face_style: optional(
|
||||
defaulted(
|
||||
union([
|
||||
@@ -78,7 +84,8 @@ export class HuiClockCardEditor
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
clockStyle: ClockCardConfig["clock_style"],
|
||||
ticks: ClockCardConfig["ticks"]
|
||||
ticks: ClockCardConfig["ticks"],
|
||||
showSeconds: boolean | undefined
|
||||
) =>
|
||||
[
|
||||
{ name: "title", selector: { text: {} } },
|
||||
@@ -170,6 +177,33 @@ export class HuiClockCardEditor
|
||||
},
|
||||
},
|
||||
},
|
||||
...(showSeconds
|
||||
? ([
|
||||
{
|
||||
name: "seconds_motion",
|
||||
description: {
|
||||
suffix: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
|
||||
),
|
||||
},
|
||||
default: "continuous",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["continuous", "tick"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.label`
|
||||
),
|
||||
description: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.description`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
...(ticks !== "none"
|
||||
? ([
|
||||
{
|
||||
@@ -257,7 +291,8 @@ export class HuiClockCardEditor
|
||||
.schema=${this._schema(
|
||||
this.hass.localize,
|
||||
this._data(this._config).clock_style,
|
||||
this._data(this._config).ticks
|
||||
this._data(this._config).ticks,
|
||||
this._data(this._config).show_seconds
|
||||
)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@@ -278,10 +313,17 @@ export class HuiClockCardEditor
|
||||
ev.detail.value.border = ev.detail.value.border ?? false;
|
||||
ev.detail.value.ticks = ev.detail.value.ticks ?? "hour";
|
||||
ev.detail.value.face_style = ev.detail.value.face_style ?? "markers";
|
||||
if (ev.detail.value.show_seconds) {
|
||||
ev.detail.value.seconds_motion =
|
||||
ev.detail.value.seconds_motion ?? "continuous";
|
||||
} else {
|
||||
delete ev.detail.value.seconds_motion;
|
||||
}
|
||||
} else {
|
||||
delete ev.detail.value.border;
|
||||
delete ev.detail.value.ticks;
|
||||
delete ev.detail.value.face_style;
|
||||
delete ev.detail.value.seconds_motion;
|
||||
}
|
||||
|
||||
if (ev.detail.value.ticks !== "none") {
|
||||
@@ -333,6 +375,10 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.label`
|
||||
);
|
||||
case "seconds_motion":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.label`
|
||||
);
|
||||
case "face_style":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.face_style.label`
|
||||
@@ -354,6 +400,10 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.description`
|
||||
);
|
||||
case "seconds_motion":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
|
||||
);
|
||||
case "face_style":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.face_style.description`
|
||||
|
@@ -33,14 +33,18 @@ import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "./hui-card-features-editor";
|
||||
import type { FeatureType } from "./hui-card-features-editor";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
|
||||
const COMPATIBLE_FEATURES_TYPES: FeatureType[] = [
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
"climate-fan-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
];
|
||||
const COMPATIBLE_FEATURES_TYPES: Record<string, FeatureType[]> = {
|
||||
climate: [
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
"climate-fan-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
],
|
||||
water_heater: ["water-heater-operation-modes"],
|
||||
};
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@@ -53,24 +57,6 @@ const cardConfigStruct = assign(
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "entity", selector: { entity: { domain: "climate" } } },
|
||||
{
|
||||
type: "grid",
|
||||
name: "",
|
||||
schema: [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "show_current_as_primary",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
@customElement("hui-thermostat-card-editor")
|
||||
export class HuiThermostatCardEditor
|
||||
extends LitElement
|
||||
@@ -91,19 +77,48 @@ export class HuiThermostatCardEditor
|
||||
})
|
||||
);
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(domain: string) =>
|
||||
[
|
||||
{
|
||||
name: "entity",
|
||||
selector: { entity: { domain: ["climate", "water_heater"] } },
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
name: "",
|
||||
schema: [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
],
|
||||
},
|
||||
...(domain === "climate"
|
||||
? [
|
||||
{
|
||||
name: "show_current_as_primary",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const entityId = this._config.entity;
|
||||
const domain = computeDomain(entityId);
|
||||
const featureContext = this._featureContext(entityId);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${this._schema(domain)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
@@ -118,7 +133,7 @@ export class HuiThermostatCardEditor
|
||||
<hui-card-features-editor
|
||||
.hass=${this.hass}
|
||||
.context=${featureContext}
|
||||
.featuresTypes=${COMPATIBLE_FEATURES_TYPES}
|
||||
.featuresTypes=${COMPATIBLE_FEATURES_TYPES[domain]}
|
||||
.features=${this._config!.features ?? []}
|
||||
@features-changed=${this._featuresChanged}
|
||||
@edit-detail-element=${this._editDetailElement}
|
||||
@@ -177,7 +192,9 @@ export class HuiThermostatCardEditor
|
||||
});
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
if (schema.name === "show_current_as_primary") {
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.thermostat.show_current_as_primary"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { object, string, any } from "superstruct";
|
||||
import { object, string, any, optional, boolean } from "superstruct";
|
||||
|
||||
export const baseLovelaceBadgeConfig = object({
|
||||
type: string(),
|
||||
visibility: any(),
|
||||
disabled: optional(boolean()),
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { object, string, any } from "superstruct";
|
||||
import { object, string, any, optional, boolean } from "superstruct";
|
||||
|
||||
export const baseLovelaceCardConfig = object({
|
||||
type: string(),
|
||||
@@ -6,4 +6,5 @@ export const baseLovelaceCardConfig = object({
|
||||
layout_options: any(),
|
||||
grid_options: any(),
|
||||
visibility: any(),
|
||||
disabled: optional(boolean()),
|
||||
});
|
||||
|
@@ -32,13 +32,13 @@ const processAreasForMediaPlayers = (
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const entityId of areaEntities) {
|
||||
cards.push({
|
||||
areaCards.push({
|
||||
type: "media-control",
|
||||
entity: entityId,
|
||||
} satisfies MediaControlCardConfig);
|
||||
}
|
||||
|
||||
if (areaEntities.length > 0) {
|
||||
if (areaCards.length > 0) {
|
||||
cards.push({
|
||||
heading_style: "subtitle",
|
||||
type: "heading",
|
||||
|
@@ -4,13 +4,14 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { getCommonControlUsagePrediction } from "../../../../data/usage_prediction";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { TileCardConfig } from "../../cards/types";
|
||||
import type { HeadingCardConfig, TileCardConfig } from "../../cards/types";
|
||||
|
||||
const DEFAULT_LIMIT = 8;
|
||||
|
||||
export interface CommonControlSectionStrategyConfig {
|
||||
type: "common-controls";
|
||||
title?: string;
|
||||
icon?: string;
|
||||
limit?: number;
|
||||
exclude_entities?: string[];
|
||||
hide_empty?: boolean;
|
||||
@@ -31,7 +32,8 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
section.cards?.push({
|
||||
type: "heading",
|
||||
heading: config.title,
|
||||
});
|
||||
icon: config.icon,
|
||||
} satisfies HeadingCardConfig);
|
||||
}
|
||||
|
||||
if (!isComponentLoaded(hass, "usage_prediction")) {
|
||||
@@ -46,7 +48,9 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
const predictedCommonControl = await getCommonControlUsagePrediction(hass);
|
||||
let predictedEntities = predictedCommonControl.entities;
|
||||
let predictedEntities = predictedCommonControl.entities.filter(
|
||||
(entity) => entity in hass.states
|
||||
);
|
||||
|
||||
if (config.exclude_entities) {
|
||||
predictedEntities = predictedEntities.filter(
|
||||
|
@@ -155,12 +155,8 @@ export const haStyleDialog = css`
|
||||
/* make dialog fullscreen on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-max-width: calc(
|
||||
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
--mdc-dialog-min-height: 100%;
|
||||
--mdc-dialog-max-height: 100%;
|
||||
--vertical-align-dialog: flex-end;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import scrollLockStyles from "@home-assistant/webawesome/dist/styles/utilities/scroll-lock.css.js";
|
||||
import { css } from "lit";
|
||||
import { extractDerivedVars } from "../../common/style/derived-css-vars";
|
||||
|
||||
@@ -18,6 +19,8 @@ export const waMainStyles = css`
|
||||
--wa-border-width-l: var(--ha-border-radius-l);
|
||||
--wa-space-xl: 32px;
|
||||
}
|
||||
|
||||
${scrollLockStyles}
|
||||
`;
|
||||
|
||||
export const waMainDerivedVariables = extractDerivedVars(waMainStyles);
|
||||
|
@@ -30,6 +30,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
{
|
||||
entityId: ev.detail.entityId,
|
||||
view: ev.detail.view || ev.detail.tab,
|
||||
data: ev.detail.data,
|
||||
},
|
||||
() => import("../dialogs/more-info/ha-more-info-dialog")
|
||||
);
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"config": "Settings",
|
||||
"states": "Overview",
|
||||
"map": "Map",
|
||||
"logbook": "Logbook",
|
||||
"logbook": "Activity",
|
||||
"history": "History",
|
||||
"todo": "To-do lists",
|
||||
"developer_tools": "Developer tools",
|
||||
@@ -233,7 +233,8 @@
|
||||
"join": "Join",
|
||||
"media_players": "Media players",
|
||||
"select_all": "Select all",
|
||||
"idle": "Idle"
|
||||
"idle": "Idle",
|
||||
"track_position": "Track position"
|
||||
},
|
||||
"persistent_notification": {
|
||||
"dismiss": "Dismiss"
|
||||
@@ -416,6 +417,7 @@
|
||||
"next": "Next",
|
||||
"back": "Back",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"move": "Move",
|
||||
"save": "Save",
|
||||
"apply": "Apply",
|
||||
@@ -526,7 +528,7 @@
|
||||
}
|
||||
},
|
||||
"logbook": {
|
||||
"entries_not_found": "No logbook events found.",
|
||||
"entries_not_found": "No activity found.",
|
||||
"triggered_by": "triggered by",
|
||||
"triggered_by_automation": "triggered by automation",
|
||||
"triggered_by_script": "triggered by script",
|
||||
@@ -539,7 +541,7 @@
|
||||
"triggered_by_homeassistant_stopping": "triggered by Home Assistant stopping",
|
||||
"triggered_by_homeassistant_starting": "triggered by Home Assistant starting",
|
||||
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
|
||||
"retrieval_error": "Could not load logbook",
|
||||
"retrieval_error": "Could not load activity",
|
||||
"not_loaded": "[%key:ui::dialogs::helper_settings::platform_not_loaded%]",
|
||||
"messages": {
|
||||
"was_away": "was detected away",
|
||||
@@ -954,7 +956,8 @@
|
||||
"target": "Targets",
|
||||
"target_secondary": "What should this action use as targeted areas, devices or entities.",
|
||||
"action_data": "Action data",
|
||||
"integration_doc": "Integration documentation"
|
||||
"integration_doc": "Integration documentation",
|
||||
"script_variables": "Script variables"
|
||||
},
|
||||
"related-items": {
|
||||
"no_related_found": "No related items found.",
|
||||
@@ -1218,7 +1221,9 @@
|
||||
"yaml-editor": {
|
||||
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
|
||||
"error": "Error in parsing YAML: {reason}",
|
||||
"error_location": "line: {line}, column: {column}"
|
||||
"error_location": "line: {line}, column: {column}",
|
||||
"enter_fullscreen": "Enter fullscreen",
|
||||
"exit_fullscreen": "Exit fullscreen"
|
||||
},
|
||||
"state-content-picker": {
|
||||
"state": "State",
|
||||
@@ -1382,7 +1387,8 @@
|
||||
"info": "Information",
|
||||
"related": "Related",
|
||||
"history": "History",
|
||||
"logbook": "Logbook",
|
||||
"aggregate": "5-minute aggregated",
|
||||
"logbook": "Activity",
|
||||
"device_or_service_info": "[%key:ui::panel::config::devices::device_info%]",
|
||||
"device_type": {
|
||||
"device": "[%key:ui::panel::config::devices::type::device_heading%]",
|
||||
@@ -2066,7 +2072,9 @@
|
||||
"cut": "to cut the selected item and place it on the clipboard",
|
||||
"delete": "to delete the selected item",
|
||||
"paste": "to paste automation/script YAML from clipboard to editor",
|
||||
"save": "to save automation/script"
|
||||
"save": "to save automation/script",
|
||||
"undo": "to undo last change",
|
||||
"redo": "to redo last undone change"
|
||||
},
|
||||
"charts": {
|
||||
"title": "Charts",
|
||||
@@ -3880,6 +3888,8 @@
|
||||
"add": "Add trigger",
|
||||
"search": "Search trigger",
|
||||
"id": "Trigger ID",
|
||||
"id_helper": "Helps identify each run based on which trigger fired.",
|
||||
"optional": "Optional",
|
||||
"edit_id": "Edit ID",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
"re_order": "Re-order",
|
||||
@@ -4284,7 +4294,7 @@
|
||||
},
|
||||
"trigger": {
|
||||
"label": "Triggered by",
|
||||
"no_triggers": "No triggers have an ID set. Use the three-dot menu on a trigger and select Edit ID to assign one.",
|
||||
"no_triggers": "There are no triggers with ID's set in this automation. Edit a trigger and give it a Trigger ID name.",
|
||||
"id": "Trigger",
|
||||
"description": {
|
||||
"picker": "If the automation has been triggered by a specific trigger.",
|
||||
@@ -4598,7 +4608,7 @@
|
||||
"tabs": {
|
||||
"details": "Step details",
|
||||
"timeline": "Trace timeline",
|
||||
"logbook": "Related logbook entries",
|
||||
"logbook": "Related activity",
|
||||
"automation_config": "Automation config",
|
||||
"step_config": "Step config",
|
||||
"changed_variables": "Changed variables",
|
||||
@@ -4615,7 +4625,7 @@
|
||||
"error": "Error: {error}",
|
||||
"result": "Result:",
|
||||
"step_not_executed": "This step was not executed.",
|
||||
"no_logbook_entries": "No logbook entries found for this step.",
|
||||
"no_logbook_entries": "No activity found for this step.",
|
||||
"no_variables_changed": "No variables changed",
|
||||
"unable_to_find_config": "Unable to find config"
|
||||
},
|
||||
@@ -4641,8 +4651,8 @@
|
||||
"disabled": "(disabled)",
|
||||
"triggered_by": "{triggeredBy, select, \n alias {{alias} triggered}\n other {Triggered} \n} {triggeredPath, select, \n trigger {by the {trigger}}\n other {manually} \n} at {time}",
|
||||
"path_error": "Unable to extract path {path}. Download trace and report as bug.",
|
||||
"not_all_entries_are_related_automation_note": "Not all shown logbook entries might be related to this automation.",
|
||||
"not_all_entries_are_related_script_note": "Not all shown logbook entries might be related to this script."
|
||||
"not_all_entries_are_related_automation_note": "Not all shown activity might be related to this automation.",
|
||||
"not_all_entries_are_related_script_note": "Not all shown activity might be related to this script."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5355,7 +5365,7 @@
|
||||
"person_not_found_title": "Person not found",
|
||||
"person_not_found": "We couldn't find the person you were trying to edit.",
|
||||
"detail": {
|
||||
"new_person": "New person",
|
||||
"new_person": "Add person",
|
||||
"name": "Name",
|
||||
"name_error_msg": "Name is required",
|
||||
"linked_user": "Linked user",
|
||||
@@ -5366,7 +5376,6 @@
|
||||
"device_tracker_picked": "Track device",
|
||||
"device_tracker_pick": "Pick device to track",
|
||||
"delete": "Delete",
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"confirm_delete_user_title": "Delete user account",
|
||||
"confirm_delete_user_text": "The user account for ''{name}'' will be permanently deleted. You can still track the user, but the person will no longer be able to log in.",
|
||||
@@ -7582,8 +7591,8 @@
|
||||
"square": "Render cards as squares"
|
||||
},
|
||||
"logbook": {
|
||||
"name": "Logbook",
|
||||
"description": "The Logbook card shows a list of events for entities."
|
||||
"name": "Activity",
|
||||
"description": "The Activity card shows a list of events for entities."
|
||||
},
|
||||
"history-graph": {
|
||||
"name": "History graph",
|
||||
@@ -7825,6 +7834,18 @@
|
||||
"description": "60 ticks (Every minute)"
|
||||
}
|
||||
},
|
||||
"seconds_motion": {
|
||||
"label": "Seconds motion",
|
||||
"description": "How the seconds hand moves on the clock",
|
||||
"continuous": {
|
||||
"label": "Continuous",
|
||||
"description": "The seconds hand moves continuously"
|
||||
},
|
||||
"tick": {
|
||||
"label": "Tick",
|
||||
"description": "The seconds hand moves in 1 second steps"
|
||||
}
|
||||
},
|
||||
"face_style": {
|
||||
"label": "Clock face style",
|
||||
"description": "Which kind of indices to use for the clock face",
|
||||
@@ -7919,7 +7940,7 @@
|
||||
},
|
||||
"thermostat": {
|
||||
"name": "Thermostat",
|
||||
"description": "The Thermostat card gives control of a climate entity for heating or cooling, allowing you to set its mode and desired temperature.",
|
||||
"description": "The Thermostat card gives control of a climate or water heater entity for heating or cooling, allowing you to set its mode and desired temperature.",
|
||||
"show_current_as_primary": "Show current temperature as primary information"
|
||||
},
|
||||
"tile": {
|
||||
@@ -8514,7 +8535,7 @@
|
||||
},
|
||||
"entity_id_picker": {
|
||||
"title": "Display entity IDs in picker",
|
||||
"description": "Shows the full entity IDs when selecting entities with a picker."
|
||||
"description": "Shows the full entity IDs when selecting entities from a menu list."
|
||||
},
|
||||
"refresh_tokens": {
|
||||
"header": "Refresh tokens",
|
||||
|
58
yarn.lock
58
yarn.lock
@@ -1646,10 +1646,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:9.35.0":
|
||||
version: 9.35.0
|
||||
resolution: "@eslint/js@npm:9.35.0"
|
||||
checksum: 10/a8764d0592ebe9a4804f8c0dafa7f49dbcdb38cabf30dd50587a3cfa51d898b90a3a0b93975d549f47debdd96b3e21da95081935f74213e45ec8c25f11f2ba1e
|
||||
"@eslint/js@npm:9.36.0":
|
||||
version: 9.36.0
|
||||
resolution: "@eslint/js@npm:9.36.0"
|
||||
checksum: 10/a0542f529f87b9ad69ef85c47b0c070b763591a61773b131a9d1d53934a587f0708c05a1a8f48a6805486004a4922c91d696c1e4835ff61f8750ffbded2f0c30
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4491,10 +4491,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/chromecast-caf-receiver@npm:6.0.24":
|
||||
version: 6.0.24
|
||||
resolution: "@types/chromecast-caf-receiver@npm:6.0.24"
|
||||
checksum: 10/1f2b95e8a15dbb36d5328895229d4a5cb255b33e62d46335bd6ed75e16aa9ea6a7d765a64ae120d19b3134fb3e51e9547d2544c7277f7bffe0bf0b3999f026da
|
||||
"@types/chromecast-caf-receiver@npm:6.0.22":
|
||||
version: 6.0.22
|
||||
resolution: "@types/chromecast-caf-receiver@npm:6.0.22"
|
||||
checksum: 10/6c51cb52527776ddfa187a261b88184c98bdd61c129dd8719cba213894d565cf69073734d6473696ffd60a768f6fb5a3fe9932693f43174fbc5e7af201db8a90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7352,17 +7352,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"del@npm:8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "del@npm:8.0.0"
|
||||
"del@npm:8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "del@npm:8.0.1"
|
||||
dependencies:
|
||||
globby: "npm:^14.0.2"
|
||||
is-glob: "npm:^4.0.3"
|
||||
is-path-cwd: "npm:^3.0.0"
|
||||
is-path-inside: "npm:^4.0.0"
|
||||
p-map: "npm:^7.0.2"
|
||||
presentable-error: "npm:^0.0.1"
|
||||
slash: "npm:^5.1.0"
|
||||
checksum: 10/502dea7a846f989e1d921733f5d41ae4ae9b3eff168d335bfc050c9ce938ddc46198180be133814269268c4b0aed441a82fbace948c0ec5eed4ed086a4ad3b0e
|
||||
checksum: 10/53ed4a379a68c90e7d6d3bcce09c49229e77de9a946d0a5fc25f45b16c950cb8665986b7d0d0423416c03bfd43e0f31e528c5a19c558fe47449be9d6fae7f846
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8193,9 +8194,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:9.35.0":
|
||||
version: 9.35.0
|
||||
resolution: "eslint@npm:9.35.0"
|
||||
"eslint@npm:9.36.0":
|
||||
version: 9.36.0
|
||||
resolution: "eslint@npm:9.36.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.8.0"
|
||||
"@eslint-community/regexpp": "npm:^4.12.1"
|
||||
@@ -8203,7 +8204,7 @@ __metadata:
|
||||
"@eslint/config-helpers": "npm:^0.3.1"
|
||||
"@eslint/core": "npm:^0.15.2"
|
||||
"@eslint/eslintrc": "npm:^3.3.1"
|
||||
"@eslint/js": "npm:9.35.0"
|
||||
"@eslint/js": "npm:9.36.0"
|
||||
"@eslint/plugin-kit": "npm:^0.3.5"
|
||||
"@humanfs/node": "npm:^0.16.6"
|
||||
"@humanwhocodes/module-importer": "npm:^1.0.1"
|
||||
@@ -8239,7 +8240,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10/238155639343d53bac639319ba92895083cbd15826081ac51204b29d64fbb52cebf0d355f11f57f146d2b15c4f2e1d85e3df0b0ac7ec0e2ef5e759c99dcab75e
|
||||
checksum: 10/6e512a82e680e6cdc554e97c7e232b83171f24a52fb46f89c2df74bcb80fe59b6e0a989485c9fe7e9ca81556b1953dd8604ace4ed37f651eded9a37816c06b7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9441,7 +9442,7 @@ __metadata:
|
||||
"@tsparticles/engine": "npm:3.9.1"
|
||||
"@tsparticles/preset-links": "npm:3.2.0"
|
||||
"@types/babel__plugin-transform-runtime": "npm:7.9.5"
|
||||
"@types/chromecast-caf-receiver": "npm:6.0.24"
|
||||
"@types/chromecast-caf-receiver": "npm:6.0.22"
|
||||
"@types/chromecast-caf-sender": "npm:1.0.11"
|
||||
"@types/color-name": "npm:2.0.0"
|
||||
"@types/culori": "npm:4.0.1"
|
||||
@@ -9478,11 +9479,11 @@ __metadata:
|
||||
date-fns: "npm:4.1.0"
|
||||
deep-clone-simple: "npm:1.1.1"
|
||||
deep-freeze: "npm:0.0.1"
|
||||
del: "npm:8.0.0"
|
||||
del: "npm:8.0.1"
|
||||
dialog-polyfill: "npm:0.5.6"
|
||||
echarts: "npm:6.0.0"
|
||||
element-internals-polyfill: "npm:3.0.2"
|
||||
eslint: "npm:9.35.0"
|
||||
eslint: "npm:9.36.0"
|
||||
eslint-config-airbnb-base: "npm:15.0.0"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
eslint-import-resolver-webpack: "npm:0.13.10"
|
||||
@@ -9532,7 +9533,7 @@ __metadata:
|
||||
qrcode: "npm:1.5.4"
|
||||
roboto-fontface: "npm:0.10.0"
|
||||
rrule: "npm:2.8.1"
|
||||
rspack-manifest-plugin: "npm:5.0.3"
|
||||
rspack-manifest-plugin: "npm:5.1.0"
|
||||
serve: "npm:14.2.5"
|
||||
sinon: "npm:21.0.0"
|
||||
sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch"
|
||||
@@ -12361,6 +12362,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"presentable-error@npm:^0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "presentable-error@npm:0.0.1"
|
||||
checksum: 10/013809ee7a47ced847a8d860e9b89a56cdd8c4f1ad04ad8da1e58fd60843f77f497d204146bb15aaa9793d3b94ad8626eed01256fc9eb5839a545af2000a5fa4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prettier@npm:3.6.2":
|
||||
version: 3.6.2
|
||||
resolution: "prettier@npm:3.6.2"
|
||||
@@ -13022,9 +13030,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rspack-manifest-plugin@npm:5.0.3":
|
||||
version: 5.0.3
|
||||
resolution: "rspack-manifest-plugin@npm:5.0.3"
|
||||
"rspack-manifest-plugin@npm:5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "rspack-manifest-plugin@npm:5.1.0"
|
||||
dependencies:
|
||||
"@rspack/lite-tapable": "npm:^1.0.1"
|
||||
peerDependencies:
|
||||
@@ -13032,7 +13040,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
"@rspack/core":
|
||||
optional: true
|
||||
checksum: 10/59e951e25801e39e798ef0042a509f1ebf8f81350eab6016b30519ade6f1f949162fe3ecf3447b9c99afc495d4b83576edb6e886c98d66db7918c82de8936ab9
|
||||
checksum: 10/672de1eb405b48c7a50f03d1813048b1a6711afd1e7f6829471c020bc16de96ecbf8d8a10c3f8cdb6bfcaa6e93461bd22cef92203f17d8494a53e285d21a16ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user