Compare commits

..

164 Commits

Author SHA1 Message Date
Paulus Schoutsen
a769f84755 Bumped version to 20220226.0 2022-02-26 13:28:52 -08:00
Bram Kragten
7abf9c2473 Fix condition time (#11866)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-26 21:18:33 +00:00
Paulus Schoutsen
298296a81f Fix iOS audio (#11863) 2022-02-26 15:08:42 -06:00
Marc Mueller
6907fa5c8e Add py.typed (#11865) 2022-02-26 13:03:12 -08:00
Zack Barett
546461b70f Fix Render Pane on Mobile (#11856) 2022-02-25 11:51:55 -08:00
Joakim Sørensen
4031009c26 Only set tip once (#11853) 2022-02-25 10:03:55 -06:00
Paulus Schoutsen
91e4557625 Guard controls in more info media player (#11851) 2022-02-25 01:31:59 -06:00
Paulus Schoutsen
f0c4b92dbb Small fixes for actions (#11850) 2022-02-24 21:48:54 -08:00
Zack Barett
04ae8c9d14 Bumped version to 20220224.0 (#11847) 2022-02-24 16:22:09 -08:00
Zack Barett
0158610d42 Finishing up the editors (#11846) 2022-02-24 23:35:28 +00:00
Bram Kragten
5ab6121581 Fix quickbar showing on ha-select (#11845) 2022-02-24 14:24:32 -08:00
Bram Kragten
3d9c31aef9 Allow to clear integration filter on mobile (#11844) 2022-02-24 14:21:25 -08:00
Paulus Schoutsen
acfeea5c92 Show browse media even if media player has no controls (#11843) 2022-02-24 14:17:33 -08:00
Zack Barett
75e8e17073 Statistics Graph Editor to Ha Form (#11820) 2022-02-24 22:49:43 +01:00
Zack Barett
976fd4b32d weather forecast editor to HA form (#11823) 2022-02-24 22:49:20 +01:00
Bram Kragten
49beafbe5f Fix dev states filter field on iOS (#11839) 2022-02-24 21:57:20 +01:00
Bram Kragten
151f8d5524 Fix time trigger (#11841) 2022-02-24 14:36:18 -06:00
Bram Kragten
48355aa98e Fix icon color in select (#11842) 2022-02-24 14:35:09 -06:00
Paulus Schoutsen
fc31929f41 Fix saving home name (#11838) 2022-02-24 18:51:56 +00:00
Paulus Schoutsen
b7c149fcc1 Fix timer entity exception (#11837) 2022-02-24 10:30:45 -08:00
Paulus Schoutsen
02d058561b Lovelace: Datetime and text to match look select (#11836) 2022-02-24 18:16:42 +00:00
Zack Barett
4e57fb1ec1 Fix for Sidebar view rendering issue (#11835) 2022-02-24 10:15:34 -08:00
Patrick ZAJDA
30f79c5a46 Ask confirmation before logging out from Home Assistant Cloud (#11833)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-02-24 10:13:19 -08:00
Zack Barett
30f7252d84 Update Media Browser Bar to show image on mobile but a bit less (#11818) 2022-02-24 11:56:20 -06:00
Bram Kragten
8af795a7ce Make automation name field wide (#11832) 2022-02-24 09:52:40 -06:00
Joakim Sørensen
8576eeae41 Add tip rotation on dashboard (#11826)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-24 09:07:32 -06:00
Paulus Schoutsen
cd740ed135 Fix cast receiver background (#11825)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-24 10:52:12 +00:00
Zack Barett
892f774792 Picture Glance to HA Form (#11821) 2022-02-23 21:40:04 -08:00
Zack Barett
aa504fe1f8 Shopping list to MWC (#11822) 2022-02-23 21:36:58 -08:00
Paulus Schoutsen
be491451d5 Add run action to dropdown (#11817) 2022-02-23 21:36:25 -08:00
Zack Barett
bad184210d Control where the tip breaks (#11819) 2022-02-23 19:42:32 -06:00
Bram Kragten
a43b3b64b3 Link to filtered logs (#11816) 2022-02-23 23:14:16 +01:00
Bram Kragten
aa831a9adf bump marked and flatmap (#11814) 2022-02-23 22:11:13 +01:00
Bram Kragten
43d4f55392 Update workbox (#11813)
* Update workbox

* dedupe
2022-02-23 22:10:16 +01:00
Bram Kragten
130c66fb24 Update yarn (#11812)
* Update yarn

* Update yarn.lock

* dedupe
2022-02-23 22:08:43 +01:00
Zack Barett
684c232c8c Sensor Card Editor to Ha Form (#11810)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-23 21:46:52 +01:00
Bram Kragten
1719d062b3 mwc-select -> ha-select (#11806) 2022-02-23 18:59:36 +00:00
Bram Kragten
87290c4330 Merge branch 'master' into dev 2022-02-23 19:52:13 +01:00
Bram Kragten
fec0dc0032 Bumped version to 20220223.0 2022-02-23 19:51:07 +01:00
Zack Barett
70ca27c8c9 Button editor to ha-form (#11808) 2022-02-23 19:49:15 +01:00
Zack Barett
9ae1f01ad6 Grid Card to HA Form (#11798) 2022-02-23 18:51:40 +01:00
Zack Barett
0113cc3cf6 Glance editor to ha-form (#11804) 2022-02-23 09:48:18 -08:00
Zack Barett
2a98ace0b3 History Graph Editor to ha form (#11797) 2022-02-23 17:15:17 +00:00
Zack Barett
5f69a4c165 Graph Footer to MWC (#11803) 2022-02-23 17:31:33 +01:00
Zack Barett
8db22d4f88 Calendar card to HA Form (#11784) 2022-02-23 17:28:49 +01:00
Tomasz
3204dbfc4d change repository url and project description (#11801) 2022-02-23 17:27:23 +01:00
Bram Kragten
430b47fc4a Migrate single textfields (#11799)
* Migrate single textfields

* Update ha-config-name-form.ts

* Update dialog-area-registry-detail.ts

* Update manual-automation-editor.ts

* Update manual-automation-editor.ts

* required to number selector fix script

* review
2022-02-23 17:27:03 +01:00
Zack Barett
5d8b3227f3 Fix Entities picker (#11802) 2022-02-23 16:18:56 +00:00
Bram Kragten
b341ee9d38 Stop spinning when opening media in dialog (#11800) 2022-02-23 16:13:09 +01:00
Zack Barett
e6dbbc31a8 Gauge Editor to Ha Form (#11793) 2022-02-23 15:35:58 +01:00
Bram Kragten
0010bf5a8f Input conversion in dev tools (#11795) 2022-02-23 15:24:00 +01:00
Bram Kragten
6e2e80a297 Dont render double label on number selector (#11796) 2022-02-23 07:52:52 -06:00
Zack Barett
aa9ff01030 Add Margin to Tip (#11790)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-23 14:32:50 +01:00
Bram Kragten
7f8ecf57d7 Convert more info lock (#11794) 2022-02-23 07:09:13 -06:00
Bram Kragten
6be6755f6f Migrate more-info configurator (#11792)
* Migrate more-info configurator

* Update more-info-configurator.ts

* Update src/dialogs/more-info/controls/more-info-configurator.ts

* Update src/dialogs/more-info/controls/more-info-configurator.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Import

Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-23 14:02:19 +01:00
Bram Kragten
64459a06c6 Convert alarm control panel more info (#11791)
* Convert alarm control panel more info

* Update more-info-alarm_control_panel.ts

* Update src/dialogs/more-info/controls/more-info-alarm_control_panel.ts

* Apply suggestions from code review

Co-authored-by: Zack Barett <zackbarett@hey.com>

* import

Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-23 14:01:44 +01:00
Paulus Schoutsen
df35496c6e Add media management dialog (#11787)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-23 05:43:49 -06:00
Bram Kragten
aa988c758d Put volume slider in the middle of the button (#11788) 2022-02-23 04:34:06 -06:00
Paulus Schoutsen
1dd1095d19 Show number of hidden items (#11786) 2022-02-23 04:31:46 -06:00
Zack Barett
7e68393c84 Condition Card Editor to MWC (#11783) 2022-02-23 09:42:06 +01:00
Robin Wittebol
540c06c9f7 Fix ripple corner radius for button card (#11780) 2022-02-22 21:18:20 -08:00
Zack Barett
f633cc2b0d entities card editor to MWC (#11785) 2022-02-22 21:16:54 -08:00
Zack Barett
1baaf76471 Fix State Condition 'For' Data (#11782) 2022-02-22 23:22:04 +00:00
Paulus Schoutsen
8263e299a8 Bumped version to 20220222.0 2022-02-22 15:03:52 -08:00
Zack Barett
ebd6a26554 Add community section (#11779) 2022-02-22 23:03:37 +00:00
Paulus Schoutsen
5335772a7a Allow changing volume media player entity (#11781)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-22 14:51:25 -08:00
Paulus Schoutsen
f5b5414461 Show triggered in automation editor (#11771)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-22 23:03:32 +01:00
Bram Kragten
1e6f402d0f Include scoped custom element polyfill (#11776) 2022-02-22 09:32:56 -08:00
Bram Kragten
ed9d886009 no need for memoize 2022-02-22 13:38:44 +01:00
Pascal Winters
940f5c0002 Change icons for cover with device_class curtain (#11752)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-22 13:33:08 +01:00
Zack Barett
15d1b8b2ac Alarm Card Editor to HA Form (#11760)
* Move to ha-form

* Update hui-alarm-panel-card-editor.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-22 11:00:13 +01:00
Zack Barett
73855e6f99 Thermostat Editor to HA - Form (#11763)
* Thermostat - Ha Form

* Update hui-thermostat-card-editor.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-22 10:56:04 +01:00
Paulus Schoutsen
d230541256 Only show description when set (#11772) 2022-02-22 09:57:56 +01:00
Bram Kragten
b1f369a355 Paper input migrations (#11766) 2022-02-21 23:09:13 +01:00
Bram Kragten
e6d1e86c64 set theme to undefined when no theme (#11765) 2022-02-21 15:56:10 -06:00
Allen Porter
eb1f94c370 Fix WebRTC player stream playback when disconnected/connected (#11764) 2022-02-21 20:35:37 +00:00
Zack Barett
27750b8b5d Area Card Editor to Ha Form (#11762) 2022-02-21 13:21:21 -06:00
J. Nick Koston
564a725284 Allow config entries to be reloaded when they are in setup_retry state (#11759) 2022-02-21 10:52:59 -08:00
Bram Kragten
a5ee610af5 Fix zwave migration (#11751) 2022-02-21 10:52:09 -08:00
Joakim Sørensen
eaf97ee7f5 Show Home Assistant when creating partial backup (#11758) 2022-02-21 09:33:02 -08:00
Paulus Schoutsen
a14d75deec Add support for the media browser My link (#11757) 2022-02-21 11:21:29 -06:00
Paulus Schoutsen
72b5721c88 Radio Browser is now added during onboarding (#11756) 2022-02-21 09:12:15 -08:00
Bram Kragten
94b4b818aa Convert date-range-picker to mwc (#11755) 2022-02-21 16:48:31 +00:00
Bram Kragten
98699b640a Selector: remove text value when not required and empty (#11754) 2022-02-21 16:37:29 +00:00
Zack Barett
decc0d3e0d Convert Automation Actions to mwc/ha-form + other automation items (#11753) 2022-02-21 16:37:11 +00:00
Steve Repsher
2281f5bafa Set initial focus for supervisor dialogs (#11710) 2022-02-21 17:02:55 +01:00
Zack Barett
6cac7eeff0 Lovelace Entity Card Editor to Ha Form - Adds Theme Selector and HaFormColumn (#11731)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-21 16:53:03 +01:00
Erik Montnemery
794bc161c8 Merge pull request #11716 from emontnemery/remove_config_entry_from_device
Add support for removing config entry from a device
2022-02-21 12:36:07 +00:00
Paulus Schoutsen
28cd9b6408 Show when media is being loaded (#11750) 2022-02-21 09:55:01 +01:00
Paulus Schoutsen
9b4c6eea63 Handle inifinity media duration (#11749) 2022-02-21 04:07:10 +00:00
Paulus Schoutsen
afe044d152 Fix media upload on iOS (#11740) 2022-02-20 10:53:25 -06:00
Paulus Schoutsen
dc2038916b Improve logo rendering for playing media in browser (#11741) 2022-02-20 10:53:03 -06:00
Paulus Schoutsen
cf8e2a6d02 TTS form no longer showed due to import oopsie (#11742) 2022-02-20 10:52:38 -06:00
Paulus Schoutsen
3269b2878b Add link to the selector docs 2022-02-19 22:13:42 -08:00
Paulus Schoutsen
29e1b7b452 Bumped version to 20220220.0 2022-02-19 21:36:14 -08:00
Paulus Schoutsen
3d6d07e5bd Pass hass to ha-form to enable selectors (#11739) 2022-02-19 21:35:58 -08:00
Paulus Schoutsen
7bac41fe41 Update media player more info (#11734) 2022-02-19 00:57:54 +00:00
Paulus Schoutsen
6e4b027575 Change words for trigger condition (#11733) 2022-02-19 00:34:17 +00:00
Paulus Schoutsen
728c391b5d Show why relayer is reconnecting (#11732) 2022-02-18 16:06:19 -08:00
Zack Barett
8999ca2ea0 Entity Settings Page to MWC 3 (#11694) 2022-02-18 12:51:37 -08:00
Steve Repsher
4fc0617289 Set initial focus for energy dialogs (#11730) 2022-02-18 14:48:59 -06:00
Zack Barett
494cc3a569 Automation Conditions to conversion to ha-form or mwc (#11727) 2022-02-18 14:48:17 -06:00
Erik Montnemery
cc177ef911 Remove custom Tasmota delete device button (#11725) 2022-02-18 12:40:09 -08:00
Paulus Schoutsen
41ec65ef3d Merge pull request #11729 from home-assistant/20220203.1 2022-02-18 11:11:24 -08:00
Bram Kragten
79e1e195a0 Fix service control for older browsers (#11629) 2022-02-18 10:51:29 -08:00
Paulus Schoutsen
dfbf7fb436 Revert compute state display show empty string as unknown (#11677) 2022-02-18 10:51:29 -08:00
lintaba
f37a5fa021 hotfix history view on missing state (#11663)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-02-18 10:51:29 -08:00
Paulus Schoutsen
5e2fcf928c Bumped version to 20220203.1 2022-02-18 10:50:01 -08:00
Erik
bc6ef7780c Remove useless Array.isArray check 2022-02-18 16:49:23 +01:00
Erik
b29563a254 Prettier 2022-02-18 16:41:18 +01:00
Erik
fe8a1152c4 Correct typing 2022-02-18 16:36:15 +01:00
Erik Montnemery
26689a0a85 Update src/panels/config/devices/ha-config-device-page.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-18 16:21:11 +01:00
Erik Montnemery
4f6a241817 Apply suggestions from code review
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-18 15:57:57 +01:00
Erik Montnemery
eae7e82127 Remove custom MQTT delete device button (#11724) 2022-02-18 08:28:53 -06:00
Paulus Schoutsen
9500ac498c Debounce refresh the cloud status if Google events happen (#11721) 2022-02-18 15:04:45 +01:00
Bram Kragten
5c5459bcaf Add play media action (#11702)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-18 13:21:00 +01:00
Erik
246724c59e Prettier 2022-02-18 09:13:18 +01:00
Erik
8f5c9295d3 Tweak 2022-02-18 08:48:53 +01:00
Erik
0abafff4c9 Fix lint error 2022-02-18 08:26:37 +01:00
Erik
f88ce269a7 Tweak 2022-02-17 17:12:17 +01:00
Erik
0dc56d7983 Add support for removing config entry from a device 2022-02-17 16:46:08 +01:00
Joakim Sørensen
cbd0ef6b65 Add signed add-on capability and adjust max rating (#11703) 2022-02-17 10:43:26 +01:00
Zack Barett
f923228078 Fix mwc-select in lovelace editors (#11708) 2022-02-17 10:41:45 +01:00
Raman Gupta
b55c7edd70 Make zwave_js config panel inclusion state aware (#11556)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-17 10:41:12 +01:00
uvjustin
bfb90632ac Bump hls.js to v1.1.5 (#11712) 2022-02-17 10:40:47 +01:00
Josh McCarty
3a664d45a9 Add bottom padding to config links list with safe-area-inset-bottom (#11704) 2022-02-16 22:07:45 -06:00
Paulus Schoutsen
53607fe8c6 Remove duplicate gallery page (#11711) 2022-02-16 22:01:51 -06:00
Bram Kragten
9dec0f8ccd Fix mode selection in automation editor (#11707) 2022-02-16 21:47:49 +01:00
Bram Kragten
89f4fe9d20 Convert scene action to service call (#11705)
* Convert scene action to service call

* fix describeAction

* rename to metadata

* Update script.ts
2022-02-16 20:47:21 +01:00
Josh McCarty
f43655eea5 Fixes remote icon state color (#11698) 2022-02-16 16:54:12 +01:00
Philip Allgaier
6563984fdd Convert triple dots to single char in translations (#11697) 2022-02-16 16:20:19 +01:00
Zack Barett
16d8eb0be3 Clean up some imports (#11696) 2022-02-15 12:53:20 -08:00
Paulus Schoutsen
965fc9bc4e Fix import 2022-02-15 11:16:51 -08:00
Bram Kragten
56cb958a47 Migrate all lovelace elements to mwc (#11695)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-15 11:09:34 -08:00
Steve Repsher
f5feb1d8aa Set initial focus for lovelace dialogs (#11667)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-15 16:42:46 +00:00
Matthias de Baat
e95065ed08 Updated text part 2 (#11686)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-15 09:57:26 -06:00
Bram Kragten
68a411838d This adds back mobile click accessibility (#11693) 2022-02-15 15:24:54 +00:00
Bram Kragten
ba63ab8b7a Latest paper-dropdown -> mwc-select conversion (#11692) 2022-02-15 09:11:43 -06:00
Erik Montnemery
26d4599ef4 Display transmitted messages in MQTT debug info dialog (#11531) 2022-02-15 11:18:05 +00:00
Brandon Rothweiler
d049990f04 Improve stripPrefixFromEntityName to handle colon and space separator (#11691) 2022-02-15 09:03:58 +00:00
Paulus Schoutsen
9c8d683a19 Group helpers not in an area in a single card (#11690) 2022-02-14 23:13:35 -08:00
Paulus Schoutsen
901677bbdf Bumped version to 20220214.0 2022-02-14 15:33:08 -08:00
Paulus Schoutsen
8bb2374b1b Allow uploading multiple files (#11687) 2022-02-14 17:25:23 -06:00
Paulus Schoutsen
520896a3c2 Try to keep the browsing stack when changing players in media panel (#11681) 2022-02-14 15:21:17 -08:00
Bram Kragten
92db272759 Dont exclude domain for area and device (#11689) 2022-02-14 16:56:50 -06:00
Bram Kragten
fc654d86c6 hassio fixes (#11688) 2022-02-14 22:33:12 +01:00
Bram Kragten
523afe2f6f Another round of paper-dropdown -> mwc-select conversion (#11674)
* Another round of paper-dropdown -> mwc-select conversion

* ha-pick-language-row -> Lit

* Update hui-view-editor.ts

* Cleanup imports

* hassio

* Add explicit imports
2022-02-14 20:08:18 +01:00
Zack Barett
460b9003fc Script Editor to Ha Form (#11601)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-14 11:27:29 -06:00
kpine
2ac0ad1d98 Omit Device info and actions for connected controller nodes (#11673) 2022-02-14 17:06:03 +01:00
Paulus Schoutsen
a321432175 Add TTS to media browser (#11679) 2022-02-14 07:50:44 -08:00
Zack Barett
63c9b3f830 Don't show toggle always on more info (#11640) 2022-02-14 16:21:46 +01:00
Paulus Schoutsen
806b1296b0 Limit types of media that can be uploaded to local media (#11683) 2022-02-14 15:33:21 +01:00
Steve Repsher
7f90ffa82f Set initial focus for some more dialogs (#11676) 2022-02-13 22:02:48 +01:00
Paulus Schoutsen
db33c38e21 Revert compute state display show empty string as unknown (#11677) 2022-02-13 20:26:12 +01:00
Allen Porter
a8c1fdd21e Improve robustness of hls media player (#11672) 2022-02-12 20:21:26 -08:00
lintaba
d86a18b80b hotfix history view on missing state (#11663)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-02-12 14:00:50 -08:00
Michael
bef6591548 Add WORKSPACE_DIRECTORY environment variable to devcontainer and script.core (#11477)
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2022-02-12 07:30:19 +01:00
Bram Kragten
e1c07f109c Filter fixes (#11664) 2022-02-11 23:24:29 +01:00
Zack Barett
fb66d224ae Numerical State to HA-Form (#11646)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-11 22:39:33 +01:00
Paulus Schoutsen
ee1fd3e865 Allow adding Zigbee/Zwave device (#11650) 2022-02-11 19:49:16 +01:00
Bram Kragten
a9bfea233c Improve search and filters on mobile + fix close button in search field (#11662)
Co-authored-by: Zack <zackbarett@hey.com>
2022-02-11 18:34:50 +00:00
Shay Levy
35cc291118 Add support for media player assumed state (#11642) 2022-02-11 08:42:22 -08:00
Bram Kragten
51938fb51f 20220203.0 (#11533)
* Only upload wheels to PyPI (#11514)

* Make sure we load data in update card (#11516)

* Guard load diagnostics (#11518)

* Design home - Fix GitHub Links (#11519)

* Add filtering to system log card and error log card (#11166)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Handle unknown toggle state (#11522)

* Fix dialog heading aria label (#11524)

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>

* Use css to hide hint in quickbar (#11527)

* Revert "Mobile click accessibility" (#11526)

* Clear old src when disconnected so we can't fetch it with the wrong t… (#11528)

* Add name of integration to diagnostics when more than 1 (#11523)

* Bumped version to 20220203.0

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: fpro1212 <75439345+fpro1212@users.noreply.github.com>
Co-authored-by: Kuba Wolanin <hi@kubawolanin.com>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2022-02-03 20:52:49 +01:00
Bram Kragten
c85236e251 Merge pull request #11512 from home-assistant/dev 2022-02-02 14:47:08 +01:00
337 changed files with 9979 additions and 8653 deletions

View File

@@ -16,6 +16,9 @@
"runem.lit-plugin",
"ms-python.vscode-pylance"
],
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"files.eol": "\n",

File diff suppressed because one or more lines are too long

785
.yarn/releases/yarn-3.2.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.0.2.cjs
yarnPath: .yarn/releases/yarn-3.2.0.cjs

View File

@@ -2,7 +2,7 @@
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/home-assistant-polymer/master/docs/screenshot.png)](https://demo.home-assistant.io/)
[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/frontend/master/docs/screenshot.png)](https://demo.home-assistant.io/)
- [View demo of Home Assistant](https://demo.home-assistant.io/)
- [More information about Home Assistant](https://home-assistant.io)

View File

@@ -33,6 +33,10 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
),
isHassioBuild &&
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts")
),
].filter(Boolean);
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({

View File

@@ -3,7 +3,7 @@
const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const marked = require("marked");
const { marked } = require("marked");
const glob = require("glob");
const yaml = require("js-yaml");

View File

@@ -7,7 +7,7 @@ const source = require("vinyl-source-stream");
const vinylBuffer = require("vinyl-buffer");
const gulp = require("gulp");
const fs = require("fs");
const foreach = require("gulp-foreach");
const flatmap = require("gulp-flatmap");
const merge = require("gulp-merge-json");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
@@ -183,7 +183,7 @@ gulp.task("build-merged-translations", () =>
})
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
.pipe(
foreach((stream, file) => {
flatmap((stream, file) => {
// For each language generate a merged json file. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag

View File

@@ -1,5 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace";
import { Lovelace } from "../../../../src/panels/lovelace/types";
@@ -20,6 +20,8 @@ class HcLovelace extends LitElement {
@property() public urlPath: string | null = null;
@query("hui-view") private _huiView?: HTMLElement;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@@ -78,12 +80,12 @@ class HcLovelace extends LitElement {
this.lovelaceConfig.background;
if (configBackground) {
(this.shadowRoot!.querySelector(
"hui-view"
) as HTMLElement)!.style.setProperty(
this._huiView!.style.setProperty(
"--lovelace-background",
configBackground
);
} else {
this._huiView!.style.removeProperty("--lovelace-background");
}
}
}
@@ -116,6 +118,9 @@ class HcLovelace extends LitElement {
:host > * {
flex: 1;
}
hui-view {
background: var(--lovelace-background, var(--primary-background-color));
}
`;
}
}

View File

@@ -2,8 +2,3 @@ import "../../src/resources/ha-style";
import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch";
import "./ha-demo";
/* polyfill for paper-dropdown */
setTimeout(() => {
import("web-animations-js/web-animations-next-lite.min");
}, 1000);

View File

@@ -20,7 +20,6 @@ module.exports = [
"editor-trigger",
"editor-condition",
"editor-action",
"selectors",
"trace",
"trace-timeline",
],

View File

@@ -3,10 +3,20 @@ import { html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
import { describeAction } from "../../../../src/data/script_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
const actions = [
const ENTITIES = [
getEntity("scene", "kitchen_morning", "scening", {
friendly_name: "Kitchen Morning",
}),
getEntity("media_player", "kitchen", "playing", {
friendly_name: "Sonos Kitchen",
}),
];
const ACTIONS = [
{ wait_template: "{{ true }}", alias: "Something with an alias" },
{ delay: "0:05" },
{ wait_template: "{{ true }}" },
@@ -19,8 +29,20 @@ const actions = [
device_id: "abcdefgh",
domain: "plex",
entity_id: "media_player.kitchen",
type: "turn_on",
},
{ scene: "scene.kitchen_morning" },
{
service: "scene.turn_on",
target: { entity_id: "scene.kitchen_morning" },
metadata: {},
},
{
service: "media_player.play_media",
target: { entity_id: "media_player.kitchen" },
data: { media_content_id: "", media_content_type: "" },
metadata: { title: "Happy Song" },
},
{
wait_for_trigger: [
{
@@ -52,7 +74,7 @@ export class DemoAutomationDescribeAction extends LitElement {
}
return html`
<ha-card header="Actions">
${actions.map(
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, conf as any)}</span>
@@ -68,6 +90,7 @@ export class DemoAutomationDescribeAction extends LitElement {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
static get styles() {

View File

@@ -14,7 +14,7 @@ import { HaDelayAction } from "../../../../src/panels/config/automation/action/t
import { HaDeviceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-scene";
import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-activate_scene";
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";

View File

@@ -1,3 +0,0 @@
---
title: Selectors
---

View File

@@ -1,102 +0,0 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { Selector } from "../../../../src/data/selector";
import "../../../../src/components/ha-selector/ha-selector";
const SCHEMAS: { name: string; selector: Selector }[] = [
{ name: "Addon", selector: { addon: {} } },
{ name: "Entity", selector: { entity: {} } },
{ name: "Device", selector: { device: {} } },
{ name: "Area", selector: { area: {} } },
{ name: "Target", selector: { target: {} } },
{
name: "Number",
selector: {
number: {
min: 0,
max: 10,
},
},
},
{ name: "Boolean", selector: { boolean: {} } },
{ name: "Time", selector: { time: {} } },
{ name: "Action", selector: { action: {} } },
{ name: "Text", selector: { text: { multiline: false } } },
{ name: "Text Multiline", selector: { text: { multiline: true } } },
{ name: "Object", selector: { object: {} } },
{
name: "Select",
selector: {
select: {
options: ["Everyone Home", "Some Home", "All gone"],
},
},
},
];
@customElement("demo-automation-selectors")
class DemoHaSelector extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map(() => undefined);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${{ selector: info.selector, data: this.data[sampleIdx] }}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-selector
slot=${slot}
.hass=${this.hass}
.selector=${info.selector}
.label=${info.name}
.value=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-selector>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-selectors": DemoHaSelector;
}
}

View File

@@ -36,6 +36,8 @@ const SCHEMAS: {
text_multiline: "Text Multiline",
object: "Object",
select: "Select",
icon: "Icon",
media: "Media",
},
schema: [
{ name: "addon", selector: { addon: {} } },
@@ -61,6 +63,18 @@ const SCHEMAS: {
select: { options: ["Everyone Home", "Some Home", "All gone"] },
},
},
{
name: "icon",
selector: {
icon: {},
},
},
{
name: "media",
selector: {
media: {},
},
},
],
},
{

View File

@@ -1,3 +1,5 @@
---
title: Target Selectors
title: Selectors
---
See the website for [list of available selectors](https://www.home-assistant.io/docs/blueprint/selectors/).

View File

@@ -12,6 +12,100 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { getEntity } from "../../../../src/fake_data/entity";
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
friendly_name: "Alarm",
}),
getEntity("media_player", "livingroom", "playing", {
friendly_name: "Livingroom",
}),
getEntity("media_player", "lounge", "idle", {
friendly_name: "Lounge",
supported_features: 444983,
}),
getEntity("light", "bedroom", "on", {
friendly_name: "Bedroom",
}),
getEntity("switch", "coffee", "off", {
friendly_name: "Coffee",
}),
];
const DEVICES = [
{
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_1",
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_2",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_3",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,
hw_version: null,
via_device_id: null,
},
];
const AREAS = [
{
area_id: "backyard",
name: "Backyard",
picture: null,
},
{
area_id: "bedroom",
name: "Bedroom",
picture: null,
},
{
area_id: "livingroom",
name: "Livingroom",
picture: null,
},
];
const SCHEMAS: {
name: string;
@@ -72,13 +166,15 @@ const SCHEMAS: {
name: "Select",
selector: { select: { options: ["Option 1", "Option 2"] } },
},
icon: { name: "Icon", selector: { icon: {} } },
media: { name: "Media", selector: { media: {} } },
},
},
];
@customElement("demo-components-ha-selector")
class DemoHaSelector extends LitElement {
@state() private hass!: HomeAssistant;
class DemoHaSelector extends LitElement implements ProvideHassElement {
@state() public hass!: HomeAssistant;
private data = SCHEMAS.map(() => ({}));
@@ -87,12 +183,130 @@ class DemoHaSelector extends LitElement {
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockDeviceRegistry(hass, DEVICES);
mockAreaRegistry(hass, AREAS);
mockHassioSupervisor(hass);
hass.mockWS("auth/sign_path", (params) => params);
hass.mockWS("media_player/browse_media", this._browseMedia);
}
public provideHass(el) {
el.hass = this.hass;
}
public connectedCallback() {
super.connectedCallback();
this.addEventListener("show-dialog", this._dialogManager);
}
public disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("show-dialog", this._dialogManager);
}
private _browseMedia = ({ media_content_id }) => {
if (media_content_id === undefined) {
return {
title: "Media",
media_class: "directory",
media_content_type: "",
media_content_id: "media-source://media_source/local/.",
can_play: false,
can_expand: true,
children_media_class: "directory",
thumbnail: null,
children: [
{
title: "Misc",
media_class: "directory",
media_content_type: "",
media_content_id: "media-source://media_source/local/misc",
can_play: false,
can_expand: true,
children_media_class: null,
thumbnail: null,
},
{
title: "Movies",
media_class: "directory",
media_content_type: "",
media_content_id: "media-source://media_source/local/movies",
can_play: true,
can_expand: true,
children_media_class: "movie",
thumbnail: null,
},
{
title: "Music",
media_class: "album",
media_content_type: "",
media_content_id: "media-source://media_source/local/music",
can_play: false,
can_expand: true,
children_media_class: "music",
thumbnail: "/images/album_cover_2.jpg",
},
],
};
}
return {
title: "Subfolder",
media_class: "directory",
media_content_type: "",
media_content_id: "media-source://media_source/local/sub",
can_play: false,
can_expand: true,
children_media_class: "directory",
thumbnail: null,
children: [
{
title: "audio.mp3",
media_class: "music",
media_content_type: "audio/mpeg",
media_content_id: "media-source://media_source/local/audio.mp3",
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: "/images/album_cover.jpg",
},
{
title: "image.jpg",
media_class: "image",
media_content_type: "image/jpeg",
media_content_id: "media-source://media_source/local/image.jpg",
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: null,
},
{
title: "movie.mp4",
media_class: "movie",
media_content_type: "image/jpeg",
media_content_id: "media-source://media_source/local/movie.mp4",
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: null,
},
],
};
};
private _dialogManager = (e) => {
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
showDialog(
this,
this.shadowRoot!,
dialogTag,
dialogParams,
dialogImport,
addHistory
);
};
protected render(): TemplateResult {
return html`
${SCHEMAS.map((info, idx) => {
@@ -131,7 +345,6 @@ class DemoHaSelector extends LitElement {
}
static styles = css`
paper-input,
ha-selector {
width: 60;
}

View File

@@ -29,6 +29,7 @@ const createConfigEntry = (
source: "zeroconf",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
disabled_by: null,
pref_disable_new_entities: false,

View File

@@ -14,7 +14,7 @@ import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input";
import "../../../src/components/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-icon-button";
@@ -221,13 +221,14 @@ class HassioAddonStore extends LitElement {
margin-top: 24px;
}
.search {
padding: 0 16px;
background: var(--sidebar-background-color);
border-bottom: 1px solid var(--divider-color);
position: sticky;
top: 0;
z-index: 2;
}
.search search-input {
position: relative;
top: 2px;
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
.advanced {
padding: 12px;

View File

@@ -1,7 +1,5 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
@@ -11,10 +9,11 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "web-animations-js/web-animations-next-lite.min";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-select";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
@@ -57,49 +56,44 @@ class HassioAddonAudio extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<paper-dropdown-menu
${this._inputDevices &&
html`<ha-select
.label=${this.supervisor.localize(
"addon.configuration.audio.input"
)}
@iron-select=${this._setInputDevice}
@selected=${this._setInputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedInput!}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedInput!}
>
${this._inputDevices &&
this._inputDevices.map(
(item) => html`
<paper-item device=${item.device || ""}>
${item.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
${this._inputDevices.map(
(item) => html`
<mwc-list-item .value=${item.device || ""}>
${item.name}
</mwc-list-item>
`
)}
</ha-select>`}
${this._outputDevices &&
html`<ha-select
.label=${this.supervisor.localize(
"addon.configuration.audio.output"
)}
@iron-select=${this._setOutputDevice}
@selected=${this._setOutputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedOutput!}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedOutput!}
>
${this._outputDevices &&
this._outputDevices.map(
(item) => html`
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
`
)}
</paper-listbox>
</paper-dropdown-menu>
${this._outputDevices.map(
(item) => html`
<mwc-list-item .value=${item.device || ""}
>${item.name}</mwc-list-item
>
`
)}
</ha-select>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._saveSettings}>
@@ -116,8 +110,7 @@ class HassioAddonAudio extends LitElement {
hassioStyle,
css`
:host,
ha-card,
paper-dropdown-menu {
ha-card {
display: block;
}
paper-item {
@@ -126,24 +119,30 @@ class HassioAddonAudio extends LitElement {
.card-actions {
text-align: right;
}
ha-select {
width: 100%;
}
ha-select:last-child {
margin-top: 8px;
}
`,
];
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("addon")) {
this._addonChanged();
}
}
private _setInputDevice(ev): void {
const device = ev.detail.item.getAttribute("device");
const device = ev.target.value;
this._selectedInput = device;
}
private _setOutputDevice(ev): void {
const device = ev.detail.item.getAttribute("device");
const device = ev.target.value;
this._selectedOutput = device;
}

View File

@@ -9,6 +9,7 @@ import {
mdiFlask,
mdiHomeAssistant,
mdiKey,
mdiLinkLock,
mdiNetwork,
mdiNumeric1,
mdiNumeric2,
@@ -16,6 +17,8 @@ import {
mdiNumeric4,
mdiNumeric5,
mdiNumeric6,
mdiNumeric7,
mdiNumeric8,
mdiPound,
mdiShield,
} from "@mdi/js";
@@ -31,6 +34,7 @@ import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-chip";
import "../../../../src/components/ha-chip-set";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
@@ -84,6 +88,8 @@ const RATING_ICON = {
4: mdiNumeric4,
5: mdiNumeric5,
6: mdiNumeric6,
7: mdiNumeric7,
8: mdiNumeric8,
};
@customElement("hassio-addon-info")
@@ -209,7 +215,7 @@ class HassioAddonInfo extends LitElement {
>`}
</div>
<div class="capabilities">
<ha-chip-set class="capabilities">
${this.addon.stage !== "stable"
? html` <ha-chip
hasIcon
@@ -234,9 +240,9 @@ class HassioAddonInfo extends LitElement {
<ha-chip
hasIcon
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
green: Number(this.addon.rating) >= 6,
yellow: [3, 4, 5].includes(Number(this.addon.rating)),
red: Number(this.addon.rating) >= 2,
})}
@click=${this._showMoreInfo}
id="rating"
@@ -364,7 +370,17 @@ class HassioAddonInfo extends LitElement {
</ha-chip>
`
: ""}
</div>
${this.addon.signed
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="signed">
<ha-svg-icon slot="icon" .path=${mdiLinkLock}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.signed"
)}
</ha-chip>
`
: ""}
</ha-chip-set>
<div class="description light-color">
${this.addon.description}.<br />

View File

@@ -1,7 +1,7 @@
import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
@@ -92,6 +92,8 @@ export class SupervisorBackupContent extends LitElement {
@property() public confirmBackupPassword = "";
@query("paper-input, ha-radio, ha-checkbox", true) private _focusTarget;
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
@@ -109,6 +111,10 @@ export class SupervisorBackupContent extends LitElement {
}
}
public override focus() {
this._focusTarget?.focus();
}
private _localize = (string: string) =>
this.supervisor?.localize(`backup.${string}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
@@ -169,24 +175,23 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupType === "partial"
? html`<div class="partial-picker">
${this.backup && this.backup.homeassistant
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.backup.homeassistant}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.homeAssistant}
@click=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>
`
: ""}
<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.backup
? this.backup.homeassistant
: this.hass.config.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.homeAssistant}
@click=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>
${foldersSection?.templates.length
? html`
<ha-formfield

View File

@@ -148,7 +148,6 @@ export class HassioUpdate extends LitElement {
}
ha-settings-row {
padding: 0;
--paper-item-body-two-line-min-height: 32px;
}
`,
];

View File

@@ -64,6 +64,7 @@ export class DialogHassioBackupUpload
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
dialogInitialFocus
></ha-icon-button>
</ha-header-bar>
</div>

View File

@@ -92,6 +92,7 @@ class HassioBackupDialog
.backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
dialogInitialFocus
>
</supervisor-backup-content>`}
${this._error

View File

@@ -61,6 +61,7 @@ class HassioCreateBackupDialog extends LitElement {
: html`<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
dialogInitialFocus
>
</supervisor-backup-content>`}
${this._error

View File

@@ -1,12 +1,11 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-select";
import {
extractApiErrorMessage,
ignoreSupervisorError,
@@ -90,18 +89,20 @@ class HassioDatadiskDialog extends LitElement {
)}
<br /><br />
<paper-dropdown-menu
<ha-select
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@value-changed=${this._select_device}
@selected=${this._select_device}
dialogInitialFocus
>
<paper-listbox slot="dropdown-content">
${this.devices.map(
(device) => html`<paper-item>${device}</paper-item>`
)}
</paper-listbox>
</paper-dropdown-menu>
${this.devices.map(
(device) =>
html`<mwc-list-item .value=${device}
>${device}</mwc-list-item
>`
)}
</ha-select>
`
: this.devices === undefined
? this.dialogParams.supervisor.localize(
@@ -111,7 +112,11 @@ class HassioDatadiskDialog extends LitElement {
"dialog.datadisk_move.no_devices"
)}
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.cancel"
)}
@@ -130,8 +135,8 @@ class HassioDatadiskDialog extends LitElement {
`;
}
private _select_device(event) {
this.selectedDevice = event.detail.value;
private _select_device(ev) {
this.selectedDevice = ev.target.value;
}
private async _moveDatadisk() {
@@ -156,7 +161,7 @@ class HassioDatadiskDialog extends LitElement {
haStyle,
haStyleDialog,
css`
paper-dropdown-menu {
ha-select {
width: 100%;
}
ha-circular-progress {

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/common/search/search-input";
import "../../../../src/components/search-input";
import { stringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
@@ -80,7 +80,7 @@ class HassioHardwareDialog extends LitElement {
></ha-icon-button>
<search-input
.hass=${this.hass}
autofocus
dialogInitialFocus
no-label-float
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
@@ -178,7 +178,7 @@ class HassioHardwareDialog extends LitElement {
padding: 0.2em 0.4em;
}
search-input {
margin: 0 16px;
margin: 8px 16px 0;
display: block;
}
.device-property {

View File

@@ -37,7 +37,10 @@ class HassioMarkdownDialog extends LitElement {
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this.title)}
>
<ha-markdown .content=${this.content || ""}></ha-markdown>
<ha-markdown
.content=${this.content || ""}
dialogInitialFocus
></ha-markdown>
</ha-dialog>
`;
}

View File

@@ -119,6 +119,7 @@ export class DialogHassioNetwork
html`<mwc-tab
.id=${device.interface}
.label=${device.interface}
dialogInitialFocus
>
</mwc-tab>`
)}
@@ -315,6 +316,7 @@ export class DialogHassioNetwork
value="auto"
name="${version}method"
.checked=${this._interface![version]?.method === "auto"}
dialogInitialFocus
>
</ha-radio>
</ha-formfield>

View File

@@ -80,6 +80,7 @@ class HassioRegistriesDialog extends LitElement {
.schema=${SCHEMA}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabel}
dialogInitialFocus
></ha-form>
<div class="action">
<mwc-button
@@ -124,7 +125,7 @@ class HassioRegistriesDialog extends LitElement {
</ha-alert>
`}
<div class="action">
<mwc-button @click=${this._addRegistry}>
<mwc-button @click=${this._addRegistry} dialogInitialFocus>
${this.supervisor.localize(
"dialog.registries.add_new_registry"
)}

View File

@@ -139,6 +139,7 @@ class HassioRepositoriesDialog extends LitElement {
"dialog.repositories.add"
)}
@keydown=${this._handleKeyAdd}
dialogInitialFocus
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._processing

View File

@@ -1,12 +1,10 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-card";
import "../../../src/components/ha-select";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
@@ -73,24 +71,19 @@ class HassioSupervisorLog extends LitElement {
: ""}
${this.hass.userData?.showAdvanced
? html`
<paper-dropdown-menu
<ha-select
.label=${this.supervisor.localize("system.log.log_provider")}
@iron-select=${this._setLogProvider}
@selected=${this._setLogProvider}
.value=${this._selectedLogProvider}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="provider"
.selected=${this._selectedLogProvider}
>
${logProviders.map(
(provider) => html`
<paper-item provider=${provider.key}>
${provider.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
${logProviders.map(
(provider) => html`
<mwc-list-item .value=${provider.key}>
${provider.name}
</mwc-list-item>
`
)}
</ha-select>
`
: ""}
@@ -110,7 +103,7 @@ class HassioSupervisorLog extends LitElement {
}
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
const provider = ev.target.value;
this._selectedLogProvider = provider;
this._loadData();
}
@@ -153,9 +146,9 @@ class HassioSupervisorLog extends LitElement {
pre {
white-space: pre-wrap;
}
paper-dropdown-menu {
padding: 0 2%;
width: 96%;
ha-select {
width: 100%;
margin-bottom: 4px;
}
`,
];

View File

@@ -10,7 +10,6 @@ import {
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";

View File

@@ -1,8 +1,8 @@
{
"description": "A frontend for Home Assistant using the Polymer framework",
"description": "A frontend for Home Assistant",
"repository": {
"type": "git",
"url": "https://github.com/home-assistant/home-assistant-polymer"
"url": "https://github.com/home-assistant/frontend"
},
"name": "home-assistant-frontend",
"version": "1.0.0",
@@ -46,6 +46,7 @@
"@fullcalendar/daygrid": "5.9.0",
"@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0",
"@lit-labs/motion": "^1.0.2",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
"@material/chips": "14.0.0-canary.261f2db59.0",
"@material/data-table": "14.0.0-canary.261f2db59.0",
@@ -95,6 +96,7 @@
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
"@webcomponents/webcomponentsjs": "^2.2.10",
"app-datepicker": "^5.0.1",
"chart.js": "^3.3.2",
@@ -106,7 +108,7 @@
"deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.0.11",
"hls.js": "^1.1.5",
"home-assistant-js-websocket": "^6.0.1",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
@@ -115,7 +117,7 @@
"leaflet-draw": "^1.0.4",
"lit": "^2.1.2",
"lit-vaadin-helpers": "^0.3.0",
"marked": "^3.0.2",
"marked": "^4.0.12",
"memoize-one": "^5.2.1",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2",
@@ -135,12 +137,12 @@
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.1.5",
"workbox-core": "^6.1.5",
"workbox-expiration": "^6.1.5",
"workbox-precaching": "^6.1.5",
"workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",
"workbox-precaching": "^6.4.2",
"workbox-routing": "^6.4.2",
"workbox-strategies": "^6.4.2",
"xss": "^1.0.9"
},
"devDependencies": {
@@ -169,7 +171,7 @@
"@types/js-yaml": "^4",
"@types/leaflet": "^1",
"@types/leaflet-draw": "^1",
"@types/marked": "^2",
"@types/marked": "^4",
"@types/mocha": "^8",
"@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1",
@@ -196,7 +198,7 @@
"fs-extra": "^7.0.1",
"glob": "^7.2.0",
"gulp": "^4.0.2",
"gulp-foreach": "^0.1.0",
"gulp-flatmap": "^1.0.2",
"gulp-json-transform": "^0.4.6",
"gulp-merge-json": "^1.3.1",
"gulp-rename": "^2.0.0",
@@ -233,7 +235,7 @@
"webpack-dev-server": "^4.3.0",
"webpack-manifest-plugin": "^4.0.2",
"webpackbar": "^5.0.0-3",
"workbox-build": "^6.1.5"
"workbox-build": "^6.4.2"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
@@ -253,5 +255,6 @@
"prettier": {
"trailingComma": "es5",
"arrowParens": "always"
}
},
"packageManager": "yarn@3.2.0"
}

View File

@@ -2,6 +2,6 @@
from pathlib import Path
def where():
def where() -> Path:
"""Return path to the frontend."""
return Path(__file__).parent

0
public/py.typed Normal file
View File

View File

@@ -4,6 +4,8 @@
# Stop on errors
set -e
WD="${WORKSPACE_DIRECTORY:=/workspaces/frontend}"
if [ -z "${DEVCONTAINER}" ]; then
echo "This task should only run inside a devcontainer, for local install HA Core in a venv."
exit 1
@@ -16,9 +18,9 @@ if [ -z $(which hass) ]; then
git+git://github.com/home-assistant/home-assistant.git@dev
fi
if [ ! -d "/workspaces/frontend/config" ]; then
if [ ! -d "${WD}/config" ]; then
echo "Creating default configuration."
mkdir -p "/workspaces/frontend/config";
mkdir -p "${WD}/config";
hass --script ensure_config -c config
echo "demo:
@@ -26,24 +28,24 @@ logger:
default: info
logs:
homeassistant.components.frontend: debug
" >> /workspaces/frontend/config/configuration.yaml
" >> "${WD}/config/configuration.yaml"
if [ ! -z "${HASSIO}" ]; then
echo "
# frontend:
# development_repo: /workspaces/frontend
# development_repo: ${WD}
hassio:
development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
else
echo "
frontend:
development_repo: /workspaces/frontend
development_repo: ${WD}
# hassio:
# development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
# development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
fi
fi
hass -c /workspaces/frontend/config
hass -c "${WD}/config"

View File

@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220203.0
version = 20220226.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
@@ -19,3 +19,8 @@ python_requires = >= 3.4.0
[options.packages.find]
include =
hass_frontend*
[mypy]
python_version = 3.4
show_error_codes = True
strict = True

View File

@@ -3,9 +3,9 @@ import type { ForDict } from "../../data/automation";
export const createDurationData = (
duration: string | number | ForDict | undefined
): HaDurationData => {
): HaDurationData | undefined => {
if (duration === undefined) {
return {};
return undefined;
}
if (typeof duration !== "object") {
if (typeof duration === "string" || isNaN(duration)) {

View File

@@ -1,4 +1,4 @@
import { HomeAssistant } from "../../types";
import type { HomeAssistant } from "../../types";
export const canToggleDomain = (hass: HomeAssistant, domain: string) => {
const services = hass.services[domain];

View File

@@ -1,14 +1,30 @@
import { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../types";
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../../types";
import { canToggleDomain } from "./can_toggle_domain";
import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
const domain = computeStateDomain(stateObj);
if (domain === "group") {
return stateObj.state === "on" || stateObj.state === "off";
if (
stateObj.attributes?.entity_id?.some((entity) => {
const entityStateObj = hass.states[entity];
if (!entityStateObj) {
return false;
}
const entityDomain = computeStateDomain(entityStateObj);
return canToggleDomain(hass, entityDomain);
})
) {
return stateObj.state === "on" || stateObj.state === "off";
}
return false;
}
if (domain === "climate") {
return supportsFeature(stateObj, 4096);
}

View File

@@ -123,7 +123,11 @@ export const computeStateDisplay = (
domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) {
return formatDateTime(new Date(compareState), locale);
try {
return formatDateTime(new Date(compareState), locale);
} catch (_err) {
return compareState;
}
}
return (

View File

@@ -120,6 +120,7 @@ export const computeOpenIcon = (stateObj: HassEntity): string => {
case "awning":
case "door":
case "gate":
case "curtain":
return mdiArrowExpandHorizontal;
default:
return mdiArrowUp;
@@ -131,6 +132,7 @@ export const computeCloseIcon = (stateObj: HassEntity): string => {
case "awning":
case "door":
case "gate":
case "curtain":
return mdiArrowCollapseHorizontal;
default:
return mdiArrowDown;

View File

@@ -1,24 +1,32 @@
const SUFFIXES = [" ", ": "];
/**
* Strips a device name from an entity name.
* @param entityName the entity name
* @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix
* @param lowerCasedPrefix the prefix to strip, lower cased
* @returns
*/
export const stripPrefixFromEntityName = (
entityName: string,
lowerCasedPrefixWithSpaceSuffix: string
lowerCasedPrefix: string
) => {
if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) {
return undefined;
const lowerCasedEntityName = entityName.toLowerCase();
for (const suffix of SUFFIXES) {
const lowerCasedPrefixWithSuffix = `${lowerCasedPrefix}${suffix}`;
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
? newName
: newName[0].toUpperCase() + newName.slice(1);
}
}
const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length);
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
? newName
: newName[0].toUpperCase() + newName.slice(1);
return undefined;
};
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;

View File

@@ -15,6 +15,7 @@ export const iconColorCSS = css`
ha-state-icon[data-domain="media_player"][data-state="on"],
ha-state-icon[data-domain="media_player"][data-state="paused"],
ha-state-icon[data-domain="media_player"][data-state="playing"],
ha-state-icon[data-domain="remote"][data-state="on"],
ha-state-icon[data-domain="script"][data-state="on"],
ha-state-icon[data-domain="sun"][data-state="above_horizon"],
ha-state-icon[data-domain="switch"][data-state="on"],

View File

@@ -11,7 +11,7 @@ export const debounce = <T extends any[]>(
immediate = false
) => {
let timeout: number | undefined;
return (...args: T): void => {
const debouncedFunc = (...args: T): void => {
const later = () => {
timeout = undefined;
if (!immediate) {
@@ -25,4 +25,8 @@ export const debounce = <T extends any[]>(
func(...args);
}
};
debouncedFunc.cancel = () => {
clearTimeout(timeout);
};
return debouncedFunc;
};

View File

@@ -21,7 +21,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import "../../common/search/search-input";
import "../search-input";
import { debounce } from "../../common/util/debounce";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";

View File

@@ -1,5 +1,4 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
@@ -8,6 +7,7 @@ import {
deviceAutomationsEqual,
} from "../../data/device_automation";
import { HomeAssistant } from "../../types";
import "../ha-select";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
@@ -90,7 +90,7 @@ export abstract class HaDeviceAutomationPicker<
}
const value = this._value;
return html`
<mwc-select
<ha-select
.label=${this.label}
.value=${value}
@selected=${this._automationChanged}
@@ -113,7 +113,7 @@ export abstract class HaDeviceAutomationPicker<
</mwc-list-item>
`
)}
</mwc-select>
</ha-select>
`;
}
@@ -167,7 +167,7 @@ export abstract class HaDeviceAutomationPicker<
static get styles(): CSSResultGroup {
return css`
mwc-select {
ha-select {
width: 100%;
margin-top: 4px;
}

View File

@@ -114,7 +114,7 @@ class HaEntitiesPickerLight extends LitElement {
const newValue = event.detail.value;
if (
newValue === curValue ||
(newValue !== "" && !isValidEntityId(newValue))
(newValue !== undefined && !isValidEntityId(newValue))
) {
return;
}
@@ -147,7 +147,7 @@ class HaEntitiesPickerLight extends LitElement {
}
static override styles = css`
ha-entity-picker {
div {
margin-top: 8px;
}
`;

View File

@@ -1,6 +1,3 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";

View File

@@ -1,6 +1,6 @@
import { LitElement, html, TemplateResult, css } from "lit";
import { customElement, property } from "lit/decorators";
import "@material/mwc-select/mwc-select";
import "./ha-select";
import "@material/mwc-list/mwc-list-item";
import "./ha-textfield";
import { fireEvent } from "../common/dom/fire_event";
@@ -193,7 +193,7 @@ export class HaBaseTimeInput extends LitElement {
: ""}
${this.format === 24
? ""
: html`<mwc-select
: html`<ha-select
.required=${this.required}
.value=${this.amPm}
.disabled=${this.disabled}
@@ -205,7 +205,7 @@ export class HaBaseTimeInput extends LitElement {
>
<mwc-list-item value="AM">AM</mwc-list-item>
<mwc-list-item value="PM">PM</mwc-list-item>
</mwc-select>`}
</ha-select>`}
</div>
`;
}
@@ -280,7 +280,7 @@ export class HaBaseTimeInput extends LitElement {
ha-textfield:last-child {
--text-field-border-top-right-radius: var(--mdc-shape-medium);
}
mwc-select {
ha-select {
--mdc-shape-small: 0;
width: 85px;
}

View File

@@ -1,5 +1,5 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "./ha-select";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -24,7 +24,7 @@ class HaBluePrintPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
public open() {
const select = this.shadowRoot?.querySelector("mwc-select");
const select = this.shadowRoot?.querySelector("ha-select");
if (select) {
// @ts-expect-error
select.menuOpen = true;
@@ -49,7 +49,7 @@ class HaBluePrintPicker extends LitElement {
return html``;
}
return html`
<mwc-select
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.label")}
fixedMenuPosition
@@ -71,7 +71,7 @@ class HaBluePrintPicker extends LitElement {
</mwc-list-item>
`
)}
</mwc-select>
</ha-select>
`;
}
@@ -101,7 +101,7 @@ class HaBluePrintPicker extends LitElement {
:host {
display: inline-block;
}
mwc-select {
ha-select {
width: 100%;
min-width: 200px;
display: block;

View File

@@ -1,5 +1,5 @@
import "@material/mwc-menu";
import type { Corner, Menu } from "@material/mwc-menu";
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
@@ -7,6 +7,12 @@ import { customElement, property, query } from "lit/decorators";
export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "TOP_START";
@property() public menuCorner: MenuCorner = "START";
@property({ type: Number }) public x?: number;
@property({ type: Number }) public y?: number;
@property({ type: Boolean }) public multi = false;
@property({ type: Boolean }) public activatable = false;
@@ -32,9 +38,12 @@ export class HaButtonMenu extends LitElement {
</div>
<mwc-menu
.corner=${this.corner}
.menuCorner=${this.menuCorner}
.fixed=${this.fixed}
.multi=${this.multi}
.activatable=${this.activatable}
.y=${this.y}
.x=${this.x}
>
<slot></slot>
</mwc-menu>

View File

@@ -4,6 +4,7 @@ import { mdiFilterVariant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, 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 { computeDeviceName } from "../data/device_registry";
import { findRelated, RelatedResult } from "../data/search";
@@ -65,6 +66,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.fullwidth=${this.narrow}
.corner=${this.corner}
@closed=${this._onClosed}
@input=${stopPropagation}
>
<ha-area-picker
.label=${this.hass.localize(
@@ -74,6 +76,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.value=${this.value?.area}
no-add
@value-changed=${this._areaPicked}
@click=${this._preventDefault}
></ha-area-picker>
<ha-device-picker
.label=${this.hass.localize(
@@ -82,6 +85,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.hass=${this.hass}
.value=${this.value?.device}
@value-changed=${this._devicePicked}
@click=${this._preventDefault}
></ha-device-picker>
<ha-entity-picker
.label=${this.hass.localize(
@@ -91,6 +95,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.value=${this.value?.entity}
.excludeDomains=${this.excludeDomains}
@value-changed=${this._entityPicked}
@click=${this._preventDefault}
></ha-entity-picker>
</mwc-menu-surface>
`;
@@ -103,11 +108,17 @@ export class HaRelatedFilterButtonMenu extends LitElement {
this._open = true;
}
private _onClosed(): void {
private _onClosed(ev): void {
ev.stopPropagation();
this._open = false;
}
private _preventDefault(ev) {
ev.preventDefault();
}
private async _entityPicked(ev: CustomEvent) {
ev.stopPropagation();
const entityId = ev.detail.value;
if (!entityId) {
fireEvent(this, "related-changed", { value: undefined });
@@ -127,6 +138,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
}
private async _devicePicked(ev: CustomEvent) {
ev.stopPropagation();
const deviceId = ev.detail.value;
if (!deviceId) {
fireEvent(this, "related-changed", { value: undefined });
@@ -150,6 +162,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
}
private async _areaPicked(ev: CustomEvent) {
ev.stopPropagation();
const areaId = ev.detail.value;
if (!areaId) {
fireEvent(this, "related-changed", { value: undefined });
@@ -173,9 +186,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
:host {
display: inline-block;
position: relative;
}
:host([narrow]) {
position: static;
--mdc-menu-min-width: 250px;
}
ha-area-picker,
ha-device-picker,
@@ -185,8 +196,15 @@ export class HaRelatedFilterButtonMenu extends LitElement {
padding: 4px 16px;
box-sizing: border-box;
}
ha-area-picker {
padding-top: 16px;
}
ha-entity-picker {
padding-bottom: 16px;
}
:host([narrow]) ha-area-picker,
:host([narrow]) ha-device-picker {
:host([narrow]) ha-device-picker,
:host([narrow]) ha-entity-picker {
width: 100%;
}
`;

View File

@@ -1,11 +1,11 @@
import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateNumeric } from "../common/datetime/format_date";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-textfield";
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
@@ -38,17 +38,17 @@ export class HaDateInput extends LitElement {
@property() public label?: string;
render() {
return html`<paper-input
return html`<ha-textfield
.label=${this.label}
.disabled=${this.disabled}
no-label-float
iconTrailing="calendar"
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
: ""}
>
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>`;
<ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon>
</ha-textfield>`;
}
private _openDialog() {
@@ -73,9 +73,6 @@ export class HaDateInput extends LitElement {
static get styles(): CSSResultGroup {
return css`
paper-input {
width: 110px;
}
ha-svg-icon {
color: var(--secondary-text-color);
}

View File

@@ -3,7 +3,6 @@ import "@material/mwc-list/mwc-list";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
@@ -19,6 +18,7 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types";
import "./date-range-picker";
import "./ha-svg-icon";
import "./ha-textfield";
export interface DateRangePickerRanges {
[key: string]: [Date, Date];
@@ -61,7 +61,7 @@ export class HaDateRangePicker extends LitElement {
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input
<ha-textfield
.value=${formatDateTime(this.startDate, this.hass.locale)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
@@ -69,16 +69,16 @@ export class HaDateRangePicker extends LitElement {
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
<paper-input
></ha-textfield>
<ha-textfield
.value=${formatDateTime(this.endDate, this.hass.locale)}
label=${this.hass.localize(
.label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
></ha-textfield>
</div>
${this.ranges
? html`<div
@@ -158,13 +158,13 @@ export class HaDateRangePicker extends LitElement {
border-top: 1px solid var(--divider-color);
}
paper-input {
ha-textfield {
display: inline-block;
max-width: 250px;
min-width: 200px;
}
paper-input:last-child {
ha-textfield:last-child {
margin-left: 8px;
}
@@ -176,7 +176,7 @@ export class HaDateRangePicker extends LitElement {
}
@media only screen and (max-width: 500px) {
paper-input {
ha-textfield {
min-width: inherit;
}

View File

@@ -0,0 +1,95 @@
import "./ha-form";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import type {
HaFormGridSchema,
HaFormDataContainer,
HaFormElement,
HaFormSchema,
} from "./types";
import type { HomeAssistant } from "../../types";
@customElement("ha-form-grid")
export class HaFormGrid extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data!: HaFormDataContainer;
@property({ attribute: false }) public schema!: HaFormGridSchema;
@property({ type: Boolean }) public disabled = false;
@property() public computeLabel?: (
schema: HaFormSchema,
data?: HaFormDataContainer
) => string;
@property() public computeHelper?: (schema: HaFormSchema) => string;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.setAttribute("own-margin", "");
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("schema")) {
if (this.schema.column_min_width) {
this.style.setProperty(
"--form-grid-min-width",
this.schema.column_min_width
);
} else {
this.style.setProperty("--form-grid-min-width", "");
}
}
}
protected render(): TemplateResult {
return html`
${this.schema.schema.map(
(item) =>
html`
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${[item]}
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
></ha-form>
`
)}
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: grid !important;
grid-template-columns: repeat(
var(--form-grid-column-count, auto-fit),
minmax(var(--form-grid-min-width, 200px), 1fr)
);
grid-gap: 8px;
}
:host > ha-form {
display: block;
margin-bottom: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-grid": HaFormGrid;
}
}

View File

@@ -1,4 +1,3 @@
import "@material/mwc-select/mwc-select";
import { mdiMenuDown, mdiMenuUp } from "@mdi/js";
import {
css,
@@ -11,7 +10,8 @@ import {
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-button-menu";
import { HaCheckListItem } from "../ha-check-list-item";
import "../ha-check-list-item";
import type { HaCheckListItem } from "../ha-check-list-item";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-formfield";

View File

@@ -1,14 +1,13 @@
import "@material/mwc-select";
import type { Select } from "@material/mwc-select";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-radio";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
import { stopPropagation } from "../../common/dom/stop_propagation";
import "../ha-radio";
import type { HaRadio } from "../ha-radio";
import "../ha-select";
import type { HaSelect } from "../ha-select";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@@ -20,7 +19,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("mwc-select", true) private _input?: HTMLElement;
@query("ha-select", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
@@ -50,7 +49,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
}
return html`
<mwc-select
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
@@ -67,13 +66,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
<mwc-list-item .value=${value}>${label}</mwc-list-item>
`
)}
</mwc-select>
</ha-select>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
let value: string | undefined = (ev.target as Select | HaRadio).value;
let value: string | undefined = (ev.target as HaSelect | HaRadio).value;
if (value === this.data) {
return;
@@ -90,7 +89,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
static get styles(): CSSResultGroup {
return css`
mwc-select,
ha-select,
mwc-formfield {
display: block;
}

View File

@@ -1,10 +1,18 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-alert";
import "./ha-form-boolean";
import "./ha-form-constant";
import "./ha-form-grid";
import "./ha-form-float";
import "./ha-form-integer";
import "./ha-form-multi_select";
@@ -14,17 +22,18 @@ import "./ha-form-string";
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
import { HomeAssistant } from "../../types";
const getValue = (obj, item) => (obj ? obj[item.name] : null);
const getValue = (obj, item) =>
obj ? (!item.name ? obj : obj[item.name]) : null;
let selectorImported = false;
@customElement("ha-form")
export class HaForm extends LitElement implements HaFormElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public data!: HaFormDataContainer;
@property({ attribute: false }) public data!: HaFormDataContainer;
@property() public schema!: HaFormSchema[];
@property({ attribute: false }) public schema!: HaFormSchema[];
@property() public error?: Record<string, string>;
@@ -32,7 +41,12 @@ export class HaForm extends LitElement implements HaFormElement {
@property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (schema: HaFormSchema) => string;
@property() public computeLabel?: (
schema: HaFormSchema,
data?: HaFormDataContainer
) => string;
@property() public computeHelper?: (schema: HaFormSchema) => string;
public focus() {
const root = this.shadowRoot?.querySelector(".root");
@@ -59,7 +73,7 @@ export class HaForm extends LitElement implements HaFormElement {
}
}
protected render() {
protected render(): TemplateResult {
return html`
<div class="root">
${this.error && this.error.base
@@ -71,6 +85,7 @@ export class HaForm extends LitElement implements HaFormElement {
: ""}
${this.schema.map((item) => {
const error = getValue(this.error, item);
return html`
${error
? html`
@@ -85,15 +100,19 @@ export class HaForm extends LitElement implements HaFormElement {
.hass=${this.hass}
.selector=${item.selector}
.value=${getValue(this.data, item)}
.label=${this._computeLabel(item)}
.label=${this._computeLabel(item, this.data)}
.disabled=${this.disabled}
.helper=${this._computeHelper(item)}
.required=${item.required || false}
></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, {
schema: item,
data: getValue(this.data, item),
label: this._computeLabel(item),
label: this._computeLabel(item, this.data),
disabled: this.disabled,
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
})}
`;
})}
@@ -107,21 +126,30 @@ export class HaForm extends LitElement implements HaFormElement {
root.addEventListener("value-changed", (ev) => {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
const newValue = !schema.name
? ev.detail.value
: { [schema.name]: ev.detail.value };
fireEvent(this, "value-changed", {
value: { ...this.data, [schema.name]: ev.detail.value },
value: { ...this.data, ...newValue },
});
});
return root;
}
private _computeLabel(schema: HaFormSchema) {
private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) {
return this.computeLabel
? this.computeLabel(schema)
? this.computeLabel(schema, data)
: schema
? schema.name
: "";
}
private _computeHelper(schema: HaFormSchema) {
return this.computeHelper ? this.computeHelper(schema) : "";
}
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error;
}

View File

@@ -11,7 +11,8 @@ export type HaFormSchema =
| HaFormSelectSchema
| HaFormMultiSelectSchema
| HaFormTimeSchema
| HaFormSelector;
| HaFormSelector
| HaFormGridSchema;
export interface HaFormBaseSchema {
name: string;
@@ -25,6 +26,13 @@ export interface HaFormBaseSchema {
};
}
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
name: "";
column_min_width?: string;
schema: HaFormSchema[];
}
export interface HaFormSelector extends HaFormBaseSchema {
type?: never;
selector: Selector;
@@ -49,7 +57,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema {
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select";
options: Record<string, string> | string[];
options: Record<string, string> | string[] | Array<[string, string]>;
}
export interface HaFormFloatSchema extends HaFormBaseSchema {

View File

@@ -43,6 +43,8 @@ class HaHLSPlayer extends LitElement {
@state() private _error?: string;
@state() private _errorIsFatal = false;
private _hlsPolyfillInstance?: HlsLite;
private _exoPlayer = false;
@@ -53,6 +55,7 @@ class HaHLSPlayer extends LitElement {
super.connectedCallback();
HaHLSPlayer.streamCount += 1;
if (this.hasUpdated) {
this._resetError();
this._startHls();
}
}
@@ -64,16 +67,23 @@ class HaHLSPlayer extends LitElement {
}
protected render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`
<video
?autoplay=${this.autoPlay}
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
></video>
${this._error
? html`<ha-alert
alert-type="error"
class=${this._errorIsFatal ? "fatal" : "retry"}
>
${this._error}
</ha-alert>`
: ""}
${!this._errorIsFatal
? html`<video
?autoplay=${this.autoPlay}
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
></video>`
: ""}
`;
}
@@ -87,12 +97,11 @@ class HaHLSPlayer extends LitElement {
}
this._cleanUp();
this._resetError();
this._startHls();
}
private async _startHls(): Promise<void> {
this._error = undefined;
const masterPlaylistPromise = fetch(this.url);
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
@@ -110,8 +119,8 @@ class HaHLSPlayer extends LitElement {
}
if (!hlsSupported) {
this._error = this.hass.localize(
"ui.components.media-browser.video_not_supported"
this._setFatalError(
this.hass.localize("ui.components.media-browser.video_not_supported")
);
return;
}
@@ -219,9 +228,16 @@ class HaHLSPlayer extends LitElement {
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
this._resetError();
hls.loadSource(url);
});
hls.on(Hls.Events.ERROR, (_, data: any) => {
hls.on(Hls.Events.FRAG_LOADED, (_event, _data: any) => {
this._resetError();
});
hls.on(Hls.Events.ERROR, (_event, data: any) => {
// Some errors are recovered automatically by the hls player itself, and the others handled
// in this function require special actions to recover. Errors retried in this function
// are done with backoff to not cause unecessary failures.
if (!data.fatal) {
return;
}
@@ -241,22 +257,22 @@ class HaHLSPlayer extends LitElement {
error += " (" + data.response.code + ")";
}
}
this._error = error;
return;
this._setRetryableError(error);
break;
}
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
this._error = "Timeout while starting stream";
return;
this._setRetryableError("Timeout while starting stream");
break;
default:
this._error = "Unknown stream network error (" + data.details + ")";
return;
this._setRetryableError("Stream network error");
break;
}
this._error = "Error with media stream contents (" + data.details + ")";
hls.startLoad();
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
this._error = "Error with media stream contents (" + data.details + ")";
this._setRetryableError("Error with media stream contents");
hls.recoverMediaError();
} else {
this._error =
"Unknown error with stream (" + data.type + ", " + data.details + ")";
this._setFatalError("Error playing stream");
}
});
}
@@ -284,6 +300,21 @@ class HaHLSPlayer extends LitElement {
}
}
private _resetError() {
this._error = undefined;
this._errorIsFatal = false;
}
private _setFatalError(errorMessage: string) {
this._error = errorMessage;
this._errorIsFatal = true;
}
private _setRetryableError(errorMessage: string) {
this._error = errorMessage;
this._errorIsFatal = false;
}
static get styles(): CSSResultGroup {
return css`
:host,
@@ -296,10 +327,14 @@ class HaHLSPlayer extends LitElement {
max-height: var(--video-max-height, calc(100vh - 97px));
}
ha-alert {
.fatal {
display: block;
padding: 100px 16px;
}
.retry {
display: block;
}
`;
}
}

View File

@@ -1,28 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import { PolymerElement } from "@polymer/polymer";
import { Constructor } from "../types";
const paperDropdownClass = customElements.get(
"paper-dropdown-menu"
) as Constructor<PolymerElement>;
// patches paper drop down to properly support RTL - https://github.com/PolymerElements/paper-dropdown-menu/issues/183
export class HaPaperDropdownClass extends paperDropdownClass {
public ready() {
super.ready();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
if (window.getComputedStyle(this).direction === "rtl") {
this.style.textAlign = "right";
}
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-paper-dropdown-menu": HaPaperDropdownClass;
}
}
customElements.define("ha-paper-dropdown-menu", HaPaperDropdownClass);

View File

@@ -1,5 +1,4 @@
import { mdiImagePlus } from "@mdi/js";
import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";

View File

@@ -1,6 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { mdiCamera } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";

View File

@@ -0,0 +1,54 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
@customElement("ha-select")
export class HaSelect extends SelectBase {
// @ts-ignore
@property({ type: Boolean }) public icon?: boolean;
protected override renderLeadingIcon() {
if (!this.icon) {
return nothing;
}
return html`<span class="mdc-select__icon"
><slot name="icon"></slot
></span>`;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
);
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
static override styles = [
styles,
css`
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-select": HaSelect;
}
}

View File

@@ -35,9 +35,12 @@ export class HaBooleanSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
height: 56px;
display: flex;
}
ha-formfield {
width: 100%;
margin: 16px 0;
--mdc-typography-body2-font-size: 1em;
}
`;

View File

@@ -50,7 +50,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
private _filterEntities = (entity: HassEntity): boolean => {
if (this.selector.entity?.domain) {
if (computeStateDomain(entity) !== this.selector.entity.domain) {
const filterDomain = this.selector.entity.domain;
const entityDomain = computeStateDomain(entity);
if (
(Array.isArray(filterDomain) && !filterDomain.includes(entityDomain)) ||
entityDomain !== filterDomain
) {
return false;
}
}

View File

@@ -0,0 +1,41 @@
import "../ha-icon-picker";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../types";
import { IconSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-selector-icon")
export class HaIconSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: IconSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-icon-picker
.label=${this.label}
.value=${this.value}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}
></ha-icon-picker>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", { value: ev.detail.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-icon": HaIconSelector;
}
}

View File

@@ -0,0 +1,264 @@
import { mdiPlayBox, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { supportsFeature } from "../../common/entity/supports-feature";
import { getSignedPath } from "../../data/auth";
import {
MediaClassBrowserSettings,
MediaPickedEvent,
SUPPORT_BROWSE_MEDIA,
} from "../../data/media-player";
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-form/ha-form";
import type { HaFormSchema } from "../ha-form/types";
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
const MANUAL_SCHEMA = [
{ name: "media_content_id", required: false, selector: { text: {} } },
{ name: "media_content_type", required: false, selector: { text: {} } },
];
@customElement("ha-selector-media")
export class HaMediaSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: MediaSelector;
@property({ attribute: false }) public value?: MediaSelectorValue;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@state() private _thumbnailUrl?: string | null;
willUpdate(changedProps: PropertyValues<this>) {
if (changedProps.has("value")) {
const thumbnail = this.value?.metadata?.thumbnail;
const oldThumbnail = (changedProps.get("value") as this["value"])
?.metadata?.thumbnail;
if (thumbnail === oldThumbnail) {
return;
}
if (thumbnail && thumbnail.startsWith("/")) {
this._thumbnailUrl = undefined;
// Thumbnails served by local API require authentication
getSignedPath(this.hass, thumbnail).then((signedPath) => {
this._thumbnailUrl = signedPath.path;
});
} else {
this._thumbnailUrl = thumbnail;
}
}
}
protected render() {
const stateObj = this.value?.entity_id
? this.hass.states[this.value.entity_id]
: undefined;
const supportsBrowse =
!this.value?.entity_id ||
(stateObj && supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA));
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value?.entity_id}
.label=${this.label ||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
.disabled=${this.disabled}
include-domains='["media_player"]'
allow-custom-entity
@value-changed=${this._entityChanged}
></ha-entity-picker>
${!supportsBrowse
? html`<ha-alert>
${this.hass.localize(
"ui.components.selectors.media.browse_not_supported"
)}
</ha-alert>
<ha-form
.hass=${this.hass}
.data=${this.value}
.schema=${MANUAL_SCHEMA}
.computeLabel=${this._computeLabelCallback}
></ha-form>`
: html`<ha-card
outlined
@click=${this._pickMedia}
class=${this.disabled || !this.value?.entity_id ? "disabled" : ""}
>
<div
class="thumbnail ${classMap({
portrait:
!!this.value?.metadata?.media_class &&
MediaClassBrowserSettings[
this.value.metadata.children_media_class ||
this.value.metadata.media_class
].thumbnail_ratio === "portrait",
})}"
>
${this.value?.metadata?.thumbnail
? html`
<div
class="${classMap({
"centered-image":
!!this.value.metadata.media_class &&
["app", "directory"].includes(
this.value.metadata.media_class
),
})}
image"
style=${this._thumbnailUrl
? `background-image: url(${this._thumbnailUrl});`
: ""}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${!this.value?.media_content_id
? mdiPlus
: this.value?.metadata?.media_class
? MediaClassBrowserSettings[
this.value.metadata.media_class === "directory"
? this.value.metadata.children_media_class ||
this.value.metadata.media_class
: this.value.metadata.media_class
].icon
: mdiPlayBox}
></ha-svg-icon>
</div>
`}
</div>
<div class="title">
${!this.value?.media_content_id
? this.hass.localize("ui.components.selectors.media.pick_media")
: this.value.metadata?.title || this.value.media_content_id}
</div>
</ha-card>`}`;
}
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(`ui.components.selectors.media.${schema.name}`);
private _entityChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
entity_id: ev.detail.value,
media_content_id: "",
media_content_type: "",
},
});
}
private _pickMedia() {
showMediaBrowserDialog(this, {
action: "pick",
entityId: this.value!.entity_id!,
navigateIds: this.value!.metadata?.navigateIds,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
fireEvent(this, "value-changed", {
value: {
...this.value,
media_content_id: pickedMedia.item.media_content_id,
media_content_type: pickedMedia.item.media_content_type,
metadata: {
title: pickedMedia.item.title,
thumbnail: pickedMedia.item.thumbnail,
media_class: pickedMedia.item.media_class,
children_media_class: pickedMedia.item.children_media_class,
navigateIds: pickedMedia.navigateIds?.map((id) => ({
media_content_type: id.media_content_type,
media_content_id: id.media_content_id,
})),
},
},
});
},
});
}
static get styles(): CSSResultGroup {
return css`
ha-entity-picker {
display: block;
margin-bottom: 16px;
}
mwc-button {
margin-top: 8px;
}
ha-alert {
display: block;
margin-bottom: 16px;
}
ha-card {
position: relative;
width: 200px;
box-sizing: border-box;
cursor: pointer;
}
ha-card.disabled {
pointer-events: none;
color: var(--disabled-text-color);
}
ha-card .thumbnail {
width: 100%;
position: relative;
box-sizing: border-box;
transition: padding-bottom 0.1s ease-out;
padding-bottom: 100%;
}
ha-card .thumbnail.portrait {
padding-bottom: 150%;
}
ha-card .image {
border-radius: 3px 3px 0 0;
}
.folder {
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
}
.title {
font-size: 16px;
padding-top: 16px;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 16px;
padding-left: 16px;
padding-right: 4px;
white-space: nowrap;
}
.image {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
.centered-image {
margin: 0 8px;
background-size: contain;
}
.icon-holder {
display: flex;
justify-content: center;
align-items: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-media": HaMediaSelector;
}
}

View File

@@ -19,22 +19,24 @@ export class HaNumberSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public required = true;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.label}
${this.selector.number.mode !== "box"
? html`<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>`
return html`${this.selector.number.mode !== "box"
? html`${this.label}<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>`
: ""}
<ha-textfield
inputMode="numeric"
@@ -44,9 +46,10 @@ export class HaNumberSelector extends LitElement {
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value}
.value=${this.value || ""}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}
type="number"
autoValidate
@@ -57,14 +60,16 @@ export class HaNumberSelector extends LitElement {
}
private get _value() {
return this.value ?? 0;
return this.value ?? (this.selector.number.min || 0);
}
private _handleInputChange(ev) {
ev.stopPropagation();
const value =
ev.target.value === "" || isNaN(ev.target.value)
? undefined
? this.required
? this.selector.number.min || 0
: undefined
: Number(ev.target.value);
if (this.value === value) {
return;

View File

@@ -1,11 +1,11 @@
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { SelectSelector } from "../../data/selector";
import { SelectOption, SelectSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
import "../ha-select";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@@ -17,24 +17,28 @@ export class HaSelectSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<mwc-select
return html`<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${this.selector.select.options.map(
(item: string) => html`
<mwc-list-item .value=${item}>${item}</mwc-list-item>
`
)}
</mwc-select>`;
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</ha-select>`;
}
private _valueChanged(ev) {
@@ -49,7 +53,7 @@ export class HaSelectSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
mwc-select {
ha-select {
width: 100%;
}
`;

View File

@@ -69,10 +69,14 @@ export class HaTextSelector extends LitElement {
}
private _handleChange(ev) {
const value = ev.target.value;
let value = ev.target.value;
if (this.value === value) {
return;
}
if (value === "" && !this.required) {
value = undefined;
}
fireEvent(this, "value-changed", { value });
}

View File

@@ -0,0 +1,34 @@
import "../../panels/lovelace/components/hui-theme-select-editor";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ThemeSelector } from "../../data/selector";
@customElement("ha-selector-theme")
export class HaThemeSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ThemeSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<hui-theme-select-editor
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
></hui-theme-select-editor>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-theme": HaThemeSelector;
}
}

View File

@@ -22,6 +22,7 @@ export class HaTimeSelector extends LitElement {
.value=${this.value}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.label=${this.label}
enable-second
></ha-time-input>
`;

View File

@@ -1,8 +1,8 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { Selector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import type { Selector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "./ha-selector-action";
import "./ha-selector-addon";
import "./ha-selector-area";
@@ -17,6 +17,9 @@ import "./ha-selector-select";
import "./ha-selector-target";
import "./ha-selector-text";
import "./ha-selector-time";
import "./ha-selector-icon";
import "./ha-selector-media";
import "./ha-selector-theme";
@customElement("ha-selector")
export class HaSelector extends LitElement {
@@ -28,6 +31,8 @@ export class HaSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: any;
@property({ type: Boolean }) public disabled = false;
@@ -52,6 +57,7 @@ export class HaSelector extends LitElement {
placeholder: this.placeholder,
disabled: this.disabled,
required: this.required,
helper: this.helper,
id: "selector",
})}
`;

View File

@@ -489,9 +489,6 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 60%;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}

View File

@@ -53,6 +53,10 @@ export class HaTextField extends TextFieldBase {
padding-right: var(--text-field-suffix-padding-right, 0px);
}
.mdc-text-field__icon {
color: var(--secondary-text-color);
}
input {
text-align: var(--text-field-text-align);
}
@@ -68,6 +72,14 @@ export class HaTextField extends TextFieldBase {
:host([no-spinner]) input[type="number"] {
-moz-appearance: textfield;
}
.mdc-text-field__ripple {
overflow: hidden;
}
.mdc-text-field {
overflow: var(--text-field-overflow);
}
`,
];
}

View File

@@ -43,7 +43,7 @@ class HaWebRtcPlayer extends LitElement {
private _remoteStream?: MediaStream;
protected render(): TemplateResult {
protected override render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
@@ -58,12 +58,19 @@ class HaWebRtcPlayer extends LitElement {
`;
}
public disconnectedCallback() {
public override connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._startWebRtc();
}
}
public override disconnectedCallback() {
super.disconnectedCallback();
this._cleanUp();
}
protected updated(changedProperties: PropertyValues<this>) {
protected override updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("entityid")) {
return;
}

View File

@@ -0,0 +1,337 @@
import { animate } from "@lit-labs/motion";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiDelete } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import {
MediaClassBrowserSettings,
MediaPlayerItem,
} from "../../data/media-player";
import {
browseLocalMediaPlayer,
removeLocalMedia,
} from "../../data/media_source";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-circular-progress";
import "../ha-dialog";
import "../ha-header-bar";
import "../ha-svg-icon";
import "../ha-check-list-item";
import "./ha-media-player-browse";
import "./ha-media-upload-button";
import type { MediaManageDialogParams } from "./show-media-manage-dialog";
@customElement("dialog-media-manage")
class DialogMediaManage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _currentItem?: MediaPlayerItem;
@state() private _params?: MediaManageDialogParams;
@state() private _uploading = false;
@state() private _deleting = false;
@state() private _selected = new Set<number>();
private _filesChanged = false;
public showDialog(params: MediaManageDialogParams): void {
this._params = params;
this._refreshMedia();
}
public closeDialog() {
if (this._filesChanged && this._params!.onClose) {
this._params!.onClose();
}
this._params = undefined;
this._currentItem = undefined;
this._uploading = false;
this._deleting = false;
this._filesChanged = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
const children =
this._currentItem?.children?.filter((child) => !child.can_expand) || [];
let fileIndex = 0;
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
flexContent
.heading=${this._params.currentItem.title}
@closed=${this.closeDialog}
>
<ha-header-bar slot="heading">
${this._selected.size === 0
? html`
<span slot="title">
${this.hass.localize(
"ui.components.media-browser.file_management.title"
)}
</span>
<ha-media-upload-button
.disabled=${this._deleting}
.hass=${this.hass}
.currentItem=${this._params.currentItem}
@uploading=${this._startUploading}
@media-refresh=${this._doneUploading}
slot="actionItems"
></ha-media-upload-button>
${this._uploading
? ""
: html`
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
slot="actionItems"
class="header_button"
dir=${computeRTLDirection(this.hass)}
></ha-icon-button>
`}
`
: html`
<mwc-button
class="danger"
slot="title"
.disabled=${this._deleting}
.label=${this.hass.localize(
`ui.components.media-browser.file_management.${
this._deleting ? "deleting" : "delete"
}`,
{ count: this._selected.size }
)}
@click=${this._handleDelete}
>
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
</mwc-button>
${this._deleting
? ""
: html`
<mwc-button
slot="actionItems"
.label=${`Deselect all`}
@click=${this._handleDeselectAll}
>
<ha-svg-icon
.path=${mdiClose}
slot="icon"
></ha-svg-icon>
</mwc-button>
`}
`}
</ha-header-bar>
${!this._currentItem
? html`
<div class="refresh">
<ha-circular-progress active></ha-circular-progress>
</div>
`
: !children.length
? html`<div class="no-items">
<p>
${this.hass.localize(
"ui.components.media-browser.file_management.no_items"
)}
</p>
${this._currentItem?.children?.length
? html`<span class="folders"
>${this.hass.localize(
"ui.components.media-browser.file_management.folders_not_supported"
)}</span
>`
: ""}
</div>`
: html`
<mwc-list multi @selected=${this._handleSelected}>
${repeat(
children,
(item) => item.media_content_id,
(item) => {
const icon = html`
<ha-svg-icon
slot="graphic"
.path=${MediaClassBrowserSettings[
item.media_class === "directory"
? item.children_media_class || item.media_class
: item.media_class
].icon}
></ha-svg-icon>
`;
return html`
<ha-check-list-item
${animate({
id: item.media_content_id,
skipInitial: true,
})}
graphic="icon"
.disabled=${this._uploading || this._deleting}
.selected=${this._selected.has(fileIndex++)}
.item=${item}
>
${icon} ${item.title}
</ha-check-list-item>
`;
}
)}
</mwc-list>
`}
</ha-dialog>
`;
}
private _handleSelected(ev) {
this._selected = ev.detail.index;
}
private _startUploading() {
this._uploading = true;
this._filesChanged = true;
}
private _doneUploading() {
this._uploading = false;
this._refreshMedia();
}
private _handleDeselectAll() {
if (this._selected.size) {
this._selected = new Set();
}
}
private async _handleDelete() {
if (
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.file_management.confirm_delete",
{ count: this._selected.size }
),
warning: true,
}))
) {
return;
}
this._filesChanged = true;
this._deleting = true;
const toDelete: MediaPlayerItem[] = [];
let fileIndex = 0;
this._currentItem!.children!.forEach((item) => {
if (item.can_expand) {
return;
}
if (this._selected.has(fileIndex++)) {
toDelete.push(item);
}
});
try {
await Promise.all(
toDelete.map(async (item) => {
await removeLocalMedia(this.hass, item.media_content_id);
this._currentItem = {
...this._currentItem!,
children: this._currentItem!.children!.filter((i) => i !== item),
};
})
);
} finally {
this._deleting = false;
this._selected = new Set();
}
}
private async _refreshMedia() {
this._selected = new Set();
this._currentItem = undefined;
this._currentItem = await browseLocalMediaPlayer(
this.hass,
this._params!.currentItem.media_content_id
);
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 8;
--dialog-content-padding: 0;
}
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100vh - 72px);
}
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
}
ha-media-upload-button,
mwc-button {
--mdc-theme-primary: var(--mdc-theme-on-primary);
}
.danger {
--mdc-theme-primary: var(--error-color);
}
ha-svg-icon[slot="icon"] {
vertical-align: middle;
}
.refresh {
display: flex;
height: 200px;
justify-content: center;
align-items: center;
}
.no-items {
text-align: center;
padding: 16px;
}
.folders {
color: var(--secondary-text-color);
font-style: italic;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-media-manage": DialogMediaManage;
}
}

View File

@@ -1,7 +1,7 @@
import "../ha-header-bar";
import { mdiArrowLeft, mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import type {
@@ -13,7 +13,11 @@ import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-dialog";
import "./ha-media-player-browse";
import type { MediaPlayerItemId } from "./ha-media-player-browse";
import "./ha-media-manage-button";
import type {
HaMediaPlayerBrowse,
MediaPlayerItemId,
} from "./ha-media-player-browse";
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
@customElement("dialog-media-player-browse")
@@ -26,12 +30,14 @@ class DialogMediaPlayerBrowse extends LitElement {
@state() private _params?: MediaPlayerBrowseDialogParams;
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
public showDialog(params: MediaPlayerBrowseDialogParams): void {
this._params = params;
this._navigateIds = [
this._navigateIds = params.navigateIds || [
{
media_content_id: this._params.mediaContentId,
media_content_type: this._params.mediaContentType,
media_content_id: undefined,
media_content_type: undefined,
},
];
}
@@ -80,6 +86,12 @@ class DialogMediaPlayerBrowse extends LitElement {
: this._currentItem.title}
</span>
<ha-media-manage-button
slot="actionItems"
.hass=${this.hass}
.currentItem=${this._currentItem}
@media-refresh=${this._refreshMedia}
></ha-media-manage-button>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
@@ -124,6 +136,10 @@ class DialogMediaPlayerBrowse extends LitElement {
return this._params!.action || "play";
}
private _refreshMedia() {
this._browser.refresh();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
@@ -157,6 +173,10 @@ class DialogMediaPlayerBrowse extends LitElement {
flex-shrink: 0;
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
}
ha-media-manage-button {
--mdc-theme-primary: var(--mdc-theme-on-primary);
}
`,
];
}

View File

@@ -0,0 +1,280 @@
import "@material/mwc-list/mwc-list-item";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { LocalStorage } from "../../common/decorators/local-storage";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { fetchCloudStatus, updateCloudPref } from "../../data/cloud";
import {
CloudTTSInfo,
getCloudTTSInfo,
getCloudTtsLanguages,
getCloudTtsSupportedGenders,
} from "../../data/cloud/tts";
import {
MediaPlayerBrowseAction,
MediaPlayerItem,
} from "../../data/media-player";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { buttonLinkStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "../ha-select";
import "../ha-textarea";
export interface TtsMediaPickedEvent {
item: MediaPlayerItem;
}
declare global {
interface HASSDomEvents {
"tts-picked": TtsMediaPickedEvent;
}
}
@customElement("ha-browse-media-tts")
class BrowseMediaTTS extends LitElement {
@property() public hass!: HomeAssistant;
@property() public item;
@property() public action!: MediaPlayerBrowseAction;
@state() private _cloudDefaultOptions?: [string, string];
@state() private _cloudOptions?: [string, string];
@state() private _cloudTTSInfo?: CloudTTSInfo;
@LocalStorage("cloudTtsTryMessage", true, false) private _message!: string;
protected render() {
return html`<ha-card>
<div class="card-content">
<ha-textarea
autogrow
.label=${this.hass.localize(
"ui.components.media-browser.tts.message"
)}
.value=${this._message ||
this.hass.localize(
"ui.components.media-browser.tts.example_message",
{
name: this.hass.user?.name || "",
}
)}
>
</ha-textarea>
${this._cloudDefaultOptions ? this._renderCloudOptions() : ""}
</div>
<div class="card-actions">
${this._cloudDefaultOptions &&
(this._cloudDefaultOptions![0] !== this._cloudOptions![0] ||
this._cloudDefaultOptions![1] !== this._cloudOptions![1])
? html`
<button class="link" @click=${this._storeDefaults}>
${this.hass.localize(
"ui.components.media-browser.tts.set_as_default"
)}
</button>
`
: html`<span></span>`}
<mwc-button @click=${this._ttsClicked}>
${this.hass.localize(
`ui.components.media-browser.tts.action_${this.action}`
)}
</mwc-button>
</div>
</ha-card> `;
}
private _renderCloudOptions() {
if (!this._cloudTTSInfo || !this._cloudOptions) {
return "";
}
const languages = this.getLanguages(this._cloudTTSInfo);
const selectedVoice = this._cloudOptions;
const genders = this.getSupportedGenders(
selectedVoice[0],
this._cloudTTSInfo,
this.hass.localize
);
return html`
<div class="cloud-options">
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize(
"ui.components.media-browser.tts.language"
)}
.value=${selectedVoice[0]}
@selected=${this._handleLanguageChange}
@closed=${stopPropagation}
>
${languages.map(
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</ha-select>
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize("ui.components.media-browser.tts.gender")}
.value=${selectedVoice[1]}
@selected=${this._handleGenderChange}
@closed=${stopPropagation}
>
${genders.map(
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</ha-select>
</div>
`;
}
protected override willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("item")) {
if (this.item.media_content_id) {
const params = new URLSearchParams(
this.item.media_content_id.split("?")[1]
);
const message = params.get("message");
const language = params.get("language");
const gender = params.get("gender");
if (message) {
this._message = message;
}
if (language && gender) {
this._cloudOptions = [language, gender];
}
}
if (this.isCloudItem && !this._cloudTTSInfo) {
getCloudTTSInfo(this.hass).then((info) => {
this._cloudTTSInfo = info;
});
fetchCloudStatus(this.hass).then((status) => {
if (status.logged_in) {
this._cloudDefaultOptions = status.prefs.tts_default_voice;
if (!this._cloudOptions) {
this._cloudOptions = { ...this._cloudDefaultOptions };
}
}
});
}
}
if (changedProps.has("message")) {
return;
}
// Re-rendering can reset message because textarea content is newer than local storage.
// But we don't want to write every keystroke to local storage.
// So instead we just do it when we're going to render.
const message = this.shadowRoot!.querySelector("ha-textarea")?.value;
if (message !== undefined && message !== this._message) {
this._message = message;
}
}
async _handleLanguageChange(ev) {
if (ev.target.value === this._cloudOptions![0]) {
return;
}
this._cloudOptions = [ev.target.value, this._cloudOptions![1]];
}
async _handleGenderChange(ev) {
if (ev.target.value === this._cloudOptions![1]) {
return;
}
this._cloudOptions = [this._cloudOptions![0], ev.target.value];
}
private getLanguages = memoizeOne(getCloudTtsLanguages);
private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders);
private get isCloudItem(): boolean {
return this.item.media_content_id.startsWith("media-source://tts/cloud");
}
private async _ttsClicked(): Promise<void> {
const message = this.shadowRoot!.querySelector("ha-textarea")!.value;
this._message = message;
const item = { ...this.item };
const query = new URLSearchParams();
query.append("message", message);
if (this._cloudOptions) {
query.append("language", this._cloudOptions[0]);
query.append("gender", this._cloudOptions[1]);
}
item.media_content_id = `${
item.media_content_id.split("?")[0]
}?${query.toString()}`;
item.can_play = true;
item.title = message;
fireEvent(this, "tts-picked", { item });
}
private async _storeDefaults() {
const oldDefaults = this._cloudDefaultOptions!;
this._cloudDefaultOptions = [...this._cloudOptions!];
try {
await updateCloudPref(this.hass, {
tts_default_voice: this._cloudDefaultOptions,
});
} catch (err: any) {
this._cloudDefaultOptions = oldDefaults;
showAlertDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.tts.faild_to_store_defaults",
{ error: err.message || err }
),
});
}
}
static override styles = [
buttonLinkStyle,
css`
:host {
margin: 16px auto;
padding: 0 8px;
display: flex;
flex-direction: column;
max-width: 400px;
}
.cloud-options {
margin-top: 16px;
display: flex;
justify-content: space-between;
}
.cloud-options ha-select {
width: 48%;
}
ha-textarea {
width: 100%;
}
button.link {
color: var(--primary-color);
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-browse-media-tts": BrowseMediaTTS;
}
}

View File

@@ -0,0 +1,69 @@
import { mdiFolderEdit } from "@mdi/js";
import "@material/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { MediaPlayerItem } from "../../data/media-player";
import "../ha-svg-icon";
import { isLocalMediaSourceContentId } from "../../data/media_source";
import type { HomeAssistant } from "../../types";
import { showMediaManageDialog } from "./show-media-manage-dialog";
import { fireEvent } from "../../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"media-refresh": unknown;
}
}
@customElement("ha-media-manage-button")
class MediaManageButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() currentItem?: MediaPlayerItem;
@state() _uploading = 0;
protected render(): TemplateResult {
if (
!this.currentItem ||
!isLocalMediaSourceContentId(this.currentItem.media_content_id || "")
) {
return html``;
}
return html`
<mwc-button
.label=${this.hass.localize(
"ui.components.media-browser.file_management.manage"
)}
@click=${this._manage}
>
<ha-svg-icon .path=${mdiFolderEdit} slot="icon"></ha-svg-icon>
</mwc-button>
`;
}
private _manage() {
showMediaManageDialog(this, {
currentItem: this.currentItem!,
onClose: () => fireEvent(this, "media-refresh"),
});
}
static styles = css`
mwc-button {
/* We use icon + text to show disabled state */
--mdc-button-disabled-ink-color: --mdc-theme-primary;
}
ha-svg-icon[slot="icon"],
ha-circular-progress[slot="icon"] {
vertical-align: middle;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-media-manage-button": MediaManageButton;
}
}

View File

@@ -1,8 +1,7 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
@@ -49,11 +48,21 @@ import "../ha-icon-button";
import "../ha-svg-icon";
import "../ha-fab";
import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import "./ha-browse-media-tts";
declare global {
interface HASSDomEvents {
"media-picked": MediaPickedEvent;
"media-browsed": { ids: MediaPlayerItemId[]; current?: MediaPlayerItem };
"media-browsed": {
// Items of the new browse stack
ids: MediaPlayerItemId[];
// Current fetched item for this browse stack
current?: MediaPlayerItem;
// If the new stack should replace the old stack
replace?: boolean;
};
}
}
@@ -122,6 +131,11 @@ export class HaMediaPlayerBrowse extends LitElement {
currentId.media_content_id,
currentId.media_content_type
);
// Update the parent with latest item.
fireEvent(this, "media-browsed", {
ids: this.navigateIds,
current: this._currentItem,
});
} catch (err) {
this._setError(err);
}
@@ -149,10 +163,11 @@ export class HaMediaPlayerBrowse extends LitElement {
const subtitle = this.hass.localize(
`ui.components.media-browser.class.${currentItem.media_class}`
);
const children = currentItem.children || [];
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass =
MediaClassBrowserSettings[currentItem.children_media_class];
const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[currentItem.children_media_class]
: MediaClassBrowserSettings.directory;
return html`
${
@@ -246,159 +261,191 @@ export class HaMediaPlayerBrowse extends LitElement {
${this._renderError(this._error)}
</div>
`
: currentItem.children?.length
? childrenMediaClass.layout === "grid"
? html`
<div
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${currentItem.children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${[
"app",
"directory",
].includes(child.media_class)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip
fitToVisibleBounds
position="top"
offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
: isTTSMediaSource(currentItem.media_content_id)
? html`
<ha-browse-media-tts
.item=${currentItem}
.hass=${this.hass}
.action=${this.action}
@tts-picked=${this._ttsPicked}
></ha-browse-media-tts>
`
: !children.length && !currentItem.not_shown
? html`
<div class="container no-items">
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`
<div class="highlight-add-button">
<span>
<ha-svg-icon
.path=${mdiArrowUpRight}
></ha-svg-icon>
</span>
<span>
${this.hass.localize(
"ui.components.media-browser.file_management.highlight_button"
)}
</span>
</div>
`
)}
</div>
`
: html`
<mwc-list>
${currentItem.children.map(
(child) => html`
: this.hass.localize(
"ui.components.media-browser.no_items"
)}
</div>
`
: childrenMediaClass.layout === "grid"
? html`
<div
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${["app", "directory"].includes(
child.media_class
)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip
fitToVisibleBounds
position="top"
offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`
)}
${currentItem.not_shown
? html`
<div class="grid not-shown">
<div class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</div>
</div>
`
: ""}
</div>
`
: html`
<mwc-list>
${children.map(
(child) => html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
lazythumbnail:
mediaClass.show_list_images === true,
})}
data-src=${ifDefined(
mediaClass.show_list_images && child.thumbnail
? child.thumbnail
: undefined
)}
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show:
!mediaClass.show_list_images ||
!child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)}
${currentItem.not_shown
? html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
noninteractive
class="not-shown"
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
lazythumbnail:
mediaClass.show_list_images === true,
})}
data-src=${ifDefined(
mediaClass.show_list_images && child.thumbnail
? child.thumbnail
: undefined
<span class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show:
!mediaClass.show_list_images ||
!child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)}
</mwc-list>
`
: html`
<div class="container no-items">
${this.hass.localize(
"ui.components.media-browser.no_items"
)}
<br />
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`<br />${this.hass.localize(
"ui.components.media-browser.learn_adding_local_media",
"documentation",
html`<a
href=${documentationUrl(
this.hass,
"/more-info/local-media/add-media"
)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.components.media-browser.documentation"
)}</a
>`
)}
<br />
${this.hass.localize(
"ui.components.media-browser.local_media_files"
)}`
: ""}
</div>
</mwc-list>
`
}
</div>
@@ -425,8 +472,8 @@ export class HaMediaPlayerBrowse extends LitElement {
if (changedProps.has("entityId")) {
this._setError(undefined);
}
if (!changedProps.has("navigateIds")) {
} else if (!changedProps.has("navigateIds")) {
// Neither entity ID or navigateIDs changed, nothing to fetch
return;
}
@@ -435,6 +482,7 @@ export class HaMediaPlayerBrowse extends LitElement {
const oldNavigateIds = changedProps.get("navigateIds") as
| this["navigateIds"]
| undefined;
const navigateIds = this.navigateIds;
// We're navigating. Reset the shizzle.
this._content?.scrollTo(0, 0);
@@ -443,11 +491,9 @@ export class HaMediaPlayerBrowse extends LitElement {
const oldParentItem = this._parentItem;
this._currentItem = undefined;
this._parentItem = undefined;
const currentId = this.navigateIds[this.navigateIds.length - 1];
const currentId = navigateIds[navigateIds.length - 1];
const parentId =
this.navigateIds.length > 1
? this.navigateIds[this.navigateIds.length - 2]
: undefined;
navigateIds.length > 1 ? navigateIds[navigateIds.length - 2] : undefined;
let currentProm: Promise<MediaPlayerItem> | undefined;
let parentProm: Promise<MediaPlayerItem> | undefined;
@@ -456,9 +502,9 @@ export class HaMediaPlayerBrowse extends LitElement {
if (
// Check if we navigated to a child
oldNavigateIds &&
this.navigateIds.length > oldNavigateIds.length &&
navigateIds.length === oldNavigateIds.length + 1 &&
oldNavigateIds.every((oldVal, idx) => {
const curVal = this.navigateIds[idx];
const curVal = navigateIds[idx];
return (
curVal.media_content_id === oldVal.media_content_id &&
curVal.media_content_type === oldVal.media_content_type
@@ -469,8 +515,8 @@ export class HaMediaPlayerBrowse extends LitElement {
} else if (
// Check if we navigated to a parent
oldNavigateIds &&
this.navigateIds.length < oldNavigateIds.length &&
this.navigateIds.every((curVal, idx) => {
navigateIds.length === oldNavigateIds.length - 1 &&
navigateIds.every((curVal, idx) => {
const oldVal = oldNavigateIds[idx];
return (
curVal.media_content_id === oldVal.media_content_id &&
@@ -493,11 +539,33 @@ export class HaMediaPlayerBrowse extends LitElement {
(item) => {
this._currentItem = item;
fireEvent(this, "media-browsed", {
ids: this.navigateIds,
ids: navigateIds,
current: item,
});
},
(err) => this._setError(err)
(err) => {
// When we change entity ID, we will first try to see if the new entity is
// able to resolve the new path. If that results in an error, browse the root.
const isNewEntityWithSamePath =
oldNavigateIds &&
changedProps.has("entityId") &&
navigateIds.length === oldNavigateIds.length &&
oldNavigateIds.every(
(oldItem, idx) =>
navigateIds[idx].media_content_id === oldItem.media_content_id &&
navigateIds[idx].media_content_type === oldItem.media_content_type
);
if (isNewEntityWithSamePath) {
fireEvent(this, "media-browsed", {
ids: [
{ media_content_id: undefined, media_content_type: undefined },
],
replace: true,
});
} else {
this._setError(err);
}
}
);
// Fetch parent
if (!parentProm && parentId !== undefined) {
@@ -533,7 +601,17 @@ export class HaMediaPlayerBrowse extends LitElement {
}
private _runAction(item: MediaPlayerItem): void {
fireEvent(this, "media-picked", { item });
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
}
private _ttsPicked(ev: CustomEvent<TtsMediaPickedEvent>): void {
ev.stopPropagation();
const navigateIds = this.navigateIds.slice(0, -1);
navigateIds.push(ev.detail.item);
fireEvent(this, "media-picked", {
...ev.detail,
navigateIds,
});
}
private async _childClicked(ev: MouseEvent): Promise<void> {
@@ -736,6 +814,18 @@ export class HaMediaPlayerBrowse extends LitElement {
padding-left: 32px;
}
.highlight-add-button {
display: flex;
flex-direction: row-reverse;
margin-right: 48px;
}
.highlight-add-button ha-svg-icon {
position: relative;
top: -0.5em;
margin-left: 8px;
}
.content {
overflow-y: auto;
box-sizing: border-box;
@@ -821,6 +911,17 @@ export class HaMediaPlayerBrowse extends LitElement {
transition: height 0.5s, margin 0.5s;
}
.not-shown {
font-style: italic;
color: var(--secondary-text-color);
}
.grid.not-shown {
display: flex;
align-items: center;
text-align: center;
}
/* ============= CHILDREN ============= */
mwc-list {

View File

@@ -0,0 +1,129 @@
import { mdiUpload } from "@mdi/js";
import "@material/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { MediaPlayerItem } from "../../data/media-player";
import "../ha-circular-progress";
import "../ha-svg-icon";
import {
isLocalMediaSourceContentId,
uploadLocalMedia,
} from "../../data/media_source";
import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
declare global {
interface HASSDomEvents {
uploading: unknown;
"media-refresh": unknown;
}
}
@customElement("ha-media-upload-button")
class MediaUploadButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() currentItem?: MediaPlayerItem;
@state() _uploading = 0;
protected render(): TemplateResult {
if (
!this.currentItem ||
!isLocalMediaSourceContentId(this.currentItem.media_content_id || "")
) {
return html``;
}
return html`
<mwc-button
.label=${this._uploading > 0
? this.hass.localize(
"ui.components.media-browser.file_management.uploading",
{
count: this._uploading,
}
)
: this.hass.localize(
"ui.components.media-browser.file_management.add_media"
)}
.disabled=${this._uploading > 0}
@click=${this._startUpload}
>
${this._uploading > 0
? html`
<ha-circular-progress
size="tiny"
active
alt=""
slot="icon"
></ha-circular-progress>
`
: html` <ha-svg-icon .path=${mdiUpload} slot="icon"></ha-svg-icon> `}
</mwc-button>
`;
}
private async _startUpload() {
if (this._uploading > 0) {
return;
}
const input = document.createElement("input");
input.type = "file";
input.accept = "audio/*,video/*,image/*";
input.multiple = true;
input.addEventListener(
"change",
async () => {
fireEvent(this, "uploading");
const files = input.files!;
document.body.removeChild(input);
const target = this.currentItem!.media_content_id!;
for (let i = 0; i < files.length; i++) {
this._uploading = files.length - i;
try {
// eslint-disable-next-line no-await-in-loop
await uploadLocalMedia(this.hass, target, files[i]);
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.file_management.upload_failed",
{
reason: err.message || err,
}
),
});
break;
}
}
this._uploading = 0;
fireEvent(this, "media-refresh");
},
{ once: true }
);
// https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only
input.style.display = "none";
document.body.append(input);
input.click();
}
static styles = css`
mwc-button {
/* We use icon + text to show disabled state */
--mdc-button-disabled-ink-color: --mdc-theme-primary;
}
ha-svg-icon[slot="icon"],
ha-circular-progress[slot="icon"] {
vertical-align: middle;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-media-upload-button": MediaUploadButton;
}
}

View File

@@ -3,13 +3,13 @@ import {
MediaPickedEvent,
MediaPlayerBrowseAction,
} from "../../data/media-player";
import { MediaPlayerItemId } from "./ha-media-player-browse";
export interface MediaPlayerBrowseDialogParams {
action: MediaPlayerBrowseAction;
entityId: string;
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
mediaContentId?: string;
mediaContentType?: string;
navigateIds?: MediaPlayerItemId[];
}
export const showMediaBrowserDialog = (

View File

@@ -0,0 +1,18 @@
import { fireEvent } from "../../common/dom/fire_event";
import { MediaPlayerItem } from "../../data/media-player";
export interface MediaManageDialogParams {
currentItem: MediaPlayerItem;
onClose?: () => void;
}
export const showMediaManageDialog = (
element: HTMLElement,
dialogParams: MediaManageDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-media-manage",
dialogImport: () => import("./dialog-media-manage"),
dialogParams,
});
};

View File

@@ -1,12 +1,12 @@
import { mdiClose, mdiMagnify } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../components/ha-icon-button";
import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../dom/fire_event";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
import { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
@customElement("search-input")
class SearchInput extends LitElement {
@@ -14,6 +14,9 @@ class SearchInput extends LitElement {
@property() public filter?: string;
@property({ type: Boolean })
public suffix = false;
@property({ type: Boolean })
public autofocus = false;
@@ -33,7 +36,7 @@ class SearchInput extends LitElement {
.label=${this.label || "Search"}
.value=${this.filter || ""}
.icon=${true}
.iconTrailing=${this.filter}
.iconTrailing=${this.filter || this.suffix}
@input=${this._filterInputChanged}
>
<slot name="prefix" slot="leadingIcon">
@@ -43,16 +46,18 @@ class SearchInput extends LitElement {
.path=${mdiMagnify}
></ha-svg-icon>
</slot>
${this.filter &&
html`
<ha-icon-button
slot="trailingIcon"
@click=${this._clearSearch}
.label=${this.hass.localize("ui.common.clear")}
.path=${mdiClose}
class="clear-button"
></ha-icon-button>
`}
<div class="trailing" slot="trailingIcon">
${this.filter &&
html`
<ha-icon-button
@click=${this._clearSearch}
.label=${this.hass.localize("ui.common.clear")}
.path=${mdiClose}
class="clear-button"
></ha-icon-button>
`}
<slot name="suffix"></slot>
</div>
</ha-textfield>
`;
}
@@ -81,15 +86,16 @@ class SearchInput extends LitElement {
ha-svg-icon {
outline: none;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
}
.clear-button {
--mdc-icon-size: 20px;
}
ha-textfield {
display: inherit;
}
.trailing {
display: flex;
align-items: center;
}
`;
}
}

View File

@@ -1,3 +1,4 @@
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -5,9 +6,8 @@ import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import { fetchUsers, User } from "../../data/user";
import { HomeAssistant } from "../../types";
import "../ha-select";
import "./ha-user-badge";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
class HaUserPicker extends LitElement {
public hass?: HomeAssistant;
@@ -34,7 +34,7 @@ class HaUserPicker extends LitElement {
protected render(): TemplateResult {
return html`
<mwc-select
<ha-select
.label=${this.label}
.disabled=${this.disabled}
.value=${this.value}
@@ -58,7 +58,7 @@ class HaUserPicker extends LitElement {
</mwc-list-item>
`
)}
</mwc-select>
</ha-select>
`;
}

View File

@@ -47,6 +47,7 @@ export interface CloudPreferences {
export interface CloudStatusLoggedIn {
logged_in: true;
cloud: "disconnected" | "connecting" | "connected";
cloud_last_disconnect_reason: { clean: boolean; reason: string } | null;
email: string;
google_registered: boolean;
google_entities: EntityFilter;
@@ -186,10 +187,3 @@ export const updateCloudAlexaEntityConfig = (
entity_id: entityId,
...values,
});
export interface CloudTTSInfo {
languages: Array<[string, string]>;
}
export const getCloudTTSInfo = (hass: HomeAssistant) =>
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });

70
src/data/cloud/tts.ts Normal file
View File

@@ -0,0 +1,70 @@
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 {
languages: Array<[string, string]>;
}
export const getCloudTTSInfo = (hass: HomeAssistant) =>
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });
export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
const languages: Array<[string, string]> = [];
if (!info) {
return languages;
}
const seen = new Set<string>();
for (const [lang] of info.languages) {
if (seen.has(lang)) {
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]);
}
return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
};
export const getCloudTtsSupportedGenders = (
language: string,
info: CloudTTSInfo | undefined,
localize: LocalizeFunc
) => {
const genders: Array<[string, string]> = [];
if (!info) {
return genders;
}
for (const [curLang, gender] of info.languages) {
if (curLang === language) {
genders.push([
gender,
localize(`ui.panel.media-browser.tts.gender_${gender}`) ||
localize(`ui.panel.config.cloud.account.tts.${gender}`) ||
gender,
]);
}
}
return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
};

24
src/data/config.ts Normal file
View File

@@ -0,0 +1,24 @@
import { HomeAssistant } from "../types";
interface ValidConfig {
valid: true;
error: null;
}
interface InvalidConfig {
valid: false;
error: string;
}
type ValidKeys = "trigger" | "action" | "condition";
export const validateConfig = <
T extends Partial<{ [key in ValidKeys]: unknown }>
>(
hass: HomeAssistant,
config: T
): Promise<Record<keyof T, ValidConfig | InvalidConfig>> =>
hass.callWS({
type: "validate_config",
...config,
});

View File

@@ -13,6 +13,7 @@ export interface ConfigEntry {
| "not_loaded"
| "failed_unload";
supports_options: boolean;
supports_remove_device: boolean;
supports_unload: boolean;
pref_disable_new_entities: boolean;
pref_disable_polling: boolean;

View File

@@ -1,4 +1,5 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
@@ -77,12 +78,26 @@ export const updateDeviceRegistryEntry = (
...updates,
});
export const fetchDeviceRegistry = (conn) =>
conn.sendMessagePromise({
export const removeConfigEntryFromDevice = (
hass: HomeAssistant,
deviceId: string,
configEntryId: string
) =>
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/remove_config_entry",
device_id: deviceId,
config_entry_id: configEntryId,
});
export const fetchDeviceRegistry = (conn: Connection) =>
conn.sendMessagePromise<DeviceRegistryEntry[]>({
type: "config/device_registry/list",
});
const subscribeDeviceRegistryUpdates = (conn, store) =>
const subscribeDeviceRegistryUpdates = (
conn: Connection,
store: Store<DeviceRegistryEntry[]>
) =>
conn.subscribeEvents(
debounce(
() =>

View File

@@ -84,9 +84,10 @@ export interface HassioAddonDetails extends HassioAddonInfo {
options: Record<string, unknown>;
privileged: any;
protected: boolean;
rating: "1-6";
rating: "1-8";
schema: HaFormSchema[] | null;
services_role: string[];
signed: boolean;
slug: string;
startup: AddonStartup;
stdin: boolean;

View File

@@ -28,11 +28,13 @@ import type {
HassEntityBase,
} from "home-assistant-js-websocket";
import { supportsFeature } from "../common/entity/supports-feature";
import { MediaPlayerItemId } from "../components/media-player/ha-media-player-browse";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity";
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
media_content_type?: any;
media_content_id?: string;
media_content_type?: string;
media_artist?: string;
media_playlist?: string;
media_series_title?: string;
@@ -147,6 +149,7 @@ export const MediaClassBrowserSettings: {
export interface MediaPickedEvent {
item: MediaPlayerItem;
navigateIds: MediaPlayerItemId[];
}
export interface MediaPlayerThumbnail {
@@ -165,11 +168,12 @@ export interface MediaPlayerItem {
media_content_type: string;
media_content_id: string;
media_class: string;
children_media_class: string;
children_media_class?: string;
can_play: boolean;
can_expand: boolean;
thumbnail?: string;
children?: MediaPlayerItem[];
not_shown?: number;
}
export const browseMediaPlayer = (
@@ -261,8 +265,10 @@ export const computeMediaControls = (
});
}
const assumedState = stateObj.attributes.assumed_state === true;
if (
(state === "playing" || state === "paused") &&
(state === "playing" || state === "paused" || assumedState) &&
supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)
) {
buttons.push({
@@ -272,14 +278,15 @@ export const computeMediaControls = (
}
if (
(state === "playing" &&
!assumedState &&
((state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
supportsFeature(stateObj, SUPPORT_STOP))) ||
((state === "paused" || state === "idle") &&
supportsFeature(stateObj, SUPPORT_PLAY)) ||
(state === "on" &&
(supportsFeature(stateObj, SUPPORT_PLAY) ||
supportsFeature(stateObj, SUPPORT_PAUSE)))
((state === "paused" || state === "idle") &&
supportsFeature(stateObj, SUPPORT_PLAY)) ||
(state === "on" &&
(supportsFeature(stateObj, SUPPORT_PLAY) ||
supportsFeature(stateObj, SUPPORT_PAUSE))))
) {
buttons.push({
icon:
@@ -299,8 +306,29 @@ export const computeMediaControls = (
});
}
if (assumedState && supportsFeature(stateObj, SUPPORT_PLAY)) {
buttons.push({
icon: mdiPlay,
action: "media_play",
});
}
if (assumedState && supportsFeature(stateObj, SUPPORT_PAUSE)) {
buttons.push({
icon: mdiPause,
action: "media_pause",
});
}
if (assumedState && supportsFeature(stateObj, SUPPORT_STOP)) {
buttons.push({
icon: mdiStop,
action: "media_stop",
});
}
if (
(state === "playing" || state === "paused") &&
(state === "playing" || state === "paused" || assumedState) &&
supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
) {
buttons.push({
@@ -313,7 +341,7 @@ export const computeMediaControls = (
};
export const formatMediaTime = (seconds: number | undefined): string => {
if (seconds === undefined) {
if (seconds === undefined || seconds === Infinity) {
return "";
}
@@ -333,3 +361,17 @@ export const cleanupMediaTitle = (title?: string): string | undefined => {
const index = title.indexOf("?authSig=");
return index > 0 ? title.slice(0, index) : title;
};
/**
* Set volume of a media player entity.
* @param hass Home Assistant object
* @param entity_id entity ID of media player
* @param volume_level number between 0..1
* @returns
*/
export const setMediaPlayerVolume = (
hass: HomeAssistant,
entity_id: string,
volume_level: number
) =>
hass.callService("media_player", "volume_set", { entity_id, volume_level });

View File

@@ -49,3 +49,12 @@ export const uploadLocalMedia = async (
}
return resp.json();
};
export const removeLocalMedia = async (
hass: HomeAssistant,
media_content_id: string
) =>
hass.callWS({
type: "media_source/local_source/remove",
media_content_id,
});

View File

@@ -22,6 +22,7 @@ export interface MQTTEntityDebugInfo {
entity_id: string;
discovery_data: MQTTDiscoveryDebugInfo;
subscriptions: MQTTTopicDebugInfo[];
transmitted: MQTTTopicDebugInfo[];
}
export interface MQTTTriggerDebugInfo {
@@ -43,15 +44,6 @@ export const subscribeMQTTTopic = (
topic,
});
export const removeMQTTDeviceEntry = (
hass: HomeAssistant,
deviceId: string
): Promise<void> =>
hass.callWS({
type: "mqtt/device/remove",
device_id: deviceId,
});
export const fetchMQTTDebugInfo = (
hass: HomeAssistant,
deviceId: string

Some files were not shown because too many files have changed in this diff Show More