mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-19 01:49:45 +00:00
Compare commits
131 Commits
add-suppor
...
20230503.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bc81499e7c | ||
![]() |
c31378ad75 | ||
![]() |
8ce8b63cfc | ||
![]() |
11f77b09e6 | ||
![]() |
ba7d7556a8 | ||
![]() |
31da0d8678 | ||
![]() |
9f195534b5 | ||
![]() |
a293e7b91c | ||
![]() |
8205a30baf | ||
![]() |
25af9a9770 | ||
![]() |
6c25e23ad2 | ||
![]() |
a39c2a314a | ||
![]() |
1ea70f3191 | ||
![]() |
02a94c04af | ||
![]() |
fc97ca324c | ||
![]() |
4a0d84d2f6 | ||
![]() |
b550c67a9f | ||
![]() |
a3ec83a684 | ||
![]() |
822f47143b | ||
![]() |
da1df9d8cc | ||
![]() |
aa155261f5 | ||
![]() |
29aa762f7c | ||
![]() |
d56e4afe92 | ||
![]() |
e766c277f5 | ||
![]() |
6c0011fb45 | ||
![]() |
a6e71f4c0a | ||
![]() |
d6382e59c6 | ||
![]() |
ab308af61f | ||
![]() |
15eab18e07 | ||
![]() |
c8e0227a5c | ||
![]() |
f2a8528429 | ||
![]() |
3ed3dab0a1 | ||
![]() |
d0c7f65256 | ||
![]() |
f99f554f19 | ||
![]() |
e069b5eed1 | ||
![]() |
3a481ebb1a | ||
![]() |
a209fadf18 | ||
![]() |
9f1bd1e085 | ||
![]() |
edc6da04f7 | ||
![]() |
2fb1dd0ec1 | ||
![]() |
3f2aac0842 | ||
![]() |
71dd822978 | ||
![]() |
d1877595a5 | ||
![]() |
6379713f57 | ||
![]() |
3b33195ff6 | ||
![]() |
c7f1f1bcd1 | ||
![]() |
c50aad8403 | ||
![]() |
fac4795f14 | ||
![]() |
062e402ef1 | ||
![]() |
29be64a858 | ||
![]() |
b3b74b8328 | ||
![]() |
37ba34cb0d | ||
![]() |
8ecdde3507 | ||
![]() |
04d34aa80c | ||
![]() |
26bb1ba146 | ||
![]() |
b7667d2cbf | ||
![]() |
0c36600a81 | ||
![]() |
feaf61a0ae | ||
![]() |
3e2844a65a | ||
![]() |
3a2d7baa25 | ||
![]() |
349cca5ff2 | ||
![]() |
5cd3ce66f6 | ||
![]() |
8cf8c41698 | ||
![]() |
ff4c01e15c | ||
![]() |
addb66f21d | ||
![]() |
a5759e36b2 | ||
![]() |
9852186ff7 | ||
![]() |
19c9486351 | ||
![]() |
8c06712ab7 | ||
![]() |
63de324224 | ||
![]() |
10d476195d | ||
![]() |
e0c4b85ef1 | ||
![]() |
3441a86613 | ||
![]() |
643b168c69 | ||
![]() |
db0e5a8a41 | ||
![]() |
64a693332b | ||
![]() |
327927baa7 | ||
![]() |
ce8fc17ef8 | ||
![]() |
62ed1d54b0 | ||
![]() |
2498f1db41 | ||
![]() |
8a50bb058d | ||
![]() |
e793675c47 | ||
![]() |
07cef18918 | ||
![]() |
a0263f25c4 | ||
![]() |
52546ab567 | ||
![]() |
c0ec7e4f09 | ||
![]() |
8b61390e19 | ||
![]() |
9ba114777e | ||
![]() |
4e1e76ccc2 | ||
![]() |
609300f40b | ||
![]() |
eac9ac4757 | ||
![]() |
708d1b81da | ||
![]() |
35baf4c779 | ||
![]() |
8d1ae71741 | ||
![]() |
9b32c9c6b4 | ||
![]() |
439f34f724 | ||
![]() |
cef3b99e16 | ||
![]() |
9dbdf611c5 | ||
![]() |
86f8d2d737 | ||
![]() |
1ded47d368 | ||
![]() |
85a27e8bb1 | ||
![]() |
878f3b8df4 | ||
![]() |
50c25a8276 | ||
![]() |
c0de3b8269 | ||
![]() |
09f4e19d4c | ||
![]() |
6e91ac2a34 | ||
![]() |
49b0c7c3d1 | ||
![]() |
be1867900e | ||
![]() |
a43f49f4af | ||
![]() |
ea0f29782d | ||
![]() |
65161ce581 | ||
![]() |
088cc69083 | ||
![]() |
be005b4c88 | ||
![]() |
aac28efd32 | ||
![]() |
0d020e0300 | ||
![]() |
eeb84f65b9 | ||
![]() |
22f5d6cacb | ||
![]() |
52a7b41096 | ||
![]() |
f9f87d1147 | ||
![]() |
0b3dff00df | ||
![]() |
d48a4ab00a | ||
![]() |
d89ac0f30d | ||
![]() |
3cb3f8d352 | ||
![]() |
f507a7b8b3 | ||
![]() |
910244f751 | ||
![]() |
a998465600 | ||
![]() |
21645c2361 | ||
![]() |
e781be885d | ||
![]() |
b15b0e25d6 | ||
![]() |
2d74873db0 | ||
![]() |
d50f2bf38c |
21
.browserslistrc
Normal file
21
.browserslistrc
Normal file
@@ -0,0 +1,21 @@
|
||||
[modern]
|
||||
# Support for dynamic import is the main litmus test for serving modern builds.
|
||||
# Although officially a ES2020 feature, browsers implemented it early, so this
|
||||
# enables all of ES2017 and some features in ES2018.
|
||||
supports es6-module-dynamic-import
|
||||
|
||||
# Exclude Safari 11-12 because of a bug in tagged template literals
|
||||
# https://bugs.webkit.org/show_bug.cgi?id=190756
|
||||
# Note: Dropping version 11 also enables several more ES2018 features
|
||||
not Safari < 13
|
||||
not iOS < 13
|
||||
|
||||
# Exclude unsupported browsers
|
||||
not dead
|
||||
|
||||
[legacy]
|
||||
# Legacy builds are transpiled to ES5 (strict mode) but also must support some features that cannot be polyfilled:
|
||||
# - web sockets to communicate with backend
|
||||
# - inline SVG used widely in buttons, widgets, etc.
|
||||
# - custom events used for most user interactions
|
||||
supports use-strict and supports websockets and supports svg-html5 and supports customevent
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2022.10.1
|
||||
uses: home-assistant/wheels@2023.04.0
|
||||
with:
|
||||
abi: cp310
|
||||
tag: musllinux_1_2
|
||||
|
@@ -84,17 +84,23 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
||||
module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
babelrc: false,
|
||||
compact: false,
|
||||
assumptions: {
|
||||
privateFieldsAsProperties: true,
|
||||
setPublicClassFields: true,
|
||||
setSpreadProperties: true,
|
||||
},
|
||||
browserslistEnv: latestBuild ? "modern" : "legacy",
|
||||
presets: [
|
||||
!latestBuild && [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: "entry",
|
||||
corejs: { version: "3.30", proposals: true },
|
||||
useBuiltIns: latestBuild ? false : "entry",
|
||||
corejs: latestBuild ? false : { version: "3.30", proposals: true },
|
||||
bugfixes: true,
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
].filter(Boolean),
|
||||
],
|
||||
plugins: [
|
||||
[
|
||||
path.resolve(
|
||||
@@ -106,22 +112,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
ignoreModuleNotFound: true,
|
||||
},
|
||||
],
|
||||
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||
!latestBuild && [
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
{ loose: true, useBuiltIns: true },
|
||||
],
|
||||
// Only support the syntax, Webpack will handle it.
|
||||
"@babel/plugin-syntax-import-meta",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-syntax-top-level-await",
|
||||
// Support various proposals
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
// Support some proposals still in TC39 process
|
||||
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
|
||||
["@babel/plugin-proposal-private-methods", { loose: true }],
|
||||
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
|
||||
["@babel/plugin-proposal-class-properties", { loose: true }],
|
||||
// Minify template literals for production
|
||||
isProdBuild && [
|
||||
"template-html-minifier",
|
||||
|
@@ -24,8 +24,7 @@ gulp.task(
|
||||
gulp.parallel(
|
||||
"gen-service-worker-app-dev",
|
||||
"gen-icons-json",
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
"gen-pages-app-dev",
|
||||
"build-translations",
|
||||
"build-locale-data"
|
||||
),
|
||||
@@ -50,10 +49,6 @@ gulp.task(
|
||||
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
|
||||
// Don't compress running tests
|
||||
...(env.isTestBuild() ? [] : ["compress-app"]),
|
||||
gulp.parallel(
|
||||
"gen-pages-prod",
|
||||
"gen-index-app-prod",
|
||||
"gen-service-worker-app-prod"
|
||||
)
|
||||
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod")
|
||||
)
|
||||
);
|
||||
|
@@ -19,7 +19,7 @@ gulp.task(
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-cast",
|
||||
"gen-index-cast-dev",
|
||||
"gen-pages-cast-dev",
|
||||
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
|
||||
)
|
||||
);
|
||||
@@ -35,6 +35,6 @@ gulp.task(
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-cast",
|
||||
env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast",
|
||||
"gen-index-cast-prod"
|
||||
"gen-pages-cast-prod"
|
||||
)
|
||||
);
|
||||
|
@@ -21,7 +21,7 @@ gulp.task(
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"gen-index-demo-dev",
|
||||
"gen-pages-demo-dev",
|
||||
"build-translations",
|
||||
"build-locale-data"
|
||||
),
|
||||
@@ -42,6 +42,6 @@ gulp.task(
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-demo",
|
||||
env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo",
|
||||
"gen-index-demo-prod"
|
||||
"gen-pages-demo-prod"
|
||||
)
|
||||
);
|
||||
|
@@ -8,344 +8,223 @@ const paths = require("../paths.cjs");
|
||||
const env = require("../env.cjs");
|
||||
const { htmlMinifierOptions, terserOptions } = require("../bundle.cjs");
|
||||
|
||||
const templatePath = (tpl) =>
|
||||
path.resolve(paths.polymer_dir, "src/html/", `${tpl}.html.template`);
|
||||
|
||||
const readFile = (pth) => fs.readFileSync(pth).toString();
|
||||
|
||||
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||
const compiled = template(readFile(pathFunc(pth)));
|
||||
const renderTemplate = (templateFile, data = {}) => {
|
||||
const compiled = template(
|
||||
fs.readFileSync(templateFile, { encoding: "utf-8" })
|
||||
);
|
||||
return compiled({
|
||||
...data,
|
||||
useRollup: env.useRollup(),
|
||||
useWDS: env.useWDS(),
|
||||
renderTemplate,
|
||||
// Resolve any child/nested templates relative to the parent and pass the same data
|
||||
renderTemplate: (childTemplate) =>
|
||||
renderTemplate(
|
||||
path.resolve(path.dirname(templateFile), childTemplate),
|
||||
data
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const renderDemoTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, (tpl) =>
|
||||
path.resolve(paths.demo_dir, "src/html/", `${tpl}.html.template`)
|
||||
);
|
||||
const WRAP_TAGS = { ".js": "script", ".css": "style" };
|
||||
|
||||
const renderCastTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, (tpl) =>
|
||||
path.resolve(paths.cast_dir, "src/html/", `${tpl}.html.template`)
|
||||
);
|
||||
|
||||
const renderGalleryTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, (tpl) =>
|
||||
path.resolve(paths.gallery_dir, "src/html/", `${tpl}.html.template`)
|
||||
);
|
||||
|
||||
const minifyHtml = (content) =>
|
||||
minify(content, {
|
||||
const minifyHtml = (content, ext) => {
|
||||
const wrapTag = WRAP_TAGS[ext] || "";
|
||||
const begTag = wrapTag && `<${wrapTag}>`;
|
||||
const endTag = wrapTag && `</${wrapTag}>`;
|
||||
return minify(begTag + content + endTag, {
|
||||
...htmlMinifierOptions,
|
||||
conservativeCollapse: false,
|
||||
minifyJS: terserOptions({
|
||||
latestBuild: false, // Shared scripts should be ES5
|
||||
isTestBuild: true, // Don't need source maps
|
||||
}),
|
||||
});
|
||||
}).then((wrapped) =>
|
||||
wrapTag ? wrapped.slice(begTag.length, -endTag.length) : wrapped
|
||||
);
|
||||
};
|
||||
|
||||
const PAGES = ["onboarding", "authorize"];
|
||||
// Function to generate a dev task for each project's configuration
|
||||
// Note Currently WDS paths are hard-coded to only work for app
|
||||
const genPagesDevTask =
|
||||
(
|
||||
pageEntries,
|
||||
inputRoot,
|
||||
outputRoot,
|
||||
useWDS = false,
|
||||
inputSub = "src/html",
|
||||
publicRoot = ""
|
||||
) =>
|
||||
async () => {
|
||||
for (const [page, entries] of Object.entries(pageEntries)) {
|
||||
const content = renderTemplate(
|
||||
path.resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
latestEntryJS: entries.map((entry) =>
|
||||
useWDS
|
||||
? `http://localhost:8000/src/entrypoints/${entry}.ts`
|
||||
: `${publicRoot}/frontend_latest/${entry}.js`
|
||||
),
|
||||
es5EntryJS: entries.map(
|
||||
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
|
||||
),
|
||||
latestCustomPanelJS: useWDS
|
||||
? "http://localhost:8000/src/entrypoints/custom-panel.ts"
|
||||
: `${publicRoot}/frontend_latest/custom-panel.js`,
|
||||
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
|
||||
}
|
||||
);
|
||||
fs.outputFileSync(path.resolve(outputRoot, page), content);
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task("gen-pages-dev", (done) => {
|
||||
for (const page of PAGES) {
|
||||
const content = renderTemplate(page, {
|
||||
latestPageJS: `/frontend_latest/${page}.js`,
|
||||
|
||||
es5PageJS: `/frontend_es5/${page}.js`,
|
||||
});
|
||||
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.app_output_root, `${page}.html`),
|
||||
content
|
||||
);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-pages-prod", async () => {
|
||||
const latestManifest = require(path.resolve(
|
||||
paths.app_output_latest,
|
||||
"manifest.json"
|
||||
));
|
||||
const es5Manifest = require(path.resolve(
|
||||
paths.app_output_es5,
|
||||
"manifest.json"
|
||||
));
|
||||
|
||||
const minifiedHTML = [];
|
||||
for (const page of PAGES) {
|
||||
const content = renderTemplate(page, {
|
||||
latestPageJS: latestManifest[`${page}.js`],
|
||||
es5PageJS: es5Manifest[`${page}.js`],
|
||||
});
|
||||
|
||||
minifiedHTML.push(
|
||||
minifyHtml(content).then((minified) =>
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.app_output_root, `${page}.html`),
|
||||
minified
|
||||
// Same as previous but for production builds
|
||||
// (includes minification and hashed file names from manifest)
|
||||
const genPagesProdTask =
|
||||
(
|
||||
pageEntries,
|
||||
inputRoot,
|
||||
outputRoot,
|
||||
outputLatest,
|
||||
outputES5,
|
||||
inputSub = "src/html"
|
||||
) =>
|
||||
async () => {
|
||||
const latestManifest = require(path.resolve(outputLatest, "manifest.json"));
|
||||
const es5Manifest = outputES5
|
||||
? require(path.resolve(outputES5, "manifest.json"))
|
||||
: {};
|
||||
const minifiedHTML = [];
|
||||
for (const [page, entries] of Object.entries(pageEntries)) {
|
||||
const content = renderTemplate(
|
||||
path.resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
|
||||
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
|
||||
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
||||
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
||||
}
|
||||
);
|
||||
minifiedHTML.push(
|
||||
minifyHtml(content, path.extname(page)).then((minified) =>
|
||||
fs.outputFileSync(path.resolve(outputRoot, page), minified)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
await Promise.all(minifiedHTML);
|
||||
});
|
||||
);
|
||||
}
|
||||
await Promise.all(minifiedHTML);
|
||||
};
|
||||
|
||||
gulp.task("gen-index-app-dev", (done) => {
|
||||
let latestAppJS;
|
||||
let latestCoreJS;
|
||||
let latestCustomPanelJS;
|
||||
// Map HTML pages to their required entrypoints
|
||||
const APP_PAGE_ENTRIES = {
|
||||
"authorize.html": ["authorize"],
|
||||
"onboarding.html": ["onboarding"],
|
||||
"index.html": ["core", "app"],
|
||||
};
|
||||
|
||||
if (env.useWDS()) {
|
||||
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts";
|
||||
latestCoreJS = "http://localhost:8000/src/entrypoints/core.ts";
|
||||
latestCustomPanelJS =
|
||||
"http://localhost:8000/src/entrypoints/custom-panel.ts";
|
||||
} else {
|
||||
latestAppJS = "/frontend_latest/app.js";
|
||||
latestCoreJS = "/frontend_latest/core.js";
|
||||
latestCustomPanelJS = "/frontend_latest/custom-panel.js";
|
||||
}
|
||||
gulp.task(
|
||||
"gen-pages-app-dev",
|
||||
genPagesDevTask(
|
||||
APP_PAGE_ENTRIES,
|
||||
paths.polymer_dir,
|
||||
paths.app_output_root,
|
||||
env.useWDS()
|
||||
)
|
||||
);
|
||||
|
||||
const content = renderTemplate("index", {
|
||||
latestAppJS,
|
||||
latestCoreJS,
|
||||
latestCustomPanelJS,
|
||||
|
||||
es5AppJS: "/frontend_es5/app.js",
|
||||
es5CoreJS: "/frontend_es5/core.js",
|
||||
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
|
||||
}).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
|
||||
fs.outputFileSync(path.resolve(paths.app_output_root, "index.html"), content);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-app-prod", async () => {
|
||||
const latestManifest = require(path.resolve(
|
||||
gulp.task(
|
||||
"gen-pages-app-prod",
|
||||
genPagesProdTask(
|
||||
APP_PAGE_ENTRIES,
|
||||
paths.polymer_dir,
|
||||
paths.app_output_root,
|
||||
paths.app_output_latest,
|
||||
"manifest.json"
|
||||
));
|
||||
const es5Manifest = require(path.resolve(
|
||||
paths.app_output_es5,
|
||||
"manifest.json"
|
||||
));
|
||||
const content = renderTemplate("index", {
|
||||
latestAppJS: latestManifest["app.js"],
|
||||
latestCoreJS: latestManifest["core.js"],
|
||||
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
||||
paths.app_output_es5
|
||||
)
|
||||
);
|
||||
|
||||
es5AppJS: es5Manifest["app.js"],
|
||||
es5CoreJS: es5Manifest["core.js"],
|
||||
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
||||
});
|
||||
const minified = (await minifyHtml(content)).replace(
|
||||
/#THEMEC/g,
|
||||
"{{ theme_color }}"
|
||||
);
|
||||
const CAST_PAGE_ENTRIES = {
|
||||
"faq.html": ["launcher"],
|
||||
"index.html": ["launcher"],
|
||||
"media.html": ["media"],
|
||||
"receiver.html": ["receiver"],
|
||||
};
|
||||
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.app_output_root, "index.html"),
|
||||
minified
|
||||
);
|
||||
});
|
||||
gulp.task(
|
||||
"gen-pages-cast-dev",
|
||||
genPagesDevTask(CAST_PAGE_ENTRIES, paths.cast_dir, paths.cast_output_root)
|
||||
);
|
||||
|
||||
gulp.task("gen-index-cast-dev", (done) => {
|
||||
const contentReceiver = renderCastTemplate("receiver", {
|
||||
latestReceiverJS: "/frontend_latest/receiver.js",
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "receiver.html"),
|
||||
contentReceiver
|
||||
);
|
||||
|
||||
const contentMedia = renderCastTemplate("media", {
|
||||
latestMediaJS: "/frontend_latest/media.js",
|
||||
es5MediaJS: "/frontend_es5/media.js",
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "media.html"),
|
||||
contentMedia
|
||||
);
|
||||
|
||||
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||
latestLauncherJS: "/frontend_latest/launcher.js",
|
||||
es5LauncherJS: "/frontend_es5/launcher.js",
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "faq.html"),
|
||||
contentFAQ
|
||||
);
|
||||
|
||||
const contentLauncher = renderCastTemplate("launcher", {
|
||||
latestLauncherJS: "/frontend_latest/launcher.js",
|
||||
es5LauncherJS: "/frontend_es5/launcher.js",
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "index.html"),
|
||||
contentLauncher
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-cast-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(
|
||||
gulp.task(
|
||||
"gen-pages-cast-prod",
|
||||
genPagesProdTask(
|
||||
CAST_PAGE_ENTRIES,
|
||||
paths.cast_dir,
|
||||
paths.cast_output_root,
|
||||
paths.cast_output_latest,
|
||||
"manifest.json"
|
||||
));
|
||||
const es5Manifest = require(path.resolve(
|
||||
paths.cast_output_es5,
|
||||
"manifest.json"
|
||||
));
|
||||
paths.cast_output_es5
|
||||
)
|
||||
);
|
||||
|
||||
const contentReceiver = renderCastTemplate("receiver", {
|
||||
latestReceiverJS: latestManifest["receiver.js"],
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "receiver.html"),
|
||||
contentReceiver
|
||||
);
|
||||
const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
|
||||
|
||||
const contentMedia = renderCastTemplate("media", {
|
||||
latestMediaJS: latestManifest["media.js"],
|
||||
es5MediaJS: es5Manifest["media.js"],
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "media.html"),
|
||||
contentMedia
|
||||
);
|
||||
gulp.task(
|
||||
"gen-pages-demo-dev",
|
||||
genPagesDevTask(DEMO_PAGE_ENTRIES, paths.demo_dir, paths.demo_output_root)
|
||||
);
|
||||
|
||||
const contentFAQ = renderCastTemplate("launcher-faq", {
|
||||
latestLauncherJS: latestManifest["launcher.js"],
|
||||
es5LauncherJS: es5Manifest["launcher.js"],
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "faq.html"),
|
||||
contentFAQ
|
||||
);
|
||||
|
||||
const contentLauncher = renderCastTemplate("launcher", {
|
||||
latestLauncherJS: latestManifest["launcher.js"],
|
||||
es5LauncherJS: es5Manifest["launcher.js"],
|
||||
});
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.cast_output_root, "index.html"),
|
||||
contentLauncher
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-demo-dev", (done) => {
|
||||
const content = renderDemoTemplate("index", {
|
||||
latestDemoJS: "/frontend_latest/main.js",
|
||||
|
||||
es5DemoJS: "/frontend_es5/main.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.demo_output_root, "index.html"),
|
||||
content
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-demo-prod", async () => {
|
||||
const latestManifest = require(path.resolve(
|
||||
gulp.task(
|
||||
"gen-pages-demo-prod",
|
||||
genPagesProdTask(
|
||||
DEMO_PAGE_ENTRIES,
|
||||
paths.demo_dir,
|
||||
paths.demo_output_root,
|
||||
paths.demo_output_latest,
|
||||
"manifest.json"
|
||||
));
|
||||
const es5Manifest = require(path.resolve(
|
||||
paths.demo_output_es5,
|
||||
"manifest.json"
|
||||
));
|
||||
const content = renderDemoTemplate("index", {
|
||||
latestDemoJS: latestManifest["main.js"],
|
||||
paths.demo_output_es5
|
||||
)
|
||||
);
|
||||
|
||||
es5DemoJS: es5Manifest["main.js"],
|
||||
});
|
||||
const minified = await minifyHtml(content);
|
||||
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
||||
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.demo_output_root, "index.html"),
|
||||
minified
|
||||
);
|
||||
});
|
||||
gulp.task(
|
||||
"gen-pages-gallery-dev",
|
||||
genPagesDevTask(
|
||||
GALLERY_PAGE_ENTRIES,
|
||||
paths.gallery_dir,
|
||||
paths.gallery_output_root
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("gen-index-gallery-dev", (done) => {
|
||||
const content = renderGalleryTemplate("index", {
|
||||
latestGalleryJS: "./frontend_latest/entrypoint.js",
|
||||
});
|
||||
gulp.task(
|
||||
"gen-pages-gallery-prod",
|
||||
genPagesProdTask(
|
||||
GALLERY_PAGE_ENTRIES,
|
||||
paths.gallery_dir,
|
||||
paths.gallery_output_root,
|
||||
paths.gallery_output_latest
|
||||
)
|
||||
);
|
||||
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.gallery_output_root, "index.html"),
|
||||
content
|
||||
);
|
||||
done();
|
||||
});
|
||||
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
|
||||
|
||||
gulp.task("gen-index-gallery-prod", async () => {
|
||||
const latestManifest = require(path.resolve(
|
||||
paths.gallery_output_latest,
|
||||
"manifest.json"
|
||||
));
|
||||
const content = renderGalleryTemplate("index", {
|
||||
latestGalleryJS: latestManifest["entrypoint.js"],
|
||||
});
|
||||
const minified = await minifyHtml(content);
|
||||
gulp.task(
|
||||
"gen-pages-hassio-dev",
|
||||
genPagesDevTask(
|
||||
HASSIO_PAGE_ENTRIES,
|
||||
paths.hassio_dir,
|
||||
paths.hassio_output_root,
|
||||
undefined,
|
||||
"src",
|
||||
paths.hassio_publicPath
|
||||
)
|
||||
);
|
||||
|
||||
fs.outputFileSync(
|
||||
path.resolve(paths.gallery_output_root, "index.html"),
|
||||
minified
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("gen-index-hassio-dev", async () => {
|
||||
writeHassioEntrypoint(
|
||||
`${paths.hassio_publicPath}/frontend_latest/entrypoint.js`,
|
||||
`${paths.hassio_publicPath}/frontend_es5/entrypoint.js`
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("gen-index-hassio-prod", async () => {
|
||||
const latestManifest = require(path.resolve(
|
||||
gulp.task(
|
||||
"gen-pages-hassio-prod",
|
||||
genPagesProdTask(
|
||||
HASSIO_PAGE_ENTRIES,
|
||||
paths.hassio_dir,
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
"manifest.json"
|
||||
));
|
||||
const es5Manifest = require(path.resolve(
|
||||
paths.hassio_output_es5,
|
||||
"manifest.json"
|
||||
));
|
||||
writeHassioEntrypoint(
|
||||
latestManifest["entrypoint.js"],
|
||||
es5Manifest["entrypoint.js"]
|
||||
);
|
||||
});
|
||||
|
||||
function writeHassioEntrypoint(latestEntrypoint, es5Entrypoint) {
|
||||
fs.mkdirSync(paths.hassio_output_root, { recursive: true });
|
||||
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
|
||||
fs.writeFileSync(
|
||||
path.resolve(paths.hassio_output_root, "entrypoint.js"),
|
||||
`
|
||||
function loadES5() {
|
||||
var el = document.createElement('script');
|
||||
el.src = '${es5Entrypoint}';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
if (/.*Version\\/(?:11|12)(?:\\.\\d+)*.*Safari\\//.test(navigator.userAgent)) {
|
||||
loadES5();
|
||||
} else {
|
||||
try {
|
||||
new Function("import('${latestEntrypoint}')")();
|
||||
} catch (err) {
|
||||
loadES5();
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
}
|
||||
"src"
|
||||
)
|
||||
);
|
||||
|
@@ -159,7 +159,7 @@ gulp.task(
|
||||
"gather-gallery-pages"
|
||||
),
|
||||
"copy-static-gallery",
|
||||
"gen-index-gallery-dev",
|
||||
"gen-pages-gallery-dev",
|
||||
gulp.parallel(
|
||||
env.useRollup()
|
||||
? "rollup-dev-server-gallery"
|
||||
@@ -193,6 +193,6 @@ gulp.task(
|
||||
),
|
||||
"copy-static-gallery",
|
||||
env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery",
|
||||
"gen-index-gallery-prod"
|
||||
"gen-pages-gallery-prod"
|
||||
)
|
||||
);
|
||||
|
@@ -17,7 +17,7 @@ gulp.task(
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"gen-index-hassio-dev",
|
||||
"gen-pages-hassio-dev",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
@@ -39,7 +39,7 @@ gulp.task(
|
||||
"build-locale-data",
|
||||
"copy-locale-data-supervisor",
|
||||
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
|
||||
"gen-index-hassio-prod",
|
||||
"gen-pages-hassio-prod",
|
||||
...// Don't compress running tests
|
||||
(env.isTestBuild() ? [] : ["compress-hassio"])
|
||||
)
|
||||
|
@@ -19,6 +19,7 @@ const modules = {
|
||||
"intl-relativetimeformat": "RelativeTimeFormat",
|
||||
"intl-datetimeformat": "DateTimeFormat",
|
||||
"intl-numberformat": "NumberFormat",
|
||||
"intl-displaynames": "DisplayNames",
|
||||
};
|
||||
|
||||
gulp.task("create-locale-data", (done) => {
|
||||
|
59
build-scripts/list-plugins-and-polyfills.js
Executable file
59
build-scripts/list-plugins-and-polyfills.js
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
// Script to print Babel plugins and Core JS polyfills that will be used by browserslist environments
|
||||
|
||||
import { version as babelVersion } from "@babel/core";
|
||||
import presetEnv from "@babel/preset-env";
|
||||
import compilationTargets from "@babel/helper-compilation-targets";
|
||||
import coreJSCompat from "core-js-compat";
|
||||
import { logPlugin } from "@babel/preset-env/lib/debug.js";
|
||||
import { babelOptions } from "./bundle.cjs";
|
||||
|
||||
const detailsOpen = (heading) =>
|
||||
`<details>\n<summary><h4>${heading}</h4></summary>\n`;
|
||||
const detailsClose = "</details>\n";
|
||||
|
||||
const dummyAPI = {
|
||||
version: babelVersion,
|
||||
assertVersion: () => {},
|
||||
caller: (callback) =>
|
||||
callback({
|
||||
name: "Dummy Bundler",
|
||||
supportsStaticESM: true,
|
||||
supportsDynamicImport: true,
|
||||
supportsTopLevelAwait: true,
|
||||
supportsExportNamespaceFrom: true,
|
||||
}),
|
||||
targets: () => ({}),
|
||||
};
|
||||
|
||||
for (const buildType of ["Modern", "Legacy"]) {
|
||||
const browserslistEnv = buildType.toLowerCase();
|
||||
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
|
||||
const presetEnvOpts = babelOpts.presets[0][1];
|
||||
|
||||
// Invoking preset-env in debug mode will log the included plugins
|
||||
console.log(detailsOpen(`${buildType} Build Babel Plugins`));
|
||||
presetEnv.default(dummyAPI, {
|
||||
...presetEnvOpts,
|
||||
browserslistEnv,
|
||||
debug: true,
|
||||
});
|
||||
console.log(detailsClose);
|
||||
|
||||
// Manually log the Core-JS polyfills using the same technique
|
||||
if (presetEnvOpts.useBuiltIns) {
|
||||
console.log(detailsOpen(`${buildType} Build Core-JS Polyfills`));
|
||||
const targets = compilationTargets.default(babelOpts?.targets, {
|
||||
browserslistEnv,
|
||||
});
|
||||
const polyfillList = coreJSCompat({ targets }).list;
|
||||
console.log(
|
||||
"The following %i polyfills may be injected by Babel:\n",
|
||||
polyfillList.length
|
||||
);
|
||||
for (const polyfill of polyfillList) {
|
||||
logPlugin(polyfill, targets, coreJSCompat.data);
|
||||
}
|
||||
console.log(detailsClose);
|
||||
}
|
||||
}
|
24
cast/src/html/_social_meta.html.template
Normal file
24
cast/src/html/_social_meta.html.template
Normal file
@@ -0,0 +1,24 @@
|
||||
<meta property="fb:app_id" content="338291289691179" />
|
||||
<meta property="og:title" content="Home Assistant Cast" />
|
||||
<meta property="og:site_name" content="Home Assistant Cast" />
|
||||
<meta property="og:url" content="https://cast.home-assistant.io/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://cast.home-assistant.io/images/google-nest-hub.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@home_assistant" />
|
||||
<meta name="twitter:title" content="Home Assistant Cast" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://cast.home-assistant.io/images/google-nest-hub.png"
|
||||
/>
|
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<title>Home Assistant Cast - FAQ</title>
|
||||
<link rel="icon" href="/images/ha-cast-icon.png" type="image/png" />
|
||||
<%= renderTemplate('_style_base') %>
|
||||
<%= renderTemplate("../../../src/html/_style_base.html.template") %>
|
||||
<style>
|
||||
body {
|
||||
background-color: #e5e5e5;
|
||||
@@ -35,25 +35,14 @@
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<script>
|
||||
import("<%= latestLauncherJS %>");
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
System.import("<%= es5LauncherJS %>");
|
||||
};
|
||||
<% } else { %>
|
||||
_ls("<%= es5LauncherJS %>");
|
||||
<% } %>
|
||||
}
|
||||
</script>
|
||||
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
<hc-layout subtitle="FAQ">
|
||||
<style>
|
||||
a {
|
35
cast/src/html/index.html.template
Normal file
35
cast/src/html/index.html.template
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Home Assistant Cast</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" href="/images/ha-cast-icon.png" type="image/png" />
|
||||
<%= renderTemplate("../../../src/html/_style_base.html.template") %>
|
||||
<style>
|
||||
body {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
</style>
|
||||
<%= renderTemplate("_social_meta.html.template") %>
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<hc-connect></hc-connect>
|
||||
<script>
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-57927901-9', 'auto');
|
||||
ga('send', 'pageview', location.pathname.includes("auth_callback") === -1 ? location.pathname : "/");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,57 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Home Assistant Cast</title>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" href="/images/ha-cast-icon.png" type="image/png" />
|
||||
<%= renderTemplate('_style_base') %>
|
||||
<style>
|
||||
body {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
</style>
|
||||
<meta property="fb:app_id" content="338291289691179">
|
||||
<meta property="og:title" content="Home Assistant Cast">
|
||||
<meta property="og:site_name" content="Home Assistant Cast">
|
||||
<meta property="og:url" content="https://cast.home-assistant.io/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen.">
|
||||
<meta property="og:image" content="https://cast.home-assistant.io/images/google-nest-hub.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@home_assistant">
|
||||
<meta name="twitter:title" content="Home Assistant Cast">
|
||||
<meta name="twitter:description" content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen.">
|
||||
<meta name="twitter:image" content="https://cast.home-assistant.io/images/google-nest-hub.png">
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<hc-connect></hc-connect>
|
||||
|
||||
<script>
|
||||
import("<%= latestLauncherJS %>");
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
System.import("<%= es5LauncherJS %>");
|
||||
};
|
||||
<% } else { %>
|
||||
_ls("<%= es5LauncherJS %>");
|
||||
<% } %>
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-57927901-9', 'auto');
|
||||
ga('send', 'pageview', location.pathname.includes("auth_callback") === -1 ? location.pathname : "/");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -22,25 +22,14 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<cast-media-player></cast-media-player>
|
||||
|
||||
<script>
|
||||
import("<%= latestMediaJS %>");
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
System.import("<%= es5MediaJS %>");
|
||||
};
|
||||
<% } else { %>
|
||||
_ls("<%= es5MediaJS %>");
|
||||
<% } %>
|
||||
}
|
||||
</script>
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,8 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
|
||||
<script type="module" src="<%= latestReceiverJS %>"></script>
|
||||
<%= renderTemplate('_style_base') %>
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
<script type="module" src="<%= entry %>"></script>
|
||||
<% } %>
|
||||
<%= renderTemplate("../../../src/html/_style_base.html.template") %>
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
|
26
demo/src/html/_social_meta.html.template
Normal file
26
demo/src/html/_social_meta.html.template
Normal file
@@ -0,0 +1,26 @@
|
||||
<meta property="fb:app_id" content="338291289691179" />
|
||||
<meta property="og:title" content="Home Assistant Demo" />
|
||||
<meta property="og:site_name" content="Home Assistant" />
|
||||
<meta property="og:url" content="https://demo.home-assistant.io/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Open source home automation that puts local control and privacy first."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://www.home-assistant.io/images/default-social.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@home_assistant" />
|
||||
|
||||
<meta name="twitter:title" content="Home Assistant" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Open source home automation that puts local control and privacy first."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://www.home-assistant.io/images/default-social.png"
|
||||
/>
|
@@ -1,9 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="icon" href="/static/icons/favicon.ico" />
|
||||
<title>Home Assistant Demo</title>
|
||||
<%= renderTemplate("../../../src/html/_header.html.template") %>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
@@ -35,33 +34,7 @@
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#03a9f4" />
|
||||
<meta property="fb:app_id" content="338291289691179" />
|
||||
<meta property="og:title" content="Home Assistant Demo" />
|
||||
<meta property="og:site_name" content="Home Assistant" />
|
||||
<meta property="og:url" content="https://demo.home-assistant.io/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Open source home automation that puts local control and privacy first."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://www.home-assistant.io/images/default-social.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@home_assistant" />
|
||||
|
||||
<meta name="twitter:title" content="Home Assistant" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Open source home automation that puts local control and privacy first."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://www.home-assistant.io/images/default-social.png"
|
||||
/>
|
||||
<title>Home Assistant Demo</title>
|
||||
<%= renderTemplate("_social_meta.html.template") %>
|
||||
<style>
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
@@ -107,29 +80,22 @@
|
||||
</svg>
|
||||
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
|
||||
</div>
|
||||
|
||||
<ha-demo></ha-demo>
|
||||
|
||||
<%= renderTemplate('_js_base') %>
|
||||
<%= renderTemplate('_preload_roboto') %>
|
||||
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
|
||||
<script>
|
||||
import("<%= latestDemoJS %>");
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
System.import("<%= es5DemoJS %>");
|
||||
};
|
||||
<% } else { %>
|
||||
_ls("<%= es5DemoJS %>");
|
||||
if (!window.globalThis) {
|
||||
window.globalThis = window;
|
||||
}
|
||||
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
|
||||
if (!isS11_12) {
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
<script>
|
||||
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
|
||||
(function (d, t) {
|
||||
|
@@ -8,8 +8,9 @@
|
||||
/>
|
||||
<meta name="theme-color" content="#2157BC" />
|
||||
<title>Home Assistant Design</title>
|
||||
|
||||
<script type="module" src="<%= latestGalleryJS %>"></script>
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
<script type="module" src="<%= entry %>"></script>
|
||||
<% } %>
|
||||
<style>
|
||||
body {
|
||||
font-family: Roboto, Noto, sans-serif;
|
||||
|
@@ -336,7 +336,7 @@ const SCHEMAS: {
|
||||
["and", "another_one"],
|
||||
["option", "1000"],
|
||||
],
|
||||
name: "select many otions",
|
||||
name: "select many options",
|
||||
default: "default",
|
||||
},
|
||||
],
|
||||
@@ -364,7 +364,7 @@ const SCHEMAS: {
|
||||
and: "another_one",
|
||||
option: "1000",
|
||||
},
|
||||
name: "multi many otions",
|
||||
name: "multi many options",
|
||||
default: ["default"],
|
||||
},
|
||||
],
|
||||
|
@@ -9,7 +9,6 @@ import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addons";
|
||||
import "./hassio-update";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
|
||||
@customElement("hassio-dashboard")
|
||||
@@ -22,6 +21,12 @@ class HassioDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
firstUpdated() {
|
||||
if (!atLeastVersion(this.hass.config.version, 2022, 5)) {
|
||||
import("./hassio-update");
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
|
||||
return html`<hass-subpage
|
||||
@@ -44,7 +49,7 @@ class HassioDashboard extends LitElement {
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStorePlus}
|
||||
></ha-svg-icon> </ha-fab
|
||||
></ha-svg-icon></ha-fab
|
||||
></a>
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
22
hassio/src/entrypoint.js.template
Normal file
22
hassio/src/entrypoint.js.template
Normal file
@@ -0,0 +1,22 @@
|
||||
(function () {
|
||||
function loadES5(src) {
|
||||
var el = document.createElement("script");
|
||||
el.src = src;
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
if (/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent)) {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
} else {
|
||||
try {
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
new Function("import('<%= entry %>')")();
|
||||
<% } %>
|
||||
} catch (err) {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
})();
|
@@ -2,6 +2,7 @@
|
||||
import "../../src/resources/compatibility";
|
||||
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
|
||||
import "../../src/resources/roboto";
|
||||
import "../../src/resources/ha-style";
|
||||
import "../../src/resources/safari-14-attachshadow-patch";
|
||||
import "./hassio-main";
|
||||
|
||||
|
@@ -9,7 +9,6 @@ import { navigate } from "../../src/common/navigate";
|
||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import "../../src/layouts/hass-loading-screen";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import "./hassio-router";
|
||||
import { SupervisorBaseElement } from "./supervisor-base-element";
|
||||
|
@@ -5,12 +5,8 @@ import {
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import "./addon-store/hassio-addon-store";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./dashboard/hassio-dashboard";
|
||||
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
|
||||
import "./backups/hassio-backups";
|
||||
import "./system/hassio-system";
|
||||
|
||||
@customElement("hassio-panel-router")
|
||||
class HassioPanelRouter extends HassRouterPage {
|
||||
@@ -31,12 +27,15 @@ class HassioPanelRouter extends HassRouterPage {
|
||||
},
|
||||
store: {
|
||||
tag: "hassio-addon-store",
|
||||
load: () => import("./addon-store/hassio-addon-store"),
|
||||
},
|
||||
backups: {
|
||||
tag: "hassio-backups",
|
||||
load: () => import("./backups/hassio-backups"),
|
||||
},
|
||||
system: {
|
||||
tag: "hassio-system",
|
||||
load: () => import("./system/hassio-system"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
Supervisor,
|
||||
supervisorCollection,
|
||||
} from "../../src/data/supervisor/supervisor";
|
||||
import "../../src/layouts/hass-loading-screen";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import "./hassio-panel-router";
|
||||
|
||||
|
@@ -5,7 +5,6 @@ import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import "../../src/resources/ha-style";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-panel";
|
||||
|
@@ -42,9 +42,6 @@ import { updateCore } from "../../../src/data/supervisor/core";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||
|
||||
|
41
package.json
41
package.json
@@ -27,19 +27,20 @@
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@codemirror/autocomplete": "6.5.1",
|
||||
"@codemirror/commands": "6.2.2",
|
||||
"@codemirror/commands": "6.2.3",
|
||||
"@codemirror/language": "6.6.0",
|
||||
"@codemirror/legacy-modes": "6.3.2",
|
||||
"@codemirror/search": "6.3.0",
|
||||
"@codemirror/state": "6.2.0",
|
||||
"@codemirror/view": "6.9.4",
|
||||
"@codemirror/view": "6.9.5",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.5.1",
|
||||
"@formatjs/intl-datetimeformat": "6.7.0",
|
||||
"@formatjs/intl-displaynames": "6.3.1",
|
||||
"@formatjs/intl-getcanonicallocales": "2.1.0",
|
||||
"@formatjs/intl-locale": "3.1.1",
|
||||
"@formatjs/intl-numberformat": "8.3.5",
|
||||
"@formatjs/intl-pluralrules": "5.1.10",
|
||||
"@formatjs/intl-relativetimeformat": "11.1.10",
|
||||
"@formatjs/intl-locale": "3.2.1",
|
||||
"@formatjs/intl-numberformat": "8.4.1",
|
||||
"@formatjs/intl-pluralrules": "5.2.1",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.1",
|
||||
"@fullcalendar/core": "6.1.5",
|
||||
"@fullcalendar/daygrid": "6.1.5",
|
||||
"@fullcalendar/interaction": "6.1.5",
|
||||
@@ -90,8 +91,8 @@
|
||||
"@polymer/paper-toast": "3.0.1",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "23.3.10",
|
||||
"@vaadin/vaadin-themable-mixin": "23.3.10",
|
||||
"@vaadin/combo-box": "23.3.11",
|
||||
"@vaadin/vaadin-themable-mixin": "23.3.11",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -108,11 +109,11 @@
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"fuse.js": "6.6.2",
|
||||
"google-timezones-json": "1.0.2",
|
||||
"google-timezones-json": "1.1.0",
|
||||
"hls.js": "1.3.5",
|
||||
"home-assistant-js-websocket": "8.0.1",
|
||||
"idb-keyval": "6.2.0",
|
||||
"intl-messageformat": "10.3.3",
|
||||
"intl-messageformat": "10.3.4",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.3",
|
||||
"leaflet-draw": "1.0.4",
|
||||
@@ -149,15 +150,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.4",
|
||||
"@babel/plugin-external-helpers": "7.18.6",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "7.21.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.21.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "7.10.4",
|
||||
"@babel/plugin-syntax-top-level-await": "7.14.5",
|
||||
"@babel/preset-env": "7.21.4",
|
||||
"@babel/preset-typescript": "7.21.4",
|
||||
"@koa/cors": "4.0.0",
|
||||
@@ -185,8 +178,8 @@
|
||||
"@types/sortablejs": "1.15.1",
|
||||
"@types/tar": "6.1.4",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||
"@typescript-eslint/parser": "5.58.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.0",
|
||||
"@typescript-eslint/parser": "5.59.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.2",
|
||||
@@ -200,14 +193,14 @@
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-disable": "2.0.3",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-lit": "1.8.2",
|
||||
"eslint-plugin-lit": "1.8.3",
|
||||
"eslint-plugin-lit-a11y": "2.4.1",
|
||||
"eslint-plugin-unused-imports": "2.0.0",
|
||||
"eslint-plugin-wc": "1.4.0",
|
||||
"esprima": "4.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "10.1.0",
|
||||
"glob": "10.2.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-flatmap": "1.0.2",
|
||||
"gulp-json-transform": "0.4.8",
|
||||
@@ -234,7 +227,7 @@
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.9.0",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "15.0.3",
|
||||
"sinon": "15.0.4",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.1",
|
||||
"tar": "6.1.13",
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20230411.0"
|
||||
version = "20230503.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -50,6 +50,7 @@ import {
|
||||
mdiRobotVacuum,
|
||||
mdiScriptText,
|
||||
mdiSineWave,
|
||||
mdiSpeakerMessage,
|
||||
mdiSpeedometer,
|
||||
mdiSunWireless,
|
||||
mdiThermometer,
|
||||
@@ -75,8 +76,8 @@ export const DEFAULT_DOMAIN_ICON = mdiBookmark;
|
||||
|
||||
/** Icons for each domain */
|
||||
export const FIXED_DOMAIN_ICONS = {
|
||||
alert: mdiAlert,
|
||||
air_quality: mdiAirFilter,
|
||||
alert: mdiAlert,
|
||||
calendar: mdiCalendar,
|
||||
climate: mdiThermostat,
|
||||
configurator: mdiCog,
|
||||
@@ -106,10 +107,12 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
script: mdiScriptText,
|
||||
select: mdiFormatListBulleted,
|
||||
sensor: mdiEye,
|
||||
siren: mdiBullhorn,
|
||||
simple_alarm: mdiBell,
|
||||
siren: mdiBullhorn,
|
||||
stt: mdiMicrophoneMessage,
|
||||
text: mdiFormTextbox,
|
||||
timer: mdiTimerOutline,
|
||||
tts: mdiSpeakerMessage,
|
||||
updater: mdiCloudUpload,
|
||||
vacuum: mdiRobotVacuum,
|
||||
zone: mdiMapMarkerRadius,
|
||||
|
@@ -1,7 +0,0 @@
|
||||
export const SpeechRecognition =
|
||||
window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
export const SpeechGrammarList =
|
||||
window.SpeechGrammarList || window.webkitSpeechGrammarList;
|
||||
export const SpeechRecognitionEvent =
|
||||
// @ts-expect-error
|
||||
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
|
@@ -193,11 +193,9 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
);
|
||||
}
|
||||
|
||||
// state of button is a timestamp
|
||||
// state is a timestamp
|
||||
if (
|
||||
domain === "button" ||
|
||||
domain === "input_button" ||
|
||||
domain === "scene" ||
|
||||
["button", "input_button", "scene", "stt", "tts"].includes(domain) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
|
22
src/common/language/format_language.ts
Normal file
22
src/common/language/format_language.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
|
||||
export const formatLanguageCode = (
|
||||
languageCode: string,
|
||||
locale: FrontendLocaleData
|
||||
) => {
|
||||
try {
|
||||
return formatLanguageCodeMem(locale)?.of(languageCode) ?? languageCode;
|
||||
} catch {
|
||||
return languageCode;
|
||||
}
|
||||
};
|
||||
|
||||
const formatLanguageCodeMem = memoizeOne((locale: FrontendLocaleData) =>
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(locale.language, {
|
||||
type: "language",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined
|
||||
);
|
@@ -2,6 +2,7 @@ import { shouldPolyfill as shouldPolyfillLocale } from "@formatjs/intl-locale/li
|
||||
import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-pluralrules/lib/should-polyfill";
|
||||
import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill";
|
||||
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
|
||||
import { shouldPolyfill as shouldPolyfillDisplayName } from "@formatjs/intl-displaynames/lib/should-polyfill";
|
||||
import IntlMessageFormat from "intl-messageformat";
|
||||
import { Resources, TranslationDict } from "../../types";
|
||||
import { getLocalLanguage } from "../../util/common-translation";
|
||||
@@ -83,6 +84,10 @@ if (__BUILD__ === "latest") {
|
||||
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
|
||||
polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz"));
|
||||
}
|
||||
if (shouldPolyfillDisplayName(locale)) {
|
||||
polyfills.push(import("@formatjs/intl-displaynames/polyfill"));
|
||||
polyfills.push(import("@formatjs/intl-displaynames/locale-data/en"));
|
||||
}
|
||||
}
|
||||
|
||||
export const polyfillsLoaded =
|
||||
@@ -216,6 +221,17 @@ export const loadPolyfillLocales = async (language: string) => {
|
||||
// @ts-ignore
|
||||
Intl.DateTimeFormat.__addLocaleData(await result.json());
|
||||
}
|
||||
if (
|
||||
Intl.DisplayNames &&
|
||||
// @ts-ignore
|
||||
typeof Intl.DisplayNames.__addLocaleData === "function"
|
||||
) {
|
||||
const result = await fetch(
|
||||
`/static/locale-data/intl-displaynames/${language}.json`
|
||||
);
|
||||
// @ts-ignore
|
||||
Intl.DisplayNames.__addLocaleData(await result.json());
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
@@ -666,6 +666,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__cell.mdc-data-table__cell--flex {
|
||||
display: flex;
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell.mdc-data-table__cell--icon {
|
||||
|
@@ -15,6 +15,8 @@ class AliasesEditor extends LitElement {
|
||||
|
||||
@property() public aliases!: string[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.aliases) {
|
||||
return nothing;
|
||||
@@ -25,6 +27,7 @@ class AliasesEditor extends LitElement {
|
||||
(alias, index) => html`
|
||||
<div class="layout horizontal center-center row">
|
||||
<ha-textfield
|
||||
.disabled=${this.disabled}
|
||||
dialogInitialFocus=${index}
|
||||
.index=${index}
|
||||
class="flex-auto"
|
||||
@@ -37,6 +40,7 @@ class AliasesEditor extends LitElement {
|
||||
@keydown=${this._keyDownAlias}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.index=${index}
|
||||
slot="navigationIcon"
|
||||
label=${this.hass!.localize("ui.dialogs.aliases.remove_alias", {
|
||||
@@ -49,7 +53,7 @@ class AliasesEditor extends LitElement {
|
||||
`
|
||||
)}
|
||||
<div class="layout horizontal center-center">
|
||||
<mwc-button @click=${this._addAlias}>
|
||||
<mwc-button @click=${this._addAlias} .disabled=${this.disabled}>
|
||||
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-button>
|
||||
|
109
src/components/ha-assist-pipeline-picker.ts
Normal file
109
src/components/ha-assist-pipeline-picker.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValueMap,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { formatLanguageCode } from "../common/language/format_language";
|
||||
import { AssistPipeline, listAssistPipelines } from "../data/assist_pipeline";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const PREFERRED = "__PREFERRED_PIPELINE_OPTION__";
|
||||
|
||||
@customElement("ha-assist-pipeline-picker")
|
||||
export class HaAssistPipelinePicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() _pipelines?: AssistPipeline[];
|
||||
|
||||
@state() _preferredPipeline: string | null = null;
|
||||
|
||||
protected render() {
|
||||
if (!this._pipelines) {
|
||||
return nothing;
|
||||
}
|
||||
const value = this.value ?? PREFERRED;
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize("ui.components.pipeline-picker.pipeline")}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
<ha-list-item .value=${PREFERRED}>
|
||||
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
|
||||
preferred: this._pipelines.find(
|
||||
(pipeline) => pipeline.id === this._preferredPipeline
|
||||
)?.name,
|
||||
})}
|
||||
</ha-list-item>
|
||||
${this._pipelines.map(
|
||||
(pipeline) =>
|
||||
html`<ha-list-item .value=${pipeline.id}>
|
||||
${pipeline.name}
|
||||
(${formatLanguageCode(pipeline.language, this.hass.locale)})
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(
|
||||
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
listAssistPipelines(this.hass).then((pipelines) => {
|
||||
this._pipelines = pipelines.pipelines;
|
||||
this._preferredPipeline = pipelines.preferred_pipeline;
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === PREFERRED)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === PREFERRED ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-assist-pipeline-picker": HaAssistPipelinePicker;
|
||||
}
|
||||
}
|
@@ -13,6 +13,9 @@ export class HaButton extends Button {
|
||||
margin-inline-end: 8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-button {
|
||||
height: var(--button-height, 36px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -14,8 +14,9 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
registerStyles(
|
||||
|
@@ -4,11 +4,12 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValueMap,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { Agent, listAgents } from "../data/conversation";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
@@ -16,10 +17,13 @@ import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@customElement("ha-conversation-agent-picker")
|
||||
export class HaConversationAgentPicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public language?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -30,13 +34,19 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
|
||||
@state() _agents?: Agent[];
|
||||
|
||||
@state() _defaultAgent: string | null = null;
|
||||
|
||||
protected render() {
|
||||
if (!this._agents) {
|
||||
return nothing;
|
||||
}
|
||||
const value = this.value ?? (this.required ? this._defaultAgent : NONE);
|
||||
const value =
|
||||
this.value ??
|
||||
(this.required &&
|
||||
(!this.language ||
|
||||
this._agents
|
||||
.find((agent) => agent.id === "homeassistant")
|
||||
?.supported_languages.includes(this.language))
|
||||
? "homeassistant"
|
||||
: NONE);
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@@ -60,20 +70,56 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
: nothing}
|
||||
${this._agents.map(
|
||||
(agent) =>
|
||||
html`<ha-list-item .value=${agent.id}>${agent.name}</ha-list-item>`
|
||||
html`<ha-list-item
|
||||
.value=${agent.id}
|
||||
.disabled=${agent.supported_languages !== "*" &&
|
||||
agent.supported_languages.length === 0}
|
||||
>
|
||||
${agent.name}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(
|
||||
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
listAgents(this.hass).then((agents) => {
|
||||
this._agents = agents.agents;
|
||||
this._defaultAgent = agents.default_agent;
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
this._updateAgents();
|
||||
} else if (changedProperties.has("language")) {
|
||||
this._debouncedUpdateAgents();
|
||||
}
|
||||
}
|
||||
|
||||
private _debouncedUpdateAgents = debounce(() => this._updateAgents(), 500);
|
||||
|
||||
private async _updateAgents() {
|
||||
const { agents } = await listAgents(
|
||||
this.hass,
|
||||
this.language,
|
||||
this.hass.config.country || undefined
|
||||
);
|
||||
|
||||
this._agents = agents;
|
||||
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedAgent = agents.find((agent) => agent.id === this.value);
|
||||
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: selectedAgent?.supported_languages,
|
||||
});
|
||||
|
||||
if (
|
||||
!selectedAgent ||
|
||||
(selectedAgent.supported_languages !== "*" &&
|
||||
selectedAgent.supported_languages.length === 0)
|
||||
) {
|
||||
this.value = undefined;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -96,6 +142,10 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._agents!.find((agent) => agent.id === this.value)
|
||||
?.supported_languages,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,4 +153,7 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-conversation-agent-picker": HaConversationAgentPicker;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"supported-languages-changed": { value: "*" | string[] | undefined };
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"];
|
||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
||||
|
||||
export const createCloseHeading = (
|
||||
hass: HomeAssistant,
|
||||
@@ -92,7 +92,7 @@ export class HaDialog extends DialogBase {
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
padding: 0 24px 24px 24px;
|
||||
padding: 12px 24px 12px 24px;
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
display: block;
|
||||
|
@@ -65,6 +65,7 @@ export class HaDrawer extends DrawerBase {
|
||||
.mdc-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
|
||||
z-index: 200;
|
||||
|
@@ -33,6 +33,11 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public computeHelper?: (schema: HaFormSchema) => string;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("schema")) {
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
ReactiveElement,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -56,13 +57,18 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public localizeValue?: (key: string) => string;
|
||||
|
||||
public focus() {
|
||||
const root = this.shadowRoot?.querySelector(".root");
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
const root = this.renderRoot.querySelector(".root");
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
for (const child of root.children) {
|
||||
if (child.tagName !== "HA-ALERT") {
|
||||
if (child instanceof ReactiveElement) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await child.updateComplete;
|
||||
}
|
||||
(child as HTMLElement).focus();
|
||||
break;
|
||||
}
|
||||
|
@@ -211,6 +211,7 @@ export class Gauge extends LitElement {
|
||||
font-size: 50px;
|
||||
fill: var(--primary-text-color);
|
||||
text-anchor: middle;
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
176
src/components/ha-language-picker.ts
Normal file
176
src/components/ha-language-picker.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { formatLanguageCode } from "../common/language/format_language";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
@customElement("ha-language-picker")
|
||||
export class HaLanguagePicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public languages?: string[];
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean }) public nativeName = false;
|
||||
|
||||
@property({ type: Boolean }) public noSort = false;
|
||||
|
||||
@state() _defaultLanguages: string[] = [];
|
||||
|
||||
@query("ha-select") private _select!: HaSelect;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._computeDefaultLanguageOptions();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("languages") || changedProperties.has("value")) {
|
||||
this._select.layoutOptions();
|
||||
if (this._select.value !== this.value) {
|
||||
fireEvent(this, "value-changed", { value: this._select.value });
|
||||
}
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
const languageOptions = this._getLanguagesOptions(
|
||||
this.languages ?? this._defaultLanguages,
|
||||
this.hass.locale,
|
||||
this.nativeName
|
||||
);
|
||||
const selectedItem = languageOptions.find(
|
||||
(option) => option.value === this.value
|
||||
);
|
||||
if (!selectedItem) {
|
||||
this.value = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getLanguagesOptions = memoizeOne(
|
||||
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
|
||||
let options: { label: string; value: string }[] = [];
|
||||
|
||||
if (nativeName) {
|
||||
const translations = this.hass.translationMetadata.translations;
|
||||
options = languages.map((lang) => {
|
||||
let label = translations[lang]?.nativeName;
|
||||
if (!label) {
|
||||
try {
|
||||
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
|
||||
label = new Intl.DisplayNames(lang, {
|
||||
type: "language",
|
||||
fallback: "code",
|
||||
}).of(lang)!;
|
||||
} catch (err) {
|
||||
label = lang;
|
||||
}
|
||||
}
|
||||
return {
|
||||
value: lang,
|
||||
label,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
options = languages.map((lang) => ({
|
||||
value: lang,
|
||||
label: formatLanguageCode(lang, locale),
|
||||
}));
|
||||
}
|
||||
|
||||
if (!this.noSort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
||||
);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
);
|
||||
|
||||
private _computeDefaultLanguageOptions() {
|
||||
if (!this.hass.translationMetadata?.translations) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._defaultLanguages = Object.keys(
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const languageOptions = this._getLanguagesOptions(
|
||||
this.languages ?? this._defaultLanguages,
|
||||
this.hass.locale,
|
||||
this.nativeName
|
||||
);
|
||||
|
||||
const value =
|
||||
this.value ?? (this.required ? languageOptions[0]?.value : this.value);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.language-picker.language")}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${languageOptions.length === 0
|
||||
? html`<ha-list-item value=""
|
||||
>${this.hass.localize(
|
||||
"ui.components.language-picker.no_languages"
|
||||
)}</ha-list-item
|
||||
>`
|
||||
: languageOptions.map(
|
||||
(option) => html`
|
||||
<ha-list-item .value=${option.value}
|
||||
>${option.label}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (!this.hass || target.value === "" || target.value === this.value) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-language-picker": HaLanguagePicker;
|
||||
}
|
||||
}
|
45
src/components/ha-selector/ha-selector-assist-pipeline.ts
Normal file
45
src/components/ha-selector/ha-selector-assist-pipeline.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { AssistPipelineSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-assist-pipeline-picker";
|
||||
|
||||
@customElement("ha-selector-assist_pipeline")
|
||||
export class HaAssistPipelineSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: AssistPipelineSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-assist-pipeline-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-assist-pipeline-picker>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-conversation-agent-picker {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-assist_pipeline": HaAssistPipelineSelector;
|
||||
}
|
||||
}
|
@@ -20,10 +20,16 @@ export class HaConversationAgentSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
language?: string;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`<ha-conversation-agent-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.language=${this.selector.conversation_agent?.language ||
|
||||
this.context?.language}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
|
50
src/components/ha-selector/ha-selector-language.ts
Normal file
50
src/components/ha-selector/ha-selector-language.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { LanguageSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-language-picker";
|
||||
|
||||
@customElement("ha-selector-language")
|
||||
export class HaLanguageSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: LanguageSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-language-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.languages=${this.selector.language?.languages}
|
||||
.nativeName=${Boolean(this.selector?.language?.native_name)}
|
||||
.noSort=${Boolean(this.selector?.language?.no_sort)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-language-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-language-picker {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-language": HaLanguageSelector;
|
||||
}
|
||||
}
|
@@ -30,6 +30,13 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
(
|
||||
this.renderRoot.querySelector("ha-textarea, ha-textfield") as HTMLElement
|
||||
)?.focus();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this.selector.text?.multiline) {
|
||||
return html`<ha-textarea
|
||||
|
52
src/components/ha-selector/ha-selector-tts-voice.ts
Normal file
52
src/components/ha-selector/ha-selector-tts-voice.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { TTSVoiceSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-tts-voice-picker";
|
||||
|
||||
@customElement("ha-selector-tts_voice")
|
||||
export class HaTTSVoiceSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: TTSVoiceSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
language?: string;
|
||||
engineId?: string;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`<ha-tts-voice-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.language=${this.selector.tts_voice?.language || this.context?.language}
|
||||
.engineId=${this.selector.tts_voice?.engineId || this.context?.engineId}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-tts-voice-picker>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-tts-picker {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-tts-voice": HaTTSVoiceSelector;
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ const LOAD_ELEMENTS = {
|
||||
addon: () => import("./ha-selector-addon"),
|
||||
area: () => import("./ha-selector-area"),
|
||||
attribute: () => import("./ha-selector-attribute"),
|
||||
assist_pipeline: () => import("./ha-selector-assist-pipeline"),
|
||||
boolean: () => import("./ha-selector-boolean"),
|
||||
color_rgb: () => import("./ha-selector-color-rgb"),
|
||||
config_entry: () => import("./ha-selector-config-entry"),
|
||||
@@ -26,6 +27,7 @@ const LOAD_ELEMENTS = {
|
||||
entity: () => import("./ha-selector-entity"),
|
||||
statistic: () => import("./ha-selector-statistic"),
|
||||
file: () => import("./ha-selector-file"),
|
||||
language: () => import("./ha-selector-language"),
|
||||
navigation: () => import("./ha-selector-navigation"),
|
||||
number: () => import("./ha-selector-number"),
|
||||
object: () => import("./ha-selector-object"),
|
||||
@@ -40,6 +42,7 @@ const LOAD_ELEMENTS = {
|
||||
media: () => import("./ha-selector-media"),
|
||||
theme: () => import("./ha-selector-theme"),
|
||||
tts: () => import("./ha-selector-tts"),
|
||||
tts_voice: () => import("./ha-selector-tts-voice"),
|
||||
location: () => import("./ha-selector-location"),
|
||||
color_temp: () => import("./ha-selector-color-temp"),
|
||||
ui_action: () => import("./ha-selector-ui-action"),
|
||||
@@ -72,8 +75,9 @@ export class HaSelector extends LitElement {
|
||||
|
||||
@property() public context?: Record<string, any>;
|
||||
|
||||
public focus() {
|
||||
this.shadowRoot?.getElementById("selector")?.focus();
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
(this.renderRoot.querySelector("#selector") as HTMLElement)?.focus();
|
||||
}
|
||||
|
||||
private get _type() {
|
||||
|
@@ -52,18 +52,17 @@ export class HaSettingsRow extends LitElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.body > .secondary {
|
||||
font-family: var(--paper-font-body1_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-body1_-_-webkit-font-smoothing
|
||||
);
|
||||
font-size: var(--paper-font-body1_-_font-size);
|
||||
font-weight: var(--paper-font-body1_-_font-weight);
|
||||
line-height: var(--paper-font-body1_-_line-height);
|
||||
|
||||
color: var(
|
||||
--paper-item-body-secondary-color,
|
||||
var(--secondary-text-color)
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
font-family: var(
|
||||
--mdc-typography-body2-font-family,
|
||||
var(--mdc-typography-font-family, Roboto, sans-serif)
|
||||
);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
|
||||
font-weight: var(--mdc-typography-body2-font-weight, 400);
|
||||
line-height: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.body[two-line] {
|
||||
min-height: calc(
|
||||
|
@@ -810,6 +810,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
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 + 4}px`;
|
||||
}
|
||||
@@ -840,6 +841,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
overflow: visible;
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
@@ -5,7 +5,6 @@ import {
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -20,6 +19,8 @@ import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
const NAME_MAP = { cloud: "Home Assistant Cloud" };
|
||||
|
||||
@customElement("ha-stt-picker")
|
||||
export class HaSTTPicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
@@ -34,13 +35,18 @@ export class HaSTTPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() _engines: STTEngine[] = [];
|
||||
@state() _engines?: STTEngine[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
protected render() {
|
||||
if (!this._engines) {
|
||||
return nothing;
|
||||
}
|
||||
const value =
|
||||
this.value ??
|
||||
(this.required
|
||||
? this._engines.find((engine) => engine.language_supported)
|
||||
? this._engines.find(
|
||||
(engine) => engine.supported_languages?.length !== 0
|
||||
)
|
||||
: NONE);
|
||||
return html`
|
||||
<ha-select
|
||||
@@ -60,12 +66,18 @@ export class HaSTTPicker extends LitElement {
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._engines.map((engine) => {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
let label = engine.engine_id;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else if (engine.engine_id in NAME_MAP) {
|
||||
label = NAME_MAP[engine.engine_id];
|
||||
}
|
||||
return html`<ha-list-item
|
||||
.value=${engine.engine_id}
|
||||
.disabled=${engine.language_supported === false}
|
||||
.disabled=${engine.supported_languages?.length === 0}
|
||||
>
|
||||
${stateObj ? computeStateName(stateObj) : engine.engine_id}
|
||||
${label}
|
||||
</ha-list-item>`;
|
||||
})}
|
||||
</ha-select>
|
||||
@@ -84,13 +96,27 @@ export class HaSTTPicker extends LitElement {
|
||||
private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500);
|
||||
|
||||
private async _updateEngines() {
|
||||
this._engines = (await listSTTEngines(this.hass, this.language)).providers;
|
||||
this._engines = (
|
||||
await listSTTEngines(
|
||||
this.hass,
|
||||
this.language,
|
||||
this.hass.config.country || undefined
|
||||
)
|
||||
).providers;
|
||||
|
||||
if (
|
||||
this.value &&
|
||||
!this._engines.find((engine) => engine.engine_id === this.value)
|
||||
?.language_supported
|
||||
) {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEngine = this._engines.find(
|
||||
(engine) => engine.engine_id === this.value
|
||||
);
|
||||
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: selectedEngine?.supported_languages,
|
||||
});
|
||||
|
||||
if (!selectedEngine || selectedEngine.supported_languages?.length === 0) {
|
||||
this.value = undefined;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
@@ -116,6 +142,10 @@ export class HaSTTPicker extends LitElement {
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
?.supported_languages,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { debounce } from "chart.js/helpers";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -6,12 +5,12 @@ import {
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { listTTSEngines, TTSEngine } from "../data/tts";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
@@ -20,6 +19,11 @@ import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
const NAME_MAP = {
|
||||
cloud: "Home Assistant Cloud",
|
||||
google_translate: "Google Translate",
|
||||
};
|
||||
|
||||
@customElement("ha-tts-picker")
|
||||
export class HaTTSPicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
@@ -34,13 +38,18 @@ export class HaTTSPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() _engines: TTSEngine[] = [];
|
||||
@state() _engines?: TTSEngine[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
protected render() {
|
||||
if (!this._engines) {
|
||||
return nothing;
|
||||
}
|
||||
const value =
|
||||
this.value ??
|
||||
(this.required
|
||||
? this._engines.find((engine) => engine.language_supported)
|
||||
? this._engines.find(
|
||||
(engine) => engine.supported_languages?.length !== 0
|
||||
)
|
||||
: NONE);
|
||||
return html`
|
||||
<ha-select
|
||||
@@ -60,12 +69,18 @@ export class HaTTSPicker extends LitElement {
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._engines.map((engine) => {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
let label = engine.engine_id;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else if (engine.engine_id in NAME_MAP) {
|
||||
label = NAME_MAP[engine.engine_id];
|
||||
}
|
||||
return html`<ha-list-item
|
||||
.value=${engine.engine_id}
|
||||
.disabled=${engine.language_supported === false}
|
||||
.disabled=${engine.supported_languages?.length === 0}
|
||||
>
|
||||
${stateObj ? computeStateName(stateObj) : engine.engine_id}
|
||||
${label}
|
||||
</ha-list-item>`;
|
||||
})}
|
||||
</ha-select>
|
||||
@@ -84,13 +99,27 @@ export class HaTTSPicker extends LitElement {
|
||||
private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500);
|
||||
|
||||
private async _updateEngines() {
|
||||
this._engines = (await listTTSEngines(this.hass, this.language)).providers;
|
||||
this._engines = (
|
||||
await listTTSEngines(
|
||||
this.hass,
|
||||
this.language,
|
||||
this.hass.config.country || undefined
|
||||
)
|
||||
).providers;
|
||||
|
||||
if (
|
||||
this.value &&
|
||||
!this._engines.find((engine) => engine.engine_id === this.value)
|
||||
?.language_supported
|
||||
) {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEngine = this._engines.find(
|
||||
(engine) => engine.engine_id === this.value
|
||||
);
|
||||
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: selectedEngine?.supported_languages,
|
||||
});
|
||||
|
||||
if (!selectedEngine || selectedEngine.supported_languages?.length === 0) {
|
||||
this.value = undefined;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
@@ -116,6 +145,10 @@ export class HaTTSPicker extends LitElement {
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
?.supported_languages,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
147
src/components/ha-tts-voice-picker.ts
Normal file
147
src/components/ha-tts-voice-picker.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { listTTSVoices, TTSVoice } from "../data/tts";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@customElement("ha-tts-voice-picker")
|
||||
export class HaTTSVoicePicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public engineId?: string;
|
||||
|
||||
@property() public language?: string;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() _voices?: TTSVoice[] | null;
|
||||
|
||||
@query("ha-select") private _select?: HaSelect;
|
||||
|
||||
protected render() {
|
||||
if (!this._voices) {
|
||||
return nothing;
|
||||
}
|
||||
const value =
|
||||
this.value ?? (this.required ? this._voices[0]?.voice_id : NONE);
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize("ui.components.tts-voice-picker.voice")}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize("ui.components.tts-voice-picker.none")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._voices.map(
|
||||
(voice) => html`<ha-list-item .value=${voice.voice_id}>
|
||||
${voice.name}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
this._updateVoices();
|
||||
} else if (
|
||||
changedProperties.has("language") ||
|
||||
changedProperties.has("engineId")
|
||||
) {
|
||||
this._debouncedUpdateVoices();
|
||||
}
|
||||
}
|
||||
|
||||
private _debouncedUpdateVoices = debounce(() => this._updateVoices(), 500);
|
||||
|
||||
private async _updateVoices() {
|
||||
if (!this.engineId || !this.language) {
|
||||
this._voices = undefined;
|
||||
return;
|
||||
}
|
||||
this._voices = (
|
||||
await listTTSVoices(this.hass, this.engineId, this.language)
|
||||
).voices;
|
||||
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._voices ||
|
||||
!this._voices.find((voice) => voice.voice_id === this.value)
|
||||
) {
|
||||
this.value = undefined;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("_voices") &&
|
||||
this._select?.value !== this.value
|
||||
) {
|
||||
this._select?.layoutOptions();
|
||||
fireEvent(this, "value-changed", { value: this._select?.value });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tts-voice-picker": HaTTSVoicePicker;
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -31,6 +31,8 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public autoUpdate = false;
|
||||
|
||||
@property({ type: Boolean }) public readOnly = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
@@ -41,7 +43,11 @@ export class HaYamlEditor extends LitElement {
|
||||
try {
|
||||
this._yaml =
|
||||
value && !isEmpty(value)
|
||||
? dump(value, { schema: this.yamlSchema, quotingType: '"' })
|
||||
? dump(value, {
|
||||
schema: this.yamlSchema,
|
||||
quotingType: '"',
|
||||
noRefs: true,
|
||||
})
|
||||
: "";
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -56,6 +62,13 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (this.autoUpdate && changedProperties.has("value")) {
|
||||
this.setValue(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._yaml === undefined) {
|
||||
return nothing;
|
||||
|
@@ -21,6 +21,7 @@ import { buttonLinkStyle } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "../ha-textarea";
|
||||
import "../ha-language-picker";
|
||||
|
||||
export interface TtsMediaPickedEvent {
|
||||
item: MediaPlayerItem;
|
||||
@@ -103,21 +104,17 @@ class BrowseMediaTTS extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="cloud-options">
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
<ha-language-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.tts.language"
|
||||
)}
|
||||
.value=${selectedVoice[0]}
|
||||
@selected=${this._handleLanguageChange}
|
||||
.languages=${languages}
|
||||
@closed=${stopPropagation}
|
||||
@value-changed=${this._handleLanguageChange}
|
||||
>
|
||||
${languages.map(
|
||||
([key, label]) =>
|
||||
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
</ha-language-picker>
|
||||
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
@@ -184,10 +181,10 @@ class BrowseMediaTTS extends LitElement {
|
||||
}
|
||||
|
||||
async _handleLanguageChange(ev) {
|
||||
if (ev.target.value === this._cloudOptions![0]) {
|
||||
if (ev.detail.value === this._cloudOptions![0]) {
|
||||
return;
|
||||
}
|
||||
this._cloudOptions = [ev.target.value, this._cloudOptions![1]];
|
||||
this._cloudOptions = [ev.detail.value, this._cloudOptions![1]];
|
||||
}
|
||||
|
||||
async _handleGenderChange(ev) {
|
||||
@@ -256,7 +253,8 @@ class BrowseMediaTTS extends LitElement {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.cloud-options ha-select {
|
||||
.cloud-options ha-select,
|
||||
ha-language-picker {
|
||||
width: 48%;
|
||||
}
|
||||
ha-textarea {
|
||||
|
@@ -9,5 +9,11 @@ export interface AlexaEntity {
|
||||
export const fetchCloudAlexaEntities = (hass: HomeAssistant) =>
|
||||
hass.callWS<AlexaEntity[]>({ type: "cloud/alexa/entities" });
|
||||
|
||||
export const fetchCloudAlexaEntity = (hass: HomeAssistant, entity_id: string) =>
|
||||
hass.callWS<AlexaEntity>({
|
||||
type: "cloud/alexa/entities/get",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const syncCloudAlexaEntities = (hass: HomeAssistant) =>
|
||||
hass.callWS({ type: "cloud/alexa/sync" });
|
||||
|
@@ -5,19 +5,27 @@ import type { SpeechMetadata } from "./stt";
|
||||
|
||||
export interface AssistPipeline {
|
||||
id: string;
|
||||
conversation_engine: string;
|
||||
language: string;
|
||||
name: string;
|
||||
stt_engine: string;
|
||||
tts_engine: string;
|
||||
language: string;
|
||||
conversation_engine: string;
|
||||
conversation_language: string | null;
|
||||
stt_engine: string | null;
|
||||
stt_language: string | null;
|
||||
tts_engine: string | null;
|
||||
tts_language: string | null;
|
||||
tts_voice: string | null;
|
||||
}
|
||||
|
||||
export interface AssistPipelineMutableParams {
|
||||
conversation_engine: string;
|
||||
language: string;
|
||||
name: string;
|
||||
stt_engine: string;
|
||||
tts_engine: string;
|
||||
language: string;
|
||||
conversation_engine: string;
|
||||
conversation_language: string | null;
|
||||
stt_engine: string | null;
|
||||
stt_language: string | null;
|
||||
tts_engine: string | null;
|
||||
tts_language: string | null;
|
||||
tts_voice: string | null;
|
||||
}
|
||||
|
||||
export interface assistRunListing {
|
||||
@@ -71,6 +79,7 @@ interface PipelineIntentStartEvent extends PipelineEventBase {
|
||||
type: "intent-start";
|
||||
data: {
|
||||
engine: string;
|
||||
language: string;
|
||||
intent_input: string;
|
||||
};
|
||||
}
|
||||
@@ -85,6 +94,8 @@ interface PipelineTTSStartEvent extends PipelineEventBase {
|
||||
type: "tts-start";
|
||||
data: {
|
||||
engine: string;
|
||||
language: string;
|
||||
voice: string;
|
||||
tts_input: string;
|
||||
};
|
||||
}
|
||||
@@ -202,14 +213,15 @@ export const processEvent = (
|
||||
return run;
|
||||
};
|
||||
|
||||
export const runAssistPipeline = (
|
||||
export const runDebugAssistPipeline = (
|
||||
hass: HomeAssistant,
|
||||
callback: (event: PipelineRun) => void,
|
||||
callback: (run: PipelineRun) => void,
|
||||
options: PipelineRunOptions
|
||||
) => {
|
||||
let run: PipelineRun | undefined;
|
||||
|
||||
const unsubProm = hass.connection.subscribeMessage<PipelineRunEvent>(
|
||||
const unsubProm = runAssistPipeline(
|
||||
hass,
|
||||
(updateEvent) => {
|
||||
run = processEvent(run, updateEvent, options);
|
||||
|
||||
@@ -221,15 +233,22 @@ export const runAssistPipeline = (
|
||||
callback(run);
|
||||
}
|
||||
},
|
||||
{
|
||||
...options,
|
||||
type: "assist_pipeline/run",
|
||||
}
|
||||
options
|
||||
);
|
||||
|
||||
return unsubProm;
|
||||
};
|
||||
|
||||
export const runAssistPipeline = (
|
||||
hass: HomeAssistant,
|
||||
callback: (event: PipelineRunEvent) => void,
|
||||
options: PipelineRunOptions
|
||||
) =>
|
||||
hass.connection.subscribeMessage<PipelineRunEvent>(callback, {
|
||||
...options,
|
||||
type: "assist_pipeline/run",
|
||||
});
|
||||
|
||||
export const listAssistPipelineRuns = (
|
||||
hass: HomeAssistant,
|
||||
pipeline_id: string
|
||||
@@ -254,7 +273,7 @@ export const getAssistPipelineRun = (
|
||||
pipeline_run_id,
|
||||
});
|
||||
|
||||
export const fetchAssistPipelines = (hass: HomeAssistant) =>
|
||||
export const listAssistPipelines = (hass: HomeAssistant) =>
|
||||
hass.callWS<{
|
||||
pipelines: AssistPipeline[];
|
||||
preferred_pipeline: string | null;
|
||||
@@ -262,6 +281,12 @@ export const fetchAssistPipelines = (hass: HomeAssistant) =>
|
||||
type: "assist_pipeline/pipeline/list",
|
||||
});
|
||||
|
||||
export const getAssistPipeline = (hass: HomeAssistant, pipeline_id?: string) =>
|
||||
hass.callWS<AssistPipeline>({
|
||||
type: "assist_pipeline/pipeline/get",
|
||||
pipeline_id,
|
||||
});
|
||||
|
||||
export const createAssistPipeline = (
|
||||
hass: HomeAssistant,
|
||||
pipeline: AssistPipelineMutableParams
|
||||
@@ -274,7 +299,7 @@ export const createAssistPipeline = (
|
||||
export const updateAssistPipeline = (
|
||||
hass: HomeAssistant,
|
||||
pipeline_id: string,
|
||||
pipeline: Partial<AssistPipelineMutableParams>
|
||||
pipeline: AssistPipelineMutableParams
|
||||
) =>
|
||||
hass.callWS<AssistPipeline>({
|
||||
type: "assist_pipeline/pipeline/update",
|
||||
@@ -296,3 +321,8 @@ export const deleteAssistPipeline = (hass: HomeAssistant, pipelineId: string) =>
|
||||
type: "assist_pipeline/pipeline/delete",
|
||||
pipeline_id: pipelineId,
|
||||
});
|
||||
|
||||
export const fetchAssistPipelineLanguages = (hass: HomeAssistant) =>
|
||||
hass.callWS<{ languages: string[] }>({
|
||||
type: "assist_pipeline/language/list",
|
||||
});
|
||||
|
@@ -76,10 +76,14 @@ export const cloudLogin = (
|
||||
email: string,
|
||||
password: string
|
||||
) =>
|
||||
hass.callApi("POST", "cloud/login", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
hass.callApi<{ success: boolean; cloud_pipeline?: string }>(
|
||||
"POST",
|
||||
"cloud/login",
|
||||
{
|
||||
email,
|
||||
password,
|
||||
}
|
||||
);
|
||||
|
||||
export const cloudLogout = (hass: HomeAssistant) =>
|
||||
hass.callApi("POST", "cloud/logout");
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { translationMetadata } from "../../resources/translations-metadata";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export interface CloudTTSInfo {
|
||||
@@ -11,7 +10,7 @@ export const getCloudTTSInfo = (hass: HomeAssistant) =>
|
||||
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });
|
||||
|
||||
export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
|
||||
const languages: Array<[string, string]> = [];
|
||||
const languages: string[] = [];
|
||||
|
||||
if (!info) {
|
||||
return languages;
|
||||
@@ -23,25 +22,9 @@ export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
|
||||
continue;
|
||||
}
|
||||
seen.add(lang);
|
||||
|
||||
let label = lang;
|
||||
|
||||
if (lang in translationMetadata.translations) {
|
||||
label = translationMetadata.translations[lang].nativeName;
|
||||
} else {
|
||||
const [langFamily, dialect] = lang.split("-");
|
||||
if (langFamily in translationMetadata.translations) {
|
||||
label = `${translationMetadata.translations[langFamily].nativeName}`;
|
||||
|
||||
if (langFamily.toLowerCase() !== dialect.toLowerCase()) {
|
||||
label += ` (${dialect})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
languages.push([lang, label]);
|
||||
languages.push(lang);
|
||||
}
|
||||
return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
|
||||
return languages;
|
||||
};
|
||||
|
||||
export const getCloudTtsSupportedGenders = (
|
||||
|
@@ -59,6 +59,7 @@ export interface AgentInfo {
|
||||
export interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
supported_languages: "*" | string[];
|
||||
}
|
||||
|
||||
export const processConversationInput = (
|
||||
@@ -76,10 +77,14 @@ export const processConversationInput = (
|
||||
});
|
||||
|
||||
export const listAgents = (
|
||||
hass: HomeAssistant
|
||||
): Promise<{ agents: Agent[]; default_agent: string | null }> =>
|
||||
hass: HomeAssistant,
|
||||
language?: string,
|
||||
country?: string
|
||||
): Promise<{ agents: Agent[] }> =>
|
||||
hass.callWS({
|
||||
type: "conversation/agent/list",
|
||||
language,
|
||||
country,
|
||||
});
|
||||
|
||||
export const getAgentInfo = (
|
||||
|
@@ -90,6 +90,9 @@ export interface EntityRegistryOptions {
|
||||
number?: NumberEntityOptions;
|
||||
sensor?: SensorEntityOptions;
|
||||
weather?: WeatherEntityOptions;
|
||||
conversation?: Record<string, unknown>;
|
||||
"cloud.alexa"?: Record<string, unknown>;
|
||||
"cloud.google_assistant"?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface EntityRegistryEntryUpdateParams {
|
||||
|
@@ -12,7 +12,11 @@ export const voiceAssistants = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const voiceAssistantKeys = Object.keys(voiceAssistants);
|
||||
export interface ExposeEntitySettings {
|
||||
conversation?: boolean;
|
||||
"cloud.alexa"?: boolean;
|
||||
"cloud.google_assistant"?: boolean;
|
||||
}
|
||||
|
||||
export const setExposeNewEntities = (
|
||||
hass: HomeAssistant,
|
||||
@@ -43,3 +47,8 @@ export const exposeEntities = (
|
||||
entity_ids,
|
||||
should_expose,
|
||||
});
|
||||
|
||||
export const listExposedEntities = (hass: HomeAssistant) =>
|
||||
hass.callWS<{ exposed_entities: Record<string, ExposeEntitySettings> }>({
|
||||
type: "homeassistant/expose_entity/list",
|
||||
});
|
@@ -4,6 +4,7 @@ export interface GoogleEntity {
|
||||
entity_id: string;
|
||||
traits: string[];
|
||||
might_2fa: boolean;
|
||||
disable_2fa?: boolean;
|
||||
}
|
||||
|
||||
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
|
||||
|
@@ -26,11 +26,13 @@ export type Selector =
|
||||
| LegacyEntitySelector
|
||||
| FileSelector
|
||||
| IconSelector
|
||||
| LanguageSelector
|
||||
| LocationSelector
|
||||
| MediaSelector
|
||||
| NavigationSelector
|
||||
| NumberSelector
|
||||
| ObjectSelector
|
||||
| AssistPipelineSelector
|
||||
| SelectSelector
|
||||
| StateSelector
|
||||
| StatisticSelector
|
||||
@@ -41,6 +43,7 @@ export type Selector =
|
||||
| ThemeSelector
|
||||
| TimeSelector
|
||||
| TTSSelector
|
||||
| TTSVoiceSelector
|
||||
| UiActionSelector
|
||||
| UiColorSelector;
|
||||
|
||||
@@ -89,8 +92,7 @@ export interface ColorTempSelector {
|
||||
}
|
||||
|
||||
export interface ConversationAgentSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
conversation_agent: {} | null;
|
||||
conversation_agent: { language?: string } | null;
|
||||
}
|
||||
|
||||
export interface ConfigEntrySelector {
|
||||
@@ -209,6 +211,14 @@ export interface IconSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LanguageSelector {
|
||||
language: {
|
||||
languages?: string[];
|
||||
native_name?: boolean;
|
||||
no_sort?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LocationSelector {
|
||||
location: { radius?: boolean; icon?: string } | null;
|
||||
}
|
||||
@@ -257,6 +267,11 @@ export interface ObjectSelector {
|
||||
object: {} | null;
|
||||
}
|
||||
|
||||
export interface AssistPipelineSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
assist_pipeline: {} | null;
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: any;
|
||||
label: string;
|
||||
@@ -331,6 +346,10 @@ export interface TTSSelector {
|
||||
tts: { language?: string } | null;
|
||||
}
|
||||
|
||||
export interface TTSVoiceSelector {
|
||||
tts_voice: { engineId?: string; language?: string } | null;
|
||||
}
|
||||
|
||||
export interface UiActionSelector {
|
||||
ui_action: {
|
||||
actions?: UiAction[];
|
||||
|
@@ -20,14 +20,16 @@ export interface SpeechMetadata {
|
||||
|
||||
export interface STTEngine {
|
||||
engine_id: string;
|
||||
language_supported?: boolean;
|
||||
supported_languages?: string[];
|
||||
}
|
||||
|
||||
export const listSTTEngines = (
|
||||
hass: HomeAssistant,
|
||||
language?: string
|
||||
language?: string,
|
||||
country?: string
|
||||
): Promise<{ providers: STTEngine[] }> =>
|
||||
hass.callWS({
|
||||
type: "stt/engine/list",
|
||||
language,
|
||||
country,
|
||||
});
|
||||
|
@@ -2,7 +2,7 @@ import { HomeAssistant } from "../types";
|
||||
|
||||
export interface TTSEngine {
|
||||
engine_id: string;
|
||||
language_supported?: boolean;
|
||||
supported_languages?: string[];
|
||||
}
|
||||
|
||||
export interface TTSVoice {
|
||||
@@ -31,18 +31,20 @@ export const getProviderFromTTSMediaSource = (mediaContentId: string) =>
|
||||
|
||||
export const listTTSEngines = (
|
||||
hass: HomeAssistant,
|
||||
language?: string
|
||||
language?: string,
|
||||
country?: string
|
||||
): Promise<{ providers: TTSEngine[] }> =>
|
||||
hass.callWS({
|
||||
type: "tts/engine/list",
|
||||
language,
|
||||
country,
|
||||
});
|
||||
|
||||
export const listTTSVoices = (
|
||||
hass: HomeAssistant,
|
||||
engine_id: string,
|
||||
language: string
|
||||
): Promise<{ voices: TTSVoice[] }> =>
|
||||
): Promise<{ voices: TTSVoice[] | null }> =>
|
||||
hass.callWS({
|
||||
type: "tts/engine/voices",
|
||||
engine_id,
|
||||
|
@@ -161,8 +161,6 @@ class DialogBox extends LitElement {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
ha-dialog {
|
||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
|
@@ -176,8 +176,6 @@ export class DialogEnterCode
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
|
@@ -569,6 +569,7 @@ class MoreInfoViewLightColorPicker extends LitElement {
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.1px;
|
||||
margin: 0;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.color-temp {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ExtEntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import { ExposeEntitySettings, voiceAssistants } from "../../../../data/expose";
|
||||
import "../../../../panels/config/voice-assistants/entity-voice-settings";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
@@ -12,13 +14,23 @@ class MoreInfoViewVoiceAssistants extends LitElement {
|
||||
|
||||
@property() public params?;
|
||||
|
||||
private _calculateExposed = memoizeOne((entry: ExtEntityRegistryEntry) => {
|
||||
const exposed: ExposeEntitySettings = {};
|
||||
Object.keys(voiceAssistants).forEach((key) => {
|
||||
exposed[key] = entry.options?.[key]?.should_expose;
|
||||
});
|
||||
return exposed;
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<entity-voice-settings
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entry.entity_id}
|
||||
.entry=${this.entry}
|
||||
.exposed=${this._calculateExposed(this.entry)}
|
||||
></entity-voice-settings>`;
|
||||
}
|
||||
|
||||
|
@@ -27,7 +27,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const statTypes: StatisticsTypes = ["min", "mean", "max"];
|
||||
const statTypes: StatisticsTypes = ["state", "min", "mean", "max"];
|
||||
|
||||
@customElement("ha-more-info-history")
|
||||
export class MoreInfoHistory extends LitElement {
|
||||
|
@@ -5,11 +5,12 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import "../../components/ha-alert";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
@@ -39,18 +40,17 @@ export class HaMoreInfoSettings extends LitElement {
|
||||
if (this.entry === null) {
|
||||
return html`
|
||||
<div class="content">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.no_unique_id",
|
||||
"entity_id",
|
||||
this.entityId,
|
||||
"faq_link",
|
||||
html`<a
|
||||
href=${documentationUrl(this.hass, "/faq/unique_id")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize("ui.dialogs.entity_registry.faq")}</a
|
||||
>`
|
||||
)}
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.dialogs.entity_registry.no_unique_id", {
|
||||
entity_id: this.entityId,
|
||||
faq_link: html`<a
|
||||
href=${documentationUrl(this.hass, "/faq/unique_id")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize("ui.dialogs.entity_registry.faq")}</a
|
||||
>`,
|
||||
})}
|
||||
</ha-alert>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@@ -108,7 +108,6 @@ class DialogRestart extends LitElement {
|
||||
graphic="avatar"
|
||||
twoline
|
||||
multiline-secondary
|
||||
hasMeta
|
||||
@request-selected=${this._reload}
|
||||
>
|
||||
<div slot="graphic" class="icon-background reload">
|
||||
@@ -131,7 +130,6 @@ class DialogRestart extends LitElement {
|
||||
graphic="avatar"
|
||||
twoline
|
||||
multiline-secondary
|
||||
hasMeta
|
||||
@request-selected=${this._restart}
|
||||
>
|
||||
<div slot="graphic" class="icon-background restart">
|
||||
|
187
src/dialogs/tts-try/dialog-tts-try.ts
Normal file
187
src/dialogs/tts-try/dialog-tts-try.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { mdiPlayCircleOutline } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { LocalStorage } from "../../common/decorators/local-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";
|
||||
import { convertTextToSpeech } from "../../data/tts";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import { TTSTryDialogParams } from "./show-dialog-tts-try";
|
||||
import "../../components/ha-circular-progress";
|
||||
|
||||
@customElement("dialog-tts-try")
|
||||
export class TTSTryDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _loadingExample = false;
|
||||
|
||||
@state() private _params?: TTSTryDialogParams;
|
||||
|
||||
@state() private _valid = false;
|
||||
|
||||
@query("#message") private _messageInput?: HaTextArea;
|
||||
|
||||
@LocalStorage("ttsTryMessages", false, false) private _messages?: Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
|
||||
public showDialog(params: TTSTryDialogParams) {
|
||||
this._params = params;
|
||||
this._valid = Boolean(this._defaultMessage);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private get _defaultMessage() {
|
||||
const language = this._params!.language?.substring(0, 2);
|
||||
const userLanguage = this.hass.locale.language.substring(0, 2);
|
||||
// Load previous message in the right language
|
||||
if (language && this._messages?.[language]) {
|
||||
return this._messages[language];
|
||||
}
|
||||
// Only display example message if it's interface language
|
||||
if (language === userLanguage) {
|
||||
return this.hass.localize("ui.dialogs.tts-try.message_example");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.tts-try.header")
|
||||
)}
|
||||
>
|
||||
<ha-textarea
|
||||
autogrow
|
||||
id="message"
|
||||
.label=${this.hass.localize("ui.dialogs.tts-try.message")}
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.dialogs.tts-try.message_placeholder"
|
||||
)}
|
||||
.value=${this._defaultMessage}
|
||||
@input=${this._inputChanged}
|
||||
?dialogInitialFocus=${!this._defaultMessage}
|
||||
>
|
||||
</ha-textarea>
|
||||
${this._loadingExample
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
alt=""
|
||||
slot="primaryAction"
|
||||
class="loading"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: 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>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _inputChanged() {
|
||||
this._valid = Boolean(this._messageInput?.value);
|
||||
}
|
||||
|
||||
private async _playExample() {
|
||||
const message = this._messageInput?.value;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const platform = this._params!.engine;
|
||||
const language = this._params!.language;
|
||||
const voice = this._params!.voice;
|
||||
|
||||
if (language) {
|
||||
this._messages = {
|
||||
...this._messages,
|
||||
[language.substring(0, 2)]: message,
|
||||
};
|
||||
}
|
||||
|
||||
this._loadingExample = true;
|
||||
|
||||
const audio = new Audio();
|
||||
audio.play();
|
||||
|
||||
let url;
|
||||
try {
|
||||
const result = await convertTextToSpeech(this.hass, {
|
||||
platform,
|
||||
message,
|
||||
language,
|
||||
options: { voice },
|
||||
});
|
||||
url = result.path;
|
||||
} catch (err: any) {
|
||||
this._loadingExample = false;
|
||||
showAlertDialog(this, {
|
||||
text: `Unable to load example. ${err.error || err.body || err}`,
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
audio.src = url;
|
||||
audio.addEventListener("canplaythrough", () => audio.play());
|
||||
audio.addEventListener("playing", () => {
|
||||
this._loadingExample = false;
|
||||
});
|
||||
audio.addEventListener("error", () => {
|
||||
showAlertDialog(this, { title: "Error playing audio." });
|
||||
this._loadingExample = false;
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
}
|
||||
ha-textarea,
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.loading {
|
||||
height: 36px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-tts-try": TTSTryDialog;
|
||||
}
|
||||
}
|
21
src/dialogs/tts-try/show-dialog-tts-try.ts
Normal file
21
src/dialogs/tts-try/show-dialog-tts-try.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface TTSTryDialogParams {
|
||||
engine: string;
|
||||
language?: string;
|
||||
voice?: string;
|
||||
}
|
||||
|
||||
export const loadTTSTryDialog = () => import("./dialog-tts-try");
|
||||
|
||||
export const showTTSTryDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: TTSTryDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
addHistory: false,
|
||||
dialogTag: "dialog-tts-try",
|
||||
dialogImport: loadTTSTryDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,88 +1,109 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiChevronDown,
|
||||
mdiClose,
|
||||
mdiHelpCircleOutline,
|
||||
mdiMicrophone,
|
||||
mdiSend,
|
||||
mdiStar,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { SpeechRecognition } from "../../common/dom/speech-recognition";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-button-menu";
|
||||
import "../../components/ha-dialog";
|
||||
import type { HaDialog } from "../../components/ha-dialog";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import {
|
||||
AgentInfo,
|
||||
getAgentInfo,
|
||||
prepareConversation,
|
||||
processConversationInput,
|
||||
} from "../../data/conversation";
|
||||
AssistPipeline,
|
||||
getAssistPipeline,
|
||||
listAssistPipelines,
|
||||
runAssistPipeline,
|
||||
} from "../../data/assist_pipeline";
|
||||
import { AgentInfo, getAgentInfo } from "../../data/conversation";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { AudioRecorder } from "../../util/audio-recorder";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
|
||||
interface Message {
|
||||
who: string;
|
||||
text?: string;
|
||||
text?: string | TemplateResult;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
interface Results {
|
||||
transcript: string;
|
||||
final: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-voice-command-dialog")
|
||||
export class HaVoiceCommandDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public results: Results | null = null;
|
||||
|
||||
@state() private _conversation: Message[] = [
|
||||
{
|
||||
who: "hass",
|
||||
text: "",
|
||||
},
|
||||
];
|
||||
@state() private _conversation?: Message[];
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@LocalStorage("AssistPipelineId", true, false) private _pipelineId?: string;
|
||||
|
||||
@state() private _pipeline?: AssistPipeline;
|
||||
|
||||
@state() private _agentInfo?: AgentInfo;
|
||||
|
||||
@state() private _showSendButton = false;
|
||||
|
||||
@query("#scroll-container") private _scrollContainer!: HaDialog;
|
||||
@state() private _pipelines?: AssistPipeline[];
|
||||
|
||||
@state() private _preferredPipeline?: string;
|
||||
|
||||
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
|
||||
|
||||
@query("#message-input") private _messageInput!: HaTextField;
|
||||
|
||||
private recognition!: SpeechRecognition;
|
||||
|
||||
private _conversationId: string | null = null;
|
||||
|
||||
private _audioRecorder?: AudioRecorder;
|
||||
|
||||
private _audioBuffer?: Int16Array[];
|
||||
|
||||
private _audio?: HTMLAudioElement;
|
||||
|
||||
private _stt_binary_handler_id?: number | null;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
},
|
||||
];
|
||||
this._opened = true;
|
||||
this._agentInfo = await getAgentInfo(this.hass);
|
||||
await this.updateComplete;
|
||||
this._scrollMessagesBottom();
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
this._opened = false;
|
||||
if (this.recognition) {
|
||||
this.recognition.abort();
|
||||
}
|
||||
this._pipeline = undefined;
|
||||
this._pipelines = undefined;
|
||||
this._agentInfo = undefined;
|
||||
this._conversation = undefined;
|
||||
this._conversationId = null;
|
||||
this._audioRecorder?.close();
|
||||
this._audioRecorder = undefined;
|
||||
this._audio?.pause();
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -90,6 +111,10 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsMicrophone = AudioRecorder.isSupported;
|
||||
const supportsSTT = this._pipeline?.stt_engine;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -99,15 +124,58 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
${this.hass.localize("ui.dialogs.voice_command.title")}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<div slot="title">
|
||||
${this.hass.localize("ui.dialogs.voice_command.title")}
|
||||
<ha-button-menu
|
||||
@opened=${this._loadPipelines}
|
||||
@closed=${stopPropagation}
|
||||
activatable
|
||||
fixed
|
||||
>
|
||||
<ha-button slot="trigger">
|
||||
${this._pipeline?.name}
|
||||
<ha-svg-icon
|
||||
slot="trailingIcon"
|
||||
.path=${mdiChevronDown}
|
||||
></ha-svg-icon>
|
||||
</ha-button>
|
||||
${this._pipelines?.map(
|
||||
(pipeline) => html`<ha-list-item
|
||||
?selected=${pipeline.id === this._pipelineId ||
|
||||
(!this._pipelineId &&
|
||||
pipeline.id === this._preferredPipeline)}
|
||||
.pipeline=${pipeline.id}
|
||||
@click=${this._selectPipeline}
|
||||
.hasMeta=${pipeline.id === this._preferredPipeline}
|
||||
>
|
||||
${pipeline.name}${pipeline.id === this._preferredPipeline
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="meta"
|
||||
.path=${mdiStar}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
${this.hass.user?.is_admin
|
||||
? html`<li divider role="separator"></li>
|
||||
<a href="/config/voice-assistants/assistants"
|
||||
><ha-list-item @click=${this.closeDialog}
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.voice_command.manage_assistants"
|
||||
)}</ha-list-item
|
||||
></a
|
||||
>`
|
||||
: nothing}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/docs/assist/")}
|
||||
slot="actionItems"
|
||||
@@ -123,25 +191,13 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
</div>
|
||||
<div class="messages">
|
||||
<div class="messages-container" id="scroll-container">
|
||||
${this._conversation.map(
|
||||
${this._conversation!.map(
|
||||
(message) => html`
|
||||
<div class=${this._computeMessageClasses(message)}>
|
||||
${message.text}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.results
|
||||
? html`
|
||||
<div class="message user">
|
||||
<span
|
||||
class=${classMap({
|
||||
interimTranscript: !this.results.final,
|
||||
})}
|
||||
>${this.results.transcript}</span
|
||||
>${!this.results.final ? "…" : ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input" slot="primaryAction">
|
||||
@@ -154,7 +210,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
iconTrailing
|
||||
>
|
||||
<span slot="trailingIcon">
|
||||
${this._showSendButton
|
||||
${this._showSendButton || !supportsSTT
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="listening-icon"
|
||||
@@ -166,27 +222,35 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: SpeechRecognition
|
||||
? html`
|
||||
${this.results
|
||||
: html`
|
||||
${this._audioRecorder?.active
|
||||
? html`
|
||||
<div class="bouncer">
|
||||
<div class="double-bounce1"></div>
|
||||
<div class="double-bounce2"></div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-icon-button
|
||||
class="listening-icon"
|
||||
.path=${mdiMicrophone}
|
||||
@click=${this._toggleListening}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.voice_command.start_listening"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
|
||||
<div class="listening-icon">
|
||||
<ha-icon-button
|
||||
.path=${mdiMicrophone}
|
||||
@click=${this._toggleListening}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.voice_command.start_listening"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${!supportsMicrophone
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${mdiAlertCircle}
|
||||
class="unsupported"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
`}
|
||||
</span>
|
||||
</ha-textfield>
|
||||
${this._agentInfo && this._agentInfo.attribution
|
||||
@@ -205,15 +269,51 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("_pipelineId") ||
|
||||
(changedProperties.has("_opened") && this._opened === true)
|
||||
) {
|
||||
this._getPipeline();
|
||||
}
|
||||
}
|
||||
|
||||
private async _getPipeline() {
|
||||
try {
|
||||
this._pipeline = await getAssistPipeline(this.hass, this._pipelineId);
|
||||
} catch (e: any) {
|
||||
if (e.code === "not_found") {
|
||||
this._pipelineId = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._agentInfo = await getAgentInfo(
|
||||
this.hass,
|
||||
this._pipeline.conversation_engine
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadPipelines() {
|
||||
if (this._pipelines) {
|
||||
return;
|
||||
}
|
||||
const { pipelines, preferred_pipeline } = await listAssistPipelines(
|
||||
this.hass
|
||||
);
|
||||
this._pipelines = pipelines;
|
||||
this._preferredPipeline = preferred_pipeline || undefined;
|
||||
}
|
||||
|
||||
private async _selectPipeline(ev: CustomEvent) {
|
||||
this._pipelineId = (ev.currentTarget as any).pipeline;
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
},
|
||||
];
|
||||
prepareConversation(this.hass, this.hass.language);
|
||||
await this.updateComplete;
|
||||
this._scrollMessagesBottom();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -224,7 +324,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
}
|
||||
|
||||
private _addMessage(message: Message) {
|
||||
this._conversation = [...this._conversation, message];
|
||||
this._conversation = [...this._conversation!, message];
|
||||
}
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
@@ -253,75 +353,8 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _initRecognition() {
|
||||
this.recognition = new SpeechRecognition();
|
||||
this.recognition.interimResults = true;
|
||||
this.recognition.lang = this.hass.language;
|
||||
this.recognition.continuous = false;
|
||||
|
||||
this.recognition.addEventListener("start", () => {
|
||||
this.results = {
|
||||
final: false,
|
||||
transcript: "",
|
||||
};
|
||||
});
|
||||
this.recognition.addEventListener("nomatch", () => {
|
||||
this._addMessage({
|
||||
who: "user",
|
||||
text: `<${this.hass.localize(
|
||||
"ui.dialogs.voice_command.did_not_understand"
|
||||
)}>`,
|
||||
error: true,
|
||||
});
|
||||
});
|
||||
this.recognition.addEventListener("error", (event) => {
|
||||
// eslint-disable-next-line
|
||||
console.error("Error recognizing text", event);
|
||||
this.recognition!.abort();
|
||||
// @ts-ignore
|
||||
if (event.error !== "aborted" && event.error !== "no-speech") {
|
||||
const text =
|
||||
this.results && this.results.transcript
|
||||
? this.results.transcript
|
||||
: `<${this.hass.localize(
|
||||
"ui.dialogs.voice_command.did_not_hear"
|
||||
)}>`;
|
||||
this._addMessage({ who: "user", text, error: true });
|
||||
}
|
||||
this.results = null;
|
||||
});
|
||||
this.recognition.addEventListener("end", () => {
|
||||
// Already handled by onerror
|
||||
if (this.results == null) {
|
||||
return;
|
||||
}
|
||||
const text = this.results.transcript;
|
||||
this.results = null;
|
||||
if (text) {
|
||||
this._processText(text);
|
||||
} else {
|
||||
this._addMessage({
|
||||
who: "user",
|
||||
text: `<${this.hass.localize(
|
||||
"ui.dialogs.voice_command.did_not_hear"
|
||||
)}>`,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.recognition.addEventListener("result", (event) => {
|
||||
const result = event.results[0];
|
||||
this.results = {
|
||||
transcript: result[0].transcript,
|
||||
final: result.isFinal,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async _processText(text: string) {
|
||||
if (this.recognition) {
|
||||
this.recognition.abort();
|
||||
}
|
||||
this._audio?.pause();
|
||||
this._addMessage({ who: "user", text });
|
||||
const message: Message = {
|
||||
who: "hass",
|
||||
@@ -330,21 +363,33 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
this._addMessage(message);
|
||||
try {
|
||||
const response = await processConversationInput(
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
text,
|
||||
this._conversationId,
|
||||
this.hass.language
|
||||
(event) => {
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
if (plain) {
|
||||
message.text = plain.speech;
|
||||
}
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
if (event.type === "error") {
|
||||
message.text = event.data.message;
|
||||
message.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
},
|
||||
{
|
||||
start_stage: "intent",
|
||||
input: { text },
|
||||
end_stage: "intent",
|
||||
pipeline: this._pipelineId,
|
||||
conversation_id: this._conversationId,
|
||||
}
|
||||
);
|
||||
this._conversationId = response.conversation_id;
|
||||
const plain = response.response.speech?.plain;
|
||||
if (plain) {
|
||||
message.text = plain.speech;
|
||||
} else {
|
||||
message.text = "<silence>";
|
||||
}
|
||||
|
||||
this.requestUpdate("_conversation");
|
||||
} catch {
|
||||
message.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||
message.error = true;
|
||||
@@ -352,38 +397,216 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleListening() {
|
||||
if (!this.results) {
|
||||
private _toggleListening(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const supportsMicrophone = AudioRecorder.isSupported;
|
||||
if (!supportsMicrophone) {
|
||||
this._showNotSupportedMessage();
|
||||
return;
|
||||
}
|
||||
if (!this._audioRecorder?.active) {
|
||||
this._startListening();
|
||||
} else {
|
||||
this._stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
private _stopListening() {
|
||||
if (this.recognition) {
|
||||
this.recognition.stop();
|
||||
private async _showNotSupportedMessage() {
|
||||
this._addMessage({
|
||||
who: "hass",
|
||||
text: html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||
{
|
||||
documentation_link: html`
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/configuration/securing/#remote-access"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||
)}
|
||||
</a>
|
||||
`,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _startListening() {
|
||||
this._audio?.pause();
|
||||
if (!this._audioRecorder) {
|
||||
this._audioRecorder = new AudioRecorder((audio) => {
|
||||
if (this._audioBuffer) {
|
||||
this._audioBuffer.push(audio);
|
||||
} else {
|
||||
this._sendAudioChunk(audio);
|
||||
}
|
||||
});
|
||||
}
|
||||
this._stt_binary_handler_id = undefined;
|
||||
this._audioBuffer = [];
|
||||
const userMessage: Message = {
|
||||
who: "user",
|
||||
text: "…",
|
||||
};
|
||||
this._audioRecorder.start().then(() => {
|
||||
this._addMessage(userMessage);
|
||||
this.requestUpdate("_audioRecorder");
|
||||
});
|
||||
const hassMessage: Message = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
};
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
(event) => {
|
||||
if (event.type === "run-start") {
|
||||
this._stt_binary_handler_id =
|
||||
event.data.runner_data.stt_binary_handler_id;
|
||||
}
|
||||
|
||||
// When we start STT stage, the WS has a binary handler
|
||||
if (event.type === "stt-start" && this._audioBuffer) {
|
||||
// Send the buffer over the WS to the STT engine.
|
||||
for (const buffer of this._audioBuffer) {
|
||||
this._sendAudioChunk(buffer);
|
||||
}
|
||||
this._audioBuffer = undefined;
|
||||
}
|
||||
|
||||
// Stop recording if the server is done with STT stage
|
||||
if (event.type === "stt-end") {
|
||||
this._stt_binary_handler_id = undefined;
|
||||
this._stopListening();
|
||||
userMessage.text = event.data.stt_output.text;
|
||||
this.requestUpdate("_conversation");
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
if (plain) {
|
||||
hassMessage.text = plain.speech;
|
||||
}
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
|
||||
if (event.type === "tts-end") {
|
||||
const url = event.data.tts_output.url;
|
||||
this._audio = new Audio(url);
|
||||
this._audio.play();
|
||||
this._audio.addEventListener("ended", this._unloadAudio);
|
||||
this._audio.addEventListener("pause", this._unloadAudio);
|
||||
this._audio.addEventListener("canplaythrough", this._playAudio);
|
||||
this._audio.addEventListener("error", this._audioError);
|
||||
}
|
||||
|
||||
if (event.type === "run-end") {
|
||||
this._stt_binary_handler_id = undefined;
|
||||
unsub();
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
this._stt_binary_handler_id = undefined;
|
||||
if (userMessage.text === "…") {
|
||||
userMessage.text = event.data.message;
|
||||
userMessage.error = true;
|
||||
} else {
|
||||
hassMessage.text = event.data.message;
|
||||
hassMessage.error = true;
|
||||
}
|
||||
this._stopListening();
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
},
|
||||
{
|
||||
start_stage: "stt",
|
||||
end_stage: this._pipeline?.tts_engine ? "tts" : "intent",
|
||||
input: { sample_rate: this._audioRecorder.sampleRate! },
|
||||
pipeline: this._pipelineId,
|
||||
conversation_id: this._conversationId,
|
||||
}
|
||||
);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: "Error starting pipeline",
|
||||
text: err.message || err,
|
||||
});
|
||||
this._stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
private _startListening() {
|
||||
if (!this.recognition) {
|
||||
this._initRecognition();
|
||||
private _stopListening() {
|
||||
this._audioRecorder?.stop();
|
||||
this.requestUpdate("_audioRecorder");
|
||||
// We're currently STTing, so finish audio
|
||||
if (this._stt_binary_handler_id) {
|
||||
if (this._audioBuffer) {
|
||||
for (const chunk of this._audioBuffer) {
|
||||
this._sendAudioChunk(chunk);
|
||||
}
|
||||
}
|
||||
// Send empty message to indicate we're done streaming.
|
||||
this._sendAudioChunk(new Int16Array());
|
||||
this._stt_binary_handler_id = undefined;
|
||||
}
|
||||
this._audioBuffer = undefined;
|
||||
}
|
||||
|
||||
if (this.results) {
|
||||
private _sendAudioChunk(chunk: Int16Array) {
|
||||
this.hass.connection.socket!.binaryType = "arraybuffer";
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this._stt_binary_handler_id == undefined) {
|
||||
return;
|
||||
}
|
||||
// Turn into 8 bit so we can prefix our handler ID.
|
||||
const data = new Uint8Array(1 + chunk.length * 2);
|
||||
data[0] = this._stt_binary_handler_id;
|
||||
data.set(new Uint8Array(chunk.buffer), 1);
|
||||
|
||||
this.results = {
|
||||
transcript: "",
|
||||
final: false,
|
||||
};
|
||||
this.recognition!.start();
|
||||
this.hass.connection.socket!.send(data);
|
||||
}
|
||||
|
||||
private _playAudio = () => {
|
||||
this._audio?.play();
|
||||
};
|
||||
|
||||
private _audioError = () => {
|
||||
showAlertDialog(this, { title: "Error playing audio." });
|
||||
this._audio?.removeAttribute("src");
|
||||
};
|
||||
|
||||
private _unloadAudio = () => {
|
||||
this._audio?.removeAttribute("src");
|
||||
this._audio = undefined;
|
||||
};
|
||||
|
||||
private _scrollMessagesBottom() {
|
||||
this._scrollContainer.scrollTo(0, 99999);
|
||||
const scrollContainer = this._scrollContainer;
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
}
|
||||
scrollContainer.scrollTo(0, 99999);
|
||||
}
|
||||
|
||||
private _computeMessageClasses(message: Message) {
|
||||
@@ -394,7 +617,8 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-icon-button.listening-icon {
|
||||
.listening-icon {
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
margin-inline-end: -24px;
|
||||
@@ -402,25 +626,78 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-icon-button.listening-icon[active] {
|
||||
.listening-icon[active] {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.unsupported {
|
||||
color: var(--error-color);
|
||||
position: absolute;
|
||||
--mdc-icon-size: 16px;
|
||||
right: 5px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--primary-action-button-flex: 1;
|
||||
--secondary-action-button-flex: 0;
|
||||
--mdc-dialog-max-width: 450px;
|
||||
--mdc-dialog-max-width: 500px;
|
||||
--mdc-dialog-max-height: 500px;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
--header-height: 64px;
|
||||
}
|
||||
ha-header-bar a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
div[slot="title"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 8px;
|
||||
}
|
||||
ha-button-menu {
|
||||
--mdc-theme-on-primary: var(--text-primary-color);
|
||||
--mdc-theme-primary: var(--primary-color);
|
||||
margin: -8px 0 0 -8px;
|
||||
}
|
||||
ha-button-menu ha-button {
|
||||
--mdc-theme-primary: var(--secondary-text-color);
|
||||
--mdc-typography-button-text-transform: none;
|
||||
--mdc-typography-button-font-size: unset;
|
||||
--mdc-typography-button-font-weight: 400;
|
||||
--mdc-typography-button-letter-spacing: var(
|
||||
--mdc-typography-headline6-letter-spacing,
|
||||
0.0125em
|
||||
);
|
||||
--mdc-typography-button-line-height: var(
|
||||
--mdc-typography-headline6-line-height,
|
||||
2rem
|
||||
);
|
||||
--button-height: auto;
|
||||
}
|
||||
ha-button-menu ha-button ha-svg-icon {
|
||||
height: 28px;
|
||||
margin-left: 4px;
|
||||
margin-inline-start: 4px;
|
||||
margin-inline-end: 4px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-meta-size: 16px;
|
||||
}
|
||||
ha-list-item ha-svg-icon {
|
||||
margin-left: 4px;
|
||||
margin-inline-start: 4px;
|
||||
margin-inline-end: 4px;
|
||||
direction: var(--direction);
|
||||
display: block;
|
||||
}
|
||||
ha-button-menu a {
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
@@ -440,16 +717,24 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
padding: 4px;
|
||||
}
|
||||
.attribution {
|
||||
display: block;
|
||||
color: var(--secondary-text-color);
|
||||
padding-top: 4px;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
.messages {
|
||||
display: block;
|
||||
height: 300px;
|
||||
height: 400px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 100%;
|
||||
}
|
||||
.messages {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.messages-container {
|
||||
@@ -469,6 +754,12 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
padding: 8px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.message p {
|
||||
margin: 0;
|
||||
}
|
||||
.message p:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
@@ -512,10 +803,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.interimTranscript {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
@@ -1,4 +1,8 @@
|
||||
<meta charset="utf-8">
|
||||
<link rel='manifest' href='/manifest.json' crossorigin="use-credentials">
|
||||
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||
<%= renderTemplate('_style_base') %>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="icon" href="/static/icons/favicon.ico" />
|
||||
<% if (!useWDS) { %>
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
<link rel="modulepreload" href="<%= entry %>" crossorigin="use-credentials" />
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
@@ -9,13 +9,6 @@
|
||||
script.src = src;
|
||||
return script;
|
||||
}
|
||||
window.Polymer = {
|
||||
lazyRegister: true,
|
||||
useNativeCSSProperties: true,
|
||||
dom: "shadow",
|
||||
suppressTemplateNotifications: true,
|
||||
suppressBindingNotifications: true,
|
||||
};
|
||||
window.polymerSkipLoadingFontRoboto = true;
|
||||
if (!("customElements" in window &&
|
||||
"content" in document.createElement("template"))) {
|
||||
|
17
src/html/_script_load_es5.html.template
Normal file
17
src/html/_script_load_es5.html.template
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
(function() {
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
System.import("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
<% } else { %>
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
_ls("<%= entry %>");
|
||||
<% } %>
|
||||
<% } %>
|
||||
}
|
||||
})();
|
||||
</script>
|
@@ -2,8 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Home Assistant</title>
|
||||
<link rel="modulepreload" href="<%= latestPageJS %>" crossorigin="use-credentials" />
|
||||
<%= renderTemplate('_header') %>
|
||||
<%= renderTemplate("_header.html.template") %>
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<style>
|
||||
.content {
|
||||
padding: 20px 16px;
|
||||
@@ -38,36 +38,23 @@
|
||||
</div>
|
||||
<ha-authorize><p>Initializing</p></ha-authorize>
|
||||
</div>
|
||||
|
||||
<%= renderTemplate('_js_base') %>
|
||||
<%= renderTemplate('_preload_roboto') %>
|
||||
|
||||
<%= renderTemplate("_js_base.html.template") %>
|
||||
<%= renderTemplate("_preload_roboto.html.template") %>
|
||||
<script crossorigin="use-credentials">
|
||||
if (!window.globalThis) {
|
||||
window.globalThis = window;
|
||||
}
|
||||
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
|
||||
if (!isS11_12) {
|
||||
import("<%= latestPageJS %>");
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
window.providersPromise = fetch("/auth/providers", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
System.import("<%= es5PageJS %>");
|
||||
}
|
||||
<% } else { %>
|
||||
_ls("<%= es5PageJS %>");
|
||||
<% } %>
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<%= renderTemplate("_script_load_es5.html.template") %>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,12 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<% if (!useWDS) { %>
|
||||
<link rel="modulepreload" href="<%= latestCoreJS %>" crossorigin="use-credentials" />
|
||||
<link rel="modulepreload" href="<%= latestAppJS %>" crossorigin="use-credentials" />
|
||||
<% } %>
|
||||
<%= renderTemplate('_header') %>
|
||||
<title>Home Assistant</title>
|
||||
<%= renderTemplate("_header.html.template") %>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
@@ -36,8 +32,9 @@
|
||||
<meta name="msapplication-TileColor" content="#03a9f4ff" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="referrer" content="same-origin" />
|
||||
<meta name="theme-color" content="#THEMEC" />
|
||||
<meta name="theme-color" content="{{ theme_color }}" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<style>
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
@@ -83,20 +80,18 @@
|
||||
</svg>
|
||||
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
|
||||
</div>
|
||||
|
||||
<home-assistant></home-assistant>
|
||||
|
||||
<%= renderTemplate('_js_base') %>
|
||||
<%= renderTemplate('_preload_roboto') %>
|
||||
|
||||
<%= renderTemplate("_js_base.html.template") %>
|
||||
<%= renderTemplate("_preload_roboto.html.template") %>
|
||||
<script <% if (!useWDS) { %>crossorigin="use-credentials"<% } %>>
|
||||
if (!window.globalThis) {
|
||||
window.globalThis = window;
|
||||
}
|
||||
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
|
||||
if (!isS11_12) {
|
||||
import("<%= latestCoreJS %>");
|
||||
import("<%= latestAppJS %>");
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.customPanelJS = "<%= latestCustomPanelJS %>";
|
||||
window.latestJS = true;
|
||||
}
|
||||
@@ -106,7 +101,6 @@
|
||||
import("{{ extra_module }}");
|
||||
{%- endfor -%}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if (!window.latestJS) {
|
||||
window.customPanelJS = "<%= es5CustomPanelJS %>";
|
||||
@@ -115,13 +109,14 @@
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
// Although core and app can load in any order, we need to
|
||||
// force loading core first because it contains polyfills
|
||||
return System.import("<%= es5CoreJS %>").then(function() {
|
||||
System.import("<%= es5AppJS %>");
|
||||
return System.import("<%= es5EntryJS[0] %>").then(function() {
|
||||
System.import("<%= es5EntryJS[1] %>");
|
||||
});
|
||||
}
|
||||
<% } else { %>
|
||||
_ls("<%= es5CoreJS %>");
|
||||
_ls("<%= es5AppJS %>");
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
_ls("<%= entry %>");
|
||||
<% } %>
|
||||
<% } %>
|
||||
}
|
||||
</script>
|
||||
|
@@ -2,12 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Home Assistant</title>
|
||||
<link
|
||||
rel="modulepreload"
|
||||
href="<%= latestPageJS %>"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<%= renderTemplate('_header') %>
|
||||
<%= renderTemplate("_header.html.template") %>
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<style>
|
||||
html {
|
||||
color: var(--primary-text-color, #212121);
|
||||
@@ -70,39 +66,25 @@
|
||||
<img src="/static/icons/favicon-192x192.png" height="52" width="52" alt="" />
|
||||
Home Assistant
|
||||
</div>
|
||||
|
||||
<ha-onboarding></ha-onboarding>
|
||||
</div>
|
||||
|
||||
<%= renderTemplate('_js_base') %>
|
||||
<%= renderTemplate('_preload_roboto') %>
|
||||
|
||||
<%= renderTemplate("_js_base.html.template") %>
|
||||
<%= renderTemplate("_preload_roboto.html.template") %>
|
||||
<script crossorigin="use-credentials">
|
||||
if (!window.globalThis) {
|
||||
window.globalThis = window;
|
||||
}
|
||||
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
|
||||
if (!isS11_12) {
|
||||
import("<%= latestPageJS %>");
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
window.stepsPromise = fetch("/api/onboarding", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
System.import("<%= es5PageJS %>");
|
||||
}
|
||||
<% } else { %>
|
||||
_ls("<%= es5PageJS %>");
|
||||
<% } %>
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<%= renderTemplate("_script_load_es5.html.template") %>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -11,7 +11,6 @@ import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../common/config/version";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import "../components/ha-card";
|
||||
import "../resources/ha-style";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./hass-subpage";
|
||||
|
@@ -1,10 +1,25 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { createCountryListEl } from "../components/country-datalist";
|
||||
import { createCurrencyListEl } from "../components/currency-datalist";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-formfield";
|
||||
import "../components/ha-radio";
|
||||
import type { HaRadio } from "../components/ha-radio";
|
||||
import "../components/ha-textfield";
|
||||
import type { HaTextField } from "../components/ha-textfield";
|
||||
import { createLanguageListEl } from "../components/language-datalist";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import type {
|
||||
HaLocationsEditor,
|
||||
@@ -20,14 +35,7 @@ import { SYMBOL_TO_ISO } from "../data/currency";
|
||||
import { onboardCoreConfigStep } from "../data/onboarding";
|
||||
import type { PolymerChangedEvent } from "../polymer-types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "../components/ha-radio";
|
||||
import "../components/ha-formfield";
|
||||
import type { HaRadio } from "../components/ha-radio";
|
||||
import type { HaTextField } from "../components/ha-textfield";
|
||||
import "../components/ha-textfield";
|
||||
import { getLocalLanguage } from "../util/common-translation";
|
||||
import { createCountryListEl } from "../components/country-datalist";
|
||||
import { createLanguageListEl } from "../components/language-datalist";
|
||||
|
||||
const amsterdam: [number, number] = [52.3731339, 4.8903147];
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
@@ -57,10 +65,18 @@ class OnboardingCoreConfig extends LitElement {
|
||||
|
||||
@state() private _country?: ConfigUpdateValues["country"];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${
|
||||
this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<p>
|
||||
${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.intro",
|
||||
@@ -114,21 +130,26 @@ class OnboardingCoreConfig extends LitElement {
|
||||
<div class="row">
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.country"
|
||||
)}
|
||||
.label=${
|
||||
this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.country"
|
||||
) || "Country"
|
||||
}
|
||||
name="country"
|
||||
required
|
||||
.disabled=${this._working}
|
||||
.value=${this._countryValue}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.language"
|
||||
)}
|
||||
.label=${
|
||||
this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.language"
|
||||
) || "Language"
|
||||
}
|
||||
name="language"
|
||||
required
|
||||
.disabled=${this._working}
|
||||
.value=${this._languageValue}
|
||||
@change=${this._handleChange}
|
||||
@@ -251,7 +272,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(
|
||||
() => this.shadowRoot!.querySelector("ha-textfield")!.focus(),
|
||||
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||
100
|
||||
);
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
@@ -259,39 +280,43 @@ class OnboardingCoreConfig extends LitElement {
|
||||
this._save(ev);
|
||||
}
|
||||
});
|
||||
const tzInput = this.shadowRoot!.querySelector(
|
||||
const tzInput = this.renderRoot.querySelector(
|
||||
"[name=timeZone]"
|
||||
) as HaTextField;
|
||||
tzInput.updateComplete.then(() => {
|
||||
tzInput.shadowRoot!.appendChild(createTimezoneListEl());
|
||||
tzInput.renderRoot.appendChild(createTimezoneListEl());
|
||||
tzInput.formElement.setAttribute("list", "timezones");
|
||||
});
|
||||
|
||||
const curInput = this.shadowRoot!.querySelector(
|
||||
const curInput = this.renderRoot.querySelector(
|
||||
"[name=currency]"
|
||||
) as HaTextField;
|
||||
curInput.updateComplete.then(() => {
|
||||
curInput.shadowRoot!.appendChild(
|
||||
curInput.renderRoot.appendChild(
|
||||
createCurrencyListEl(this.hass.locale.language)
|
||||
);
|
||||
curInput.formElement.setAttribute("list", "currencies");
|
||||
});
|
||||
|
||||
const countryInput = this.shadowRoot!.querySelector(
|
||||
const countryInput = this.renderRoot.querySelector(
|
||||
"[name=country]"
|
||||
) as HaTextField;
|
||||
countryInput.updateComplete.then(() => {
|
||||
countryInput.shadowRoot!.appendChild(
|
||||
countryInput.renderRoot.appendChild(
|
||||
createCountryListEl(this.hass.locale.language)
|
||||
);
|
||||
|
||||
countryInput.formElement.setAttribute("list", "countries");
|
||||
});
|
||||
|
||||
const langInput = this.shadowRoot!.querySelector(
|
||||
const langInput = this.renderRoot.querySelector(
|
||||
"[name=language]"
|
||||
) as HaTextField;
|
||||
langInput.updateComplete.then(() => {
|
||||
langInput.shadowRoot!.appendChild(createLanguageListEl(this.hass));
|
||||
langInput.renderRoot.appendChild(createLanguageListEl(this.hass));
|
||||
langInput.renderRoot
|
||||
.querySelector("#label")
|
||||
?.classList.add("mdc-floating-label--required");
|
||||
langInput.formElement.setAttribute("list", "languages");
|
||||
});
|
||||
}
|
||||
@@ -401,7 +426,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
}
|
||||
this._language = getLocalLanguage();
|
||||
} catch (err: any) {
|
||||
alert(`Failed to detect location information: ${err.message}`);
|
||||
this._error = `Failed to detect location information: ${err.message}`;
|
||||
} finally {
|
||||
this._working = false;
|
||||
}
|
||||
@@ -430,7 +455,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._working = false;
|
||||
alert(`Failed to save: ${err.message}`);
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -48,7 +48,7 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public onboardingLocalize!: LocalizeFunc;
|
||||
|
||||
@state() private _entries?: ConfigEntry[];
|
||||
@state() private _entries: ConfigEntry[] = [];
|
||||
|
||||
@state() private _discovered?: DataEntryFlowProgress[];
|
||||
|
||||
@@ -105,7 +105,7 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._entries || !this._discovered) {
|
||||
if (!this._discovered) {
|
||||
return nothing;
|
||||
}
|
||||
// Render discovered and existing entries together sorted by localized title.
|
||||
|
@@ -125,8 +125,6 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
ha-dialog {
|
||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
|
@@ -498,12 +498,22 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
this._submitting = false;
|
||||
return;
|
||||
}
|
||||
const eventData = this._calculateData();
|
||||
if (eventData.rrule && range === RecurrenceRange.THISEVENT) {
|
||||
// Updates to a single instance of a recurring event by definition
|
||||
// cannot change the recurrence rule and doing so would be invalid.
|
||||
// It is difficult to detect if the user changed the recurrence rule
|
||||
// since updating the date may change it implicitly (e.g. day of week
|
||||
// of the event changes) so we just assume the users intent based on
|
||||
// recurrence range and drop any other rrule changes.
|
||||
eventData.rrule = undefined;
|
||||
}
|
||||
try {
|
||||
await updateCalendarEvent(
|
||||
this.hass!,
|
||||
this._calendarId!,
|
||||
entry.uid!,
|
||||
this._calculateData(),
|
||||
eventData,
|
||||
entry.recurrence_id || "",
|
||||
range!
|
||||
);
|
||||
|
@@ -21,7 +21,6 @@ import {
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
@@ -222,10 +221,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
hass-loading-screen {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
|
@@ -520,7 +520,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
}
|
||||
|
||||
.main {
|
||||
height: calc(100% - var(--header-height));
|
||||
min-height: calc(100% - var(--header-height));
|
||||
display: flex;
|
||||
background-color: var(--card-background-color);
|
||||
direction: ltr;
|
||||
|
@@ -8,6 +8,7 @@ import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-language-picker";
|
||||
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
|
||||
import {
|
||||
CloudTTSInfo,
|
||||
@@ -54,34 +55,33 @@ export class CloudTTSPref extends LitElement {
|
||||
'"tts.cloud_say"'
|
||||
)}
|
||||
<br /><br />
|
||||
<div class="row">
|
||||
<ha-language-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.tts.default_language"
|
||||
)}
|
||||
.disabled=${this.savingPreferences}
|
||||
.value=${defaultVoice[0]}
|
||||
.languages=${languages}
|
||||
@value-changed=${this._handleLanguageChange}
|
||||
>
|
||||
</ha-language-picker>
|
||||
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.tts.default_language"
|
||||
)}
|
||||
.disabled=${this.savingPreferences}
|
||||
.value=${defaultVoice[0]}
|
||||
@selected=${this._handleLanguageChange}
|
||||
>
|
||||
${languages.map(
|
||||
([key, label]) =>
|
||||
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.tts.default_gender"
|
||||
)}
|
||||
.disabled=${this.savingPreferences}
|
||||
.value=${defaultVoice[1]}
|
||||
@selected=${this._handleGenderChange}
|
||||
>
|
||||
${genders.map(
|
||||
([key, label]) =>
|
||||
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.tts.default_gender"
|
||||
)}
|
||||
.disabled=${this.savingPreferences}
|
||||
.value=${defaultVoice[1]}
|
||||
@selected=${this._handleGenderChange}
|
||||
>
|
||||
${genders.map(
|
||||
([key, label]) =>
|
||||
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._openTryDialog}>
|
||||
@@ -115,11 +115,11 @@ export class CloudTTSPref extends LitElement {
|
||||
}
|
||||
|
||||
async _handleLanguageChange(ev) {
|
||||
if (ev.target.value === this.cloudStatus!.prefs.tts_default_voice[0]) {
|
||||
if (ev.detail.value === this.cloudStatus!.prefs.tts_default_voice[0]) {
|
||||
return;
|
||||
}
|
||||
this.savingPreferences = true;
|
||||
const language = ev.target.value;
|
||||
const language = ev.detail.value;
|
||||
|
||||
const curGender = this.cloudStatus!.prefs.tts_default_voice[1];
|
||||
const genders = this.getSupportedGenders(
|
||||
@@ -185,6 +185,18 @@ export class CloudTTSPref extends LitElement {
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
}
|
||||
.row > * {
|
||||
flex: 1;
|
||||
}
|
||||
.row > *:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.row > *:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
@@ -12,12 +12,16 @@ import "../../../../components/ha-icon-next";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-textfield";
|
||||
import { cloudLogin } from "../../../../data/cloud";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import "../../../../styles/polymer-ha-style";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "../../ha-config-section";
|
||||
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
|
||||
|
||||
@customElement("cloud-login")
|
||||
export class CloudLogin extends LitElement {
|
||||
@@ -210,10 +214,24 @@ export class CloudLogin extends LitElement {
|
||||
this._requestInProgress = true;
|
||||
|
||||
try {
|
||||
await cloudLogin(this.hass, email, password);
|
||||
const result = await cloudLogin(this.hass, email, password);
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
if (result.cloud_pipeline) {
|
||||
if (
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
||||
),
|
||||
})
|
||||
) {
|
||||
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "PasswordChangeRequired") {
|
||||
|
@@ -6,13 +6,16 @@ import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
|
||||
import { getCountryOptions } from "../../../components/country-datalist";
|
||||
import { getCurrencyOptions } from "../../../components/currency-datalist";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-language-picker";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
@@ -22,13 +25,10 @@ import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { SYMBOL_TO_ISO } from "../../../data/currency";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-alert";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HaCheckbox } from "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-checkbox";
|
||||
|
||||
@customElement("ha-config-section-general")
|
||||
class HaConfigSectionGeneral extends LitElement {
|
||||
@@ -54,8 +54,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
@state() private _languages?: { value: string; label: string }[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _updateUnits?: boolean;
|
||||
@@ -255,25 +253,19 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
</mwc-list-item>`
|
||||
)}</ha-select
|
||||
>
|
||||
<ha-select
|
||||
<ha-language-picker
|
||||
.hass=${this.hass}
|
||||
nativeName
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.language"
|
||||
)}
|
||||
name="language"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._language}
|
||||
.disabled=${disabled}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${this._languages?.map(
|
||||
({ value, label }) =>
|
||||
html`<mwc-list-item .value=${value}
|
||||
>${label}</mwc-list-item
|
||||
>`
|
||||
)}</ha-select
|
||||
@value-changed=${this._handleLanguageChange}
|
||||
>
|
||||
</ha-language-picker>
|
||||
</div>
|
||||
${this.narrow
|
||||
? html`
|
||||
@@ -330,25 +322,10 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
this._timeZone = this.hass.config.time_zone || "Etc/GMT";
|
||||
this._name = this.hass.config.location_name;
|
||||
this._updateUnits = true;
|
||||
this._computeLanguages();
|
||||
}
|
||||
|
||||
private _computeLanguages() {
|
||||
if (!this.hass.translationMetadata?.translations) {
|
||||
return;
|
||||
}
|
||||
this._languages = Object.entries(this.hass.translationMetadata.translations)
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a[1].nativeName,
|
||||
b[1].nativeName,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
.map(([value, metaData]) => ({
|
||||
value,
|
||||
label: metaData.nativeName,
|
||||
}));
|
||||
private _handleLanguageChange(ev) {
|
||||
this._language = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
|
@@ -62,7 +62,6 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
|
@@ -3,8 +3,8 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
@@ -28,8 +28,9 @@ import "../../../helpers/forms/ha-input_select-form";
|
||||
import "../../../helpers/forms/ha-input_text-form";
|
||||
import "../../../helpers/forms/ha-schedule-form";
|
||||
import "../../../helpers/forms/ha-timer-form";
|
||||
import "../../entity-registry-basic-editor";
|
||||
import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor";
|
||||
import "../../../voice-assistants/entity-voice-settings";
|
||||
import "../../entity-registry-settings-editor";
|
||||
import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor";
|
||||
|
||||
@customElement("entity-settings-helper-tab")
|
||||
export class EntityRegistrySettingsHelper extends LitElement {
|
||||
@@ -45,8 +46,8 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
|
||||
@state() private _componentLoaded?: boolean;
|
||||
|
||||
@query("ha-registry-basic-editor")
|
||||
private _registryEditor?: HaEntityRegistryBasicEditor;
|
||||
@query("entity-registry-settings-editor")
|
||||
private _registryEditor?: EntityRegistrySettingsEditor;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
@@ -75,7 +76,9 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
const stateObj = this.hass.states[this.entry.entity_id];
|
||||
return html`
|
||||
<div class="form">
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${!this._componentLoaded
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.helper_settings.platform_not_loaded",
|
||||
@@ -93,10 +96,14 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
})}
|
||||
</span>
|
||||
`}
|
||||
<ha-registry-basic-editor
|
||||
<entity-registry-settings-editor
|
||||
.hass=${this.hass}
|
||||
.entry=${this.entry}
|
||||
></ha-registry-basic-editor>
|
||||
.disabled=${this._submitting}
|
||||
@change=${this._entityRegistryChanged}
|
||||
hideName
|
||||
hideIcon
|
||||
></entity-registry-settings-editor>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<mwc-button
|
||||
@@ -117,6 +124,10 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entityRegistryChanged() {
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
this._error = undefined;
|
||||
this._item = ev.detail.value;
|
||||
@@ -137,7 +148,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
this._item
|
||||
);
|
||||
}
|
||||
await this._registryEditor?.updateEntry();
|
||||
await this._registryEditor!.updateEntry();
|
||||
fireEvent(this, "close-dialog");
|
||||
} catch (err: any) {
|
||||
this._error = err.message || "Unknown error";
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user