mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-30 07:19:34 +00:00
Compare commits
16 Commits
config-lab
...
dashboard_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a3cbbde25e | ||
![]() |
d59be1b279 | ||
![]() |
240b525aaa | ||
![]() |
6a991b0a81 | ||
![]() |
30ff6752e4 | ||
![]() |
e05ac1df34 | ||
![]() |
2789a66be5 | ||
![]() |
52bc358e18 | ||
![]() |
2333b85a72 | ||
![]() |
6b3e48be6f | ||
![]() |
1fa6c57eb3 | ||
![]() |
f251555b0c | ||
![]() |
d32960a24a | ||
![]() |
b35ffcffd6 | ||
![]() |
4cd0e89ff4 | ||
![]() |
118dd1e690 |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev
|
||||
npx -y netlify-cli deploy --dir=cast/dist --alias dev
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod
|
||||
npx -y netlify-cli deploy --dir=cast/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
||||
|
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
|
||||
npx -y netlify-cli deploy --dir=demo/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
|
||||
npx -y netlify-cli deploy --dir=demo/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod
|
||||
npx -y netlify-cli deploy --dir=gallery/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Deploy preview to Netlify
|
||||
id: deploy
|
||||
run: |
|
||||
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
|
||||
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
|
||||
--json > deploy_output.json
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.09.1
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
with:
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
|
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.10.3.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.10.0.cjs
|
||||
|
@@ -5,17 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
framework.messages.MessageType.LOAD,
|
||||
"LOAD" as 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 = framework.messages.StreamType.LIVE;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
media.hlsVideoSegmentFormat =
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
|
@@ -75,7 +75,7 @@ export const castDemoEntities: () => Entity[] = () =>
|
||||
longitude: 4.8903147,
|
||||
radius: 100,
|
||||
friendly_name: "Home",
|
||||
icon: "mdi:home",
|
||||
icon: "hass:home",
|
||||
},
|
||||
},
|
||||
"input_number.harmonyvolume": {
|
||||
@@ -88,7 +88,7 @@ export const castDemoEntities: () => Entity[] = () =>
|
||||
step: 1,
|
||||
mode: "slider",
|
||||
friendly_name: "Volume",
|
||||
icon: "mdi:volume-high",
|
||||
icon: "hass:volume-high",
|
||||
},
|
||||
},
|
||||
"climate.upstairs": {
|
||||
|
@@ -56,7 +56,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
|
||||
type: "weblink",
|
||||
url: "/lovelace/climate",
|
||||
name: "Climate controls",
|
||||
icon: "mdi:arrow-right",
|
||||
icon: "hass:arrow-right",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -76,7 +76,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
|
||||
type: "weblink",
|
||||
url: "/lovelace/overview",
|
||||
name: "Back",
|
||||
icon: "mdi:arrow-left",
|
||||
icon: "hass:arrow-left",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -40,7 +40,8 @@ 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 = framework.messages.StreamType.NONE;
|
||||
loadRequestData.media.streamType =
|
||||
"NONE" as framework.messages.StreamType.NONE;
|
||||
const metadata = new framework.messages.GenericMediaMetadata();
|
||||
metadata.title = viewTitle;
|
||||
loadRequestData.media.metadata = metadata;
|
||||
@@ -89,7 +90,7 @@ const showMediaPlayer = () => {
|
||||
const options = new framework.CastReceiverOptions();
|
||||
options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
[CAST_NS]: framework.system.MessageType.JSON,
|
||||
[CAST_NS]: "json" as framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
castContext.addCustomMessageListener(
|
||||
@@ -97,9 +98,7 @@ 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() !== framework.messages.PlayerState.IDLE
|
||||
) {
|
||||
if (playerManager.getPlayerState() !== "IDLE") {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
@@ -113,7 +112,7 @@ castContext.addCustomMessageListener(
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
framework.messages.MessageType.LOAD,
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
@@ -127,24 +126,23 @@ playerManager.setMessageInterceptor(
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
media.hlsVideoSegmentFormat =
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
);
|
||||
|
||||
playerManager.addEventListener(
|
||||
framework.events.EventType.MEDIA_STATUS,
|
||||
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
||||
(event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
|
||||
event.mediaStatus?.playerState === "IDLE" &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !==
|
||||
framework.messages.IdleReason.INTERRUPTED
|
||||
event.mediaStatus?.idleReason !== "INTERRUPTED"
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
|
@@ -143,7 +143,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
state: "on",
|
||||
attributes: {
|
||||
friendly_name: "Home Automation",
|
||||
icon: "mdi:home-automation",
|
||||
icon: "hass:home-automation",
|
||||
},
|
||||
},
|
||||
"input_boolean.tvtime": {
|
||||
|
@@ -4,7 +4,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
|
||||
title: "Home Assistant",
|
||||
views: [
|
||||
{
|
||||
icon: "mdi:home-assistant",
|
||||
icon: "hass:home-assistant",
|
||||
id: "home",
|
||||
title: "Home",
|
||||
cards: [
|
||||
|
@@ -1236,7 +1236,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
],
|
||||
path: "security",
|
||||
icon: "mdi:shield-home",
|
||||
icon: "hass:shield-home",
|
||||
name: "Security",
|
||||
background:
|
||||
'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed',
|
||||
|
@@ -17,10 +17,6 @@ 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",
|
||||
@@ -28,8 +24,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 + Grouping
|
||||
supported_features: 784959,
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
|
||||
supported_features: 195135,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 0,
|
||||
@@ -38,9 +34,6 @@ 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",
|
||||
@@ -156,18 +149,15 @@ 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,
|
||||
|
@@ -117,7 +117,7 @@ export class DemoHaBadge extends LitElement {
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
gap: var(--ha-space-6);
|
||||
gap: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -155,11 +155,11 @@ export class DemoHaButton extends LitElement {
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-6);
|
||||
gap: 24px;
|
||||
}
|
||||
.card-content div {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,37 +0,0 @@
|
||||
---
|
||||
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. |
|
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -123,11 +123,11 @@ export class DemoHaProgressButton extends LitElement {
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-6);
|
||||
gap: 24px;
|
||||
}
|
||||
.card-content div {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -88,7 +88,7 @@ export class DemoHaSlider extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-6);
|
||||
gap: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -70,7 +70,7 @@ export class DemoHaSpinner extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-6);
|
||||
gap: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -11,10 +11,7 @@ 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,
|
||||
HaFormDataContainer,
|
||||
} from "../../../../src/components/ha-form/types";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
@@ -36,7 +33,6 @@ 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",
|
||||
@@ -82,124 +78,77 @@ class HassioAddonConfig extends LitElement {
|
||||
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
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 ||
|
||||
public computeLabel = (entry: HaFormSchema): string =>
|
||||
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
||||
?.name ||
|
||||
this.addon.translations.en?.configuration?.[entry.name]?.name ||
|
||||
entry.name;
|
||||
|
||||
public computeHelper = (
|
||||
entry: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
): string =>
|
||||
this._getTranslationEntry(this.hass.language, entry, options)
|
||||
public computeHelper = (entry: HaFormSchema): string =>
|
||||
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
||||
?.description ||
|
||||
this._getTranslationEntry("en", entry, options)?.description ||
|
||||
this.addon.translations.en?.configuration?.[entry.name]?.description ||
|
||||
"";
|
||||
|
||||
private _convertSchema = memoizeOne(
|
||||
// Convert supervisor schema to selectors
|
||||
(schema: readonly HaFormSchema[]): HaFormSchema[] =>
|
||||
this._convertSchemaElements(schema)
|
||||
(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
|
||||
)
|
||||
);
|
||||
|
||||
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)
|
||||
|
@@ -159,7 +159,7 @@ class HassioSystemManagedDialog extends LitElement {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-4);
|
||||
gap: 16px;
|
||||
--mdc-icon-size: 48px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ export const hassioStyle = css`
|
||||
.card-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-gap: var(--ha-space-2);
|
||||
grid-gap: 8px;
|
||||
}
|
||||
@media screen and (min-width: 640px) {
|
||||
.card-group {
|
||||
|
@@ -213,7 +213,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
ha-card .card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
gap: 16px;
|
||||
}
|
||||
ha-alert p {
|
||||
text-align: unset;
|
||||
|
26
package.json
26
package.json
@@ -28,13 +28,13 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.4",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.19.0",
|
||||
"@codemirror/autocomplete": "6.18.7",
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/legacy-modes": "6.5.1",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.38.3",
|
||||
"@codemirror/view": "6.38.2",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.9.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.1",
|
||||
"@vaadin/combo-box": "24.9.0",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.0",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -111,7 +111,7 @@
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.13",
|
||||
"hls.js": "1.6.12",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
@@ -157,11 +157,11 @@
|
||||
"@octokit/auth-oauth-device": "8.0.1",
|
||||
"@octokit/plugin-retry": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.3.0",
|
||||
"@rspack/core": "1.5.7",
|
||||
"@rsdoctor/rspack-plugin": "1.2.3",
|
||||
"@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.22",
|
||||
"@types/chromecast-caf-receiver": "6.0.24",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
@@ -183,7 +183,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.36.0",
|
||||
"eslint": "9.35.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -203,7 +203,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.1",
|
||||
"lint-staged": "16.1.6",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -213,11 +213,11 @@
|
||||
"rspack-manifest-plugin": "5.1.0",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.0",
|
||||
"tar": "7.5.1",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.44.1",
|
||||
"typescript-eslint": "8.44.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack-stats-plugin": "1.1.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.10.3"
|
||||
"packageManager": "yarn@4.10.0"
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250924.0"
|
||||
version = "20250903.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
@@ -1,40 +1,23 @@
|
||||
import { formatHex, parse } from "culori";
|
||||
|
||||
/**
|
||||
* Expands a 3-digit hex color to a 6-digit hex color.
|
||||
* @param hex - The hex color to expand.
|
||||
* @returns The expanded hex color.
|
||||
* @throws If the hex color is invalid.
|
||||
*/
|
||||
export const expandHex = (hex: string): string => {
|
||||
const color = parse(hex);
|
||||
if (!color) {
|
||||
throw new Error(`Invalid hex color: ${hex}`);
|
||||
hex = hex.replace("#", "");
|
||||
if (hex.length === 6) return hex;
|
||||
let result = "";
|
||||
for (const val of hex) {
|
||||
result += val + val;
|
||||
}
|
||||
const formattedColor = formatHex(color);
|
||||
if (!formattedColor) {
|
||||
throw new Error(`Could not format hex color: ${hex}`);
|
||||
}
|
||||
return formattedColor.replace("#", "");
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Blends two hex colors. c1 is placed over c2, blend is c1's opacity.
|
||||
* @param c1 - The first hex color.
|
||||
* @param c2 - The second hex color.
|
||||
* @param blend - The blend percentage (0-100).
|
||||
* @returns The blended hex color.
|
||||
*/
|
||||
// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity.
|
||||
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
|
||||
let color = "";
|
||||
c1 = expandHex(c1);
|
||||
c2 = expandHex(c2);
|
||||
let color = "";
|
||||
for (let i = 0; i <= 5; i += 2) {
|
||||
const h1 = parseInt(c1.substring(i, i + 2), 16);
|
||||
const h2 = parseInt(c2.substring(i, i + 2), 16);
|
||||
const hex = Math.floor(h2 + (h1 - h2) * (blend / 100))
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
|
||||
while (hex.length < 2) hex = "0" + hex;
|
||||
color += hex;
|
||||
}
|
||||
return `#${color}`;
|
||||
|
@@ -1,49 +1,28 @@
|
||||
import { wcagLuminance, wcagContrast } from "culori";
|
||||
export const luminosity = (rgb: [number, number, number]): number => {
|
||||
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
const lum: [number, number, number] = [0, 0, 0];
|
||||
for (let i = 0; i < rgb.length; i++) {
|
||||
const chan = rgb[i] / 255;
|
||||
lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the luminosity of an RGB color.
|
||||
* @param rgb - The RGB color to calculate the luminosity of.
|
||||
* @returns The luminosity of the color.
|
||||
*/
|
||||
export const luminosity = (rgb: [number, number, number]): number =>
|
||||
wcagLuminance({
|
||||
mode: "rgb",
|
||||
r: rgb[0] / 255,
|
||||
g: rgb[1] / 255,
|
||||
b: rgb[2] / 255,
|
||||
});
|
||||
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the contrast ratio between two RGB colors.
|
||||
* @param color1 - The first color to calculate the contrast ratio of.
|
||||
* @param color2 - The second color to calculate the contrast ratio of.
|
||||
* @returns The contrast ratio between the two colors.
|
||||
*/
|
||||
export const rgbContrast = (
|
||||
color1: [number, number, number],
|
||||
color2: [number, number, number]
|
||||
) =>
|
||||
wcagContrast(
|
||||
{
|
||||
mode: "rgb",
|
||||
r: color1[0] / 255,
|
||||
g: color1[1] / 255,
|
||||
b: color1[2] / 255,
|
||||
},
|
||||
{
|
||||
mode: "rgb",
|
||||
r: color2[0] / 255,
|
||||
g: color2[1] / 255,
|
||||
b: color2[2] / 255,
|
||||
}
|
||||
);
|
||||
) => {
|
||||
const lum1 = luminosity(color1);
|
||||
const lum2 = luminosity(color2);
|
||||
|
||||
if (lum1 > lum2) {
|
||||
return (lum1 + 0.05) / (lum2 + 0.05);
|
||||
}
|
||||
|
||||
return (lum2 + 0.05) / (lum1 + 0.05);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the contrast ratio between two RGB colors.
|
||||
* @param rgb1 - The first color to calculate the contrast ratio of.
|
||||
* @param rgb2 - The second color to calculate the contrast ratio of.
|
||||
* @returns The contrast ratio between the two colors.
|
||||
*/
|
||||
export const getRGBContrastRatio = (
|
||||
rgb1: [number, number, number],
|
||||
rgb2: [number, number, number]
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "./localize";
|
||||
import { computeEntityName } from "../entity/compute_entity_name";
|
||||
import { computeDeviceName } from "../entity/compute_device_name";
|
||||
import { getEntityContext } from "../entity/context/get_entity_context";
|
||||
import { computeAreaName } from "../entity/compute_area_name";
|
||||
import { computeFloorName } from "../entity/compute_floor_name";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { computeAreaName } from "../entity/compute_area_name";
|
||||
import { computeDeviceName } from "../entity/compute_device_name";
|
||||
import { computeEntityName } from "../entity/compute_entity_name";
|
||||
import { computeFloorName } from "../entity/compute_floor_name";
|
||||
import { getEntityContext } from "../entity/context/get_entity_context";
|
||||
import type { LocalizeFunc } from "./localize";
|
||||
|
||||
export type FormatEntityStateFunc = (
|
||||
stateObj: HassEntity,
|
||||
@@ -77,7 +77,6 @@ export const computeFormatFunctions = async (
|
||||
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
|
||||
formatEntityName: (stateObj, type, separator = " ") => {
|
||||
const types = ensureArray(type);
|
||||
const namesList: (string | undefined)[] = [];
|
||||
|
||||
const { device, area, floor } = getEntityContext(
|
||||
stateObj,
|
||||
@@ -87,33 +86,24 @@ export const computeFormatFunctions = async (
|
||||
floors
|
||||
);
|
||||
|
||||
for (const t of types) {
|
||||
switch (t) {
|
||||
case "entity": {
|
||||
namesList.push(computeEntityName(stateObj, entities, devices));
|
||||
break;
|
||||
const names = types
|
||||
.map((t) => {
|
||||
switch (t) {
|
||||
case "entity":
|
||||
return computeEntityName(stateObj, entities, devices);
|
||||
case "device":
|
||||
return device ? computeDeviceName(device) : undefined;
|
||||
case "area":
|
||||
return area ? computeAreaName(area) : undefined;
|
||||
case "floor":
|
||||
return floor ? computeFloorName(floor) : undefined;
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
case "device": {
|
||||
if (device) {
|
||||
namesList.push(computeDeviceName(device));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "area": {
|
||||
if (area) {
|
||||
namesList.push(computeAreaName(area));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "floor": {
|
||||
if (floor) {
|
||||
namesList.push(computeFloorName(floor));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return namesList.filter((name) => name !== undefined).join(separator);
|
||||
})
|
||||
.filter((name) => name !== undefined);
|
||||
|
||||
return names.join(separator);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -974,7 +974,7 @@ export class HaChartBase extends LitElement {
|
||||
right: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
gap: 4px;
|
||||
}
|
||||
.chart-controls.small {
|
||||
top: 0;
|
||||
@@ -1011,7 +1011,7 @@ export class HaChartBase extends LitElement {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
.chart-legend li {
|
||||
height: 24px;
|
||||
|
@@ -84,9 +84,6 @@ 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;
|
||||
@@ -149,7 +146,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
: html`${combinedItems.map((item, index) =>
|
||||
this._renderHistoryItem(item, index)
|
||||
)}`}
|
||||
${this.syncCharts && this._hasZoomedCharts
|
||||
${this._hasZoomedCharts
|
||||
? html`<ha-fab
|
||||
slot="fab"
|
||||
class="reset-button"
|
||||
@@ -193,7 +190,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
.expandLegend=${this.expandLegend}
|
||||
?hide-reset-button=${this.syncCharts}
|
||||
hide-reset-button
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
@@ -212,7 +209,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
?hide-reset-button=${this.syncCharts}
|
||||
hide-reset-button
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
@@ -305,7 +302,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
private _handleTimelineSync(
|
||||
e: CustomEvent<HASSDomEvents["chart-zoom-with-index"]>
|
||||
) {
|
||||
if (!this.syncCharts || this._isSyncing) {
|
||||
if (this._isSyncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -447,7 +444,6 @@ 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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
447
src/components/entity/ha-entity-name-picker.ts
Normal file
447
src/components/entity/ha-entity-name-picker.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiDrag, mdiPlus } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import {
|
||||
computeEntityDisplayName,
|
||||
ENTITY_DISPLAY_NAME_TYPES,
|
||||
type EntityDisplayNameType,
|
||||
} from "../../panels/lovelace/common/entity/compute-display-name";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-filter-chip";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-list-item";
|
||||
import "../ha-select";
|
||||
import "../ha-sortable";
|
||||
import "../ha-textfield";
|
||||
|
||||
interface EntityNameOption {
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const SUGGESTION_ID = "___SUGGESTION___";
|
||||
|
||||
const suggestionStyle = styleMap({
|
||||
padding: "6px 16px",
|
||||
"background-color": "var(--mdc-text-field-fill-color, whitesmoke)",
|
||||
"font-weight": "normal",
|
||||
"font-size": "var(--ha-font-size-s)",
|
||||
color: "var(--primary-text-color)",
|
||||
});
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => {
|
||||
if (item.value === SUGGESTION_ID) {
|
||||
return html` <div style=${suggestionStyle}>${item.primary}</div>`;
|
||||
}
|
||||
return html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
@customElement("ha-entity-name-picker")
|
||||
export class HaEntityNamePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property() public value?: string[] | string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query(".container", true) private _container?: HTMLDivElement;
|
||||
|
||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
private _validOptions = memoizeOne((entityId: string) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
const options: EntityDisplayNameType[] = ["entity_name"];
|
||||
if (context.device) options.push("device_name");
|
||||
if (context.area) options.push("area_name");
|
||||
if (context.floor) options.push("floor_name");
|
||||
return options;
|
||||
});
|
||||
|
||||
private _getOptions = memoizeOne((entityId?: string) => {
|
||||
if (!entityId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = this._validOptions(entityId).map<EntityNameOption>(
|
||||
(name) => ({
|
||||
primary: this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${name}`
|
||||
),
|
||||
secondary: computeEntityDisplayName(
|
||||
this.hass,
|
||||
this.hass.states[entityId],
|
||||
name
|
||||
),
|
||||
value: name,
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const value = this._value;
|
||||
const options = this._getOptions(this.entityId);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<div class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label = ENTITY_DISPLAY_NAME_TYPES.includes(item)
|
||||
? this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item as EntityDisplayNameType}`
|
||||
)
|
||||
: `"${item}"`;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
|
||||
<mwc-menu-surface
|
||||
.open=${this._opened}
|
||||
@closed=${this._onClosed}
|
||||
@opened=${this._onOpened}
|
||||
@input=${stopPropagation}
|
||||
.anchor=${this._container}
|
||||
>
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.value=${""}
|
||||
.autofocus=${this.autofocus}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.required=${this.required && !value.length}
|
||||
.helper=${this.helper}
|
||||
.items=${options}
|
||||
allow-custom-value
|
||||
item-id-path="value"
|
||||
item-value-path="value"
|
||||
item-label-path="primary"
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
</mwc-menu-surface>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onClosed(ev) {
|
||||
ev.stopPropagation();
|
||||
this._opened = false;
|
||||
this._editIndex = undefined;
|
||||
}
|
||||
|
||||
private async _onOpened(ev) {
|
||||
if (!this._opened) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
this._opened = true;
|
||||
await this._comboBox?.focus();
|
||||
await this._comboBox?.open();
|
||||
}
|
||||
|
||||
private async _addItem(ev) {
|
||||
ev.stopPropagation();
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private async _editItem(ev) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
|
||||
this._editIndex = idx;
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value ? ensureArray(this.value) : [];
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
const open = ev.detail.value;
|
||||
if (open) {
|
||||
const options = this._comboBox.items || [];
|
||||
|
||||
const initialValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : "";
|
||||
|
||||
this._comboBox.filteredItems = this._filterSelectedOptions(
|
||||
options,
|
||||
initialValue
|
||||
);
|
||||
this._comboBox.setInputValue(initialValue);
|
||||
} else {
|
||||
this._opened = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _filterSelectedOptions = (
|
||||
options: EntityNameOption[],
|
||||
current?: string
|
||||
) => {
|
||||
const value = this._value;
|
||||
const filteredOptions = options.filter(
|
||||
(option) => !value.includes(option.value) || option.value === current
|
||||
);
|
||||
if (filteredOptions.length) {
|
||||
filteredOptions.unshift({
|
||||
primary: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.suggestions"
|
||||
),
|
||||
value: SUGGESTION_ID,
|
||||
});
|
||||
}
|
||||
return filteredOptions;
|
||||
};
|
||||
|
||||
private _filterChanged(ev: ValueChangedEvent<string>) {
|
||||
const input = ev.detail.value;
|
||||
const filter = input?.toLowerCase() || "";
|
||||
const options = this._comboBox.items || [];
|
||||
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : "";
|
||||
this._comboBox.filteredItems = this._filterSelectedOptions(
|
||||
options,
|
||||
currentValue
|
||||
);
|
||||
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fuseOptions: IFuseOptions<EntityNameOption> = {
|
||||
keys: ["primary", "secondary", "value"],
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
ignoreDiacritics: true,
|
||||
};
|
||||
|
||||
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
|
||||
const filteredItems = fuse.search(filter).map((result) => result.item);
|
||||
|
||||
this._comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._value;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
await this.updateComplete;
|
||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||
}
|
||||
|
||||
private async _removeItem(ev) {
|
||||
ev.stopPropagation();
|
||||
const value: string[] = [...this._value];
|
||||
const idx = parseInt(ev.target.dataset.idx, 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
await this.updateComplete;
|
||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||
}
|
||||
|
||||
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || value === "" || value === SUGGESTION_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = [...this._value];
|
||||
|
||||
const newValue = [...currentValue];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = value;
|
||||
} else {
|
||||
newValue.push(value);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private _setValue(value: string[]) {
|
||||
const newValue =
|
||||
value.length === 0 ? [] : value.length === 1 ? value[0] : value;
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: 4px;
|
||||
border-end-end-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
.container:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) .container:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
.container:focus-within:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
mwc-menu-surface {
|
||||
--mdc-menu-min-width: 100%;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-name-picker": HaEntityNamePicker;
|
||||
}
|
||||
}
|
@@ -86,10 +86,6 @@ 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, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
@@ -73,18 +73,14 @@ export class HaAnalytics extends LitElement {
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
?disabled=${baseEnabled}
|
||||
>
|
||||
</ha-switch>
|
||||
${baseEnabled
|
||||
? nothing
|
||||
: html`<ha-tooltip
|
||||
.for="switch-${preference}"
|
||||
placement="right"
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
<ha-tooltip .for="switch-${preference}" placement="right">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
|
@@ -54,7 +54,7 @@ export class HaBadge extends LitElement {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
height: var(--ha-badge-size, 36px);
|
||||
min-width: var(--ha-badge-size, 36px);
|
||||
padding: 0px 12px;
|
||||
|
@@ -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;
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
margin-bottom: var(--safe-area-inset-bottom);
|
||||
margin-left: var(--safe-area-inset-left);
|
||||
margin-right: var(--safe-area-inset-right);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -5,16 +5,9 @@ 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,
|
||||
mdiContentCopy,
|
||||
mdiUndo,
|
||||
mdiRedo,
|
||||
} from "@mdi/js";
|
||||
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, ReactiveElement, html, render } from "lit";
|
||||
@@ -23,14 +16,11 @@ 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-toolbar";
|
||||
import type { HaIconButtonToolbar } from "./ha-icon-button-toolbar";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-code-editor-completion-items";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -78,24 +68,13 @@ 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) {
|
||||
@@ -140,7 +119,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", stopPropagation);
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
this._updateFullscreenState(false);
|
||||
if (this._isFullscreen) {
|
||||
this._toggleFullscreen();
|
||||
}
|
||||
this.updateComplete.then(() => {
|
||||
this.codemirror!.destroy();
|
||||
delete this.codemirror;
|
||||
@@ -176,7 +157,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
||||
),
|
||||
});
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (changedProps.has("linewrap")) {
|
||||
transactions.push({
|
||||
@@ -197,25 +177,14 @@ 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._updateFullscreenState();
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +253,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Create the code editor
|
||||
this.codemirror = new this._loadedCodeMirror.EditorView({
|
||||
state: this._loadedCodeMirror.EditorState.create({
|
||||
doc: this._value,
|
||||
@@ -292,160 +260,71 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}),
|
||||
parent: this.renderRoot,
|
||||
});
|
||||
this._canCopy = this._value?.length > 0;
|
||||
|
||||
// Update the toolbar. Creating it if required
|
||||
this._updateToolbar();
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
|
||||
private _fullscreenLabel(): string {
|
||||
if (this._isFullscreen)
|
||||
return (
|
||||
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
|
||||
"Exit fullscreen"
|
||||
private _updateFullscreenButton() {
|
||||
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
|
||||
|
||||
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;
|
||||
}
|
||||
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"
|
||||
);
|
||||
return (
|
||||
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
|
||||
"Enter fullscreen"
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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._updateFullscreenState(!this._isFullscreen);
|
||||
this._toggleFullscreen();
|
||||
};
|
||||
|
||||
private _toggleFullscreen() {
|
||||
this._isFullscreen = !this._isFullscreen;
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
|
||||
private _handleKeyDown = (e: KeyboardEvent) => {
|
||||
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.
|
||||
if (this._isFullscreen && e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
} else if (e.key === "F11" && !this.disableFullscreen) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -713,13 +592,10 @@ 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 });
|
||||
};
|
||||
|
||||
@@ -738,33 +614,39 @@ 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, var(--error-color)) !important;
|
||||
border-color: var(--error-state-color, red);
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-gutters {
|
||||
padding-top: 0;
|
||||
.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-focused .cm-gutters {
|
||||
padding-top: 1px;
|
||||
.fullscreen-button:hover,
|
||||
.fullscreen-button:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
: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;
|
||||
@media (hover: none) {
|
||||
.fullscreen-button {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
:host(.fullscreen) {
|
||||
@@ -773,7 +655,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: 8px !important;
|
||||
z-index: 6;
|
||||
z-index: 9999 !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
@@ -790,28 +672,15 @@ 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(: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);
|
||||
:host(.fullscreen) .fullscreen-button {
|
||||
top: calc(var(--safe-area-inset-top, 0px) + 8px);
|
||||
right: calc(var(--safe-area-inset-right, 0px) + 8px);
|
||||
}
|
||||
|
||||
.completion-info {
|
||||
|
@@ -76,7 +76,7 @@ export class HaCopyTextfield extends LitElement {
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
|
@@ -343,7 +343,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
|
@@ -100,6 +100,8 @@ 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;
|
||||
@@ -121,7 +123,11 @@ 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: var(--mdc-dialog-min-width, 100vw);
|
||||
min-width: calc(
|
||||
var(--mdc-dialog-min-width, 100vw) - var(
|
||||
--safe-area-inset-left
|
||||
) - var(--safe-area-inset-right)
|
||||
);
|
||||
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);
|
||||
@@ -138,12 +144,18 @@ export class HaDialog extends DialogBase {
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
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);
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -77,8 +77,8 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
var(--form-grid-column-count, auto-fit),
|
||||
minmax(var(--form-grid-min-width, 200px), 1fr)
|
||||
);
|
||||
grid-column-gap: var(--ha-space-2);
|
||||
grid-row-gap: var(--ha-space-6);
|
||||
grid-column-gap: 8px;
|
||||
grid-row-gap: 24px;
|
||||
}
|
||||
:host > ha-form {
|
||||
display: block;
|
||||
|
@@ -156,7 +156,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
:host {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-6);
|
||||
gap: 24px;
|
||||
}
|
||||
:host ha-form {
|
||||
display: block;
|
||||
|
@@ -144,6 +144,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
.label=${this._computeLabel(item, this.data)}
|
||||
.disabled=${item.disabled || this.disabled || false}
|
||||
.placeholder=${item.required ? "" : item.default}
|
||||
.default=${item.default}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.localizeValue=${this.localizeValue}
|
||||
.required=${item.required || false}
|
||||
|
@@ -57,7 +57,7 @@ export class HaFormfield extends FormfieldBase {
|
||||
}
|
||||
.mdc-form-field {
|
||||
align-items: var(--ha-formfield-align-items, center);
|
||||
gap: var(--ha-space-1);
|
||||
gap: 4px;
|
||||
}
|
||||
.mdc-form-field > label {
|
||||
direction: var(--direction);
|
||||
|
@@ -227,7 +227,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
"row-slider preview";
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
#columns {
|
||||
grid-area: column-slider;
|
||||
|
@@ -1,126 +0,0 @@
|
||||
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: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
@@ -1,178 +0,0 @@
|
||||
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,18 +168,16 @@ export class HaMdDialog extends Dialog {
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host(:not([type="alert"])) {
|
||||
min-width: var(--mdc-dialog-min-width, 100vw);
|
||||
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-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"]) {
|
||||
|
184
src/components/ha-multi-select-field.ts
Normal file
184
src/components/ha-multi-select-field.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { styles } from "@material/mwc-select/mwc-select.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-menu";
|
||||
|
||||
@customElement("ha-multi-select-field")
|
||||
export class HaMultiSelectField extends SelectBase {
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
public inlineArrow = false;
|
||||
|
||||
@property() public options;
|
||||
|
||||
protected override render() {
|
||||
return html`
|
||||
${super.render()}
|
||||
${this.clearable && !this.required && !this.disabled && this.value
|
||||
? html`<ha-icon-button
|
||||
label="clear"
|
||||
@click=${this._clearValue}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected override renderMenu() {
|
||||
const classes = this.getMenuClasses();
|
||||
return html`<ha-menu
|
||||
innerRole="listbox"
|
||||
wrapFocus
|
||||
class=${classMap(classes)}
|
||||
activatable
|
||||
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
|
||||
.open=${this.menuOpen}
|
||||
.anchor=${this.anchorElement}
|
||||
.fixed=${this.fixedMenuPosition}
|
||||
@selected=${this.onSelected}
|
||||
@opened=${this.onOpened}
|
||||
@closed=${this.onClosed}
|
||||
@items-updated=${this.onItemsUpdated}
|
||||
@keydown=${this.handleTypeahead}
|
||||
>
|
||||
${this.renderMenuContent()}
|
||||
</ha-menu>`;
|
||||
}
|
||||
|
||||
protected override renderLeadingIcon() {
|
||||
if (!this.icon) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<span class="mdc-select__icon"
|
||||
><slot name="icon"></slot
|
||||
></span>`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
super.firstUpdated();
|
||||
|
||||
if (this.inlineArrow) {
|
||||
this.shadowRoot
|
||||
?.querySelector(".mdc-select__selected-text-container")
|
||||
?.classList.add("inline-arrow");
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("inlineArrow")) {
|
||||
const textContainerElement = this.shadowRoot?.querySelector(
|
||||
".mdc-select__selected-text-container"
|
||||
);
|
||||
if (this.inlineArrow) {
|
||||
textContainerElement?.classList.add("inline-arrow");
|
||||
} else {
|
||||
textContainerElement?.classList.remove("inline-arrow");
|
||||
}
|
||||
}
|
||||
if (changedProperties.get("options")) {
|
||||
this.layoutOptions();
|
||||
this.selectByValue(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
"translations-updated",
|
||||
this._translationsUpdated
|
||||
);
|
||||
}
|
||||
|
||||
private _clearValue(): void {
|
||||
if (this.disabled || !this.value) {
|
||||
return;
|
||||
}
|
||||
this.valueSetDirectly = true;
|
||||
this.select(-1);
|
||||
this.mdcFoundation.handleChange();
|
||||
}
|
||||
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host([clearable]) {
|
||||
position: relative;
|
||||
}
|
||||
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
.mdc-select--filled .mdc-select__anchor {
|
||||
height: var(--ha-select-height, 56px);
|
||||
}
|
||||
.mdc-select--filled .mdc-floating-label {
|
||||
inset-inline-start: 12px;
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
|
||||
inset-inline-start: 48px;
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select .mdc-select__anchor {
|
||||
padding-inline-start: 12px;
|
||||
padding-inline-end: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select__anchor .mdc-floating-label--float-above {
|
||||
transform-origin: var(--float-start);
|
||||
}
|
||||
.mdc-select__selected-text-container {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 0px);
|
||||
}
|
||||
:host([clearable]) .mdc-select__selected-text-container {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 12px);
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 28px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 28px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.inline-arrow {
|
||||
flex-grow: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-multi-select-field": HaMultiSelectField;
|
||||
}
|
||||
}
|
@@ -105,7 +105,7 @@ export class HaPickerField extends LitElement {
|
||||
--md-list-item-bottom-space: 0px;
|
||||
--md-list-item-leading-space: 8px;
|
||||
--md-list-item-trailing-space: 8px;
|
||||
--ha-md-list-item-gap: var(--ha-space-2);
|
||||
--ha-md-list-item-gap: 8px;
|
||||
/* Remove the default focus ring */
|
||||
--md-focus-ring-width: 0px;
|
||||
--md-focus-ring-duration: 0s;
|
||||
|
@@ -16,14 +16,6 @@ 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,24 +39,22 @@ class HaSegmentedBar extends LitElement {
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
<div class="bar">
|
||||
${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>
|
||||
`
|
||||
)}
|
||||
${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>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${this.hideLegend
|
||||
? nothing
|
||||
@@ -90,7 +88,7 @@ class HaSegmentedBar extends LitElement {
|
||||
.heading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
.heading .title {
|
||||
flex: 1;
|
||||
@@ -125,7 +123,7 @@ class HaSegmentedBar extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-3);
|
||||
gap: 12px;
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
@@ -133,7 +131,7 @@ class HaSegmentedBar extends LitElement {
|
||||
.legend li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-1);
|
||||
gap: 4px;
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
.legend li .bullet {
|
||||
|
@@ -116,7 +116,7 @@ export class HaSelectBox extends LitElement {
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns, 1), minmax(0, 1fr));
|
||||
gap: var(--ha-space-3);
|
||||
gap: 12px;
|
||||
}
|
||||
.option {
|
||||
position: relative;
|
||||
@@ -128,7 +128,7 @@ export class HaSelectBox extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export class HaSelectBox extends LitElement {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export class HaSelectBox extends LitElement {
|
||||
.option .content .text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
@@ -89,7 +89,7 @@ export class HaButtonToggleSelector extends LitElement {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
@media all and (max-width: 600px) {
|
||||
|
@@ -62,7 +62,7 @@ export class HaColorTempSelector extends LitElement {
|
||||
"--ha-slider-background": `linear-gradient( to var(--float-end), ${gradient})`,
|
||||
})}
|
||||
labeled
|
||||
icon="mdi:thermometer"
|
||||
icon="hass:thermometer"
|
||||
.caption=${this.label || ""}
|
||||
.min=${min}
|
||||
.max=${max}
|
||||
|
53
src/components/ha-selector/ha-selector-entity-name.ts
Normal file
53
src/components/ha-selector/ha-selector-entity-name.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { EntityNameSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-name-picker";
|
||||
|
||||
@customElement("ha-selector-entity_name")
|
||||
export class HaSelectorEntityName extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: EntityNameSelector;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
|
||||
@property() public default?: string | string[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
entity?: string;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const value = this.value ?? this.default;
|
||||
|
||||
return html`
|
||||
<ha-entity-name-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.selector.entity_name?.entity_id ||
|
||||
this.context?.entity}
|
||||
.value=${value}
|
||||
.default=${this.default}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-entity-name-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-entity_name": HaSelectorEntityName;
|
||||
}
|
||||
}
|
@@ -321,7 +321,7 @@ export class HaMediaSelector extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
gap: var(--ha-space-3);
|
||||
gap: 12px;
|
||||
}
|
||||
ha-card .thumbnail {
|
||||
width: 40px;
|
||||
|
@@ -82,12 +82,12 @@ export class HaNumberSelector extends LitElement {
|
||||
labeled
|
||||
.min=${this.selector.number!.min}
|
||||
.max=${this.selector.number!.max}
|
||||
.value=${this.value}
|
||||
.value=${this.value ?? ""}
|
||||
.step=${sliderStep}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@change=${this._handleSliderChange}
|
||||
.withMarkers=${this.selector.number?.slider_ticks || false}
|
||||
.ticks=${this.selector.number?.slider_ticks}
|
||||
>
|
||||
</ha-slider>
|
||||
`
|
||||
|
@@ -297,7 +297,7 @@ export class HaObjectSelector extends LitElement {
|
||||
return [
|
||||
css`
|
||||
ha-md-list {
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
ha-md-list-item {
|
||||
border: 1px solid var(--divider-color);
|
||||
|
@@ -29,6 +29,7 @@ const LOAD_ELEMENTS = {
|
||||
device: () => import("./ha-selector-device"),
|
||||
duration: () => import("./ha-selector-duration"),
|
||||
entity: () => import("./ha-selector-entity"),
|
||||
entity_name: () => import("./ha-selector-entity-name"),
|
||||
statistic: () => import("./ha-selector-statistic"),
|
||||
file: () => import("./ha-selector-file"),
|
||||
floor: () => import("./ha-selector-floor"),
|
||||
@@ -77,6 +78,8 @@ export class HaSelector extends LitElement {
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public default?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
@@ -133,6 +136,7 @@ export class HaSelector extends LitElement {
|
||||
name: this.name,
|
||||
selector: this._handleLegacySelector(this.selector),
|
||||
value: this.value,
|
||||
default: this.default,
|
||||
label: this.label,
|
||||
placeholder: this.placeholder,
|
||||
disabled: this.disabled,
|
||||
|
@@ -19,7 +19,7 @@ export class HaSlider extends Slider {
|
||||
Slider.styles,
|
||||
css`
|
||||
:host {
|
||||
--wa-form-control-activated-color: var(--ha-control-color);
|
||||
--wa-form-control-activated-color: var(--primary-color);
|
||||
--track-size: var(--ha-slider-track-size, 4px);
|
||||
--marker-height: calc(var(--ha-slider-track-size, 4px) / 2);
|
||||
--marker-width: calc(var(--ha-slider-track-size, 4px) / 2);
|
||||
|
@@ -18,8 +18,6 @@ export class HaTabGroupTab extends Tab {
|
||||
opacity: 0.8;
|
||||
|
||||
color: inherit;
|
||||
|
||||
--wa-space-l: 16px;
|
||||
}
|
||||
|
||||
:host([active]:not([disabled])) {
|
||||
|
@@ -522,7 +522,6 @@ 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[] = [];
|
||||
@@ -530,7 +529,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
if (target.type === "floor_id") {
|
||||
Object.values(this.hass.areas).forEach((area) => {
|
||||
if (
|
||||
area.floor_id === id &&
|
||||
area.floor_id === target.id &&
|
||||
!this.value!.area_id?.includes(area.area_id) &&
|
||||
this._areaMeetsFilter(area)
|
||||
) {
|
||||
@@ -540,7 +539,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
} else if (target.type === "area_id") {
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
if (
|
||||
device.area_id === id &&
|
||||
device.area_id === target.id &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
this._deviceMeetsFilter(device)
|
||||
) {
|
||||
@@ -549,7 +548,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.area_id === id &&
|
||||
entity.area_id === target.id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
@@ -559,7 +558,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
} else if (target.type === "device_id") {
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.device_id === id &&
|
||||
entity.device_id === target.id &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
@@ -569,7 +568,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
} else if (target.type === "label_id") {
|
||||
Object.values(this.hass.areas).forEach((area) => {
|
||||
if (
|
||||
area.labels.includes(id) &&
|
||||
area.labels.includes(target.id) &&
|
||||
!this.value!.area_id?.includes(area.area_id) &&
|
||||
this._areaMeetsFilter(area)
|
||||
) {
|
||||
@@ -578,7 +577,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
if (
|
||||
device.labels.includes(id) &&
|
||||
device.labels.includes(target.id) &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
this._deviceMeetsFilter(device)
|
||||
) {
|
||||
@@ -587,7 +586,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.labels.includes(id) &&
|
||||
entity.labels.includes(target.id) &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity, true)
|
||||
) {
|
||||
@@ -607,15 +606,14 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
if (newAreas.length) {
|
||||
value = this._addItems(value, "area_id", newAreas);
|
||||
}
|
||||
value = this._removeItem(value, target.type, id);
|
||||
value = this._removeItem(value, target.type, target.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, id),
|
||||
value: this._removeItem(this.value, target.type, target.id),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -217,7 +217,7 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: var(--ha-space-4);
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
ha-dialog-header ha-button {
|
||||
|
@@ -1138,7 +1138,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
auto-fit,
|
||||
minmax(var(--media-browse-item-size, 175px), 0.1fr)
|
||||
);
|
||||
grid-gap: var(--ha-space-4);
|
||||
grid-gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
@@ -54,7 +54,7 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
.list-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
column-gap: var(--ha-space-4);
|
||||
column-gap: 16px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
@@ -1,28 +1,6 @@
|
||||
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;
|
||||
@@ -43,48 +21,6 @@ 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;
|
||||
@@ -100,18 +36,18 @@ export class HaTileInfo extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
.primary {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
.secondary {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -29,15 +29,9 @@ 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, AddonFieldTranslation>;
|
||||
configuration?: Record<string, { name?: string; description?: string }>;
|
||||
}
|
||||
|
||||
export interface HassioAddonInfo {
|
||||
|
@@ -279,5 +279,3 @@ export const setSupervisorOption = async (
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
export const coreLatestLogsUrl = "/api/hassio/core/logs/latest";
|
||||
|
@@ -4,7 +4,6 @@ export interface LovelaceBadgeConfig {
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
visibility?: Condition[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ensureBadgeConfig = (
|
||||
|
@@ -39,8 +39,6 @@ import type { HomeAssistant, TranslationDict } from "../types";
|
||||
import { isUnavailableState } from "./entity";
|
||||
import { isTTSMediaSource } from "./tts";
|
||||
|
||||
import { generateEntityFilter } from "../common/entity/entity_filter";
|
||||
|
||||
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
|
||||
media_content_id?: string;
|
||||
media_content_type?: string;
|
||||
@@ -524,33 +522,3 @@ export const mediaPlayerJoin = (
|
||||
|
||||
export const mediaPlayerUnjoin = (hass: HomeAssistant, entity_id: string) =>
|
||||
hass.callService("media_player", "unjoin", {}, { entity_id });
|
||||
|
||||
/**
|
||||
* Compute active media player states in a specific area.
|
||||
* @param hass Home Assistant object
|
||||
* @param areaId Area ID to filter media players by
|
||||
* @returns Array of playing media player entities
|
||||
*/
|
||||
export const computeActiveAreaMediaStates = (
|
||||
hass: HomeAssistant,
|
||||
areaId: string
|
||||
): MediaPlayerEntity[] => {
|
||||
const area = hass.areas[areaId];
|
||||
if (!area) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all media_player entities in this area
|
||||
const mediaFilter = generateEntityFilter(hass, {
|
||||
area: areaId,
|
||||
domain: "media_player",
|
||||
});
|
||||
|
||||
const mediaEntities = Object.keys(hass.entities).filter(mediaFilter);
|
||||
|
||||
return mediaEntities
|
||||
.map((entityId) => hass.states[entityId] as MediaPlayerEntity | undefined)
|
||||
.filter(
|
||||
(stateObj): stateObj is MediaPlayerEntity => stateObj?.state === "playing"
|
||||
);
|
||||
};
|
||||
|
@@ -66,9 +66,9 @@ export const getPanelIcon = (panel: PanelInfo): string | null => {
|
||||
if (!panel.icon) {
|
||||
switch (panel.component_name) {
|
||||
case "profile":
|
||||
return "mdi:account";
|
||||
return "hass:account";
|
||||
case "lovelace":
|
||||
return "mdi:view-dashboard";
|
||||
return "hass:view-dashboard";
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,20 +5,17 @@ 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,
|
||||
@@ -29,6 +26,9 @@ 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,37 +395,6 @@ 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[] => {
|
||||
|
@@ -41,6 +41,7 @@ export type Selector =
|
||||
| LegacyDeviceSelector
|
||||
| DurationSelector
|
||||
| EntitySelector
|
||||
| EntityNameSelector
|
||||
| LegacyEntitySelector
|
||||
| FileSelector
|
||||
| IconSelector
|
||||
@@ -499,6 +500,12 @@ export interface UiStateContentSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface EntityNameSelector {
|
||||
entity_name: {
|
||||
entity_id?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const expandLabelTarget = (
|
||||
hass: HomeAssistant,
|
||||
labelId: string,
|
||||
|
@@ -4,7 +4,7 @@ import type { Action } from "./script";
|
||||
export const callExecuteScript = (
|
||||
hass: HomeAssistant,
|
||||
sequence: Action | Action[]
|
||||
): Promise<{ context: Context; response: Record<string, any> | null }> =>
|
||||
): Promise<{ context: Context; response: Record<string, any> }> =>
|
||||
hass.callWS({
|
||||
type: "execute_script",
|
||||
sequence,
|
||||
|
@@ -325,7 +325,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
.device-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
.device-info img {
|
||||
width: 40px;
|
||||
|
@@ -26,21 +26,20 @@ class StepFlowLoading extends LitElement {
|
||||
this.step
|
||||
);
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
<div class="init-spinner">
|
||||
${description ? html`<div>${description}</div>` : ""}
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
margin-top: 0;
|
||||
.init-spinner {
|
||||
padding: 50px 100px;
|
||||
text-align: center;
|
||||
}
|
||||
ha-spinner {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ class StepFlowProgress extends LitElement {
|
||||
)}%</ha-progress-ring
|
||||
>
|
||||
`
|
||||
: html`<ha-spinner size="large"></ha-spinner>`}
|
||||
: html` <ha-spinner size="large"></ha-spinner> `}
|
||||
${this.flowConfig.renderShowFormProgressDescription(
|
||||
this.hass,
|
||||
this.step
|
||||
@@ -48,7 +48,6 @@ class StepFlowProgress extends LitElement {
|
||||
configFlowContentStyles,
|
||||
css`
|
||||
.content {
|
||||
margin-top: 0;
|
||||
padding: 50px 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
@@ -209,7 +209,7 @@ export class DialogEnterCode
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--keypad-columns), auto);
|
||||
grid-auto-rows: auto;
|
||||
grid-gap: var(--ha-space-6);
|
||||
grid-gap: 24px;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ export class HaMoreInfoControlSelectContainer extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
gap: var(--ha-space-3);
|
||||
gap: 12px;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
|
@@ -110,7 +110,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
? html`<ha-labeled-slider
|
||||
labeled
|
||||
.caption=${this.hass.localize("ui.card.light.color_brightness")}
|
||||
icon="mdi:brightness-7"
|
||||
icon="hass:brightness-7"
|
||||
min="0"
|
||||
max="100"
|
||||
.value=${this._colorBrightnessSliderValue}
|
||||
@@ -122,7 +122,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
<ha-labeled-slider
|
||||
labeled
|
||||
.caption=${this.hass.localize("ui.card.light.white_value")}
|
||||
icon="mdi:file-word-box"
|
||||
icon="hass:file-word-box"
|
||||
min="0"
|
||||
max="100"
|
||||
.name=${"wv"}
|
||||
@@ -136,7 +136,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
<ha-labeled-slider
|
||||
labeled
|
||||
.caption=${this.hass.localize("ui.card.light.cold_white_value")}
|
||||
icon="mdi:file-word-box-outline"
|
||||
icon="hass:file-word-box-outline"
|
||||
min="0"
|
||||
max="100"
|
||||
.name=${"cw"}
|
||||
@@ -146,7 +146,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
<ha-labeled-slider
|
||||
labeled
|
||||
.caption=${this.hass.localize("ui.card.light.warm_white_value")}
|
||||
icon="mdi:file-word-box"
|
||||
icon="hass:file-word-box"
|
||||
min="0"
|
||||
max="100"
|
||||
.name=${"ww"}
|
||||
|
@@ -202,13 +202,13 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
gap: 16px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: var(--ha-space-4);
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-control-button {
|
||||
|
@@ -34,7 +34,6 @@ 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"];
|
||||
|
@@ -107,7 +107,7 @@ class MoreInfoCamera extends LitElement {
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
z-index: 1;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -181,7 +181,7 @@ class MoreInfoLock extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
mdiLoginVariant,
|
||||
mdiMusicNote,
|
||||
mdiMusicNoteEighth,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiSpeakerMultiple,
|
||||
mdiVolumeHigh,
|
||||
@@ -9,13 +8,11 @@ import {
|
||||
mdiVolumeOff,
|
||||
mdiVolumePlus,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
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";
|
||||
@@ -30,17 +27,12 @@ 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 {
|
||||
@@ -48,503 +40,259 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: MediaPlayerEntity;
|
||||
|
||||
private _formatDuration(duration: number) {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
const seconds = Math.floor(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 coverUrl = stateObj.attributes.entity_picture || "";
|
||||
const playerObj = new HassMediaPlayerEntity(this.hass, this.stateObj);
|
||||
|
||||
const position = Math.max(Math.floor(playerObj.currentProgress || 0), 0);
|
||||
const duration = Math.max(stateObj.attributes.media_duration || 0, 0);
|
||||
const remaining = Math.max(duration - position, 0);
|
||||
const remainingFormatted = this._formatDuration(remaining);
|
||||
const positionFormatted = this._formatDuration(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");
|
||||
const groupMembers = stateObj.attributes.group_members?.length;
|
||||
|
||||
return html`
|
||||
${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="position-bar">
|
||||
<ha-slider
|
||||
min="0"
|
||||
max=${duration}
|
||||
step="1"
|
||||
.value=${position}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.card.media_player.track_position"
|
||||
)}
|
||||
@change=${this._handleMediaSeekChanged}
|
||||
?disabled=${!stateActive(stateObj) ||
|
||||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
|
||||
<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"
|
||||
>
|
||||
<span slot="reference">${positionFormatted}</span>
|
||||
<span slot="reference">${remainingFormatted}</span>
|
||||
</ha-slider>
|
||||
<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)
|
||||
? 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>
|
||||
`
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiLoginVariant} slot="icon"></ha-svg-icon>
|
||||
</ha-select>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<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-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}`
|
||||
)}
|
||||
${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}
|
||||
>
|
||||
<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}`
|
||||
${stateObj.attributes.sound_mode_list.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode}>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"sound_mode",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${turnOff.icon}></ha-svg-icon>
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<ha-svg-icon .path=${mdiMusicNote} slot="icon"></ha-svg-icon>
|
||||
</ha-select>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-6);
|
||||
margin-top: 0;
|
||||
ha-slider {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
height: 320px;
|
||||
width: 100%;
|
||||
--mdc-theme-primary: currentColor;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.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;
|
||||
.basic-controls {
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.cover-image--playing {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
.volume {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@media (max-height: 750px) {
|
||||
.cover-container {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.cover-image--playing {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
.source-input,
|
||||
.sound-input {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.empty-cover {
|
||||
background-color: var(--secondary-background-color);
|
||||
font-size: 1.5em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.main-controls {
|
||||
.volume,
|
||||
.source-input,
|
||||
.sound-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.center-control {
|
||||
--ha-button-height: 56px;
|
||||
.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);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 48px;
|
||||
.tts {
|
||||
margin-top: 16px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.volume,
|
||||
.position-bar,
|
||||
.main-controls {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.volume ha-slider,
|
||||
.position-bar ha-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.volume {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-3);
|
||||
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;
|
||||
ha-button > ha-svg-icon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 16px;
|
||||
top: -6px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -553,63 +301,9 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
border-radius: 10px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
background-color: var(--primary-color);
|
||||
background-color: var(--accent-color);
|
||||
padding: 0 4px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.position-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.position-bar ha-slider::part(references) {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.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: var(--ha-space-6);
|
||||
align-self: center;
|
||||
width: 320px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -635,29 +329,29 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSourceClick(e: Event) {
|
||||
const source = (e.currentTarget as HTMLElement).getAttribute("data-source");
|
||||
if (!source || this.stateObj!.attributes.source === source) {
|
||||
private _handleSourceChanged(e) {
|
||||
const newVal = e.target.value;
|
||||
|
||||
if (!newVal || this.stateObj!.attributes.source === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("media_player", "select_source", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
source,
|
||||
source: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSoundModeClick(e: Event) {
|
||||
const soundMode = (e.currentTarget as HTMLElement).getAttribute(
|
||||
"data-sound-mode"
|
||||
);
|
||||
if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) {
|
||||
private _handleSoundModeChanged(e) {
|
||||
const newVal = e.target.value;
|
||||
|
||||
if (!newVal || this.stateObj?.attributes.sound_mode === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("media_player", "select_sound_mode", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
sound_mode: soundMode,
|
||||
sound_mode: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -680,18 +374,6 @@ 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,24 +3,20 @@ 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 { 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/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 "../../../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";
|
||||
|
||||
@customElement("more-info-script")
|
||||
class MoreInfoScript extends LitElement {
|
||||
@@ -30,8 +26,6 @@ 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;
|
||||
@@ -116,10 +110,7 @@ class MoreInfoScript extends LitElement {
|
||||
hide-picker
|
||||
hide-description
|
||||
.hass=${this.hass}
|
||||
.value=${{
|
||||
...(this.data ? { data: this.data } : {}),
|
||||
...this._scriptData,
|
||||
}}
|
||||
.value=${this._scriptData}
|
||||
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||
.narrow=${this.narrow}
|
||||
@value-changed=${this._scriptDataChanged}
|
||||
@@ -207,13 +198,7 @@ class MoreInfoScript extends LitElement {
|
||||
|
||||
private _canRun() {
|
||||
if (
|
||||
!hasRequiredScriptFields(this.hass, this.stateObj!.entity_id) ||
|
||||
(requiredScriptFieldsFilled(
|
||||
this.hass,
|
||||
this.stateObj!.entity_id,
|
||||
this._scriptData.data
|
||||
) &&
|
||||
canRun(this.stateObj!)) ||
|
||||
canRun(this.stateObj!) ||
|
||||
// Restart can also always runs. Just cancels other run.
|
||||
this.stateObj!.attributes.mode === "restart"
|
||||
) {
|
||||
|
@@ -508,7 +508,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
z-index: 1;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@@ -64,7 +64,6 @@ export interface MoreInfoDialogParams {
|
||||
view?: View;
|
||||
/** @deprecated Use `view` instead */
|
||||
tab?: View;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
type View = "info" | "history" | "settings" | "related";
|
||||
@@ -97,8 +96,6 @@ 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;
|
||||
@@ -119,8 +116,6 @@ 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;
|
||||
@@ -575,7 +570,6 @@ 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,18 +59,9 @@ export class MoreInfoHistory extends LitElement {
|
||||
|
||||
return html`${isComponentLoaded(this.hass, "history")
|
||||
? html`<div class="header">
|
||||
<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>
|
||||
<h2>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.history")}
|
||||
</h2>
|
||||
${__DEMO__
|
||||
? nothing
|
||||
: html`<a href=${this._showMoreHref}
|
||||
@@ -255,10 +246,6 @@ 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,8 +27,6 @@ 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() {
|
||||
@@ -107,7 +105,6 @@ 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,8 +25,6 @@ class MoreInfoContent extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public editMode?: boolean;
|
||||
|
||||
@property({ attribute: false }) public data?: Record<string, any>;
|
||||
|
||||
protected render() {
|
||||
let moreInfoType: string | undefined;
|
||||
|
||||
@@ -50,7 +48,6 @@ class MoreInfoContent extends LitElement {
|
||||
stateObj: this.stateObj,
|
||||
entry: this.entry,
|
||||
editMode: this.editMode,
|
||||
data: this.data,
|
||||
})}
|
||||
${this._showEntityMembers(this.stateObj)
|
||||
? html`
|
||||
|
@@ -104,16 +104,6 @@ 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -269,7 +259,7 @@ class DialogShortcuts extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
gap: 8px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,6 @@ import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-textarea";
|
||||
import type { HaTextArea } from "../../components/ha-textarea";
|
||||
import { convertTextToSpeech } from "../../data/tts";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import type { TTSTryDialogParams } from "./show-dialog-tts-try";
|
||||
@@ -150,24 +149,21 @@ export class TTSTryDialog extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
}
|
||||
ha-textarea,
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.loading {
|
||||
height: 36px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
static styles = css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
}
|
||||
ha-textarea,
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.loading {
|
||||
height: 36px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -127,7 +127,7 @@ export class CloudStepIntro extends LitElement {
|
||||
.features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: var(--ha-space-4);
|
||||
grid-gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
.feature {
|
||||
|
@@ -427,7 +427,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
display: flex;
|
||||
gap: var(--ha-space-1);
|
||||
gap: 4px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.segment {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user