mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-20 03:17:20 +00:00
Compare commits
10 Commits
dev
...
20250327.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1770a51303 | ||
![]() |
534df3d378 | ||
![]() |
23229b3e3b | ||
![]() |
94ee99160b | ||
![]() |
b009d71e8f | ||
![]() |
2ab8209622 | ||
![]() |
ed2940edc3 | ||
![]() |
e2b9a06242 | ||
![]() |
a7acee0438 | ||
![]() |
1208af510c |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -60,7 +60,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -102,7 +102,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -63,7 +63,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -74,7 +74,7 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
uses: home-assistant/wheels@2025.02.0
|
||||
with:
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
@ -92,7 +92,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -121,7 +121,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
34
.yarn/patches/@polymer/polymer/pr-5569.patch
Normal file
34
.yarn/patches/@polymer/polymer/pr-5569.patch
Normal file
@ -0,0 +1,34 @@
|
||||
diff --git a/lib/legacy/class.js b/lib/legacy/class.js
|
||||
index aee2511be1cd9bf900ee552bc98190c1631c57c0..f2f499d68bf52034cac9c28307c99e8ce6b8417d 100644
|
||||
--- a/lib/legacy/class.js
|
||||
+++ b/lib/legacy/class.js
|
||||
@@ -304,17 +304,23 @@ function GenerateClassFromInfo(info, Base, behaviors) {
|
||||
// only proceed if the generated class' prototype has not been registered.
|
||||
const generatedProto = PolymerGenerated.prototype;
|
||||
if (!generatedProto.hasOwnProperty(JSCompiler_renameProperty('__hasRegisterFinished', generatedProto))) {
|
||||
- generatedProto.__hasRegisterFinished = true;
|
||||
+ // make sure legacy lifecycle is called on the *element*'s prototype
|
||||
+ // and not the generated class prototype; if the element has been
|
||||
+ // extended, these are *not* the same.
|
||||
+ const proto = Object.getPrototypeOf(this);
|
||||
+ // Only set flag when generated prototype itself is registered,
|
||||
+ // as this element may be extended from, and needs to run `registered`
|
||||
+ // on all behaviors on the subclass as well.
|
||||
+ if (proto === generatedProto) {
|
||||
+ generatedProto.__hasRegisterFinished = true;
|
||||
+ }
|
||||
// ensure superclass is registered first.
|
||||
super._registered();
|
||||
// copy properties onto the generated class lazily if we're optimizing,
|
||||
- if (legacyOptimizations) {
|
||||
+ if (legacyOptimizations && !Object.hasOwnProperty(generatedProto, '__hasCopiedProperties')) {
|
||||
+ generatedProto.__hasCopiedProperties = true;
|
||||
copyPropertiesToProto(generatedProto);
|
||||
}
|
||||
- // make sure legacy lifecycle is called on the *element*'s prototype
|
||||
- // and not the generated class prototype; if the element has been
|
||||
- // extended, these are *not* the same.
|
||||
- const proto = Object.getPrototypeOf(this);
|
||||
let list = lifecycle.beforeRegister;
|
||||
if (list) {
|
||||
for (let i=0; i < list.length; i++) {
|
935
.yarn/releases/yarn-4.7.0.cjs
vendored
Executable file
935
.yarn/releases/yarn-4.7.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
948
.yarn/releases/yarn-4.9.1.cjs
vendored
948
.yarn/releases/yarn-4.9.1.cjs
vendored
File diff suppressed because one or more lines are too long
@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.7.0.cjs
|
||||
|
@ -2,7 +2,7 @@ import defineProvider from "@babel/helper-define-polyfill-provider";
|
||||
import { join } from "node:path";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const POLYFILL_DIR = join(paths.root_dir, "src/resources/polyfills");
|
||||
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
|
||||
|
||||
// List of polyfill keys with supported browser targets for the functionality
|
||||
const polyfillSupport = {
|
||||
|
@ -20,16 +20,22 @@ module.exports.ignorePackages = () => [];
|
||||
// Files from NPM packages that we should replace with empty file
|
||||
module.exports.emptyPackages = ({ isHassioBuild }) =>
|
||||
[
|
||||
// Contains all color definitions for all material color sets.
|
||||
// We don't use it
|
||||
require.resolve("@polymer/paper-styles/color.js"),
|
||||
require.resolve("@polymer/paper-styles/default-theme.js"),
|
||||
// Loads stuff from a CDN
|
||||
require.resolve("@polymer/font-roboto/roboto.js"),
|
||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
isHassioBuild &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
||||
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
|
||||
),
|
||||
isHassioBuild &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
||||
path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts")
|
||||
),
|
||||
].filter(Boolean);
|
||||
|
||||
@ -44,8 +50,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
__HASS_URL__: `\`${
|
||||
"HASS_URL" in process.env
|
||||
? process.env.HASS_URL
|
||||
: // eslint-disable-next-line no-template-curly-in-string
|
||||
"${location.protocol}//${location.host}"
|
||||
: "${location.protocol}//${location.host}"
|
||||
}\``,
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
@ -159,7 +164,7 @@ module.exports.babelOptions = ({
|
||||
],
|
||||
],
|
||||
exclude: [
|
||||
path.join(paths.root_dir, "src/resources/polyfills"),
|
||||
path.join(paths.polymer_dir, "src/resources/polyfills"),
|
||||
...[
|
||||
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
|
||||
"@lit-labs/virtualizer/polyfills",
|
||||
@ -177,7 +182,6 @@ module.exports.babelOptions = ({
|
||||
include: /\/node_modules\//,
|
||||
exclude: [
|
||||
"element-internals-polyfill",
|
||||
"@shoelace-style",
|
||||
"@?lit(?:-labs|-element|-html)?",
|
||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
||||
},
|
||||
|
@ -21,7 +21,7 @@ module.exports = {
|
||||
},
|
||||
version() {
|
||||
const version = fs
|
||||
.readFileSync(path.resolve(paths.root_dir, "pyproject.toml"), "utf8")
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
|
||||
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
|
@ -169,14 +169,14 @@ const APP_PAGE_ENTRIES = {
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-app-dev",
|
||||
genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root)
|
||||
genPagesDevTask(APP_PAGE_ENTRIES, paths.polymer_dir, paths.app_output_root)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-app-prod",
|
||||
genPagesProdTask(
|
||||
APP_PAGE_ENTRIES,
|
||||
paths.root_dir,
|
||||
paths.polymer_dir,
|
||||
paths.app_output_root,
|
||||
paths.app_output_latest,
|
||||
paths.app_output_es5
|
||||
|
@ -6,8 +6,8 @@ import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const npmPath = (...parts) =>
|
||||
path.resolve(paths.root_dir, "node_modules", ...parts);
|
||||
const polyPath = (...parts) => path.resolve(paths.root_dir, ...parts);
|
||||
path.resolve(paths.polymer_dir, "node_modules", ...parts);
|
||||
const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
|
||||
|
||||
const copyFileDir = (fromFile, toDir) =>
|
||||
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));
|
||||
|
@ -4,7 +4,7 @@ import gulp from "gulp";
|
||||
import { join, resolve } from "node:path";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs");
|
||||
const formatjsDir = join(paths.polymer_dir, "node_modules", "@formatjs");
|
||||
const outDir = join(paths.build_dir, "locale-data");
|
||||
|
||||
const INTL_POLYFILLS = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
root_dir: path.resolve(__dirname, ".."),
|
||||
polymer_dir: path.resolve(__dirname, ".."),
|
||||
|
||||
build_dir: path.resolve(__dirname, "../build"),
|
||||
app_output_root: path.resolve(__dirname, "../hass_frontend"),
|
||||
|
@ -161,7 +161,7 @@ const createRspackConfig = ({
|
||||
}),
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
!isProdBuild && new LogStartCompilePlugin(),
|
||||
isProdBuild &&
|
||||
|
@ -3,6 +3,7 @@ export const demoThemeJimpower = () => ({
|
||||
"paper-item-icon-color": "var(--primary-text-color)",
|
||||
"primary-color": "#5294E2",
|
||||
"label-badge-red": "var(--accent-color)",
|
||||
"paper-tabs-selection-bar-color": "green",
|
||||
"light-primary-color": "var(--accent-color)",
|
||||
"primary-background-color": "#383C45",
|
||||
"primary-text-color": "#FFFFFF",
|
||||
|
@ -4,6 +4,7 @@ export const demoThemeKernehed = () => ({
|
||||
"paper-item-icon-color": "var(--primary-text-color)",
|
||||
"primary-color": "#2980b9",
|
||||
"label-badge-red": "var(--accent-color)",
|
||||
"paper-tabs-selection-bar-color": "green",
|
||||
"primary-text-color": "#FFFFFF",
|
||||
"light-primary-color": "var(--accent-color)",
|
||||
"primary-background-color": "#222222",
|
||||
|
@ -42,6 +42,7 @@ export default tseslint.config(
|
||||
__VERSION__: false,
|
||||
__STATIC_PATH__: false,
|
||||
__SUPERVISOR__: false,
|
||||
Polymer: true,
|
||||
},
|
||||
|
||||
parser: tseslint.parser,
|
||||
|
@ -16,14 +16,23 @@ import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
|
||||
|
||||
const _filterDevices = memoizeOne(
|
||||
(hardware: HassioHardwareInfo, filter: string, language: string) =>
|
||||
(
|
||||
showAdvanced: boolean,
|
||||
hardware: HassioHardwareInfo,
|
||||
filter: string,
|
||||
language: string
|
||||
) =>
|
||||
hardware.devices
|
||||
.filter(
|
||||
(device) =>
|
||||
device.by_id?.toLowerCase().includes(filter) ||
|
||||
device.name.toLowerCase().includes(filter) ||
|
||||
device.dev_path.toLocaleLowerCase().includes(filter) ||
|
||||
JSON.stringify(device.attributes).toLocaleLowerCase().includes(filter)
|
||||
(showAdvanced ||
|
||||
["tty", "gpio", "input"].includes(device.subsystem)) &&
|
||||
(device.by_id?.toLowerCase().includes(filter) ||
|
||||
device.name.toLowerCase().includes(filter) ||
|
||||
device.dev_path.toLocaleLowerCase().includes(filter) ||
|
||||
JSON.stringify(device.attributes)
|
||||
.toLocaleLowerCase()
|
||||
.includes(filter))
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
);
|
||||
@ -51,6 +60,7 @@ class HassioHardwareDialog extends LitElement {
|
||||
}
|
||||
|
||||
const devices = _filterDevices(
|
||||
this.hass.userData?.showAdvanced || false,
|
||||
this._dialogParams.hardware,
|
||||
(this._filter || "").toLowerCase(),
|
||||
this.hass.locale.language
|
||||
|
@ -1,6 +1,9 @@
|
||||
import "./hassio-main";
|
||||
|
||||
import("../../src/resources/ha-style");
|
||||
import("@polymer/polymer/lib/utils/settings").then(
|
||||
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
|
||||
);
|
||||
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.textContent = `
|
||||
|
80
package.json
80
package.json
@ -26,17 +26,17 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.27.0",
|
||||
"@babel/runtime": "7.26.10",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/commands": "6.8.0",
|
||||
"@codemirror/language": "6.11.0",
|
||||
"@codemirror/legacy-modes": "6.5.0",
|
||||
"@codemirror/search": "6.5.10",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.5",
|
||||
"@codemirror/view": "6.36.4",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||
"@formatjs/intl-datetimeformat": "6.17.4",
|
||||
"@formatjs/intl-displaynames": "6.8.11",
|
||||
"@formatjs/intl-durationformat": "0.7.4",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.5",
|
||||
@ -45,12 +45,12 @@
|
||||
"@formatjs/intl-numberformat": "8.15.4",
|
||||
"@formatjs/intl-pluralrules": "5.4.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.11",
|
||||
"@fullcalendar/core": "6.1.17",
|
||||
"@fullcalendar/daygrid": "6.1.17",
|
||||
"@fullcalendar/interaction": "6.1.17",
|
||||
"@fullcalendar/list": "6.1.17",
|
||||
"@fullcalendar/luxon3": "6.1.17",
|
||||
"@fullcalendar/timegrid": "6.1.17",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
"@fullcalendar/list": "6.1.15",
|
||||
"@fullcalendar/luxon3": "6.1.15",
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.8",
|
||||
@ -81,16 +81,20 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "2.3.0",
|
||||
"@material/web": "2.2.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
"@polymer/paper-listbox": "3.0.1",
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@shoelace-style/shoelace": "2.20.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.8.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.7.3",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.3",
|
||||
"@vaadin/combo-box": "24.7.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.1",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@ -107,12 +111,12 @@
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "5.6.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"element-internals-polyfill": "3.0.1",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.7.16",
|
||||
"js-yaml": "4.1.0",
|
||||
@ -121,8 +125,8 @@
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "2.8.0",
|
||||
"lit-html": "2.8.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "15.0.8",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "15.0.7",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
@ -155,15 +159,15 @@
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.26.10",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/preset-typescript": "7.27.0",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.19.1",
|
||||
"@lokalise/node-api": "14.3.0",
|
||||
"@octokit/auth-oauth-device": "7.1.5",
|
||||
"@octokit/plugin-retry": "7.2.1",
|
||||
"@lokalise/node-api": "14.2.0",
|
||||
"@octokit/auth-oauth-device": "7.1.4",
|
||||
"@octokit/plugin-retry": "7.2.0",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@rsdoctor/rspack-plugin": "1.0.1",
|
||||
"@rspack/cli": "1.3.5",
|
||||
"@rspack/core": "1.3.5",
|
||||
"@rsdoctor/rspack-plugin": "1.0.0",
|
||||
"@rspack/cli": "1.2.8",
|
||||
"@rspack/core": "1.2.8",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@ -175,24 +179,24 @@
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.6.2",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "3.1.1",
|
||||
"@vitest/coverage-v8": "3.0.9",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.24.0",
|
||||
"eslint": "9.23.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.2",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit": "2.0.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "3.0.0",
|
||||
@ -205,9 +209,9 @@
|
||||
"gulp-rename": "2.0.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "26.1.0",
|
||||
"jsdom": "26.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.5.1",
|
||||
"lint-staged": "15.5.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@ -216,27 +220,29 @@
|
||||
"prettier": "3.5.3",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "20.0.0",
|
||||
"sinon": "19.0.4",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.30.1",
|
||||
"typescript": "5.8.2",
|
||||
"typescript-eslint": "8.27.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.1.1",
|
||||
"vitest": "3.0.9",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
"@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit": "2.8.0",
|
||||
"lit-html": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.17",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "16.0.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
"packageManager": "yarn@4.7.0"
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
|
||||
<mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
|
||||
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1110_23734)">
|
||||
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
|
||||
</g>
|
||||
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
||||
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
|
||||
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,15 +0,0 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M30.824 22.3365L38.824 38.8365L30.324 50.3365" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
|
||||
<mask id="mask0_1180_4955" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
|
||||
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1180_4955)">
|
||||
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
|
||||
</g>
|
||||
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
||||
<rect x="23" y="11" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="22" y="52" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
|
||||
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,19 +0,0 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||
<circle cx="47" cy="36" r="34" fill="white"/>
|
||||
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||
<mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
|
||||
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1110_23775)">
|
||||
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
|
||||
</g>
|
||||
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.4 KiB |
@ -1,19 +0,0 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
|
||||
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||
<mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
|
||||
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1180_4965)">
|
||||
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
|
||||
</g>
|
||||
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.4 KiB |
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250326.0"
|
||||
version = "20250327.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
@ -265,10 +265,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
window.innerWidth > 450 &&
|
||||
!matchMedia("(prefers-reduced-motion)").matches
|
||||
) {
|
||||
if (window.innerWidth > 450) {
|
||||
import("../resources/particles");
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,6 @@ import {
|
||||
differenceInMilliseconds,
|
||||
differenceInMonths,
|
||||
endOfMonth,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
differenceInDays,
|
||||
addDays,
|
||||
} from "date-fns";
|
||||
import { toZonedTime, fromZonedTime } from "date-fns-tz";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
@ -104,32 +100,6 @@ export const shiftDateRange = (
|
||||
locale,
|
||||
config
|
||||
);
|
||||
} else if (
|
||||
calcDateProperty(
|
||||
startDate,
|
||||
(date) => startOfDay(date).getMilliseconds() === date.getMilliseconds(),
|
||||
locale,
|
||||
config
|
||||
) &&
|
||||
calcDateProperty(
|
||||
endDate,
|
||||
(date) => endOfDay(date).getMilliseconds() === date.getMilliseconds(),
|
||||
locale,
|
||||
config
|
||||
)
|
||||
) {
|
||||
const difference =
|
||||
((calcDateDifferenceProperty(
|
||||
endDate,
|
||||
startDate,
|
||||
differenceInDays,
|
||||
locale,
|
||||
config
|
||||
) as number) +
|
||||
1) *
|
||||
(forward ? 1 : -1);
|
||||
start = calcDate(startDate, addDays, locale, config, difference);
|
||||
end = calcDate(endDate, addDays, locale, config, difference);
|
||||
} else {
|
||||
const difference =
|
||||
((calcDateDifferenceProperty(
|
||||
|
@ -84,12 +84,12 @@ export const calcDateRange = (
|
||||
case "now-7d":
|
||||
return [
|
||||
calcDate(today, subDays, hass.locale, hass.config, 7),
|
||||
calcDate(today, subDays, hass.locale, hass.config, 0),
|
||||
calcDate(today, subDays, hass.locale, hass.config, 1),
|
||||
];
|
||||
case "now-30d":
|
||||
return [
|
||||
calcDate(today, subDays, hass.locale, hass.config, 30),
|
||||
calcDate(today, subDays, hass.locale, hass.config, 0),
|
||||
calcDate(today, subDays, hass.locale, hass.config, 1),
|
||||
];
|
||||
case "now-12m":
|
||||
return [
|
||||
|
@ -134,7 +134,10 @@ export const applyThemesOnElement = (
|
||||
element.__themes = { cacheKey, keys: newTheme?.keys };
|
||||
|
||||
// Set and/or reset styles
|
||||
if (window.ShadyCSS) {
|
||||
if (element.updateStyles) {
|
||||
// Use updateStyles() method of Polymer elements
|
||||
element.updateStyles(styles);
|
||||
} else if (window.ShadyCSS) {
|
||||
// Use ShadyCSS if available
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
|
||||
} else {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
@ -6,7 +5,6 @@ import type {
|
||||
} from "../../data/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { getDuplicates } from "../string/get_duplicates";
|
||||
|
||||
export const computeDeviceName = (
|
||||
device: DeviceRegistryEntry
|
||||
@ -38,13 +36,3 @@ export const fallbackDeviceName = (
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getDuplicatedDeviceNames = memoizeOne(
|
||||
(devices: HomeAssistant["devices"]): Set<string> => {
|
||||
const names = Object.values(devices)
|
||||
.map((device) => computeDeviceName(device))
|
||||
.filter((name): name is string => name !== undefined);
|
||||
|
||||
return getDuplicates(names);
|
||||
}
|
||||
);
|
||||
|
@ -33,14 +33,7 @@ export const computeEntityEntryName = (
|
||||
const device = entry.device_id ? hass.devices[entry.device_id] : undefined;
|
||||
|
||||
if (!device) {
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
|
||||
if (stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return undefined;
|
||||
return name;
|
||||
}
|
||||
|
||||
const deviceName = computeDeviceName(device);
|
||||
|
@ -5,7 +5,7 @@ import { getIntegrationDescriptions } from "../../data/integrations";
|
||||
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { showMatterAddDeviceDialog } from "../../panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device";
|
||||
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node";
|
||||
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { isComponentLoaded } from "../config/is_component_loaded";
|
||||
|
@ -1,14 +0,0 @@
|
||||
export function getDuplicates(array: string[]): Set<string> {
|
||||
const duplicates = new Set<string>();
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const item of array) {
|
||||
if (seen.has(item)) {
|
||||
duplicates.add(item);
|
||||
} else {
|
||||
seen.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates;
|
||||
}
|
@ -1,15 +1,13 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../ha-button";
|
||||
import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
@customElement("ha-progress-button")
|
||||
export class HaProgressButton extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public progress = false;
|
||||
@ -23,16 +21,14 @@ export class HaProgressButton extends LitElement {
|
||||
public render(): TemplateResult {
|
||||
const overlay = this._result || this.progress;
|
||||
return html`
|
||||
<ha-button
|
||||
.raised=${this.raised}
|
||||
.label=${this.label}
|
||||
<mwc-button
|
||||
?raised=${this.raised}
|
||||
.unelevated=${this.unelevated}
|
||||
.disabled=${this.disabled || this.progress}
|
||||
class=${this._result || ""}
|
||||
>
|
||||
<slot name="icon" slot="icon"></slot>
|
||||
<slot></slot>
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
${!overlay
|
||||
? nothing
|
||||
: html`
|
||||
@ -72,12 +68,12 @@ export class HaProgressButton extends LitElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-button {
|
||||
mwc-button {
|
||||
transition: all 1s;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
ha-button.success {
|
||||
mwc-button.success {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--success-color);
|
||||
transition: none;
|
||||
@ -85,13 +81,13 @@ export class HaProgressButton extends LitElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-button[unelevated].success,
|
||||
ha-button[raised].success {
|
||||
mwc-button[unelevated].success,
|
||||
mwc-button[raised].success {
|
||||
--mdc-theme-primary: var(--success-color);
|
||||
--mdc-theme-on-primary: white;
|
||||
}
|
||||
|
||||
ha-button.error {
|
||||
mwc-button.error {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--error-color);
|
||||
transition: none;
|
||||
@ -99,8 +95,8 @@ export class HaProgressButton extends LitElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-button[unelevated].error,
|
||||
ha-button[raised].error {
|
||||
mwc-button[unelevated].error,
|
||||
mwc-button[raised].error {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
--mdc-theme-on-primary: white;
|
||||
}
|
||||
@ -117,8 +113,8 @@ export class HaProgressButton extends LitElement {
|
||||
color: white;
|
||||
}
|
||||
|
||||
ha-button.success slot,
|
||||
ha-button.error slot {
|
||||
mwc-button.success slot,
|
||||
mwc-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
:host([destructive]) {
|
||||
|
@ -296,11 +296,7 @@ export class StatisticsChart extends LitElement {
|
||||
align: "left",
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
scale:
|
||||
this.chartType !== "bar" ||
|
||||
this.logarithmicScale ||
|
||||
minYAxis !== undefined ||
|
||||
maxYAxis !== undefined,
|
||||
scale: true,
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
splitLine: {
|
||||
|
@ -645,16 +645,15 @@ export class HaDataTable extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const prom =
|
||||
this.sortColumn && this._sortColumns[this.sortColumn]
|
||||
? sortData(
|
||||
filteredData,
|
||||
this._sortColumns[this.sortColumn],
|
||||
this.sortDirection,
|
||||
this.sortColumn,
|
||||
this.hass.locale.language
|
||||
)
|
||||
: filteredData;
|
||||
const prom = this.sortColumn
|
||||
? sortData(
|
||||
filteredData,
|
||||
this._sortColumns[this.sortColumn],
|
||||
this.sortDirection,
|
||||
this.sortColumn,
|
||||
this.hass.locale.language
|
||||
)
|
||||
: filteredData;
|
||||
|
||||
const [data] = await Promise.all([prom, nextRender]);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@ -19,7 +19,7 @@ import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-list-item";
|
||||
|
||||
interface Device {
|
||||
name: string;
|
||||
@ -35,14 +35,11 @@ export type HaDevicePickerDeviceFilterFunc = (
|
||||
|
||||
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">${item.name}</span>
|
||||
${item.area
|
||||
? html`<span slot="supporting-text">${item.area}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
const rowRenderer: ComboBoxLitRenderer<Device> = (item) =>
|
||||
html`<ha-list-item .twoline=${!!item.area}>
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary">${item.area}</span>
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-device-picker")
|
||||
export class HaDevicePicker extends LitElement {
|
||||
|
@ -1,78 +1,35 @@
|
||||
import { mdiMagnify, mdiPlus } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import "../ha-list-item";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityContext } from "../../common/entity/get_entity_context";
|
||||
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
||||
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { HelperDomain } from "../../panels/config/helpers/const";
|
||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
|
||||
const FAKE_ENTITY: HassEntity = {
|
||||
entity_id: "",
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
interface EntityPickerItem extends HassEntity {
|
||||
label: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
translated_domain?: string;
|
||||
show_entity_id?: boolean;
|
||||
entity_name?: string;
|
||||
area_name?: string;
|
||||
device_name?: string;
|
||||
friendly_name?: string;
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
||||
friendly_name: string;
|
||||
}
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
const DOMAIN_STYLE = styleMap({
|
||||
fontSize: "12px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "18px",
|
||||
alignSelf: "flex-end",
|
||||
maxWidth: "30%",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
const ENTITY_ID_STYLE = styleMap({
|
||||
fontFamily: "var(--code-font-family, monospace)",
|
||||
fontSize: "11px",
|
||||
});
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -149,7 +106,8 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
|
||||
@property({ attribute: "item-label-path" }) public itemLabelPath =
|
||||
"friendly_name";
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@ -165,48 +123,30 @@ export class HaEntityPicker extends LitElement {
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
private _initialItems = false;
|
||||
private _initedStates = false;
|
||||
|
||||
private _items: EntityPickerItem[] = [];
|
||||
private _states: HassEntityWithCachedName[] = [];
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
private _rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (
|
||||
item
|
||||
) =>
|
||||
html`<ha-list-item graphic="avatar" .twoline=${!!item.entity_id}>
|
||||
${item.state
|
||||
? html`<state-badge
|
||||
slot="graphic"
|
||||
.stateObj=${item}
|
||||
.hass=${this.hass}
|
||||
></state-badge>`
|
||||
: ""}
|
||||
<span>${item.friendly_name}</span>
|
||||
<span slot="secondary"
|
||||
>${item.entity_id.startsWith(CREATE_ID)
|
||||
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
|
||||
: item.entity_id}</span
|
||||
>
|
||||
</ha-list-item>`;
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<EntityPickerItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
${item.icon_path
|
||||
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
|
||||
: html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`}
|
||||
|
||||
<span slot="headline">${item.primary} </span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.entity_id && item.show_entity_id
|
||||
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
|
||||
>${item.entity_id}</span
|
||||
>`
|
||||
: nothing}
|
||||
${item.translated_domain && !item.show_entity_id
|
||||
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
|
||||
${item.translated_domain}
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
private _getStates = memoizeOne(
|
||||
(
|
||||
_opened: boolean,
|
||||
hass: this["hass"],
|
||||
@ -218,8 +158,8 @@ export class HaEntityPicker extends LitElement {
|
||||
includeEntities: this["includeEntities"],
|
||||
excludeEntities: this["excludeEntities"],
|
||||
createDomains: this["createDomains"]
|
||||
): EntityPickerItem[] => {
|
||||
let states: EntityPickerItem[] = [];
|
||||
): HassEntityWithCachedName[] => {
|
||||
let states: HassEntityWithCachedName[] = [];
|
||||
|
||||
if (!hass) {
|
||||
return [];
|
||||
@ -228,7 +168,7 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
const createItems = createDomains?.length
|
||||
? createDomains.map((domain) => {
|
||||
const primary = hass.localize(
|
||||
const newFriendlyName = hass.localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
{
|
||||
domain: isHelperDomain(domain)
|
||||
@ -240,14 +180,16 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
|
||||
return {
|
||||
...FAKE_ENTITY,
|
||||
entity_id: CREATE_ID + domain,
|
||||
primary: primary,
|
||||
label: primary,
|
||||
secondary: this.hass.localize(
|
||||
"ui.components.entity.entity-picker.new_entity"
|
||||
),
|
||||
icon_path: mdiPlus,
|
||||
state: "on",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: newFriendlyName,
|
||||
attributes: {
|
||||
icon: "mdi:plus",
|
||||
},
|
||||
strings: [domain, newFriendlyName],
|
||||
};
|
||||
})
|
||||
: [];
|
||||
@ -255,14 +197,21 @@ export class HaEntityPicker extends LitElement {
|
||||
if (!entityIds.length) {
|
||||
return [
|
||||
{
|
||||
...FAKE_ENTITY,
|
||||
primary: this.hass!.localize(
|
||||
entity_id: "",
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
),
|
||||
label: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
),
|
||||
icon_path: mdiMagnify,
|
||||
attributes: {
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
),
|
||||
icon: "mdi:magnify",
|
||||
},
|
||||
strings: [],
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
@ -292,49 +241,19 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
states = entityIds
|
||||
.map<EntityPickerItem>((entityId) => {
|
||||
const stateObj = hass!.states[entityId];
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, hass);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = computeEntityName(stateObj, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
const translatedDomain = domainToName(
|
||||
this.hass.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
|
||||
.map((key) => {
|
||||
const friendly_name = computeStateName(hass!.states[key]) || key;
|
||||
return {
|
||||
...hass!.states[entityId],
|
||||
primary: primary,
|
||||
secondary:
|
||||
secondary ||
|
||||
this.hass.localize("ui.components.device-picker.no_area"),
|
||||
label: friendlyName,
|
||||
translated_domain: translatedDomain,
|
||||
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
|
||||
entity_name: entityName || deviceName,
|
||||
area_name: areaName,
|
||||
device_name: deviceName,
|
||||
friendly_name: friendlyName,
|
||||
show_entity_id: hass.userData?.showEntityIdPicker,
|
||||
...hass!.states[key],
|
||||
friendly_name,
|
||||
strings: [key, friendly_name],
|
||||
};
|
||||
})
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.sorting_label!,
|
||||
entityB.sorting_label!,
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
@ -372,14 +291,21 @@ export class HaEntityPicker extends LitElement {
|
||||
if (!states.length) {
|
||||
return [
|
||||
{
|
||||
...FAKE_ENTITY,
|
||||
primary: this.hass!.localize(
|
||||
entity_id: "",
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
label: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
icon_path: mdiMagnify,
|
||||
attributes: {
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
icon: "mdi:magnify",
|
||||
},
|
||||
strings: [],
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
@ -405,8 +331,8 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
|
||||
this._items = this._getItems(
|
||||
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
|
||||
this._states = this._getStates(
|
||||
this._opened,
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
@ -418,10 +344,10 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeEntities,
|
||||
this.createDomains
|
||||
);
|
||||
if (this._initialItems) {
|
||||
this.comboBox.filteredItems = this._items;
|
||||
if (this._initedStates) {
|
||||
this.comboBox.filteredItems = this._states;
|
||||
}
|
||||
this._initialItems = true;
|
||||
this._initedStates = true;
|
||||
}
|
||||
|
||||
if (changedProps.has("createDomains") && this.createDomains?.length) {
|
||||
@ -441,11 +367,10 @@ export class HaEntityPicker extends LitElement {
|
||||
: this.label}
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.filteredItems=${this._items}
|
||||
.filteredItems=${this._states}
|
||||
.renderer=${this._rowRenderer}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@ -482,49 +407,12 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _fuseKeys = [
|
||||
"entity_name",
|
||||
"device_name",
|
||||
"area_name",
|
||||
"translated_domain",
|
||||
"friendly_name", // for backwards compatibility
|
||||
"entity_id", // for technical search
|
||||
];
|
||||
|
||||
private _fuseIndex = memoizeOne((states: EntityPickerItem[]) =>
|
||||
Fuse.createIndex(this._fuseKeys, states)
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
||||
|
||||
const minLength = 2;
|
||||
|
||||
const searchTerms = (filterString.split(" ") ?? []).filter(
|
||||
(term) => term.length >= minLength
|
||||
);
|
||||
|
||||
if (searchTerms.length > 0) {
|
||||
const index = this._fuseIndex(this._items);
|
||||
|
||||
const options: IFuseOptions<EntityPickerItem> = {
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.3,
|
||||
ignoreDiacritics: true,
|
||||
minMatchCharLength: minLength,
|
||||
};
|
||||
|
||||
const fuse = new Fuse(this._items, options, index);
|
||||
const results = fuse.search({
|
||||
$and: searchTerms.map((term) => ({
|
||||
$or: this._fuseKeys.map((key) => ({ [key]: term })),
|
||||
})),
|
||||
});
|
||||
target.filteredItems = results.map((result) => result.item);
|
||||
} else {
|
||||
target.filteredItems = this._items;
|
||||
}
|
||||
const filterString = ev.detail.value.trim().toLowerCase();
|
||||
target.filteredItems = filterString.length
|
||||
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
|
||||
: this._states;
|
||||
}
|
||||
|
||||
private _setValue(value: string | undefined) {
|
||||
|
@ -1,23 +1,23 @@
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
||||
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||
import type { StatisticsMetaData } from "../../data/recorder";
|
||||
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
||||
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||
|
||||
interface StatisticItem extends ScorableTextItem {
|
||||
id: string;
|
||||
@ -99,18 +99,16 @@ export class HaStatisticPicker extends LitElement {
|
||||
@state() private _filteredItems?: StatisticItem[] = undefined;
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (item) =>
|
||||
html`<ha-combo-box-item type="button">
|
||||
html`<mwc-list-item graphic="avatar" twoline>
|
||||
${item.state
|
||||
? html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.state}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: html`<span slot="start" style="width: 32px"></span>`}
|
||||
<span slot="headline">${item.name}</span>
|
||||
<span slot="supporting-text"
|
||||
? html`<state-badge
|
||||
slot="graphic"
|
||||
.stateObj=${item.state}
|
||||
.hass=${this.hass}
|
||||
></state-badge>`
|
||||
: ""}
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary"
|
||||
>${item.id === "" || item.id === "__missing"
|
||||
? html`<a
|
||||
target="_blank"
|
||||
@ -122,7 +120,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
>`
|
||||
: item.id}</span
|
||||
>
|
||||
</ha-combo-box-item>`;
|
||||
</mwc-list-item>`;
|
||||
|
||||
private _getStatistics = memoizeOne(
|
||||
(
|
||||
|
@ -79,17 +79,6 @@ export class StateBadge extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const cls = this.getClass();
|
||||
if (cls) {
|
||||
cls.forEach((toSet, className) => {
|
||||
if (!toSet) {
|
||||
this.classList.remove(className);
|
||||
} else {
|
||||
this.classList.add(className);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.icon) {
|
||||
return nothing;
|
||||
}
|
||||
@ -186,57 +175,35 @@ export class StateBadge extends LitElement {
|
||||
backgroundImage = `url(${imageUrl})`;
|
||||
this.icon = false;
|
||||
}
|
||||
|
||||
if (domain === "update") {
|
||||
this.style.borderRadius = "0";
|
||||
} else if (domain === "media_player" || domain === "camera") {
|
||||
this.style.borderRadius = "8%";
|
||||
}
|
||||
}
|
||||
|
||||
this._iconStyle = iconStyle;
|
||||
this.style.backgroundImage = backgroundImage;
|
||||
}
|
||||
|
||||
protected getClass() {
|
||||
const cls = new Map(
|
||||
["has-no-radius", "has-media-image", "has-image"].map((_cls) => [
|
||||
_cls,
|
||||
false,
|
||||
])
|
||||
);
|
||||
if (this.stateObj) {
|
||||
const domain = computeDomain(this.stateObj.entity_id);
|
||||
if (domain === "update") {
|
||||
cls.set("has-no-radius", true);
|
||||
} else if (domain === "media_player" || domain === "camera") {
|
||||
cls.set("has-media-image", true);
|
||||
} else if (this.style.backgroundImage !== "") {
|
||||
cls.set("has-image", true);
|
||||
}
|
||||
}
|
||||
return cls;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
iconColorCSS,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
border-radius: var(--state-badge-border-radius, 50%);
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
background-size: cover;
|
||||
line-height: 40px;
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
--state-inactive-color: initial;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:host(.has-image) {
|
||||
border-radius: var(--state-badge-with-image-border-radius, 50%);
|
||||
}
|
||||
:host(.has-media-image) {
|
||||
border-radius: var(--state-badge-with-media-image-border-radius, 8%);
|
||||
}
|
||||
:host(.has-no-radius) {
|
||||
border-radius: 0;
|
||||
}
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
|
@ -10,23 +10,20 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-list-item";
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">${item.name}</span>
|
||||
<span slot="supporting-text">${item.slug}</span>
|
||||
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) =>
|
||||
html`<ha-list-item twoline graphic="icon">
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary">${item.slug}</span>
|
||||
${item.icon
|
||||
? html`
|
||||
<img
|
||||
alt=""
|
||||
slot="start"
|
||||
.src="/api/hassio/addons/${item.slug}/icon"
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
? html`<img
|
||||
alt=""
|
||||
slot="graphic"
|
||||
.src="/api/hassio/addons/${item.slug}/icon"
|
||||
/>`
|
||||
: ""}
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-addon-picker")
|
||||
class HaAddonPicker extends LitElement {
|
||||
|
@ -25,7 +25,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
@ -126,38 +125,38 @@ export class HaAreaFloorPicker extends LitElement {
|
||||
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
|
||||
const rtl = computeRTL(this.hass);
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
type="button"
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
style=${item.type === "area" && item.hasFloor
|
||||
? "--md-list-item-leading-space: 48px;"
|
||||
? rtl
|
||||
? "--mdc-list-side-padding-right: 48px;"
|
||||
: "--mdc-list-side-padding-left: 48px;"
|
||||
: ""}
|
||||
>
|
||||
${item.type === "area" && item.hasFloor
|
||||
? html`
|
||||
<ha-tree-indicator
|
||||
style=${styleMap({
|
||||
width: "48px",
|
||||
position: "absolute",
|
||||
top: "0px",
|
||||
left: rtl ? undefined : "4px",
|
||||
right: rtl ? "4px" : undefined,
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
.end=${item.lastArea}
|
||||
slot="start"
|
||||
></ha-tree-indicator>
|
||||
`
|
||||
? html`<ha-tree-indicator
|
||||
style=${styleMap({
|
||||
width: "48px",
|
||||
position: "absolute",
|
||||
top: "0px",
|
||||
left: rtl ? undefined : "8px",
|
||||
right: rtl ? "8px" : undefined,
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
.end=${item.lastArea}
|
||||
slot="graphic"
|
||||
></ha-tree-indicator>`
|
||||
: nothing}
|
||||
${item.type === "floor"
|
||||
? html`<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>`
|
||||
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
|
||||
: item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${item.name}
|
||||
</ha-combo-box-item>
|
||||
</ha-list-item>
|
||||
`;
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
@ -23,21 +24,22 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
|
||||
html`<ha-list-item
|
||||
graphic="icon"
|
||||
class=${classMap({ "add-new": item.area_id === ADD_NEW_ID })}
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>`}
|
||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>`}
|
||||
${item.name}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
</ha-list-item>`;
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_ITEMS_ID = "___NO_ITEMS___";
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
@ -33,10 +32,6 @@ export class HaCameraStream extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||
|
||||
@property({ attribute: false }) public aspectRatio?: number;
|
||||
|
||||
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
|
||||
|
||||
@property({ type: Boolean, attribute: "controls" })
|
||||
public controls = false;
|
||||
|
||||
@ -106,10 +101,6 @@ export class HaCameraStream extends LitElement {
|
||||
: this._connected
|
||||
? computeMJPEGStreamUrl(this.stateObj)
|
||||
: this._posterUrl || ""}
|
||||
style=${styleMap({
|
||||
aspectRatio: this.aspectRatio,
|
||||
objectFit: this.fitMode,
|
||||
})}
|
||||
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
|
||||
/>`;
|
||||
}
|
||||
@ -126,8 +117,6 @@ export class HaCameraStream extends LitElement {
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleHlsStreams}
|
||||
class=${stream.visible ? "" : "hidden"}
|
||||
.aspectRatio=${this.aspectRatio}
|
||||
.fitMode=${this.fitMode}
|
||||
></ha-hls-player>`;
|
||||
}
|
||||
|
||||
@ -142,8 +131,6 @@ export class HaCameraStream extends LitElement {
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleWebRtcStreams}
|
||||
class=${stream.visible ? "" : "hidden"}
|
||||
.aspectRatio=${this.aspectRatio}
|
||||
.fitMode=${this.fitMode}
|
||||
></ha-web-rtc-player>`;
|
||||
}
|
||||
|
||||
@ -272,16 +259,6 @@ export class HaCameraStream extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-web-rtc-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-hls-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaMdListItem } from "./ha-md-list-item";
|
||||
|
||||
@customElement("ha-combo-box-item")
|
||||
export class HaComboBoxItem extends HaMdListItem {
|
||||
@property({ type: Boolean, reflect: true, attribute: "border-top" })
|
||||
public borderTop = false;
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-list-item-one-line-container-height: 48px;
|
||||
--md-list-item-two-line-container-height: 64px;
|
||||
}
|
||||
:host([border-top]) md-item {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
[slot="start"] {
|
||||
--paper-item-icon-color: var(--secondary-text-color);
|
||||
}
|
||||
[slot="headline"] {
|
||||
line-height: 22px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[slot="supporting-text"] {
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
::slotted(state-badge),
|
||||
::slotted(img) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-combo-box-item": HaComboBoxItem;
|
||||
}
|
||||
}
|
@ -16,8 +16,8 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@ -105,9 +105,6 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public opened = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-clear-icon" })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
||||
|
||||
@query("ha-textfield", true) private _inputElement!: HaTextField;
|
||||
@ -190,7 +187,7 @@ export class HaComboBox extends LitElement {
|
||||
>
|
||||
<slot name="icon" slot="leadingIcon"></slot>
|
||||
</ha-textfield>
|
||||
${this.value && !this.hideClearIcon
|
||||
${this.value
|
||||
? html`<ha-svg-icon
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
@ -207,7 +204,6 @@ export class HaComboBox extends LitElement {
|
||||
aria-expanded=${this.opened ? "true" : "false"}
|
||||
class="toggle-button"
|
||||
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
|
||||
?disabled=${this.disabled}
|
||||
@click=${this._toggleOpen}
|
||||
></ha-svg-icon>
|
||||
</vaadin-combo-box-light>
|
||||
@ -216,11 +212,10 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
private _defaultRowRenderer: ComboBoxLitRenderer<
|
||||
string | Record<string, any>
|
||||
> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
> = (item) =>
|
||||
html`<ha-list-item>
|
||||
${this.itemLabelPath ? item[this.itemLabelPath] : item}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
</ha-list-item>`;
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
@ -361,10 +356,6 @@ export class HaComboBox extends LitElement {
|
||||
:host([opened]) .toggle-button {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.toggle-button[disabled] {
|
||||
color: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
top: -7px;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@ -10,7 +11,6 @@ import type { ValueChangedEvent, HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
|
||||
export interface ConfigEntryExtended extends ConfigEntry {
|
||||
localized_domain_name?: string;
|
||||
@ -48,20 +48,18 @@ class HaConfigEntryPicker extends LitElement {
|
||||
this._getConfigEntries();
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
|
||||
item
|
||||
) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">
|
||||
${item.title ||
|
||||
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (item) =>
|
||||
html`<mwc-list-item twoline graphic="icon">
|
||||
<span
|
||||
>${item.title ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">${item.localized_domain_name}</span>
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">${item.localized_domain_name}</span>
|
||||
<img
|
||||
alt=""
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
src=${brandsUrl({
|
||||
domain: item.domain,
|
||||
type: "icon",
|
||||
@ -72,8 +70,7 @@ class HaConfigEntryPicker extends LitElement {
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
</mwc-list-item>`;
|
||||
|
||||
protected render() {
|
||||
if (!this._configEntries) {
|
||||
|
@ -3,6 +3,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
@ -27,9 +28,9 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
|
||||
type ScorableFloorRegistryEntry = ScorableTextItem & FloorRegistryEntry;
|
||||
|
||||
@ -37,12 +38,14 @@ const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_FLOORS_ID = "___NO_FLOORS___";
|
||||
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>
|
||||
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
|
||||
html`<ha-list-item
|
||||
graphic="icon"
|
||||
class=${classMap({ "add-new": item.floor_id === ADD_NEW_ID })}
|
||||
>
|
||||
<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>
|
||||
${item.name}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-floor-picker")
|
||||
export class HaFloorPicker extends LitElement {
|
||||
|
@ -2,13 +2,12 @@ import type HlsType from "hls.js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { fetchStreamUrl } from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import { fetchStreamUrl } from "../data/camera";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
|
||||
type HlsLite = Omit<
|
||||
HlsType,
|
||||
@ -25,10 +24,6 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
@property({ attribute: "poster-url" }) public posterUrl?: string;
|
||||
|
||||
@property({ attribute: false }) public aspectRatio?: number;
|
||||
|
||||
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
|
||||
|
||||
@property({ type: Boolean, attribute: "controls" })
|
||||
public controls = false;
|
||||
|
||||
@ -92,11 +87,6 @@ class HaHLSPlayer extends LitElement {
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
@loadeddata=${this._loadedData}
|
||||
style=${styleMap({
|
||||
height: this.aspectRatio == null ? "100%" : "auto",
|
||||
aspectRatio: this.aspectRatio,
|
||||
objectFit: this.fitMode,
|
||||
})}
|
||||
></video>`
|
||||
: ""}
|
||||
`;
|
||||
|
@ -5,13 +5,11 @@ import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-md-button-menu";
|
||||
import "./ha-button-menu";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
import "./ha-md-menu-item";
|
||||
import "./ha-md-divider";
|
||||
|
||||
export interface IconOverflowMenuItem {
|
||||
[key: string]: any;
|
||||
@ -37,9 +35,11 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
return html`
|
||||
${this.narrow
|
||||
? html` <!-- Collapsed representation for small screens -->
|
||||
<ha-md-button-menu
|
||||
<ha-button-menu
|
||||
@click=${this._handleIconOverflowMenuOpened}
|
||||
positioning="popover"
|
||||
@closed=${this._handleIconOverflowMenuClosed}
|
||||
class="ha-icon-overflow-menu-overflow"
|
||||
absolute
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
@ -49,24 +49,23 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
|
||||
${this.items.map((item) =>
|
||||
item.divider
|
||||
? html`<ha-md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>`
|
||||
: html`<ha-md-menu-item
|
||||
? html`<li divider role="separator"></li>`
|
||||
: html`<ha-list-item
|
||||
graphic="icon"
|
||||
?disabled=${item.disabled}
|
||||
.clickAction=${item.action}
|
||||
@click=${item.action}
|
||||
class=${classMap({ warning: Boolean(item.warning) })}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class=${classMap({ warning: Boolean(item.warning) })}
|
||||
.path=${item.path}
|
||||
></ha-svg-icon>
|
||||
<div slot="graphic">
|
||||
<ha-svg-icon
|
||||
class=${classMap({ warning: Boolean(item.warning) })}
|
||||
.path=${item.path}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
${item.label}
|
||||
</ha-md-menu-item> `
|
||||
</ha-list-item> `
|
||||
)}
|
||||
</ha-md-button-menu>`
|
||||
</ha-button-menu>`
|
||||
: html`
|
||||
<!-- Icon representation for big screens -->
|
||||
${this.items.map((item) =>
|
||||
@ -92,6 +91,20 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
|
||||
protected _handleIconOverflowMenuOpened(e) {
|
||||
e.stopPropagation();
|
||||
// If this component is used inside a data table, the z-index of the row
|
||||
// needs to be increased. Otherwise the ha-button-menu would be displayed
|
||||
// underneath the next row in the table.
|
||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||
if (row) {
|
||||
row.style.zIndex = "1";
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleIconOverflowMenuClosed() {
|
||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||
if (row) {
|
||||
row.style.zIndex = "";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
@ -102,10 +115,16 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
div[role="separator"] {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
width: 1px;
|
||||
}
|
||||
ha-list-item[disabled] ha-svg-icon {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -11,8 +11,8 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import "./ha-list-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-combo-box-item";
|
||||
|
||||
interface IconItem {
|
||||
icon: string;
|
||||
@ -67,12 +67,11 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) =>
|
||||
html`<ha-list-item graphic="avatar">
|
||||
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
|
||||
${item.icon}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-icon-picker")
|
||||
export class HaIconPicker extends LitElement {
|
||||
|
@ -3,6 +3,7 @@ import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
@ -25,8 +26,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
type ScorableLabelItem = ScorableTextItem & LabelRegistryEntry;
|
||||
@ -35,14 +36,16 @@ const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_LABELS_ID = "___NO_LABELS___";
|
||||
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) =>
|
||||
html`<ha-list-item
|
||||
graphic="icon"
|
||||
class=${classMap({ "add-new": item.label_id === ADD_NEW_ID })}
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${item.name}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
|
@ -1,14 +1,15 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-list-item";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
|
||||
@customElement("ha-navigation-list")
|
||||
class HaNavigationList extends LitElement {
|
||||
@ -25,21 +26,21 @@ class HaNavigationList extends LitElement {
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-md-list
|
||||
<mwc-list
|
||||
innerRole="menu"
|
||||
itemRoles="menuitem"
|
||||
innerAriaLabel=${ifDefined(this.label)}
|
||||
@action=${this._handleListAction}
|
||||
>
|
||||
${this.pages.map((page) => {
|
||||
const externalApp = page.path.endsWith("#external-app-configuration");
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
.type=${externalApp ? "button" : "link"}
|
||||
.href=${externalApp ? undefined : page.path}
|
||||
@click=${externalApp ? this._handleExternalApp : undefined}
|
||||
${this.pages.map(
|
||||
(page) => html`
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
.twoline=${this.hasSecondary}
|
||||
.hasMeta=${!this.narrow}
|
||||
>
|
||||
<div
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
class=${page.iconColor ? "icon-background" : ""}
|
||||
.style="background-color: ${page.iconColor || "undefined"}"
|
||||
>
|
||||
@ -47,23 +48,31 @@ class HaNavigationList extends LitElement {
|
||||
</div>
|
||||
<span>${page.name}</span>
|
||||
${this.hasSecondary
|
||||
? html`<span slot="supporting-text">${page.description}</span>`
|
||||
? html`<span slot="secondary">${page.description}</span>`
|
||||
: ""}
|
||||
${!this.narrow
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
? html`<ha-icon-next slot="meta"></ha-icon-next>`
|
||||
: ""}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleExternalApp() {
|
||||
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
|
||||
private _handleListAction(ev: CustomEvent<ActionDetail>) {
|
||||
const path = this.pages[ev.detail.index].path;
|
||||
if (path.endsWith("#external-app-configuration")) {
|
||||
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
|
||||
} else {
|
||||
navigate(path);
|
||||
}
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
@ -80,7 +89,8 @@ class HaNavigationList extends LitElement {
|
||||
.icon-background ha-svg-icon {
|
||||
color: #fff;
|
||||
}
|
||||
ha-md-list-item {
|
||||
ha-list-item {
|
||||
cursor: pointer;
|
||||
font-size: var(--navigation-list-item-title-font-size);
|
||||
}
|
||||
`;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
@ -9,7 +10,6 @@ import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
|
||||
interface NavigationItem {
|
||||
@ -21,13 +21,11 @@ interface NavigationItem {
|
||||
const DEFAULT_ITEMS: NavigationItem[] = [];
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
|
||||
<span slot="headline">${item.title || item.path}</span>
|
||||
${item.title
|
||||
? html`<span slot="supporting-text">${item.path}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
<mwc-list-item graphic="icon" .twoline=${!!item.title}>
|
||||
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
|
||||
<span>${item.title || item.path}</span>
|
||||
<span slot="secondary">${item.path}</span>
|
||||
</mwc-list-item>
|
||||
`;
|
||||
|
||||
const createViewNavigationItem = (
|
||||
|
@ -1,5 +1,7 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiCamera } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
// The BarcodeDetector Web API is not yet supported in all browsers,
|
||||
@ -10,13 +12,12 @@ import { prepareZXingModule } from "barcode-detector";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-button";
|
||||
import "./ha-button-menu";
|
||||
import "./ha-list-item";
|
||||
import "./ha-spinner";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@ -35,22 +36,18 @@ prepareZXingModule({
|
||||
class HaQrScanner extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@property({ attribute: "alternative_option_label" })
|
||||
public alternativeOptionLabel?: string;
|
||||
|
||||
@property({ attribute: false }) public validate?: (
|
||||
value: string
|
||||
) => string | undefined;
|
||||
@property() public error?: string;
|
||||
|
||||
@state() private _cameras?: QrScanner.Camera[];
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _warning?: string;
|
||||
@state() private _manual = false;
|
||||
|
||||
private _qrScanner?: QrScanner;
|
||||
|
||||
@ -91,40 +88,29 @@ class HaQrScanner extends LitElement {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("error") && this.error) {
|
||||
alert(`error: ${this.error}`);
|
||||
this._notifyExternalScanner(this.error);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._nativeBarcodeScanner) {
|
||||
if (this._nativeBarcodeScanner && !this._manual) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${this._error || this._warning
|
||||
? html`<ha-alert
|
||||
.alertType=${this._error ? "error" : "warning"}
|
||||
class=${this._error ? "" : "warning"}
|
||||
>
|
||||
${this._error || this._warning}
|
||||
${this._error
|
||||
? html` <ha-button @click=${this._retry} slot="action">
|
||||
${this.hass.localize("ui.components.qr-scanner.retry")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${navigator.mediaDevices
|
||||
return html`${this.error
|
||||
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
|
||||
: ""}
|
||||
${navigator.mediaDevices && !this._manual
|
||||
? html`<video></video>
|
||||
<div id="canvas-container">
|
||||
${this._loading
|
||||
? html`<div class="loading">
|
||||
<ha-spinner active></ha-spinner>
|
||||
</div>`
|
||||
: nothing}
|
||||
${!this._loading &&
|
||||
!this._error &&
|
||||
this._cameras &&
|
||||
this._cameras.length > 1
|
||||
${this._cameras && this._cameras.length > 1
|
||||
? html`<ha-button-menu fixed @closed=${stopPropagation}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
.label=${this.localize(
|
||||
"ui.components.qr-scanner.select_camera"
|
||||
)}
|
||||
.path=${mdiCamera}
|
||||
@ -142,25 +128,25 @@ class HaQrScanner extends LitElement {
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.hass.localize(
|
||||
"ui.components.qr-scanner.only_https_supported"
|
||||
)
|
||||
: this.hass.localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>
|
||||
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
: html`${this._manual
|
||||
? nothing
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.localize(
|
||||
"ui.components.qr-scanner.only_https_supported"
|
||||
)
|
||||
: this.localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>`}
|
||||
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.qr-scanner.enter_qr_code"
|
||||
)}
|
||||
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></ha-textfield>
|
||||
<ha-button @click=${this._manualSubmit}>
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
<mwc-button @click=${this._manualSubmit}>
|
||||
${this.localize("ui.common.submit")}
|
||||
</mwc-button>
|
||||
</div>`}`;
|
||||
}
|
||||
|
||||
@ -179,9 +165,7 @@ class HaQrScanner extends LitElement {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const QrScanner = (await import("qr-scanner")).default;
|
||||
if (!(await QrScanner.hasCamera())) {
|
||||
this._reportError(
|
||||
this.hass.localize("ui.components.qr-scanner.no_camera_found")
|
||||
);
|
||||
this._reportError("No camera found");
|
||||
return;
|
||||
}
|
||||
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
|
||||
@ -197,7 +181,6 @@ class HaQrScanner extends LitElement {
|
||||
canvas.style.display = "block";
|
||||
try {
|
||||
await this._qrScanner.start();
|
||||
this._loading = false;
|
||||
} catch (err: any) {
|
||||
this._reportError(err);
|
||||
}
|
||||
@ -210,8 +193,8 @@ class HaQrScanner extends LitElement {
|
||||
private _qrCodeError = (err: any) => {
|
||||
if (err.endsWith("No QR code found")) {
|
||||
this._qrNotFoundCount++;
|
||||
if (this._qrNotFoundCount >= 250) {
|
||||
this._reportWarning(err);
|
||||
if (this._qrNotFoundCount === 250) {
|
||||
this._reportError(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -221,17 +204,7 @@ class HaQrScanner extends LitElement {
|
||||
};
|
||||
|
||||
private _qrCodeScanned = (qrCodeString: string): void => {
|
||||
this._warning = undefined;
|
||||
this._qrNotFoundCount = 0;
|
||||
if (this.validate) {
|
||||
const validationMessage = this.validate(qrCodeString);
|
||||
|
||||
if (validationMessage) {
|
||||
this._reportWarning(validationMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
|
||||
};
|
||||
|
||||
@ -261,10 +234,7 @@ class HaQrScanner extends LitElement {
|
||||
if (msg.command === "bar_code/scan_result") {
|
||||
if (msg.payload.format !== "qr_code") {
|
||||
this._notifyExternalScanner(
|
||||
this.hass.localize("ui.components.qr-scanner.wrong_code", {
|
||||
format: msg.payload.format,
|
||||
rawValue: msg.payload.rawValue,
|
||||
})
|
||||
`Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
|
||||
);
|
||||
} else {
|
||||
this._qrCodeScanned(msg.payload.rawValue);
|
||||
@ -274,7 +244,7 @@ class HaQrScanner extends LitElement {
|
||||
if (msg.payload.reason === "canceled") {
|
||||
fireEvent(this, "qr-code-closed");
|
||||
} else {
|
||||
fireEvent(this, "qr-code-more-options");
|
||||
this._manual = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@ -282,17 +252,10 @@ class HaQrScanner extends LitElement {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "bar_code/scan",
|
||||
payload: {
|
||||
title:
|
||||
this.title ||
|
||||
this.hass.localize("ui.components.qr-scanner.app.title"),
|
||||
description:
|
||||
this.description ||
|
||||
this.hass.localize("ui.components.qr-scanner.app.description"),
|
||||
title: this.title || "Scan QR code",
|
||||
description: this.description || "Scan a barcode.",
|
||||
alternative_option_label:
|
||||
this.alternativeOptionLabel ||
|
||||
this.hass.localize(
|
||||
"ui.components.qr-scanner.app.alternativeOptionLabel"
|
||||
),
|
||||
this.alternativeOptionLabel || "Click to manually enter the barcode",
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -306,55 +269,25 @@ class HaQrScanner extends LitElement {
|
||||
}
|
||||
|
||||
private _notifyExternalScanner(message: string) {
|
||||
if (!this._nativeBarcodeScanner) {
|
||||
if (!this.hass.auth.external) {
|
||||
return;
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
this.hass.auth.external.fireMessage({
|
||||
type: "bar_code/notify",
|
||||
payload: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
this._warning = undefined;
|
||||
this._error = undefined;
|
||||
this.error = undefined;
|
||||
}
|
||||
|
||||
private _reportError(message: string) {
|
||||
const canvas = this._qrScanner?.$canvas;
|
||||
if (canvas) {
|
||||
canvas.style.display = "none";
|
||||
}
|
||||
this._error = message;
|
||||
}
|
||||
|
||||
private _reportWarning(message: string) {
|
||||
if (this._nativeBarcodeScanner) {
|
||||
this._notifyExternalScanner(message);
|
||||
} else {
|
||||
this._warning = message;
|
||||
}
|
||||
}
|
||||
|
||||
private async _retry() {
|
||||
if (this._qrScanner) {
|
||||
this._loading = true;
|
||||
this._error = undefined;
|
||||
this._warning = undefined;
|
||||
const canvas = this._qrScanner.$canvas;
|
||||
canvas.style.display = "block";
|
||||
this._qrNotFoundCount = 0;
|
||||
await this._qrScanner.start();
|
||||
this._loading = false;
|
||||
}
|
||||
fireEvent(this, "qr-code-error", { message });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:root {
|
||||
position: relative;
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
}
|
||||
#canvas-container {
|
||||
position: relative;
|
||||
@ -379,24 +312,6 @@ class HaQrScanner extends LitElement {
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
}
|
||||
ha-alert.warning {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background-color: var(--primary-background-color);
|
||||
top: 0;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -404,8 +319,8 @@ declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"qr-code-scanned": { value: string };
|
||||
"qr-code-error": { message: string };
|
||||
"qr-code-closed": undefined;
|
||||
"qr-code-more-options": undefined;
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiDevices,
|
||||
mdiPaletteSwatch,
|
||||
mdiTextureBox,
|
||||
mdiSofa,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@ -211,12 +211,36 @@ export class HaRelatedItems extends LitElement {
|
||||
)}
|
||||
</mwc-list>`
|
||||
: nothing}
|
||||
${this._related.device
|
||||
? html`<h3>
|
||||
${this.hass.localize("ui.components.related-items.device")}
|
||||
</h3>
|
||||
${this._related.device.map((relatedDeviceId) => {
|
||||
const device = this.hass.devices[relatedDeviceId];
|
||||
if (!device) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<a href="/config/devices/device/${relatedDeviceId}">
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${mdiDevices}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
${device.name_by_user || device.name}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
`;
|
||||
})} </mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
${this._related.area
|
||||
? html`<h3>
|
||||
${this.hass.localize("ui.components.related-items.area")}
|
||||
</h3>
|
||||
<mwc-list>
|
||||
${this._related.area.map((relatedAreaId) => {
|
||||
<mwc-list
|
||||
>${this._related.area.map((relatedAreaId) => {
|
||||
const area = this.hass.areas[relatedAreaId];
|
||||
if (!area) {
|
||||
return nothing;
|
||||
@ -235,47 +259,17 @@ export class HaRelatedItems extends LitElement {
|
||||
})}
|
||||
slot="graphic"
|
||||
></div>`
|
||||
: area.icon
|
||||
? html`<ha-icon
|
||||
slot="graphic"
|
||||
.icon=${area.icon}
|
||||
></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiSofa}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`}
|
||||
${area.name}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
`;
|
||||
})}
|
||||
</mwc-list>`
|
||||
: nothing}
|
||||
${this._related.device
|
||||
? html`<h3>
|
||||
${this.hass.localize("ui.components.related-items.device")}
|
||||
</h3>
|
||||
<mwc-list>
|
||||
${this._related.device.map((relatedDeviceId) => {
|
||||
const device = this.hass.devices[relatedDeviceId];
|
||||
if (!device) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<a href="/config/devices/device/${relatedDeviceId}">
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${mdiDevices}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
${device.name_by_user || device.name}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
`;
|
||||
})}
|
||||
</mwc-list>`
|
||||
})}</mwc-list
|
||||
>`
|
||||
: nothing}
|
||||
${this._related.entity
|
||||
? html`
|
||||
|
@ -83,10 +83,6 @@ export class HaBackgroundSelector extends LitElement {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
ha-picture-upload {
|
||||
background-color: var(--primary-background-color);
|
||||
border-radius: var(--file-upload-image-border-radius);
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -38,7 +38,6 @@ import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
import "./ha-service-section-icon";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
|
||||
const attributeFilter = (values: any[], attribute: any) => {
|
||||
if (typeof attribute === "object") {
|
||||
@ -102,8 +101,6 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
private _stickySelector: Record<string, Selector> = {};
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadBackendTranslation("services");
|
||||
@ -593,23 +590,7 @@ export class HaServiceControl extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const fieldDataHasTemplate =
|
||||
this._value?.data && hasTemplate(this._value.data[dataField.key]);
|
||||
|
||||
const selector =
|
||||
fieldDataHasTemplate &&
|
||||
typeof this._value!.data![dataField.key] === "string"
|
||||
? { template: null }
|
||||
: fieldDataHasTemplate &&
|
||||
typeof this._value!.data![dataField.key] === "object"
|
||||
? { object: null }
|
||||
: (this._stickySelector[dataField.key] ??
|
||||
dataField?.selector ?? { text: null });
|
||||
|
||||
if (fieldDataHasTemplate) {
|
||||
// Hold this selector type until the field is cleared
|
||||
this._stickySelector[dataField.key] = selector;
|
||||
}
|
||||
const selector = dataField?.selector ?? { text: undefined };
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
|
||||
@ -712,7 +693,6 @@ export class HaServiceControl extends LitElement {
|
||||
this._checkedKeys.delete(key);
|
||||
data = { ...this._value?.data };
|
||||
delete data[key];
|
||||
delete this._stickySelector[key];
|
||||
}
|
||||
if (data) {
|
||||
fireEvent(this, "value-changed", {
|
||||
@ -836,10 +816,6 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _serviceDataChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
// Don't clear an object selector that returns invalid YAML
|
||||
return;
|
||||
}
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
@ -852,13 +828,8 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
const data = { ...this._value?.data, [key]: value };
|
||||
|
||||
if (
|
||||
value === "" ||
|
||||
value === undefined ||
|
||||
(typeof value === "object" && !Object.keys(value).length)
|
||||
) {
|
||||
if (value === "" || value === undefined) {
|
||||
delete data[key];
|
||||
delete this._stickySelector[key];
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
|
@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { domainToName } from "../data/integration";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-list-item";
|
||||
import "./ha-service-icon";
|
||||
import { getServiceIcons } from "../data/icons";
|
||||
|
||||
@ -29,19 +29,18 @@ class HaServicePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
|
||||
(item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
(item) =>
|
||||
html`<ha-list-item twoline graphic="icon">
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.service=${item.service}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${item.name}</span>
|
||||
<span slot="supporting-text"
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary"
|
||||
>${item.name === item.service ? "" : item.service}</span
|
||||
>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
</ha-list-item>`;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
|
@ -17,16 +17,15 @@ import {
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResult, CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
state,
|
||||
query,
|
||||
} from "lit/decorators";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@ -49,9 +48,7 @@ import "./ha-menu-button";
|
||||
import "./ha-sortable";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
import type { HaMdListItem } from "./ha-md-list-item";
|
||||
import { preventDefault } from "../common/dom/prevent_default";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
@ -224,8 +221,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _hiddenPanels: string[] = [];
|
||||
|
||||
@query(".tooltip") private _tooltip!: HTMLDivElement;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return this.hass.user?.is_admin
|
||||
? [
|
||||
@ -243,20 +238,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Show the supervisor as being part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
||||
? "config"
|
||||
: this.hass.panelUrl;
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
${this._renderHeader()}
|
||||
${this._renderAllPanels(selectedPanel)}
|
||||
${this._renderAllPanels()}
|
||||
${this._renderDivider()}
|
||||
<ha-md-list>
|
||||
${this._renderNotifications()}
|
||||
${this._renderUserItem(selectedPanel)}
|
||||
</ha-md-list>
|
||||
${this._renderNotifications()}
|
||||
${this._renderUserItem()}
|
||||
<div disabled class="bottom-spacer"></div>
|
||||
<div class="tooltip"></div>
|
||||
`;
|
||||
@ -326,11 +314,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (
|
||||
this.hass &&
|
||||
oldHass?.connected === false &&
|
||||
changedProps.get("hass")?.connected === false &&
|
||||
this.hass.connected === true
|
||||
) {
|
||||
this._subscribePersistentNotifications();
|
||||
@ -341,8 +327,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
return;
|
||||
}
|
||||
if (oldHass?.panelUrl !== this.hass.panelUrl) {
|
||||
const selectedEl = this.shadowRoot!.querySelector(".selected");
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
|
||||
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
|
||||
if (selectedEl) {
|
||||
// @ts-ignore
|
||||
selectedEl.scrollIntoViewIfNeeded();
|
||||
@ -394,7 +381,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderAllPanels(selectedPanel: string) {
|
||||
private _renderAllPanels() {
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
this.hass.defaultPanel,
|
||||
@ -403,26 +390,34 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// Show the supervisor as being part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
||||
? "config"
|
||||
: this.hass.panelUrl;
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<ha-md-list
|
||||
<paper-listbox
|
||||
attr-for-selected="data-panel"
|
||||
class="ha-scrollbar"
|
||||
.selected=${selectedPanel}
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
@iron-activate=${preventDefault}
|
||||
>
|
||||
${this.editMode
|
||||
? this._renderPanelsEdit(beforeSpacer, selectedPanel)
|
||||
: this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
? this._renderPanelsEdit(beforeSpacer)
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
</paper-listbox>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
private _renderPanels(panels: PanelInfo[]) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
@ -434,8 +429,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
: undefined,
|
||||
selectedPanel
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -443,24 +437,30 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
private _renderPanel(
|
||||
urlPath: string,
|
||||
title: string | null,
|
||||
icon: string | null | undefined,
|
||||
iconPath: string | null | undefined,
|
||||
selectedPanel: string
|
||||
icon?: string | null,
|
||||
iconPath?: string | null
|
||||
) {
|
||||
return urlPath === "config"
|
||||
? this._renderConfiguration(title, selectedPanel)
|
||||
? this._renderConfiguration(title)
|
||||
: html`
|
||||
<ha-md-list-item
|
||||
.href=${this.editMode ? undefined : `/${urlPath}`}
|
||||
type="link"
|
||||
class=${selectedPanel === urlPath ? "selected" : ""}
|
||||
<a
|
||||
role="option"
|
||||
aria-selected=${urlPath === this.hass.panelUrl}
|
||||
href=${`/${urlPath}`}
|
||||
data-panel=${urlPath}
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
<paper-icon-item>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text">${title}</span>
|
||||
</paper-icon-item>
|
||||
${this.editMode
|
||||
? html`<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.hide_panel")}
|
||||
@ -468,10 +468,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
class="hide-panel"
|
||||
.panel=${urlPath}
|
||||
@click=${this._hidePanel}
|
||||
slot="end"
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
: ""}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -494,10 +493,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this._panelOrder = panelOrder;
|
||||
}
|
||||
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
||||
return html`
|
||||
<ha-sortable .disabled=${!this.editMode} @item-moved=${this._panelMoved}
|
||||
><div>${this._renderPanels(beforeSpacer, selectedPanel)}</div>
|
||||
<ha-sortable
|
||||
handle-selector="paper-icon-item"
|
||||
.disabled=${!this.editMode}
|
||||
@item-moved=${this._panelMoved}
|
||||
>
|
||||
<div class="reorder-list">${this._renderPanels(beforeSpacer)}</div>
|
||||
</ha-sortable>
|
||||
${this._renderSpacer()}${this._renderHiddenPanels()}
|
||||
`;
|
||||
@ -510,24 +513,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
if (!panel) {
|
||||
return "";
|
||||
}
|
||||
return html`<ha-md-list-item
|
||||
return html`<paper-icon-item
|
||||
@click=${this._unhidePanel}
|
||||
class="hidden-panel"
|
||||
.panel=${url}
|
||||
type="button"
|
||||
>
|
||||
${panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
slot="item-icon"
|
||||
.path=${PANEL_ICONS.lovelace}
|
||||
></ha-svg-icon>`
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
slot="item-icon"
|
||||
.path=${PANEL_ICONS[panel.url_path]}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon slot="start" .icon=${panel.icon}></ha-icon>`}
|
||||
<span class="item-text" slot="headline"
|
||||
: html`<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${panel.icon}
|
||||
></ha-icon>`}
|
||||
<span class="item-text"
|
||||
>${panel.url_path === this.hass.defaultPanel
|
||||
? this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) ||
|
||||
@ -537,9 +542,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
.label=${this.hass.localize("ui.sidebar.show_panel")}
|
||||
.path=${mdiPlus}
|
||||
class="show-panel"
|
||||
slot="end"
|
||||
></ha-icon-button>
|
||||
</ha-md-list-item>`;
|
||||
</paper-icon-item>`;
|
||||
})}
|
||||
${this._renderSpacer()}`
|
||||
: ""}`;
|
||||
@ -553,34 +557,41 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderConfiguration(title: string | null, selectedPanel: string) {
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
class="configuration${selectedPanel === "config" ? " selected" : ""}"
|
||||
type="button"
|
||||
href="/config"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
private _renderConfiguration(title: string | null) {
|
||||
return html`<a
|
||||
class="configuration-container"
|
||||
role="option"
|
||||
aria-selected=${this.hass.panelUrl === "config"}
|
||||
href="/config"
|
||||
data-panel="config"
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item
|
||||
class="configuration"
|
||||
role="option"
|
||||
aria-selected=${this.hass.panelUrl === "config"}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
|
||||
${!this.alwaysExpand &&
|
||||
(this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="badge" slot="start">
|
||||
<span class="configuration-badge" slot="item-icon">
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
<span class="item-text">${title}</span>
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
<span class="configuration-badge"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
</paper-icon-item>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderNotifications() {
|
||||
@ -588,67 +599,91 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
? this._notifications.length
|
||||
: 0;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
return html`<div
|
||||
class="notifications-container"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item
|
||||
class="notifications"
|
||||
role="option"
|
||||
aria-selected="false"
|
||||
@click=${this._handleShowNotificationDrawer}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
type="button"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
|
||||
${!this.alwaysExpand && notificationCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||
<span class="notification-badge" slot="item-icon">
|
||||
${notificationCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||
>
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.notification_drawer.title")}
|
||||
</span>
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
? html` <span class="notification-badge">${notificationCount}</span> `
|
||||
: ""}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
</paper-icon-item>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderUserItem(selectedPanel: string) {
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
href="/profile"
|
||||
type="link"
|
||||
class="user ${selectedPanel === "profile" ? " selected" : ""}"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
private _renderUserItem() {
|
||||
return html`<a
|
||||
class=${classMap({
|
||||
profile: true,
|
||||
// Mimic behavior that paper-listbox provides
|
||||
"iron-selected": this.hass.panelUrl === "profile",
|
||||
})}
|
||||
href="/profile"
|
||||
data-panel="panel"
|
||||
tabindex="-1"
|
||||
role="option"
|
||||
aria-selected=${this.hass.panelUrl === "profile"}
|
||||
aria-label=${this.hass.localize("panel.profile")}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-user-badge
|
||||
slot="start"
|
||||
slot="item-icon"
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
<span class="item-text">
|
||||
${this.hass.user ? this.hass.user.name : ""}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${!this.hass.user?.is_admin &&
|
||||
this.hass.auth.external?.config.hasSettingsScreen
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
<a
|
||||
role="option"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
href="#external-app-configuration"
|
||||
tabindex="-1"
|
||||
aria-selected="false"
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiCellphoneCog}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
: ""}`;
|
||||
}
|
||||
@ -660,6 +695,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private get _tooltip() {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
|
||||
if (ev.detail.action !== "hold") {
|
||||
return;
|
||||
@ -722,7 +761,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
clearTimeout(this._mouseLeaveTimeout);
|
||||
this._mouseLeaveTimeout = undefined;
|
||||
}
|
||||
this._showTooltip(ev.currentTarget as HaMdListItem);
|
||||
this._showTooltip(ev.currentTarget as PaperIconItemElement);
|
||||
}
|
||||
|
||||
private _itemMouseLeave() {
|
||||
@ -735,10 +774,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _listboxFocusIn(ev) {
|
||||
if (this.alwaysExpand || ev.target.localName !== "ha-md-list-item") {
|
||||
if (this.alwaysExpand || ev.target.nodeName !== "A") {
|
||||
return;
|
||||
}
|
||||
this._showTooltip(ev.target);
|
||||
this._showTooltip(ev.target.querySelector("paper-icon-item"));
|
||||
}
|
||||
|
||||
private _listboxFocusOut() {
|
||||
@ -762,25 +801,22 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this._recentKeydownActiveUntil = new Date().getTime() + 100;
|
||||
}
|
||||
|
||||
private _showTooltip(item: HaMdListItem) {
|
||||
private _showTooltip(item: PaperIconItemElement) {
|
||||
if (this._tooltipHideTimeout) {
|
||||
clearTimeout(this._tooltipHideTimeout);
|
||||
this._tooltipHideTimeout = undefined;
|
||||
}
|
||||
const tooltip = this._tooltip;
|
||||
const listbox = this.shadowRoot!.querySelector("ha-md-list")!;
|
||||
const listbox = this.shadowRoot!.querySelector("paper-listbox")!;
|
||||
let top = item.offsetTop + 11;
|
||||
if (listbox.contains(item)) {
|
||||
top += listbox.offsetTop;
|
||||
top -= listbox.scrollTop;
|
||||
}
|
||||
tooltip.innerText = (
|
||||
item.querySelector(".item-text") as HTMLElement
|
||||
).innerText;
|
||||
tooltip.innerHTML = item.querySelector(".item-text")!.innerHTML;
|
||||
tooltip.style.display = "block";
|
||||
tooltip.style.position = "fixed";
|
||||
tooltip.style.top = `${top}px`;
|
||||
tooltip.style.left = `${item.offsetLeft + item.clientWidth + 8}px`;
|
||||
tooltip.style.left = `${item.offsetLeft + item.clientWidth + 4}px`;
|
||||
}
|
||||
|
||||
private _hideTooltip() {
|
||||
@ -869,11 +905,12 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
.menu mwc-button {
|
||||
width: 100%;
|
||||
}
|
||||
.reorder-list,
|
||||
.hidden-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
paper-listbox {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -885,64 +922,90 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
overflow-x: hidden;
|
||||
background: none;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-inline-start: env(safe-area-inset-left);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
paper-icon-item {
|
||||
box-sizing: border-box;
|
||||
margin: 4px;
|
||||
padding-left: 12px;
|
||||
padding-inline-start: 12px;
|
||||
padding-inline-end: initial;
|
||||
border-radius: 4px;
|
||||
height: 40px;
|
||||
--md-list-item-one-line-container-height: 40px;
|
||||
--paper-item-min-height: 40px;
|
||||
width: 48px;
|
||||
position: relative;
|
||||
--md-list-item-label-text-color: var(--sidebar-text-color);
|
||||
--md-list-item-leading-space: 12px;
|
||||
--md-list-item-trailing-space: 12px;
|
||||
--md-list-item-leading-icon-size: 24px;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item {
|
||||
:host([expanded]) paper-icon-item {
|
||||
width: 248px;
|
||||
width: calc(248px - env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
ha-md-list-item.selected {
|
||||
--md-list-item-label-text-color: var(--sidebar-selected-icon-color);
|
||||
--md-ripple-hover-color: var(--sidebar-selected-icon-color);
|
||||
ha-icon[slot="item-icon"],
|
||||
ha-svg-icon[slot="item-icon"] {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
ha-md-list-item.selected::before {
|
||||
|
||||
.iron-selected paper-icon-item::before,
|
||||
a:not(.iron-selected):focus::before {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
right: 2px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
left: 2px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
.iron-selected paper-icon-item::before {
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
a:not(.iron-selected):focus::before {
|
||||
background-color: currentColor;
|
||||
opacity: var(--dark-divider-opacity);
|
||||
margin: 4px 8px;
|
||||
}
|
||||
.iron-selected paper-icon-item:focus::before,
|
||||
.iron-selected:focus paper-icon-item::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
ha-icon[slot="start"],
|
||||
ha-svg-icon[slot="start"] {
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
color: var(--sidebar-icon-color);
|
||||
.iron-selected paper-icon-item[pressed]:before {
|
||||
opacity: 0.37;
|
||||
}
|
||||
|
||||
ha-md-list-item.selected ha-svg-icon[slot="start"],
|
||||
ha-md-list-item.selected ha-icon[slot="start"] {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
ha-md-list-item .item-text {
|
||||
display: none;
|
||||
max-width: calc(100% - 56px);
|
||||
paper-icon-item span {
|
||||
color: var(--sidebar-text-color);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item .item-text {
|
||||
|
||||
a.iron-selected paper-icon-item ha-icon,
|
||||
a.iron-selected paper-icon-item ha-svg-icon {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
a.iron-selected .item-text {
|
||||
color: var(--sidebar-selected-text-color);
|
||||
}
|
||||
|
||||
paper-icon-item .item-text {
|
||||
display: none;
|
||||
max-width: calc(100% - 56px);
|
||||
}
|
||||
:host([expanded]) paper-icon-item .item-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -956,38 +1019,60 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.badge {
|
||||
.notifications-container,
|
||||
.configuration-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 8px;
|
||||
border-radius: 10px;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-inline-start: env(safe-area-inset-left);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.notifications {
|
||||
cursor: pointer;
|
||||
}
|
||||
.notifications .item-text,
|
||||
.configuration .item-text {
|
||||
flex: 1;
|
||||
}
|
||||
.profile {
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-inline-start: env(safe-area-inset-left);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.profile paper-icon-item {
|
||||
padding-left: 4px;
|
||||
padding-inline-start: 4px;
|
||||
padding-inline-end: auto;
|
||||
}
|
||||
.profile .item-text {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
.notification-badge,
|
||||
.configuration-badge {
|
||||
position: absolute;
|
||||
left: calc(var(--app-drawer-width, 248px) - 42px);
|
||||
inset-inline-start: calc(var(--app-drawer-width, 248px) - 42px);
|
||||
inset-inline-end: initial;
|
||||
min-width: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
background-color: var(--accent-color);
|
||||
padding: 2px 6px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
|
||||
ha-svg-icon + .badge {
|
||||
ha-svg-icon + .notification-badge,
|
||||
ha-svg-icon + .configuration-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 14px;
|
||||
left: 26px;
|
||||
border-radius: 10px;
|
||||
inset-inline-start: 26px;
|
||||
inset-inline-end: initial;
|
||||
font-size: 0.65em;
|
||||
line-height: 2;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
ha-md-list-item.user {
|
||||
--md-list-item-leading-icon-size: 40px;
|
||||
--md-list-item-bottom-space: 12px;
|
||||
--md-list-item-leading-space: 4px;
|
||||
--md-list-item-trailing-space: 4px;
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
@ -1003,6 +1088,19 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dev-tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
width: 256px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dev-tools a {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
115
src/components/ha-tabs.ts
Normal file
115
src/components/ha-tabs.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
|
||||
import type { PaperTabElement } from "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
import type { PaperTabsElement } from "@polymer/paper-tabs/paper-tabs";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const PaperTabs = customElements.get(
|
||||
"paper-tabs"
|
||||
) as Constructor<PaperTabsElement>;
|
||||
|
||||
let subTemplate: HTMLTemplateElement;
|
||||
|
||||
@customElement("ha-tabs")
|
||||
export class HaTabs extends PaperTabs {
|
||||
private _firstTabWidth = 0;
|
||||
|
||||
private _lastTabWidth = 0;
|
||||
|
||||
private _lastLeftHiddenState = false;
|
||||
|
||||
private _lastRightHiddenState = false;
|
||||
|
||||
static get template(): HTMLTemplateElement {
|
||||
if (!subTemplate) {
|
||||
subTemplate = (PaperTabs as any).template.cloneNode(true);
|
||||
|
||||
const superStyle = subTemplate.content.querySelector("style");
|
||||
|
||||
// Add "noink" attribute for scroll buttons to disable animation.
|
||||
subTemplate.content
|
||||
.querySelectorAll("paper-icon-button")
|
||||
.forEach((arrow: PaperIconButtonElement) => {
|
||||
arrow.setAttribute("noink", "");
|
||||
});
|
||||
|
||||
superStyle!.appendChild(
|
||||
document.createTextNode(`
|
||||
#selectionBar {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.not-visible {
|
||||
display: none;
|
||||
}
|
||||
paper-icon-button {
|
||||
width: 24px;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
`)
|
||||
);
|
||||
}
|
||||
return subTemplate;
|
||||
}
|
||||
|
||||
// Get first and last tab's width for _affectScroll
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public _tabChanged(tab: PaperTabElement, old: PaperTabElement): void {
|
||||
super._tabChanged(tab, old);
|
||||
const tabs = this.querySelectorAll("paper-tab:not(.hide-tab)");
|
||||
if (tabs.length > 0) {
|
||||
this._firstTabWidth = tabs[0].clientWidth;
|
||||
this._lastTabWidth = tabs[tabs.length - 1].clientWidth;
|
||||
}
|
||||
|
||||
// Scroll active tab into view if needed.
|
||||
const selected = this.querySelector(".iron-selected");
|
||||
if (selected) {
|
||||
selected.scrollIntoView();
|
||||
this._affectScroll(0); // Ensure scroll arrows match scroll position
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify _affectScroll so that when the scroll arrows appear
|
||||
* while scrolling and the tab container shrinks we can counteract
|
||||
* the jump in tab position so that the scroll still appears smooth.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public _affectScroll(dx: number): void {
|
||||
if (this._firstTabWidth === 0 || this._lastTabWidth === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.tabsContainer.scrollLeft += dx;
|
||||
|
||||
const scrollLeft = this.$.tabsContainer.scrollLeft;
|
||||
const dirRTL = this.dir === "rtl";
|
||||
|
||||
const boolCondition1 = Math.abs(scrollLeft) < this._firstTabWidth;
|
||||
const boolCondition2 =
|
||||
Math.abs(scrollLeft) + this._lastTabWidth > this._tabContainerScrollSize;
|
||||
|
||||
this._leftHidden = !dirRTL ? boolCondition1 : boolCondition2;
|
||||
this._rightHidden = !dirRTL ? boolCondition2 : boolCondition1;
|
||||
|
||||
if (!dirRTL) {
|
||||
if (this._lastLeftHiddenState !== this._leftHidden) {
|
||||
this._lastLeftHiddenState = this._leftHidden;
|
||||
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
|
||||
}
|
||||
} else if (this._lastRightHiddenState !== this._rightHidden) {
|
||||
this._lastRightHiddenState = this._rightHidden;
|
||||
this.$.tabsContainer.scrollLeft -= this._rightHidden ? -23 : 23;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tabs": HaTabs;
|
||||
}
|
||||
}
|
@ -136,12 +136,13 @@ export class HaTextField extends TextFieldBase {
|
||||
}
|
||||
|
||||
.mdc-floating-label:not(.mdc-floating-label--float-above) {
|
||||
max-width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
.mdc-floating-label--float-above {
|
||||
max-width: calc((100% - 16px) / 0.75);
|
||||
transition: none;
|
||||
text-overflow: ellipsis;
|
||||
width: inherit;
|
||||
padding-right: 30px;
|
||||
padding-inline-end: 30px;
|
||||
padding-inline-start: initial;
|
||||
box-sizing: border-box;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
input {
|
||||
@ -182,15 +183,11 @@ export class HaTextField extends TextFieldBase {
|
||||
}
|
||||
|
||||
.mdc-floating-label {
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: initial;
|
||||
inset-inline-start: 16px !important;
|
||||
inset-inline-end: initial !important;
|
||||
transform-origin: var(--float-start);
|
||||
direction: var(--direction);
|
||||
text-align: var(--float-start);
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
|
@ -1,9 +1,8 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
addWebRtcCandidate,
|
||||
@ -27,10 +26,6 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
@property() public entityid?: string;
|
||||
|
||||
@property({ attribute: false }) public aspectRatio?: number;
|
||||
|
||||
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
|
||||
|
||||
@property({ type: Boolean, attribute: "controls" })
|
||||
public controls = false;
|
||||
|
||||
@ -74,11 +69,6 @@ class HaWebRtcPlayer extends LitElement {
|
||||
?controls=${this.controls}
|
||||
poster=${ifDefined(this.posterUrl)}
|
||||
@loadeddata=${this._loadedData}
|
||||
style=${styleMap({
|
||||
height: this.aspectRatio == null ? "100%" : "auto",
|
||||
aspectRatio: this.aspectRatio,
|
||||
objectFit: this.fitMode,
|
||||
})}
|
||||
></video>
|
||||
`;
|
||||
}
|
||||
|
@ -1,160 +0,0 @@
|
||||
import TabGroup from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.component";
|
||||
import TabGroupStyles from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.styles";
|
||||
import "@shoelace-style/shoelace/dist/components/tab/tab";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, query } from "lit/decorators";
|
||||
|
||||
@customElement("sl-tab-group")
|
||||
// @ts-ignore
|
||||
export class HaSlTabGroup extends TabGroup {
|
||||
private _mouseIsDown = false;
|
||||
|
||||
private _scrolled = false;
|
||||
|
||||
private _mouseReleasedAt?: number;
|
||||
|
||||
private _scrollStartX = 0;
|
||||
|
||||
private _scrollLeft = 0;
|
||||
|
||||
@query(".tab-group__nav", true) private _scrollContainer?: HTMLElement;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("mousemove", this._mouseMove);
|
||||
window.removeEventListener("mouseup", this._mouseUp);
|
||||
}
|
||||
|
||||
override setAriaLabels() {
|
||||
// Override the method to prevent setting aria-labels, as we don't use panels
|
||||
// and don't want to set aria-labels for the tabs
|
||||
}
|
||||
|
||||
override getAllPanels() {
|
||||
// Override the method to prevent querying for panels
|
||||
// and return an empty array instead
|
||||
// as we don't use panels
|
||||
return [];
|
||||
}
|
||||
|
||||
protected override firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
const scrollContainer = this._scrollContainer;
|
||||
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener("mousedown", this._mouseDown);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
protected override handleClick(event: MouseEvent) {
|
||||
if (
|
||||
this._mouseReleasedAt &&
|
||||
new Date().getTime() - this._mouseReleasedAt < 100
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
super.handleClick(event);
|
||||
}
|
||||
|
||||
private _mouseDown = (event: MouseEvent) => {
|
||||
const scrollContainer = this._scrollContainer;
|
||||
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scrollStartX = event.pageX - scrollContainer.offsetLeft;
|
||||
this._scrollLeft = scrollContainer.scrollLeft;
|
||||
this._mouseIsDown = true;
|
||||
this._scrolled = false;
|
||||
|
||||
window.addEventListener("mousemove", this._mouseMove);
|
||||
window.addEventListener("mouseup", this._mouseUp, { once: true });
|
||||
};
|
||||
|
||||
private _mouseUp = () => {
|
||||
this._mouseIsDown = false;
|
||||
if (this._scrolled) {
|
||||
this._mouseReleasedAt = new Date().getTime();
|
||||
}
|
||||
window.removeEventListener("mousemove", this._mouseMove);
|
||||
};
|
||||
|
||||
private _mouseMove = (event: MouseEvent) => {
|
||||
if (!this._mouseIsDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollContainer = this._scrollContainer;
|
||||
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = event.pageX - scrollContainer.offsetLeft;
|
||||
const scroll = x - this._scrollStartX;
|
||||
|
||||
if (!this._scrolled) {
|
||||
this._scrolled = Math.abs(scroll) > 1;
|
||||
}
|
||||
|
||||
scrollContainer.scrollLeft = this._scrollLeft - scroll;
|
||||
};
|
||||
|
||||
static override styles = [
|
||||
TabGroupStyles,
|
||||
css`
|
||||
:host {
|
||||
--sl-spacing-3x-small: 0.125rem;
|
||||
--sl-spacing-2x-small: 0.25rem;
|
||||
--sl-spacing-x-small: 0.5rem;
|
||||
--sl-spacing-small: 0.75rem;
|
||||
--sl-spacing-medium: 1rem;
|
||||
--sl-spacing-large: 1.25rem;
|
||||
--sl-spacing-x-large: 1.75rem;
|
||||
--sl-spacing-2x-large: 2.25rem;
|
||||
--sl-spacing-3x-large: 3rem;
|
||||
--sl-spacing-4x-large: 4.5rem;
|
||||
|
||||
--sl-transition-x-slow: 1000ms;
|
||||
--sl-transition-slow: 500ms;
|
||||
--sl-transition-medium: 250ms;
|
||||
--sl-transition-fast: 150ms;
|
||||
--sl-transition-x-fast: 50ms;
|
||||
--transition-speed: var(--sl-transition-fast);
|
||||
--sl-border-radius-small: 0.1875rem;
|
||||
--sl-border-radius-medium: 0.25rem;
|
||||
--sl-border-radius-large: 0.5rem;
|
||||
--sl-border-radius-x-large: 1rem;
|
||||
--sl-border-radius-circle: 50%;
|
||||
--sl-border-radius-pill: 9999px;
|
||||
|
||||
--sl-color-neutral-600: inherit;
|
||||
|
||||
--sl-font-weight-semibold: 500;
|
||||
--sl-font-size-small: 14px;
|
||||
|
||||
--sl-color-primary-600: var(
|
||||
--ha-tab-active-text-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(--ha-tab-track-color, var(--divider-color));
|
||||
--indicator-color: var(--ha-tab-indicator-color, var(--primary-color));
|
||||
}
|
||||
::slotted(sl-tab:not([active])) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
// @ts-ignore
|
||||
"sl-tab-group": HaSlTabGroup;
|
||||
}
|
||||
}
|
@ -19,16 +19,9 @@ import "../../panels/logbook/ha-logbook-renderer";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { NodeInfo } from "./hat-script-graph";
|
||||
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
|
||||
import { describeCondition } from "../../data/automation_i18n";
|
||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import type { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../data/context";
|
||||
import { describeAction } from "../../data/script_i18n";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
|
||||
const TRACE_PATH_TABS = [
|
||||
"step_config",
|
||||
@ -59,14 +52,6 @@ export class HaTracePathDetails extends LitElement {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: Record<string, FloorRegistryEntry>;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="padded-box trace-info">
|
||||
@ -166,46 +151,11 @@ export class HaTracePathDetails extends LitElement {
|
||||
)}`;
|
||||
}
|
||||
|
||||
const selectedType = this.selected.type;
|
||||
|
||||
return html`
|
||||
${curPath === this.selected.path
|
||||
? currentDetail.alias
|
||||
? html`<h2>${currentDetail.alias}</h2>`
|
||||
: selectedType === "trigger"
|
||||
? html`<h2>
|
||||
${describeTrigger(
|
||||
currentDetail,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}
|
||||
</h2>`
|
||||
: selectedType === "condition"
|
||||
? html`<h2>
|
||||
${describeCondition(
|
||||
currentDetail,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}
|
||||
</h2>`
|
||||
: selectedType === "action"
|
||||
? html`<h2>
|
||||
${describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
currentDetail
|
||||
)}
|
||||
</h2>`
|
||||
: selectedType === "chooseOption"
|
||||
? html`<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
{ number: pathParts[pathParts.length - 1] }
|
||||
)}
|
||||
</h2>`
|
||||
: nothing
|
||||
: nothing
|
||||
: html`<h2>
|
||||
${curPath.substring(this.selected.path.length + 1)}
|
||||
</h2>`}
|
||||
|
@ -53,12 +53,9 @@ import "./hat-graph-node";
|
||||
import "./hat-graph-spacer";
|
||||
import { ACTION_ICONS } from "../../data/action";
|
||||
|
||||
type NodeType = "trigger" | "condition" | "action" | "chooseOption" | undefined;
|
||||
|
||||
export interface NodeInfo {
|
||||
path: string;
|
||||
config: any;
|
||||
type?: NodeType;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@ -79,16 +76,16 @@ export class HatScriptGraph extends LitElement {
|
||||
|
||||
public trackedNodes: Record<string, NodeInfo> = {};
|
||||
|
||||
private _selectNode(config, path, type?) {
|
||||
private _selectNode(config, path) {
|
||||
return () => {
|
||||
fireEvent(this, "graph-node-selected", { config, path, type });
|
||||
fireEvent(this, "graph-node-selected", { config, path });
|
||||
};
|
||||
}
|
||||
|
||||
private _renderTrigger(config: Trigger, i: number) {
|
||||
const path = `trigger/${i}`;
|
||||
const track = this.trace && path in this.trace.trace;
|
||||
this.renderedNodes[path] = { config, path, type: "trigger" };
|
||||
this.renderedNodes[path] = { config, path };
|
||||
if (track) {
|
||||
this.trackedNodes[path] = this.renderedNodes[path];
|
||||
}
|
||||
@ -96,7 +93,7 @@ export class HatScriptGraph extends LitElement {
|
||||
<hat-graph-node
|
||||
graph-start
|
||||
?track=${track}
|
||||
@focus=${this._selectNode(config, path, "trigger")}
|
||||
@focus=${this._selectNode(config, path)}
|
||||
?active=${this.selected === path}
|
||||
.iconPath=${mdiAsterisk}
|
||||
.notEnabled=${"enabled" in config && config.enabled === false}
|
||||
@ -108,7 +105,7 @@ export class HatScriptGraph extends LitElement {
|
||||
|
||||
private _renderCondition(config: Condition, i: number) {
|
||||
const path = `condition/${i}`;
|
||||
this.renderedNodes[path] = { config, path, type: "condition" };
|
||||
this.renderedNodes[path] = { config, path };
|
||||
if (this.trace && path in this.trace.trace) {
|
||||
this.trackedNodes[path] = this.renderedNodes[path];
|
||||
}
|
||||
@ -139,7 +136,7 @@ export class HatScriptGraph extends LitElement {
|
||||
) {
|
||||
const type =
|
||||
Object.keys(this._typeRenderers).find((key) => key in node) || "other";
|
||||
this.renderedNodes[path] = { config: node, path, type: "action" };
|
||||
this.renderedNodes[path] = { config: node, path };
|
||||
if (this.trace && path in this.trace.trace) {
|
||||
this.trackedNodes[path] = this.renderedNodes[path];
|
||||
}
|
||||
@ -169,7 +166,7 @@ export class HatScriptGraph extends LitElement {
|
||||
return html`
|
||||
<hat-graph-branch
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
@focus=${this._selectNode(config, path, "action")}
|
||||
@focus=${this._selectNode(config, path)}
|
||||
?track=${trace !== undefined}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${disabled || config.enabled === false}
|
||||
@ -189,11 +186,7 @@ export class HatScriptGraph extends LitElement {
|
||||
? ensureArray(config.choose)?.map((branch, i) => {
|
||||
const branchPath = `${path}/choose/${i}`;
|
||||
const trackThis = tracePath.includes(i);
|
||||
this.renderedNodes[branchPath] = {
|
||||
config: branch,
|
||||
path: branchPath,
|
||||
type: "chooseOption",
|
||||
};
|
||||
this.renderedNodes[branchPath] = { config, path: branchPath };
|
||||
if (trackThis) {
|
||||
this.trackedNodes[branchPath] = this.renderedNodes[branchPath];
|
||||
}
|
||||
@ -203,11 +196,7 @@ export class HatScriptGraph extends LitElement {
|
||||
.iconPath=${!trace || trackThis
|
||||
? mdiCheckboxMarkedOutline
|
||||
: mdiCheckboxBlankOutline}
|
||||
@focus=${this._selectNode(
|
||||
branch,
|
||||
branchPath,
|
||||
"chooseOption"
|
||||
)}
|
||||
@focus=${this._selectNode(config, branchPath)}
|
||||
?track=${trackThis}
|
||||
?active=${this.selected === branchPath}
|
||||
.notEnabled=${disabled || config.enabled === false}
|
||||
@ -267,7 +256,7 @@ export class HatScriptGraph extends LitElement {
|
||||
return html`
|
||||
<hat-graph-branch
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
@focus=${this._selectNode(config, path, "action")}
|
||||
@focus=${this._selectNode(config, path)}
|
||||
?track=${trace !== undefined}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${disabled || config.enabled === false}
|
||||
@ -348,7 +337,7 @@ export class HatScriptGraph extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<hat-graph-branch
|
||||
@focus=${this._selectNode(node, path, "condition")}
|
||||
@focus=${this._selectNode(node, path)}
|
||||
?track=${track}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${disabled || node.enabled === false}
|
||||
@ -392,7 +381,7 @@ export class HatScriptGraph extends LitElement {
|
||||
return html`
|
||||
<hat-graph-branch
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
@focus=${this._selectNode(node, path, "action")}
|
||||
@focus=${this._selectNode(node, path)}
|
||||
?track=${path in this.trace.trace}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${disabled || node.enabled === false}
|
||||
@ -438,7 +427,7 @@ export class HatScriptGraph extends LitElement {
|
||||
<hat-graph-node
|
||||
.graphStart=${graphStart}
|
||||
.iconPath=${node.action ? undefined : mdiRoomService}
|
||||
@focus=${this._selectNode(node, path, "action")}
|
||||
@focus=${this._selectNode(node, path)}
|
||||
?track=${path in this.trace.trace}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${disabled || node.enabled === false}
|
||||
@ -466,7 +455,7 @@ export class HatScriptGraph extends LitElement {
|
||||
<hat-graph-node
|
||||
.graphStart=${graphStart}
|
||||
.iconPath=${mdiCodeBraces}
|
||||
@focus=${this._selectNode(node, path, "action")}
|
||||
@focus=${this._selectNode(node, path)}
|
||||
?track=${path in this.trace.trace}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${disabled || node.enabled === false}
|
||||
@ -486,7 +475,7 @@ export class HatScriptGraph extends LitElement {
|
||||
return html`
|
||||
<hat-graph-branch
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
@focus=${this._selectNode(node, path, "action")}
|
||||
@focus=${this._selectNode(node, path)}
|
||||
?track=${path in this.trace.trace}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${disabled || node.enabled === false}
|
||||
@ -524,7 +513,7 @@ export class HatScriptGraph extends LitElement {
|
||||
return html`
|
||||
<hat-graph-branch
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
@focus=${this._selectNode(node, path, "action")}
|
||||
@focus=${this._selectNode(node, path)}
|
||||
?track=${path in this.trace.trace}
|
||||
?active=${this.selected === path}
|
||||
.notEnabled=${disabled || node.enabled === false}
|
||||
@ -573,7 +562,7 @@ export class HatScriptGraph extends LitElement {
|
||||
<hat-graph-node
|
||||
.graphStart=${graphStart}
|
||||
.iconPath=${ACTION_ICONS[getActionType(node)] || mdiCodeBrackets}
|
||||
@focus=${this._selectNode(node, path, "action")}
|
||||
@focus=${this._selectNode(node, path)}
|
||||
?track=${path in this.trace.trace}
|
||||
?active=${this.selected === path}
|
||||
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||
|
@ -84,24 +84,21 @@ class UserBadge extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: contents;
|
||||
}
|
||||
.picture {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.initials {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
background-color: var(--light-primary-color);
|
||||
text-decoration: none;
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
|
@ -49,13 +49,9 @@ export const testAssistSatelliteConnection = (
|
||||
export const assistSatelliteAnnounce = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
args: {
|
||||
message?: string;
|
||||
media_id?: string;
|
||||
preannounce?: boolean;
|
||||
preannounce_media_id?: string;
|
||||
}
|
||||
) => hass.callService("assist_satellite", "announce", args, { entity_id });
|
||||
message: string
|
||||
) =>
|
||||
hass.callService("assist_satellite", "announce", { message }, { entity_id });
|
||||
|
||||
export const fetchAssistSatelliteConfiguration = (
|
||||
hass: HomeAssistant,
|
||||
|
@ -66,16 +66,11 @@ export type ManagerStateEvent =
|
||||
|
||||
export const subscribeBackupEvents = (
|
||||
hass: HomeAssistant,
|
||||
callback: (event: ManagerStateEvent) => void,
|
||||
preCheck?: () => boolean | Promise<boolean>
|
||||
callback: (event: ManagerStateEvent) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ManagerStateEvent>(
|
||||
callback,
|
||||
{
|
||||
type: "backup/subscribe_events",
|
||||
},
|
||||
{ preCheck }
|
||||
);
|
||||
hass.connection.subscribeMessage<ManagerStateEvent>(callback, {
|
||||
type: "backup/subscribe_events",
|
||||
});
|
||||
|
||||
export const DEFAULT_MANAGER_STATE: ManagerStateEvent = {
|
||||
manager_state: "idle",
|
||||
|
@ -1,5 +1,7 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { getCollection } from "home-assistant-js-websocket";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
DataEntryFlowProgress,
|
||||
@ -91,20 +93,31 @@ export const fetchConfigFlowInProgress = (
|
||||
type: "config_entries/flow/progress",
|
||||
});
|
||||
|
||||
export interface ConfigFlowInProgressMessage {
|
||||
type: null | "added" | "removed";
|
||||
flow_id: string;
|
||||
flow: DataEntryFlowProgress;
|
||||
}
|
||||
const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) =>
|
||||
store.setState(flows, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"config_entry_discovered"
|
||||
);
|
||||
|
||||
export const getConfigFlowInProgressCollection = (conn: Connection) =>
|
||||
getCollection<DataEntryFlowProgress[]>(
|
||||
conn,
|
||||
"_configFlowProgress",
|
||||
fetchConfigFlowInProgress,
|
||||
subscribeConfigFlowInProgressUpdates
|
||||
);
|
||||
|
||||
export const subscribeConfigFlowInProgress = (
|
||||
hass: HomeAssistant,
|
||||
onChange: (update: ConfigFlowInProgressMessage[]) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ConfigFlowInProgressMessage[]>(
|
||||
(message) => onChange(message),
|
||||
{ type: "config_entries/flow/subscribe" }
|
||||
);
|
||||
onChange: (flows: DataEntryFlowProgress[]) => void
|
||||
) => getConfigFlowInProgressCollection(hass.connection).subscribe(onChange);
|
||||
|
||||
export const localizeConfigFlowTitle = (
|
||||
localize: LocalizeFunc,
|
||||
|
@ -17,15 +17,6 @@ export interface DataEntryFlowProgressedEvent {
|
||||
};
|
||||
}
|
||||
|
||||
export interface DataEntryFlowProgressEvent {
|
||||
type: "data_entry_flow_progress_update";
|
||||
data: {
|
||||
handler: string;
|
||||
flow_id: string;
|
||||
progress: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DataEntryFlowProgress {
|
||||
flow_id: string;
|
||||
handler: string;
|
||||
@ -117,12 +108,3 @@ export const subscribeDataEntryFlowProgressed = (
|
||||
callback,
|
||||
"data_entry_flow_progressed"
|
||||
);
|
||||
|
||||
export const subscribeDataEntryFlowProgress = (
|
||||
conn: Connection,
|
||||
callback: (ev: DataEntryFlowProgressEvent) => void
|
||||
) =>
|
||||
conn.subscribeEvents<DataEntryFlowProgressEvent>(
|
||||
callback,
|
||||
"data_entry_flow_progress_update"
|
||||
);
|
||||
|
@ -3,7 +3,6 @@ import { getOptimisticCollection } from "./collection";
|
||||
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
showEntityIdPicker?: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -154,9 +154,3 @@ export const subscribeLogInfo = (
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const waitForIntegrationSetup = (hass: HomeAssistant, domain: string) =>
|
||||
hass.callWS<{ integration_loaded: boolean }>({
|
||||
type: "integration/wait",
|
||||
domain,
|
||||
});
|
||||
|
@ -128,11 +128,3 @@ export const forgotPasswordHaCloud = async (email: string) =>
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
);
|
||||
|
||||
export const waitForIntegration = (domain: string) =>
|
||||
handleFetchPromise<{ integration_loaded: boolean }>(
|
||||
fetch("/api/onboarding/integration/wait", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ domain }),
|
||||
})
|
||||
);
|
||||
|
@ -94,14 +94,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
|
||||
const targets: string[] = [];
|
||||
const targetOrData = config.target || config.data;
|
||||
if (typeof targetOrData === "string" && isTemplate(targetOrData)) {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_template`,
|
||||
{ name: "target" }
|
||||
)
|
||||
);
|
||||
} else if (targetOrData) {
|
||||
if (targetOrData) {
|
||||
for (const [key, name] of Object.entries({
|
||||
area_id: "areas",
|
||||
device_id: "devices",
|
||||
|
@ -1,21 +0,0 @@
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export interface SupervisorUpdateConfig {
|
||||
add_on_backup_before_update: boolean;
|
||||
add_on_backup_retain_copies?: number;
|
||||
core_backup_before_update: boolean;
|
||||
}
|
||||
|
||||
export const getSupervisorUpdateConfig = async (hass: HomeAssistant) =>
|
||||
hass.callWS<SupervisorUpdateConfig>({
|
||||
type: "hassio/update/config/info",
|
||||
});
|
||||
|
||||
export const updateSupervisorUpdateConfig = async (
|
||||
hass: HomeAssistant,
|
||||
config: Partial<SupervisorUpdateConfig>
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "hassio/update/config/update",
|
||||
...config,
|
||||
});
|
@ -207,11 +207,7 @@ export const computeUpdateStateDisplay = (
|
||||
return hass.formatEntityState(stateObj);
|
||||
};
|
||||
|
||||
export type UpdateType =
|
||||
| "addon"
|
||||
| "home_assistant"
|
||||
| "home_assistant_os"
|
||||
| "generic";
|
||||
type UpdateType = "addon" | "home_assistant" | "generic";
|
||||
|
||||
export const getUpdateType = (
|
||||
stateObj: UpdateEntity,
|
||||
@ -219,7 +215,6 @@ export const getUpdateType = (
|
||||
): UpdateType => {
|
||||
const entity_id = stateObj.entity_id;
|
||||
const domain = entitySources[entity_id]?.domain;
|
||||
|
||||
if (domain !== "hassio") {
|
||||
return "generic";
|
||||
}
|
||||
@ -229,11 +224,13 @@ export const getUpdateType = (
|
||||
return "home_assistant";
|
||||
}
|
||||
|
||||
if (title === HOME_ASSISTANT_OS_TITLE) {
|
||||
return "home_assistant_os";
|
||||
}
|
||||
|
||||
if (title !== HOME_ASSISTANT_SUPERVISOR_TITLE) {
|
||||
if (
|
||||
![
|
||||
HOME_ASSISTANT_CORE_TITLE,
|
||||
HOME_ASSISTANT_SUPERVISOR_TITLE,
|
||||
HOME_ASSISTANT_OS_TITLE,
|
||||
].includes(title)
|
||||
) {
|
||||
return "addon";
|
||||
}
|
||||
return "generic";
|
||||
|
@ -80,7 +80,7 @@ enum QRCodeVersion {
|
||||
SmartStart = 1,
|
||||
}
|
||||
|
||||
export enum Protocols {
|
||||
enum Protocols {
|
||||
ZWave = 0,
|
||||
ZWaveLongRange = 1,
|
||||
}
|
||||
@ -151,35 +151,12 @@ export interface QRProvisioningInformation {
|
||||
maxInclusionRequestInterval?: number | undefined;
|
||||
uuid?: string | undefined;
|
||||
supportedProtocols?: Protocols[] | undefined;
|
||||
status?: ProvisioningEntryStatus;
|
||||
}
|
||||
|
||||
export interface PlannedProvisioningEntry {
|
||||
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
|
||||
dsk: string;
|
||||
securityClasses: SecurityClass[];
|
||||
status?: ProvisioningEntryStatus;
|
||||
}
|
||||
|
||||
export enum ProvisioningEntryStatus {
|
||||
Active = 0,
|
||||
Inactive = 1,
|
||||
}
|
||||
|
||||
export interface DeviceConfig {
|
||||
filename: string;
|
||||
manufacturer: string;
|
||||
manufacturerId: number;
|
||||
label: string;
|
||||
description: string;
|
||||
devices: {
|
||||
productType: number;
|
||||
productId: number;
|
||||
}[];
|
||||
firmwareVersion: {
|
||||
min: string;
|
||||
max: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const MINIMUM_QR_STRING_LENGTH = 52;
|
||||
@ -218,7 +195,6 @@ export interface ZWaveJSController {
|
||||
is_rebuilding_routes: boolean;
|
||||
inclusion_state: InclusionState;
|
||||
nodes: ZWaveJSNodeStatus[];
|
||||
supports_long_range: boolean;
|
||||
}
|
||||
|
||||
export interface ZWaveJSNodeStatus {
|
||||
@ -579,7 +555,7 @@ export const zwaveTryParseDskFromQrCode = (
|
||||
export const zwaveValidateDskAndEnterPin = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
pin: string | false
|
||||
pin: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/validate_dsk_and_enter_pin",
|
||||
@ -609,38 +585,19 @@ export const zwaveParseQrCode = (
|
||||
qr_code_string,
|
||||
});
|
||||
|
||||
export const lookupZwaveDevice = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
manufacturerId: number,
|
||||
productType: number,
|
||||
productId: number,
|
||||
applicationVersion?: string
|
||||
): Promise<DeviceConfig> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/lookup_device",
|
||||
entry_id,
|
||||
manufacturerId,
|
||||
productType,
|
||||
productId,
|
||||
applicationVersion,
|
||||
});
|
||||
|
||||
export const provisionZwaveSmartStartNode = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
qr_provisioning_information?: QRProvisioningInformation,
|
||||
protocol?: Protocols,
|
||||
device_name?: string,
|
||||
area_id?: string
|
||||
): Promise<string> =>
|
||||
qr_code_string?: string,
|
||||
planned_provisioning_entry?: PlannedProvisioningEntry
|
||||
): Promise<QRProvisioningInformation> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/provision_smart_start_node",
|
||||
entry_id,
|
||||
qr_code_string,
|
||||
qr_provisioning_information,
|
||||
protocol,
|
||||
device_name,
|
||||
area_id,
|
||||
planned_provisioning_entry,
|
||||
});
|
||||
|
||||
export const unprovisionZwaveSmartStartNode = (
|
||||
@ -656,16 +613,6 @@ export const unprovisionZwaveSmartStartNode = (
|
||||
node_id,
|
||||
});
|
||||
|
||||
export const subscribeNewDevices = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
callbackFunction: (message: any) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage((message) => callbackFunction(message), {
|
||||
type: "zwave_js/subscribe_new_devices",
|
||||
entry_id: entry_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string
|
||||
|
@ -9,10 +9,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-icon-button";
|
||||
import type { DataEntryFlowStep } from "../../data/data_entry_flow";
|
||||
import {
|
||||
subscribeDataEntryFlowProgress,
|
||||
subscribeDataEntryFlowProgressed,
|
||||
} from "../../data/data_entry_flow";
|
||||
import { subscribeDataEntryFlowProgressed } from "../../data/data_entry_flow";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
@ -55,8 +52,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
|
||||
@state() private _loading?: LoadingReason;
|
||||
|
||||
@state() private _progress?: number;
|
||||
|
||||
private _instance = instance;
|
||||
|
||||
@state() private _step:
|
||||
@ -67,7 +62,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
|
||||
@state() private _handler?: string;
|
||||
|
||||
private _unsubDataEntryFlowProgress?: UnsubscribeFunc;
|
||||
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
|
||||
|
||||
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
@ -165,9 +160,11 @@ class DataEntryFlowDialog extends LitElement {
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
this._handler = undefined;
|
||||
if (this._unsubDataEntryFlowProgress) {
|
||||
this._unsubDataEntryFlowProgress();
|
||||
this._unsubDataEntryFlowProgress = undefined;
|
||||
if (this._unsubDataEntryFlowProgressed) {
|
||||
this._unsubDataEntryFlowProgressed.then((unsub) => {
|
||||
unsub();
|
||||
});
|
||||
this._unsubDataEntryFlowProgressed = undefined;
|
||||
}
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@ -258,9 +255,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
.params=${this._params}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.handler=${this._step.handler}
|
||||
.domain=${this._params.domain ??
|
||||
this._step.handler}
|
||||
.domain=${this._step.handler}
|
||||
></step-flow-abort>
|
||||
`
|
||||
: this._step.type === "progress"
|
||||
@ -269,7 +264,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.progress=${this._progress}
|
||||
></step-flow-progress>
|
||||
`
|
||||
: this._step.type === "menu"
|
||||
@ -345,28 +339,20 @@ class DataEntryFlowDialog extends LitElement {
|
||||
}
|
||||
|
||||
private async _subscribeDataEntryFlowProgressed() {
|
||||
if (this._unsubDataEntryFlowProgress) {
|
||||
if (this._unsubDataEntryFlowProgressed) {
|
||||
return;
|
||||
}
|
||||
this._progress = undefined;
|
||||
const unsubs = [
|
||||
subscribeDataEntryFlowProgressed(this.hass.connection, (ev) => {
|
||||
this._unsubDataEntryFlowProgressed = subscribeDataEntryFlowProgressed(
|
||||
this.hass.connection,
|
||||
async (ev) => {
|
||||
if (ev.data.flow_id !== this._step?.flow_id) {
|
||||
return;
|
||||
}
|
||||
this._processStep(
|
||||
this._params!.flowConfig.fetchFlow(this.hass, this._step.flow_id)
|
||||
);
|
||||
this._progress = undefined;
|
||||
}),
|
||||
subscribeDataEntryFlowProgress(this.hass.connection, (ev) => {
|
||||
// ha-progress-ring has an issue with 0 so we round up
|
||||
this._progress = Math.ceil(ev.data.progress * 100);
|
||||
}),
|
||||
];
|
||||
this._unsubDataEntryFlowProgress = async () => {
|
||||
(await Promise.all(unsubs)).map((unsub) => unsub());
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@ -20,8 +20,6 @@ class StepFlowAbort extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public domain!: string;
|
||||
|
||||
@property({ attribute: false }) public handler!: string;
|
||||
|
||||
protected firstUpdated(changed: PropertyValues) {
|
||||
super.firstUpdated(changed);
|
||||
if (this.step.reason === "missing_credentials") {
|
||||
@ -60,7 +58,7 @@ class StepFlowAbort extends LitElement {
|
||||
applicationCredentialAddedCallback: () => {
|
||||
showConfigFlowDialog(this.params.dialogParentElement!, {
|
||||
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||
startFlowHandler: this.handler,
|
||||
startFlowHandler: this.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: this.params.navigateToResult,
|
||||
});
|
||||
|
@ -84,7 +84,7 @@ class StepFlowForm extends LitElement {
|
||||
${this._loading
|
||||
? html`
|
||||
<div class="submit-spinner">
|
||||
<ha-spinner size="small"></ha-spinner>
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
@ -263,9 +263,6 @@ class StepFlowForm extends LitElement {
|
||||
}
|
||||
|
||||
.submit-spinner {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
|
@ -2,13 +2,11 @@ import "@material/mwc-button";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../components/ha-progress-ring";
|
||||
import "../../components/ha-spinner";
|
||||
import type { DataEntryFlowStepProgress } from "../../data/data_entry_flow";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { blankBeforePercent } from "../../common/translations/blank_before_percent";
|
||||
|
||||
@customElement("step-flow-progress")
|
||||
class StepFlowProgress extends LitElement {
|
||||
@ -21,24 +19,13 @@ class StepFlowProgress extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public step!: DataEntryFlowStepProgress;
|
||||
|
||||
@property({ type: Number })
|
||||
public progress?: number;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h2>
|
||||
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
|
||||
</h2>
|
||||
<div class="content">
|
||||
${this.progress
|
||||
? html`
|
||||
<ha-progress-ring .value=${this.progress} size="large"
|
||||
>${this.progress}${blankBeforePercent(
|
||||
this.hass.locale
|
||||
)}%</ha-progress-ring
|
||||
>
|
||||
`
|
||||
: html` <ha-spinner size="large"></ha-spinner> `}
|
||||
<ha-spinner></ha-spinner>
|
||||
${this.flowConfig.renderShowFormProgressDescription(
|
||||
this.hass,
|
||||
this.step
|
||||
|
@ -45,8 +45,7 @@ class MoreInfoCamera extends LitElement {
|
||||
<ha-progress-button
|
||||
@click=${this._downloadSnapshot}
|
||||
.progress=${this._waiting}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE ||
|
||||
this.stateObj.state === "idle"}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.camera.download_snapshot"
|
||||
|
@ -1,27 +1,26 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { BINARY_STATE_OFF } from "../../../common/const";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-faded";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import type { BackupConfig } from "../../../data/backup";
|
||||
import { fetchBackupConfig } from "../../../data/backup";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import type { EntitySources } from "../../../data/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
|
||||
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
|
||||
import type { UpdateEntity, UpdateType } from "../../../data/update";
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import {
|
||||
getUpdateType,
|
||||
UpdateEntityFeature,
|
||||
@ -45,49 +44,17 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
@state() private _backupConfig?: BackupConfig;
|
||||
|
||||
@state() private _createBackup = false;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
try {
|
||||
const { config } = await fetchBackupConfig(this.hass);
|
||||
this._backupConfig = config;
|
||||
} catch (err) {
|
||||
// ignore error, because user will get a manual backup option
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchUpdateBackupConfig(type: UpdateType) {
|
||||
try {
|
||||
const config = await getSupervisorUpdateConfig(this.hass);
|
||||
|
||||
// for home assistant and OS updates
|
||||
if (this._isHaOrOsUpdate(type)) {
|
||||
this._createBackup = config.core_backup_before_update;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "addon") {
|
||||
this._createBackup = config.add_on_backup_before_update;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore error, because user can still set the config
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
const { config } = await fetchBackupConfig(this.hass);
|
||||
this._backupConfig = config;
|
||||
}
|
||||
|
||||
private async _fetchEntitySources() {
|
||||
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
|
||||
}
|
||||
|
||||
private _isHaOrOsUpdate(type: UpdateType): boolean {
|
||||
return ["home_assistant", "home_assistant_os"].includes(type);
|
||||
}
|
||||
|
||||
private _computeCreateBackupTexts():
|
||||
| { title: string; description?: string }
|
||||
| undefined {
|
||||
@ -102,7 +69,8 @@ class MoreInfoUpdate extends LitElement {
|
||||
? getUpdateType(this.stateObj, this._entitySources)
|
||||
: "generic";
|
||||
|
||||
if (this._isHaOrOsUpdate(updateType)) {
|
||||
// Automatic or manual for Home Assistant update
|
||||
if (updateType === "home_assistant") {
|
||||
const isBackupConfigValid =
|
||||
!!this._backupConfig &&
|
||||
!!this._backupConfig.automatic_backups_configured &&
|
||||
@ -288,8 +256,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
: nothing}
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${this._createBackup}
|
||||
@change=${this._createBackupChanged}
|
||||
id="create-backup"
|
||||
.disabled=${updateIsInstalling(this.stateObj)}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
@ -352,14 +319,7 @@ class MoreInfoUpdate extends LitElement {
|
||||
if (supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||
this._fetchEntitySources().then(() => {
|
||||
const type = getUpdateType(this.stateObj!, this._entitySources!);
|
||||
if (
|
||||
isComponentLoaded(this.hass, "hassio") &&
|
||||
["addon", "home_assistant", "home_assistant_os"].includes(type)
|
||||
) {
|
||||
this._fetchUpdateBackupConfig(type);
|
||||
}
|
||||
|
||||
if (this._isHaOrOsUpdate(type)) {
|
||||
if (type === "home_assistant") {
|
||||
this._fetchBackupConfig();
|
||||
}
|
||||
});
|
||||
@ -387,7 +347,13 @@ class MoreInfoUpdate extends LitElement {
|
||||
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
|
||||
return false;
|
||||
}
|
||||
return this._createBackup;
|
||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
||||
"create-backup"
|
||||
) as HaSwitch;
|
||||
if (createBackupSwitch) {
|
||||
return createBackupSwitch.checked;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _handleInstall(): void {
|
||||
@ -409,10 +375,6 @@ class MoreInfoUpdate extends LitElement {
|
||||
this.hass.callService("update", "install", installData);
|
||||
}
|
||||
|
||||
private _createBackupChanged(ev) {
|
||||
this._createBackup = ev.target.checked;
|
||||
}
|
||||
|
||||
private _handleSkip(): void {
|
||||
if (this.stateObj!.attributes.auto_update) {
|
||||
showAlertDialog(this, {
|
||||
|
@ -276,11 +276,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
this._setView("related");
|
||||
}
|
||||
|
||||
private _breadcrumbClick(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._setView("related");
|
||||
}
|
||||
|
||||
private async _loadNumericDeviceClasses() {
|
||||
const deviceClasses = await getSensorNumericDeviceClasses(this.hass);
|
||||
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
|
||||
@ -324,7 +319,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
|
||||
const title = this._childView?.viewTitle || breadcrumb.pop();
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@ -355,23 +350,18 @@ export class MoreInfoDialog extends LitElement {
|
||||
)}
|
||||
></ha-icon-button-prev>
|
||||
`}
|
||||
<span slot="title" @click=${this._enlarge} class="title">
|
||||
<span
|
||||
slot="title"
|
||||
.title=${title}
|
||||
@click=${this._enlarge}
|
||||
class="title"
|
||||
>
|
||||
${breadcrumb.length > 0
|
||||
? !__DEMO__ && isAdmin
|
||||
? html`
|
||||
<button
|
||||
class="breadcrumb"
|
||||
@click=${this._breadcrumbClick}
|
||||
aria-label=${breadcrumb.join(" > ")}
|
||||
>
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<p class="breadcrumb">
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</p>
|
||||
`
|
||||
? html`
|
||||
<p class="breadcrumb">
|
||||
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
<p class="main">${title}</p>
|
||||
</span>
|
||||
@ -666,7 +656,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.title p {
|
||||
@ -687,30 +676,11 @@ export class MoreInfoDialog extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.title .breadcrumb {
|
||||
--mdc-icon-size: 16px;
|
||||
padding: 4px;
|
||||
margin: -4px;
|
||||
margin-top: -10px;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: inline;
|
||||
border-radius: 6px;
|
||||
transition: background-color 180ms ease-in-out;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.title button.breadcrumb {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title button.breadcrumb:focus-visible,
|
||||
.title button.breadcrumb:hover {
|
||||
background-color: rgba(var(--rgb-secondary-text-color), 0.08);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -20,7 +20,6 @@ import type {
|
||||
import { fetchStatistics, getStatisticMetadata } from "../../data/recorder";
|
||||
import { getSensorNumericDeviceClasses } from "../../data/sensor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@ -59,9 +58,9 @@ export class MoreInfoHistory extends LitElement {
|
||||
|
||||
return html`${isComponentLoaded(this.hass, "history")
|
||||
? html`<div class="header">
|
||||
<h2>
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.history")}
|
||||
</h2>
|
||||
</div>
|
||||
${__DEMO__
|
||||
? nothing
|
||||
: html`<a href=${this._showMoreHref}
|
||||
@ -232,25 +231,27 @@ export class MoreInfoHistory extends LitElement {
|
||||
this._setRedrawTimer();
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.header > a,
|
||||
a:visited {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
static styles = css`
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.header > a,
|
||||
a:visited {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.title {
|
||||
font-family: var(--paper-font-title_-_font-family);
|
||||
-webkit-font-smoothing: var(--paper-font-title_-_-webkit-font-smoothing);
|
||||
font-size: var(--paper-font-subhead_-_font-size);
|
||||
font-weight: var(--paper-font-title_-_font-weight);
|
||||
letter-spacing: var(--paper-font-title_-_letter-spacing);
|
||||
line-height: var(--paper-font-title_-_line-height);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -133,8 +133,8 @@ export class MoreInfoInfo extends LitElement {
|
||||
|
||||
[data-domain="camera"] .content {
|
||||
padding: 0;
|
||||
/* max height of the video is full screen, minus the height of the header of the dialog (79px) and the max height of the dialog (mdc-dialog-max-height: calc(100% - 72px)) and the actions bar 60px */
|
||||
--video-max-height: calc(100vh - 72px - 79px - 60px);
|
||||
/* max height of the video is full screen, minus the height of the header of the dialog and the padding of the dialog (mdc-dialog-max-height: calc(100% - 72px)) */
|
||||
--video-max-height: calc(100vh - 65px - 72px);
|
||||
}
|
||||
|
||||
more-info-content {
|
||||
|
@ -7,7 +7,6 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { createSearchParam } from "../../common/url/search-params";
|
||||
import "../../panels/logbook/ha-logbook";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
|
||||
@customElement("ha-more-info-logbook")
|
||||
export class MoreInfoLogbook extends LitElement {
|
||||
@ -33,7 +32,9 @@ export class MoreInfoLogbook extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<h2>${this.hass.localize("ui.dialogs.more_info_control.logbook")}</h2>
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
|
||||
</div>
|
||||
<a href=${this._showMoreHref}
|
||||
>${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a
|
||||
>
|
||||
@ -67,7 +68,6 @@ export class MoreInfoLogbook extends LitElement {
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-logbook {
|
||||
--logbook-max-height: 250px;
|
||||
@ -88,8 +88,15 @@ export class MoreInfoLogbook extends LitElement {
|
||||
a:visited {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
.title {
|
||||
font-family: var(--paper-font-title_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-title_-_-webkit-font-smoothing
|
||||
);
|
||||
font-size: var(--paper-font-subhead_-_font-size);
|
||||
font-weight: var(--paper-font-title_-_font-weight);
|
||||
letter-spacing: var(--paper-font-title_-_letter-spacing);
|
||||
line-height: var(--paper-font-title_-_line-height);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -25,14 +25,15 @@ export class HuiNotificationItemTemplate extends LitElement {
|
||||
}
|
||||
|
||||
ha-card .header {
|
||||
font-family: var(--paper-font-headline_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-headline_-_-webkit-font-smoothing
|
||||
);
|
||||
font-size: var(--paper-font-headline_-_font-size);
|
||||
font-weight: var(--paper-font-headline_-_font-weight);
|
||||
letter-spacing: var(--paper-font-headline_-_letter-spacing);
|
||||
line-height: var(--paper-font-headline_-_line-height);
|
||||
/* start paper-font-headline style */
|
||||
font-family: "Roboto", "Noto", sans-serif;
|
||||
-webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.012em;
|
||||
line-height: 32px;
|
||||
/* end paper-font-headline style */
|
||||
|
||||
color: var(--primary-text-color);
|
||||
padding: 16px 16px 0;
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
mdiConsoleLine,
|
||||
mdiDevices,
|
||||
mdiEarth,
|
||||
mdiKeyboard,
|
||||
mdiMagnify,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
@ -32,7 +31,6 @@ import "../../components/ha-label";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-tip";
|
||||
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||
@ -42,7 +40,6 @@ import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
|
||||
|
||||
interface QuickBarItem extends ScorableTextItem {
|
||||
@ -425,12 +422,10 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
private _addSpinnerToCommandItem(index: number): void {
|
||||
const div = document.createElement("div");
|
||||
div.slot = "meta";
|
||||
const spinner = document.createElement("ha-spinner");
|
||||
spinner.size = "small";
|
||||
div.appendChild(spinner);
|
||||
this._getItemAtIndex(index)?.appendChild(div);
|
||||
spinner.slot = "meta";
|
||||
this._getItemAtIndex(index)?.appendChild(spinner);
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
@ -740,20 +735,10 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const additionalItems = [
|
||||
{
|
||||
path: "",
|
||||
primaryText: this.hass.localize("ui.panel.config.info.shortcuts"),
|
||||
action: () => showShortcutsDialog(this),
|
||||
iconPath: mdiKeyboard,
|
||||
},
|
||||
];
|
||||
|
||||
return this._finalizeNavigationCommands([
|
||||
...panelItems,
|
||||
...sectionItems,
|
||||
...supervisorItems,
|
||||
...additionalItems,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -830,12 +815,12 @@ export class QuickBar extends LitElement {
|
||||
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
||||
|
||||
const navItem = {
|
||||
...item,
|
||||
iconPath: mdiEarth,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
action: () => navigate(item.path),
|
||||
...item,
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -1,228 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/chips/ha-assist-chip";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
|
||||
interface Text {
|
||||
type: "text";
|
||||
key: LocalizeKeys;
|
||||
}
|
||||
|
||||
type ShortcutString = string | { key: LocalizeKeys };
|
||||
|
||||
interface Shortcut {
|
||||
type: "shortcut";
|
||||
shortcut: ShortcutString[];
|
||||
key: LocalizeKeys;
|
||||
}
|
||||
|
||||
interface Section {
|
||||
key: LocalizeKeys;
|
||||
items: (Text | Shortcut)[];
|
||||
}
|
||||
|
||||
const _SHORTCUTS: Section[] = [
|
||||
{
|
||||
key: "ui.dialogs.shortcuts.searching.title",
|
||||
items: [
|
||||
{ type: "text", key: "ui.dialogs.shortcuts.searching.on_any_page" },
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: ["C"],
|
||||
key: "ui.dialogs.shortcuts.searching.search_command",
|
||||
},
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: ["E"],
|
||||
key: "ui.dialogs.shortcuts.searching.search_entities",
|
||||
},
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: ["D"],
|
||||
key: "ui.dialogs.shortcuts.searching.search_devices",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
key: "ui.dialogs.shortcuts.searching.on_pages_with_tables",
|
||||
},
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "F"],
|
||||
key: "ui.dialogs.shortcuts.searching.search_in_table",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "ui.dialogs.shortcuts.assist.title",
|
||||
items: [
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: ["A"],
|
||||
key: "ui.dialogs.shortcuts.assist.open_assist",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "ui.dialogs.shortcuts.charts.title",
|
||||
items: [
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: [
|
||||
{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" },
|
||||
{ key: "ui.dialogs.shortcuts.shortcuts.drag" },
|
||||
],
|
||||
key: "ui.dialogs.shortcuts.charts.drag_to_zoom",
|
||||
},
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: [
|
||||
{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" },
|
||||
{ key: "ui.dialogs.shortcuts.shortcuts.scroll_wheel" },
|
||||
],
|
||||
key: "ui.dialogs.shortcuts.charts.scroll_to_zoom",
|
||||
},
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.double_click" }],
|
||||
key: "ui.dialogs.shortcuts.charts.double_click",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "ui.dialogs.shortcuts.other.title",
|
||||
items: [
|
||||
{
|
||||
type: "shortcut",
|
||||
shortcut: ["M"],
|
||||
key: "ui.dialogs.shortcuts.other.my_link",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("dialog-shortcuts")
|
||||
class DialogShortcuts extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
this._opened = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _renderShortcut(
|
||||
shortcuts: ShortcutString[],
|
||||
translationKey: LocalizeKeys
|
||||
) {
|
||||
const keys = shortcuts.map((shortcut) =>
|
||||
typeof shortcut === "string" ? shortcut : this.hass.localize(shortcut.key)
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="shortcut">
|
||||
${keys.map((key) => html` <span>${key.toUpperCase()}</span>`)}
|
||||
${this.hass.localize(translationKey)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
defaultAction="ignore"
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.shortcuts.title")
|
||||
)}
|
||||
>
|
||||
<div class="content">
|
||||
${_SHORTCUTS.map(
|
||||
(section) => html`
|
||||
<h3>${this.hass.localize(section.key)}</h3>
|
||||
<div class="items">
|
||||
${section.items.map((item) => {
|
||||
if (item.type === "text") {
|
||||
return html`<p>${this.hass.localize(item.key)}</p>`;
|
||||
}
|
||||
if (item.type === "shortcut") {
|
||||
return this._renderShortcut(item.shortcut, item.key);
|
||||
}
|
||||
return nothing;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-alert>
|
||||
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
|
||||
user_profile: html`<a href="/profile/general#shortcuts"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
|
||||
)}</a
|
||||
>`,
|
||||
})}
|
||||
</ha-alert>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 15;
|
||||
}
|
||||
|
||||
h3:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--outline-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.items p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-shortcuts": DialogShortcuts;
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export const showShortcutsDialog = (element: HTMLElement) =>
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-shortcuts",
|
||||
dialogImport: () => import("./dialog-shortcuts"),
|
||||
dialogParams: {},
|
||||
});
|
@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-textarea";
|
||||
import type { HaTextArea } from "../../components/ha-textarea";
|
||||
@ -10,7 +11,7 @@ import { convertTextToSpeech } from "../../data/tts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import type { TTSTryDialogParams } from "./show-dialog-tts-try";
|
||||
import "../../components/buttons/ha-progress-button";
|
||||
import "../../components/ha-spinner";
|
||||
|
||||
@customElement("dialog-tts-try")
|
||||
export class TTSTryDialog extends LitElement {
|
||||
@ -80,17 +81,28 @@ export class TTSTryDialog extends LitElement {
|
||||
?dialogInitialFocus=${!this._defaultMessage}
|
||||
>
|
||||
</ha-textarea>
|
||||
|
||||
<ha-progress-button
|
||||
.progress=${this._loadingExample}
|
||||
?dialogInitialFocus=${Boolean(this._defaultMessage)}
|
||||
slot="primaryAction"
|
||||
.label=${this.hass.localize("ui.dialogs.tts-try.play")}
|
||||
@click=${this._playExample}
|
||||
.disabled=${!this._valid}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlayCircleOutline}></ha-svg-icon>
|
||||
</ha-progress-button>
|
||||
${this._loadingExample
|
||||
? html`
|
||||
<ha-spinner
|
||||
size="small"
|
||||
slot="primaryAction"
|
||||
class="loading"
|
||||
></ha-spinner>
|
||||
`
|
||||
: html`
|
||||
<ha-button
|
||||
?dialogInitialFocus=${Boolean(this._defaultMessage)}
|
||||
slot="primaryAction"
|
||||
.label=${this.hass.localize("ui.dialogs.tts-try.play")}
|
||||
@click=${this._playExample}
|
||||
.disabled=${!this._valid}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
@ -90,9 +90,6 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
this._previousSteps = [];
|
||||
this._nextStep = undefined;
|
||||
this._step = STEP.INIT;
|
||||
this._language = undefined;
|
||||
this._languages = [];
|
||||
this._localOption = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
|
@ -243,7 +243,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
|
||||
|
||||
private readonly _ttsHostName = "core-piper";
|
||||
|
||||
private readonly _ttsPort = 10200;
|
||||
private readonly _ttsPort = "10200";
|
||||
|
||||
private get _sttProviderName() {
|
||||
return this.localOption === "focused_local"
|
||||
@ -263,7 +263,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
|
||||
: "core-whisper";
|
||||
}
|
||||
|
||||
private readonly _sttPort = 10300;
|
||||
private readonly _sttPort = "10300";
|
||||
|
||||
private async _findLocalEntities() {
|
||||
const wyomingEntities = Object.values(this.hass.entities).filter(
|
||||
@ -325,16 +325,14 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
|
||||
(flow) =>
|
||||
flow.handler === "wyoming" &&
|
||||
flow.context.source === "hassio" &&
|
||||
((flow.context.configuration_url &&
|
||||
flow.context.configuration_url.includes(
|
||||
type === "tts" ? this._ttsAddonName : this._sttAddonName
|
||||
)) ||
|
||||
(flow.context.title_placeholders.name &&
|
||||
flow.context.title_placeholders.name
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
type === "tts" ? this._ttsProviderName : this._sttProviderName
|
||||
)))
|
||||
(flow.context.configuration_url.includes(
|
||||
type === "tts" ? this._ttsHostName : this._sttHostName
|
||||
) ||
|
||||
flow.context.title_placeholders.title
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
type === "tts" ? this._ttsProviderName : this._sttProviderName
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@ -359,24 +357,40 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
|
||||
}
|
||||
|
||||
const pipelines = await listAssistPipelines(this.hass);
|
||||
|
||||
if (pipelines.preferred_pipeline) {
|
||||
pipelines.pipelines.sort((a) =>
|
||||
a.id === pipelines.preferred_pipeline ? -1 : 0
|
||||
);
|
||||
}
|
||||
const preferredPipeline = pipelines.pipelines.find(
|
||||
(pipeline) => pipeline.id === pipelines.preferred_pipeline
|
||||
);
|
||||
|
||||
const ttsEntityIds = this._localTts.map((ent) => ent.entity_id);
|
||||
const sttEntityIds = this._localStt.map((ent) => ent.entity_id);
|
||||
|
||||
if (preferredPipeline) {
|
||||
if (
|
||||
preferredPipeline.conversation_engine ===
|
||||
"conversation.home_assistant" &&
|
||||
preferredPipeline.tts_engine &&
|
||||
ttsEntityIds.includes(preferredPipeline.tts_engine) &&
|
||||
preferredPipeline.stt_engine &&
|
||||
sttEntityIds.includes(preferredPipeline.stt_engine)
|
||||
) {
|
||||
await this.hass.callService(
|
||||
"select",
|
||||
"select_option",
|
||||
{ option: "preferred" },
|
||||
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
|
||||
);
|
||||
this._nextStep();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let localPipeline = pipelines.pipelines.find(
|
||||
(pipeline) =>
|
||||
pipeline.conversation_engine === "conversation.home_assistant" &&
|
||||
pipeline.tts_engine &&
|
||||
ttsEntityIds.includes(pipeline.tts_engine) &&
|
||||
pipeline.stt_engine &&
|
||||
sttEntityIds.includes(pipeline.stt_engine) &&
|
||||
pipeline.language.split("-")[0] === this.language.split("-")[0]
|
||||
sttEntityIds.includes(pipeline.stt_engine)
|
||||
);
|
||||
|
||||
if (!localPipeline) {
|
||||
@ -449,7 +463,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
|
||||
}
|
||||
|
||||
let pipelineName = this.hass.localize(
|
||||
`ui.panel.config.voice_assistants.satellite_wizard.local.${this.localOption}_pipeline`
|
||||
"ui.panel.config.voice_assistants.satellite_wizard.local.local_pipeline"
|
||||
);
|
||||
let i = 1;
|
||||
while (
|
||||
@ -458,7 +472,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
|
||||
(pipeline) => pipeline.name === pipelineName
|
||||
)
|
||||
) {
|
||||
pipelineName = `${this.hass.localize(`ui.panel.config.voice_assistants.satellite_wizard.local.${this.localOption}_pipeline`)} ${i}`;
|
||||
pipelineName = `${this.hass.localize("ui.panel.config.voice_assistants.satellite_wizard.local.local_pipeline")} ${i}`;
|
||||
i++;
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user