Compare commits
445 Commits
20200130.3
...
20200403.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a438439ce0 | ||
![]() |
da73c9d83d | ||
![]() |
a8f9f7ac7a | ||
![]() |
006d989943 | ||
![]() |
82d6909957 | ||
![]() |
12a7fc9337 | ||
![]() |
0241334656 | ||
![]() |
b217291b04 | ||
![]() |
aa211df0ad | ||
![]() |
8eebf51447 | ||
![]() |
f5d834d130 | ||
![]() |
0f2eae4091 | ||
![]() |
81d2334f48 | ||
![]() |
45384205eb | ||
![]() |
4150ac045d | ||
![]() |
793a704871 | ||
![]() |
9e2606f1c8 | ||
![]() |
4c4549eb37 | ||
![]() |
96f7a4231b | ||
![]() |
c4cf248a1e | ||
![]() |
50717b4d19 | ||
![]() |
da80a3896d | ||
![]() |
cc374e0478 | ||
![]() |
26d27f8be9 | ||
![]() |
4767fcbdfc | ||
![]() |
e1b48dd2a2 | ||
![]() |
087492fc0b | ||
![]() |
525703d376 | ||
![]() |
236bc6aefa | ||
![]() |
ec4c29c52c | ||
![]() |
d5ed1c4c41 | ||
![]() |
dfe808cfb4 | ||
![]() |
da229d9b65 | ||
![]() |
e54d904e4c | ||
![]() |
2e17c96866 | ||
![]() |
ddeb16463d | ||
![]() |
465efa460e | ||
![]() |
c351402556 | ||
![]() |
5f765e8b96 | ||
![]() |
6f556f69d6 | ||
![]() |
2439827eff | ||
![]() |
5f467f82e0 | ||
![]() |
6692fa439a | ||
![]() |
0535247bb3 | ||
![]() |
a0a4fcaf5f | ||
![]() |
454d81facc | ||
![]() |
0bfa8260fa | ||
![]() |
f2124f1c95 | ||
![]() |
451bc2370a | ||
![]() |
0b17642c31 | ||
![]() |
214dc25576 | ||
![]() |
158eddfd44 | ||
![]() |
5daa6dbd25 | ||
![]() |
645ef3e61f | ||
![]() |
3be4b9d79b | ||
![]() |
5dcea51712 | ||
![]() |
6995968d50 | ||
![]() |
c4cb42f3c2 | ||
![]() |
0d4a7a2b3e | ||
![]() |
20cc9c9b42 | ||
![]() |
8a6bd04543 | ||
![]() |
263138a388 | ||
![]() |
e645342131 | ||
![]() |
6e4c707f9e | ||
![]() |
fca286d6c0 | ||
![]() |
5a2e08647f | ||
![]() |
f6dac98abd | ||
![]() |
ddb525f6cd | ||
![]() |
f7ee712456 | ||
![]() |
ff873e2f71 | ||
![]() |
54b57e6222 | ||
![]() |
375abfb95e | ||
![]() |
30a38fa6d1 | ||
![]() |
554c0b692d | ||
![]() |
61ac831882 | ||
![]() |
59e89a0daf | ||
![]() |
1e3950cd1d | ||
![]() |
f514ea453c | ||
![]() |
cc046478e5 | ||
![]() |
a17c1052cd | ||
![]() |
2408f9b8fa | ||
![]() |
6aae1b3378 | ||
![]() |
ed51223226 | ||
![]() |
013808b7f5 | ||
![]() |
af584e1d12 | ||
![]() |
3763d7a1d0 | ||
![]() |
ce92add096 | ||
![]() |
40c94b6596 | ||
![]() |
c894bc2e40 | ||
![]() |
415a4fa1af | ||
![]() |
b9367a33a8 | ||
![]() |
7170f06c08 | ||
![]() |
f2578a58b4 | ||
![]() |
1950656bd5 | ||
![]() |
eed3263c70 | ||
![]() |
02e01626f5 | ||
![]() |
41a2d9604e | ||
![]() |
7d6f188bfc | ||
![]() |
15a144f17a | ||
![]() |
c54f2b66da | ||
![]() |
685a0807d8 | ||
![]() |
0d404e0e37 | ||
![]() |
39bb859f57 | ||
![]() |
90e32b7e45 | ||
![]() |
63a2d9dd18 | ||
![]() |
4982693883 | ||
![]() |
f4211e3fa3 | ||
![]() |
eacf58b5a5 | ||
![]() |
f9349bc731 | ||
![]() |
3840671764 | ||
![]() |
fd62cf02d6 | ||
![]() |
254744cd7d | ||
![]() |
595d04c922 | ||
![]() |
a3969fe2c8 | ||
![]() |
220e4134b7 | ||
![]() |
56e176a6f1 | ||
![]() |
780c15d6b3 | ||
![]() |
55ff848b78 | ||
![]() |
dbe829bc7d | ||
![]() |
8d0508f320 | ||
![]() |
2741bb8b38 | ||
![]() |
16cadd53cf | ||
![]() |
a8d21c6112 | ||
![]() |
6b2e707653 | ||
![]() |
205b7451fa | ||
![]() |
1d3aeec0de | ||
![]() |
ac911dcd31 | ||
![]() |
89a94b3efc | ||
![]() |
71793dcfa5 | ||
![]() |
1e527a8350 | ||
![]() |
262b12eb93 | ||
![]() |
7fb1e699ae | ||
![]() |
9ee647329b | ||
![]() |
cd6dcec644 | ||
![]() |
d8f248c60e | ||
![]() |
2925b930ad | ||
![]() |
127aaba47b | ||
![]() |
8bc8761af6 | ||
![]() |
a88321d243 | ||
![]() |
4e19232960 | ||
![]() |
5b95bdb6b7 | ||
![]() |
0fc59ccb16 | ||
![]() |
a9daf9835a | ||
![]() |
2110d9c3b9 | ||
![]() |
b77e0b8125 | ||
![]() |
5197f102ea | ||
![]() |
447d4604c6 | ||
![]() |
5a84e34f93 | ||
![]() |
fb6d3cccdc | ||
![]() |
fad3cb185b | ||
![]() |
f61ce395f5 | ||
![]() |
17f3299152 | ||
![]() |
17e04589d0 | ||
![]() |
4b2edde81b | ||
![]() |
5d3d766f56 | ||
![]() |
94e2a0dea0 | ||
![]() |
21fe68add0 | ||
![]() |
af6ebea4a3 | ||
![]() |
3c17ee03b6 | ||
![]() |
0c3c007faf | ||
![]() |
330eb0957b | ||
![]() |
02923475e6 | ||
![]() |
01ff97b366 | ||
![]() |
f0a4a99654 | ||
![]() |
02d2368654 | ||
![]() |
e19d07e434 | ||
![]() |
669ed5cb28 | ||
![]() |
785ae4a83d | ||
![]() |
d327045802 | ||
![]() |
785ef19cce | ||
![]() |
f5653d0da5 | ||
![]() |
e0a6d2efe5 | ||
![]() |
9971e2e934 | ||
![]() |
558802c7dd | ||
![]() |
91edcf9b52 | ||
![]() |
f401aa2897 | ||
![]() |
1d0389327f | ||
![]() |
06cd7556f3 | ||
![]() |
3b1f9a5dab | ||
![]() |
f54cd18da4 | ||
![]() |
73e0fd614e | ||
![]() |
9b220cc6ce | ||
![]() |
c7a5f63e33 | ||
![]() |
11192e6065 | ||
![]() |
d83b308100 | ||
![]() |
f67bf6908f | ||
![]() |
2784edc689 | ||
![]() |
8f41e99464 | ||
![]() |
7c2b37e8ca | ||
![]() |
dbdbad2deb | ||
![]() |
e5db86363c | ||
![]() |
b2026c1cd7 | ||
![]() |
b12f29afad | ||
![]() |
04b23388b5 | ||
![]() |
7309a937e8 | ||
![]() |
5dbcd1f726 | ||
![]() |
3338459139 | ||
![]() |
35f17fc1d4 | ||
![]() |
e062940639 | ||
![]() |
ff4d5265c5 | ||
![]() |
906f417436 | ||
![]() |
1c75fe3bb8 | ||
![]() |
283e858576 | ||
![]() |
2b4ab6320b | ||
![]() |
2085260ce7 | ||
![]() |
1f143176ad | ||
![]() |
dd2163a837 | ||
![]() |
0e1eca8a3e | ||
![]() |
9da32880ec | ||
![]() |
15aee6a66a | ||
![]() |
aba74f074a | ||
![]() |
75860508de | ||
![]() |
52160a367a | ||
![]() |
5651a61604 | ||
![]() |
959d8c3181 | ||
![]() |
64ee7456dc | ||
![]() |
814fcf63a8 | ||
![]() |
b72d8cf7d7 | ||
![]() |
8e7ef58715 | ||
![]() |
56bfa01c56 | ||
![]() |
4a0fc3e087 | ||
![]() |
f3c371996f | ||
![]() |
e5467181cb | ||
![]() |
0b3d2ea4ad | ||
![]() |
9648aa3588 | ||
![]() |
1b92cbbf74 | ||
![]() |
9979c046b3 | ||
![]() |
9ad121f9e6 | ||
![]() |
a0900afba3 | ||
![]() |
1cb614c8a8 | ||
![]() |
e63723f39e | ||
![]() |
720bd03173 | ||
![]() |
9e07cf67a5 | ||
![]() |
503dec7345 | ||
![]() |
5a2649a65b | ||
![]() |
1599dc9e16 | ||
![]() |
84dc8188c4 | ||
![]() |
74657ae815 | ||
![]() |
1a3b747d17 | ||
![]() |
802db71400 | ||
![]() |
4f98524258 | ||
![]() |
b17ea09b8b | ||
![]() |
8abbc71e91 | ||
![]() |
1db31fb0f7 | ||
![]() |
e9b5725d7b | ||
![]() |
d3105b6846 | ||
![]() |
196540afc7 | ||
![]() |
2b8b9f8311 | ||
![]() |
b4f0fce600 | ||
![]() |
c6f101a487 | ||
![]() |
54739c7ccd | ||
![]() |
aa2e632df3 | ||
![]() |
f3445d99cf | ||
![]() |
7e48b21767 | ||
![]() |
1d1688093a | ||
![]() |
d392695ab7 | ||
![]() |
5066560411 | ||
![]() |
7fa6686e8c | ||
![]() |
d74fe6ed52 | ||
![]() |
319a3b4943 | ||
![]() |
226e6e9f59 | ||
![]() |
42f311a457 | ||
![]() |
e50ec2e2e2 | ||
![]() |
b72a3361c0 | ||
![]() |
7b057eaa77 | ||
![]() |
d7aaed05b7 | ||
![]() |
c5fe5565bb | ||
![]() |
a1a1763897 | ||
![]() |
724357683c | ||
![]() |
0d6de9fe73 | ||
![]() |
5646045e9e | ||
![]() |
17c7a3bbac | ||
![]() |
8d65eb1fdf | ||
![]() |
2298a55b16 | ||
![]() |
33d65bcefc | ||
![]() |
3cc7deda04 | ||
![]() |
e2de660bec | ||
![]() |
6b1e5a525f | ||
![]() |
93565f0ed9 | ||
![]() |
143d1162b6 | ||
![]() |
788d616fa2 | ||
![]() |
0de9471a5d | ||
![]() |
b229071248 | ||
![]() |
6d145730a5 | ||
![]() |
f02bb67485 | ||
![]() |
52ded635ff | ||
![]() |
a6d73828b8 | ||
![]() |
1d052fa5bb | ||
![]() |
38d758b52f | ||
![]() |
9162e9c318 | ||
![]() |
189ea00768 | ||
![]() |
25d6427aed | ||
![]() |
8a61442cf2 | ||
![]() |
106d405699 | ||
![]() |
1f23e9062f | ||
![]() |
231b498ea5 | ||
![]() |
a256e5abfa | ||
![]() |
028b370ead | ||
![]() |
18abc6adf7 | ||
![]() |
95aa29d6ca | ||
![]() |
5d2242dd16 | ||
![]() |
de8bca6967 | ||
![]() |
12234de20e | ||
![]() |
b41369a2ad | ||
![]() |
6e35c79c14 | ||
![]() |
22e4c0512e | ||
![]() |
3606b8077f | ||
![]() |
3a90a65ba8 | ||
![]() |
e59987a8ed | ||
![]() |
22d8ce0fd9 | ||
![]() |
01eae3876b | ||
![]() |
2e43f390a4 | ||
![]() |
65421fa551 | ||
![]() |
fc88922ce3 | ||
![]() |
52609dded9 | ||
![]() |
6d54496187 | ||
![]() |
2a6c38066d | ||
![]() |
924c7804c9 | ||
![]() |
23f34fa7ae | ||
![]() |
7046cba1f7 | ||
![]() |
4be1040a14 | ||
![]() |
68baeb83cb | ||
![]() |
aa94e45582 | ||
![]() |
2c58a9f802 | ||
![]() |
0a41a4f066 | ||
![]() |
e265d9581c | ||
![]() |
4675579f79 | ||
![]() |
52ae01ea74 | ||
![]() |
099430238c | ||
![]() |
af3626b215 | ||
![]() |
52ea3a5ce8 | ||
![]() |
2ab2ade642 | ||
![]() |
1cc3936ec3 | ||
![]() |
322eef1c0f | ||
![]() |
0964130782 | ||
![]() |
be9ec50e3a | ||
![]() |
da1dd45169 | ||
![]() |
9a7f7f119d | ||
![]() |
b1a414c840 | ||
![]() |
2718ada9f9 | ||
![]() |
46a596ce34 | ||
![]() |
7036cefa72 | ||
![]() |
fb7fbf2dac | ||
![]() |
49b0c8d549 | ||
![]() |
8f9a6bd544 | ||
![]() |
24e4b0b772 | ||
![]() |
1c86bd2f8b | ||
![]() |
363f548f13 | ||
![]() |
51ce481e77 | ||
![]() |
30e5611812 | ||
![]() |
67706a312d | ||
![]() |
f4eb3380b4 | ||
![]() |
73934afc7d | ||
![]() |
9d2a0c0502 | ||
![]() |
9ec75531a8 | ||
![]() |
91bdb8f742 | ||
![]() |
d8ae3439de | ||
![]() |
2d018fff6c | ||
![]() |
7d37dc6cde | ||
![]() |
c60033027d | ||
![]() |
3f7c29a6f6 | ||
![]() |
b2243f480c | ||
![]() |
f5384e8bc8 | ||
![]() |
ecc6fcf862 | ||
![]() |
46cc2aec94 | ||
![]() |
c62a5a6dcd | ||
![]() |
f6b10232ec | ||
![]() |
87559c0938 | ||
![]() |
7903541689 | ||
![]() |
c93e1b0123 | ||
![]() |
e261fafdb3 | ||
![]() |
485e2fde25 | ||
![]() |
6feaf64c90 | ||
![]() |
6b115bf06a | ||
![]() |
f45785fafe | ||
![]() |
ec046bc925 | ||
![]() |
ab5733718b | ||
![]() |
1077fb2945 | ||
![]() |
b7a84cdd60 | ||
![]() |
78102f5882 | ||
![]() |
4ea11bd928 | ||
![]() |
49422c3f63 | ||
![]() |
0b8700f725 | ||
![]() |
c5aa000a97 | ||
![]() |
4cdc4765f7 | ||
![]() |
a95290235d | ||
![]() |
fb9d7ac2d8 | ||
![]() |
d48a4e0ac6 | ||
![]() |
d33e035db7 | ||
![]() |
1437b4c4b6 | ||
![]() |
9fce60065b | ||
![]() |
d052b9ede8 | ||
![]() |
8cee5c729e | ||
![]() |
88bdf7c7ec | ||
![]() |
2c006e99f2 | ||
![]() |
e7e8dff0ec | ||
![]() |
981c798e22 | ||
![]() |
4613d8b1f6 | ||
![]() |
ba4e1949c4 | ||
![]() |
cc6686a790 | ||
![]() |
f791412f73 | ||
![]() |
0c8ac17dcb | ||
![]() |
9e11fe868e | ||
![]() |
7d91515bf5 | ||
![]() |
e0565c35ab | ||
![]() |
e5387e5806 | ||
![]() |
8a4c52aeb7 | ||
![]() |
15e7b8117c | ||
![]() |
d1703ba3e8 | ||
![]() |
c977f22047 | ||
![]() |
2e47aa1905 | ||
![]() |
c72105dca3 | ||
![]() |
e01f1cfcac | ||
![]() |
2e4c73c087 | ||
![]() |
c7f7ef28bf | ||
![]() |
aac7dbab58 | ||
![]() |
8518f774d4 | ||
![]() |
cb0d91d124 | ||
![]() |
107f428dd3 | ||
![]() |
7758ddba56 | ||
![]() |
e0376c803f | ||
![]() |
788c490bbc | ||
![]() |
cdf6e9eb75 | ||
![]() |
4aa49f66bc | ||
![]() |
1bf82f216a | ||
![]() |
004ff58c21 | ||
![]() |
f1a1654371 | ||
![]() |
862044ca23 | ||
![]() |
c54b474838 | ||
![]() |
42cbe863bb | ||
![]() |
ccc42dad79 | ||
![]() |
82ff444cec | ||
![]() |
24c591fbf3 | ||
![]() |
ad676d7fd3 | ||
![]() |
cbe4782d78 | ||
![]() |
3fdcc1c0ea | ||
![]() |
f9d64e51c4 | ||
![]() |
b082828a75 | ||
![]() |
25f5bf0042 | ||
![]() |
f5dec3c6d5 | ||
![]() |
3215437bb8 | ||
![]() |
33176d8f3d | ||
![]() |
f82b62f45c | ||
![]() |
edfdd0da89 | ||
![]() |
33d9bf4660 |
1
.gitattributes
vendored
@@ -11,3 +11,4 @@
|
||||
*.mp3 binary
|
||||
|
||||
demo/public/api/camera_proxy_stream/* binary
|
||||
demo/public/api/media_player_proxy/* binary
|
||||
|
23
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -3,6 +3,7 @@ name: Report a bug with the UI, Frontend or Lovelace
|
||||
about: Report an issue related to the Home Assistant frontend.
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
@@ -10,6 +11,7 @@ labels: bug
|
||||
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have updated to the latest available Home Assistant version.
|
||||
@@ -17,21 +19,22 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
- [ ] I have tried a different browser to see if it is related to my browser.
|
||||
|
||||
## The problem
|
||||
|
||||
<!--
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us about the current behavior.
|
||||
If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
|
||||
<!--
|
||||
Describe what you expected to happen or it should look/behave.
|
||||
If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
<!--
|
||||
Provide steps for us, that helps reproducing your issue.
|
||||
For example:
|
||||
@@ -43,8 +46,8 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
6. Set the HVAC action to cool
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
|
||||
<!--
|
||||
Provide details about the versions you are using, which helps us reproducing
|
||||
and finding the issue quicker. Version information is found in the
|
||||
@@ -54,13 +57,13 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
your issue in a different browser and be sure to include your findings.
|
||||
-->
|
||||
|
||||
- Home Assistant release with the issue:
|
||||
- Last working Home Assistant release (if known):
|
||||
- UI Type (States or Lovelace):
|
||||
- Browser and browser version:
|
||||
- Operating system:
|
||||
- Home Assistant release with the issue:
|
||||
- Last working Home Assistant release (if known):
|
||||
- Browser and browser version:
|
||||
- Operating system:
|
||||
|
||||
## Problem-relevant configuration
|
||||
|
||||
<!--
|
||||
An example configuration that caused the problem for you. Fill this out even
|
||||
if it seems unimportant to you. Please be sure to remove personal information
|
||||
@@ -72,6 +75,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
```
|
||||
|
||||
## Javascript errors shown in your browser console/inspector
|
||||
|
||||
<!--
|
||||
If you come across any javascript or other error logs, e.g., in your browser
|
||||
console/inspector please provide them.
|
||||
@@ -82,4 +86,3 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
|
||||
url: https://github.com/home-assistant/home-assistant/issues
|
||||
url: https://github.com/home-assistant/core/issues
|
||||
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
|
||||
- name: Report incorrect or missing information on our website
|
||||
url: https://github.com/home-assistant/home-assistant.io/issues
|
||||
|
127
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Setting up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Get yarn cache path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: Fetching Yarn cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
- name: Build icons
|
||||
run: ./node_modules/.bin/gulp gen-icons-hassio gen-icons-mdi gen-icons-app
|
||||
- name: Build translations
|
||||
run: ./node_modules/.bin/gulp build-translations
|
||||
- name: Run eslint
|
||||
run: ./node_modules/.bin/eslint src hassio/src gallery/src
|
||||
- name: Run tslint
|
||||
run: ./node_modules/.bin/tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts'
|
||||
- name: Run tsc
|
||||
run: ./node_modules/.bin/tsc
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Setting up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Get yarn cache path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: Fetching Yarn cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
- name: Run Mocha
|
||||
run: npm run mocha
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Setting up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Get yarn cache path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: Fetching Yarn cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
- name: Build Application
|
||||
run: ./node_modules/.bin/gulp build-app
|
||||
env:
|
||||
TRAVIS: "true"
|
||||
supervisor:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Setting up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Get yarn cache path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: Fetching Yarn cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
- name: Build Application
|
||||
run: ./node_modules/.bin/gulp build-hassio
|
||||
env:
|
||||
TRAVIS: "true"
|
39
.github/workflows/demo.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Demo
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Setting up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Get yarn cache path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: Fetching Yarn cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
- name: Build Demo
|
||||
run: ./node_modules/.bin/gulp build-demo
|
||||
- name: Deploy to Netlify
|
||||
uses: netlify/actions/cli@master
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||
with:
|
||||
args: deploy --dir=demo/dist --prod
|
3
.gitignore
vendored
@@ -31,3 +31,6 @@ src/cast/dev_const.ts
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
||||
#asdf
|
||||
.tool-versions
|
||||
|
18
.travis.yml
@@ -1,18 +0,0 @@
|
||||
sudo: false
|
||||
language: node_js
|
||||
cache:
|
||||
yarn: true
|
||||
directories:
|
||||
- bower_components
|
||||
install: yarn install
|
||||
script:
|
||||
- npm run build
|
||||
- hassio/script/build_hassio
|
||||
# Because else eslint fails because hassio has cleaned that build
|
||||
- ./node_modules/.bin/gulp gen-icons-app
|
||||
- npm run test
|
||||
# - xvfb-run wct --module-resolution=node --npm
|
||||
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
|
||||
dist: trusty
|
||||
addons:
|
||||
sauce_connect: true
|
@@ -1,4 +1,4 @@
|
||||
# Home Assistant Polymer [](https://travis-ci.org/home-assistant/home-assistant-polymer)
|
||||
# Home Assistant Frontend
|
||||
|
||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||
|
||||
@@ -19,12 +19,15 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
## Frontend development
|
||||
|
||||
### Classic environment
|
||||
|
||||
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
|
||||
|
||||
### Docker environment
|
||||
|
||||
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
|
||||
* `sh ./script/docker_run.sh build` Build all the project with one command
|
||||
* `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the *classic environment*) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
|
||||
|
||||
- `sh ./script/docker_run.sh build` Build all the project with one command
|
||||
- `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the _classic environment_) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
|
||||
|
||||
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above
|
||||
|
||||
|
@@ -8,7 +8,7 @@ trigger:
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: '1.3-3.7-alpine3.10'
|
||||
value: '1.10.1-3.7-alpine3.11'
|
||||
- name: versionNode
|
||||
value: '12.1'
|
||||
- group: twine
|
||||
@@ -50,15 +50,8 @@ stages:
|
||||
- template: templates/azp-job-wheels.yaml@azure
|
||||
parameters:
|
||||
builderVersion: '$(versionWheels)'
|
||||
builderApk: 'build-base'
|
||||
wheelsLocal: true
|
||||
wheelsRequirement: 'requirement.txt'
|
||||
preBuild:
|
||||
- task: NodeTool@0
|
||||
displayName: "Use Node $(versionNode)"
|
||||
inputs:
|
||||
versionSpec: "$(versionNode)"
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
yarn install
|
||||
script/build_frontend
|
||||
sleep 240
|
||||
echo "home-assistant-frontend==$(Build.SourceBranchName)" > requirement.txt
|
||||
|
@@ -11,7 +11,7 @@ trigger:
|
||||
pr: none
|
||||
schedules:
|
||||
- cron: "30 0 * * *"
|
||||
displayName: "translation update"
|
||||
displayName: "frontend translation update"
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
|
@@ -34,6 +34,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
},
|
||||
],
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
[
|
||||
require("@babel/plugin-proposal-decorators").default,
|
||||
{ decoratorsBeforeExport: true },
|
||||
|
@@ -1,6 +1,14 @@
|
||||
module.exports = {
|
||||
isProdBuild: process.env.NODE_ENV === "production",
|
||||
isStatsBuild: process.env.STATS === "1",
|
||||
isTravis: process.env.TRAVIS === "true",
|
||||
isNetlify: process.env.NETLIFY === "true",
|
||||
isProdBuild() {
|
||||
return process.env.NODE_ENV === "production";
|
||||
},
|
||||
isStatsBuild() {
|
||||
return process.env.STATS === "1";
|
||||
},
|
||||
isTravis() {
|
||||
return process.env.TRAVIS === "true";
|
||||
},
|
||||
isNetlify() {
|
||||
return process.env.NETLIFY === "true";
|
||||
},
|
||||
};
|
||||
|
@@ -24,7 +24,7 @@ gulp.task(
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
gulp.series("create-test-translation", "build-translations")
|
||||
"build-translations"
|
||||
),
|
||||
"copy-static",
|
||||
"webpack-watch-app"
|
||||
@@ -42,7 +42,7 @@ gulp.task(
|
||||
"copy-static",
|
||||
"webpack-prod-app",
|
||||
...// Don't compress running tests
|
||||
(envVars.isTravis ? [] : ["compress-app"]),
|
||||
(envVars.isTravis() ? [] : ["compress-app"]),
|
||||
gulp.parallel(
|
||||
"gen-pages-prod",
|
||||
"gen-index-app-prod",
|
||||
|
@@ -16,7 +16,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-gallery",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-app", "build-translations"),
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||
"copy-static-gallery",
|
||||
"gen-index-gallery-dev",
|
||||
"webpack-dev-server-gallery"
|
||||
|
@@ -65,6 +65,12 @@ function copyMapPanel(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
gulp.task("copy-translations", (done) => {
|
||||
const staticDir = paths.static;
|
||||
copyTranslations(staticDir);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("copy-static", (done) => {
|
||||
const staticDir = paths.static;
|
||||
const staticPath = genStaticPath(paths.static);
|
||||
|
@@ -2,6 +2,7 @@ const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const paths = require("../paths");
|
||||
const { mapFiles } = require("../util");
|
||||
|
||||
const ICON_PACKAGE_PATH = path.resolve(
|
||||
__dirname,
|
||||
@@ -57,20 +58,6 @@ function generateIconset(iconsetName, iconNames) {
|
||||
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
||||
}
|
||||
|
||||
// Helper function to map recursively over files in a folder and it's subfolders
|
||||
function mapFiles(startPath, filter, mapFunc) {
|
||||
const files = fs.readdirSync(startPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filename = path.join(startPath, files[i]);
|
||||
const stat = fs.lstatSync(filename);
|
||||
if (stat.isDirectory()) {
|
||||
mapFiles(filename, filter, mapFunc);
|
||||
} else if (filename.indexOf(filter) >= 0) {
|
||||
mapFunc(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all icons used by the project.
|
||||
function findIcons(searchPath, iconsetName) {
|
||||
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
|
||||
|
@@ -29,6 +29,6 @@ gulp.task(
|
||||
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
|
||||
"webpack-prod-hassio",
|
||||
...// Don't compress running tests
|
||||
(envVars.isTravis ? [] : ["compress-hassio"])
|
||||
(envVars.isTravis() ? [] : ["compress-hassio"])
|
||||
)
|
||||
);
|
||||
|
@@ -1,14 +1,18 @@
|
||||
const crypto = require("crypto");
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
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 hash = require("gulp-hash");
|
||||
const hashFilename = require("gulp-hash-filename");
|
||||
const merge = require("gulp-merge-json");
|
||||
const minify = require("gulp-jsonminify");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
const { mapFiles } = require("../util");
|
||||
const env = require("../env");
|
||||
const paths = require("../paths");
|
||||
|
||||
const inDir = "translations";
|
||||
const workDir = "build-translations";
|
||||
@@ -39,8 +43,6 @@ const TRANSLATION_FRAGMENTS = [
|
||||
"developer-tools",
|
||||
];
|
||||
|
||||
const tasks = [];
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
@@ -116,11 +118,9 @@ function lokaliseTransform(data, original, file) {
|
||||
return output;
|
||||
}
|
||||
|
||||
let taskName = "clean-translations";
|
||||
gulp.task(taskName, function() {
|
||||
return del([`${outDir}/**/*.json`]);
|
||||
gulp.task("clean-translations", function() {
|
||||
return del([workDir]);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
gulp.task("ensure-translations-build-dir", (done) => {
|
||||
if (!fs.existsSync(workDir)) {
|
||||
@@ -129,29 +129,23 @@ gulp.task("ensure-translations-build-dir", (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
taskName = "create-test-metadata";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("ensure-translations-build-dir", function writeTestMetaData(cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("create-test-metadata", function(cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
});
|
||||
|
||||
taskName = "create-test-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("create-test-metadata", function() {
|
||||
"create-test-translation",
|
||||
gulp.series("create-test-metadata", function createTestTranslation() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return recursiveEmpty(data);
|
||||
@@ -161,7 +155,6 @@ gulp.task(
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
/**
|
||||
* This task will build a master translation file, to be used as the base for
|
||||
@@ -172,235 +165,216 @@ tasks.push(taskName);
|
||||
* project is buildable immediately after merging new translation keys, since
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
taskName = "build-master-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("clean-translations", function() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("build-master-translation", function() {
|
||||
return gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
|
||||
taskName = "build-merged-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-master-translation", function() {
|
||||
return gulp
|
||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
foreach(function(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
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
gulp.task("build-merged-translations", function() {
|
||||
return gulp
|
||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
foreach(function(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
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
var taskName;
|
||||
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = "build-translation-fragment-" + fragment;
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-merged-translations", function() {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
gulp.task(taskName, function() {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||
});
|
||||
splitTasks.push(taskName);
|
||||
});
|
||||
|
||||
taskName = "build-translation-core";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-merged-translations", function() {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task(taskName, function() {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir));
|
||||
});
|
||||
|
||||
splitTasks.push(taskName);
|
||||
|
||||
taskName = "build-flattened-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series(...splitTasks, function() {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(hashFilename())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("build-flattened-translations", function() {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
});
|
||||
|
||||
taskName = "build-translation-fingerprints";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-flattened-translations", function() {
|
||||
return gulp
|
||||
.src(outDir + "/**/*.json")
|
||||
.pipe(
|
||||
rename({
|
||||
extname: "",
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
hash({
|
||||
algorithm: "md5",
|
||||
hashLength: 32,
|
||||
template: "<%= name %>.json",
|
||||
})
|
||||
)
|
||||
.pipe(hash.manifest("translationFingerprints.json"))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// After generating fingerprints of our translation files, consolidate
|
||||
// all translation fragment fingerprints under the translation name key
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const [path, _md5] = key.rsplit("-", 1);
|
||||
// let translation = key;
|
||||
let translation = path;
|
||||
const parts = translation.split("/");
|
||||
if (parts.length === 2) {
|
||||
translation = parts[1];
|
||||
}
|
||||
if (!(translation in newData)) {
|
||||
newData[translation] = {
|
||||
fingerprints: {},
|
||||
};
|
||||
}
|
||||
newData[translation].fingerprints[path] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
const fingerprints = {};
|
||||
|
||||
taskName = "build-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-translation-fingerprints", function() {
|
||||
return gulp
|
||||
.src(
|
||||
[
|
||||
"src/translations/translationMetadata.json",
|
||||
workDir + "/testMetadata.json",
|
||||
workDir + "/translationFingerprints.json",
|
||||
],
|
||||
{ allowEmpty: true }
|
||||
)
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
if (data[key].nativeName) {
|
||||
newData[key] = data[key];
|
||||
} else {
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
);
|
||||
}
|
||||
if (data[key]) newData[key] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
}))
|
||||
)
|
||||
.pipe(rename("translationMetadata.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
"build-translation-fingerprints",
|
||||
function fingerprintTranslationFiles() {
|
||||
// Fingerprint full file of each language
|
||||
const files = fs.readdirSync(fullDir);
|
||||
|
||||
module.exports = tasks;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
fingerprints[files[i].split(".")[0]] = {
|
||||
// In dev we create fake hashes
|
||||
hash: env.isProdBuild()
|
||||
? crypto
|
||||
.createHash("md5")
|
||||
.update(fs.readFileSync(path.join(fullDir, files[i]), "utf-8"))
|
||||
.digest("hex")
|
||||
: "dev",
|
||||
};
|
||||
}
|
||||
|
||||
mapFiles(outDir, ".json", (filename) => {
|
||||
const parsed = path.parse(filename);
|
||||
|
||||
// nl.json -> nl-<hash>.json
|
||||
if (!(parsed.name in fingerprints)) {
|
||||
throw new Error(`Unable to find hash for ${filename}`);
|
||||
}
|
||||
|
||||
fs.renameSync(
|
||||
filename,
|
||||
`${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${
|
||||
parsed.ext
|
||||
}`
|
||||
);
|
||||
});
|
||||
|
||||
const stream = source("translationFingerprints.json");
|
||||
stream.write(JSON.stringify(fingerprints));
|
||||
process.nextTick(() => stream.end());
|
||||
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
|
||||
}
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-translations",
|
||||
gulp.series(
|
||||
"clean-translations",
|
||||
"ensure-translations-build-dir",
|
||||
env.isProdBuild() ? (done) => done() : "create-test-translation",
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
gulp.parallel(...splitTasks),
|
||||
"build-flattened-translations",
|
||||
"build-translation-fingerprints",
|
||||
function writeMetadata() {
|
||||
return gulp
|
||||
.src(
|
||||
[
|
||||
path.join(paths.translations_src, "translationMetadata.json"),
|
||||
workDir + "/testMetadata.json",
|
||||
workDir + "/translationFingerprints.json",
|
||||
],
|
||||
{ allowEmpty: true }
|
||||
)
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
if (data[key].nativeName) {
|
||||
newData[key] = data[key];
|
||||
} else {
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
);
|
||||
}
|
||||
if (data[key]) newData[key] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
}))
|
||||
)
|
||||
.pipe(rename("translationMetadata.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
}
|
||||
)
|
||||
);
|
||||
|
@@ -3,6 +3,7 @@ const gulp = require("gulp");
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const log = require("fancy-log");
|
||||
const path = require("path");
|
||||
const paths = require("../paths");
|
||||
const {
|
||||
createAppConfig,
|
||||
@@ -57,10 +58,14 @@ const handler = (done) => (err, stats) => {
|
||||
|
||||
gulp.task("webpack-watch-app", () => {
|
||||
// we are not calling done, so this command will run forever
|
||||
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
|
||||
{},
|
||||
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
|
||||
{ ignored: /build-translations/ },
|
||||
handler()
|
||||
);
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("build-translations", "copy-translations")
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
|
@@ -29,4 +29,6 @@ module.exports = {
|
||||
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||
hassio_root: path.resolve(__dirname, "../hassio/build"),
|
||||
hassio_publicPath: "/api/hassio/app/",
|
||||
|
||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||
};
|
||||
|
16
build-scripts/util.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// Helper function to map recursively over files in a folder and it's subfolders
|
||||
module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) {
|
||||
const files = fs.readdirSync(startPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filename = path.join(startPath, files[i]);
|
||||
const stat = fs.lstatSync(filename);
|
||||
if (stat.isDirectory()) {
|
||||
mapFiles(filename, filter, mapFunc);
|
||||
} else if (filename.indexOf(filter) >= 0) {
|
||||
mapFunc(filename);
|
||||
}
|
||||
}
|
||||
};
|
@@ -148,11 +148,17 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
// Create an object mapping browser urls to their paths during build
|
||||
const translationMetadata = require("../build-translations/translationMetadata.json");
|
||||
const workBoxTranslationsTemplatedURLs = {};
|
||||
const englishFP = translationMetadata.translations.en.fingerprints;
|
||||
Object.keys(englishFP).forEach((key) => {
|
||||
const englishFilename = `en-${translationMetadata.translations.en.hash}.json`;
|
||||
|
||||
// core
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFilename}`
|
||||
] = `build-translations/output/${englishFilename}`;
|
||||
|
||||
translationMetadata.fragments.forEach((fragment) => {
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFP[key]}`
|
||||
] = `build-translations/output/${key}.json`;
|
||||
`/static/translations/${fragment}/${englishFilename}`
|
||||
] = `build-translations/output/${fragment}/${englishFilename}`;
|
||||
});
|
||||
|
||||
config.plugins.push(
|
||||
|
@@ -26,10 +26,12 @@ import { CastManager } from "../../../../src/cast/cast_manager";
|
||||
import {
|
||||
LovelaceConfig,
|
||||
getLovelaceCollection,
|
||||
getLegacyLovelaceCollection,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import "./hc-layout";
|
||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
|
||||
@customElement("hc-cast")
|
||||
class HcCast extends LitElement {
|
||||
@@ -133,7 +135,9 @@ class HcCast extends LitElement {
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
const llColl = getLovelaceCollection(this.connection);
|
||||
const llColl = atLeastVersion(this.connection.haVersion, 0, 107)
|
||||
? getLovelaceCollection(this.connection)
|
||||
: getLegacyLovelaceCollection(this.connection);
|
||||
// We first do a single refresh because we need to check if there is LL
|
||||
// configuration.
|
||||
llColl.refresh().then(
|
||||
|
@@ -15,6 +15,9 @@ import {
|
||||
import {
|
||||
LovelaceConfig,
|
||||
getLovelaceCollection,
|
||||
fetchResources,
|
||||
LegacyLovelaceConfig,
|
||||
getLegacyLovelaceCollection,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import "./hc-launch-screen";
|
||||
import { castContext } from "../cast_context";
|
||||
@@ -22,6 +25,9 @@ import { CAST_NS } from "../../../../src/cast/const";
|
||||
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
|
||||
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
|
||||
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
|
||||
let resourcesLoaded = false;
|
||||
|
||||
@customElement("hc-main")
|
||||
export class HcMain extends HassElement {
|
||||
@@ -34,6 +40,7 @@ export class HcMain extends HassElement {
|
||||
@property() private _error?: string;
|
||||
|
||||
private _unsubLovelace?: UnsubscribeFunc;
|
||||
private _urlPath?: string | null;
|
||||
|
||||
public processIncomingMessage(msg: HassMessage) {
|
||||
if (msg.type === "connect") {
|
||||
@@ -108,6 +115,7 @@ export class HcMain extends HassElement {
|
||||
if (this.hass) {
|
||||
status.hassUrl = this.hass.auth.data.hassUrl;
|
||||
status.lovelacePath = this._lovelacePath!;
|
||||
status.urlPath = this._urlPath;
|
||||
}
|
||||
|
||||
if (senderId) {
|
||||
@@ -163,8 +171,17 @@ export class HcMain extends HassElement {
|
||||
this._error = "Cannot show Lovelace because we're not connected.";
|
||||
return;
|
||||
}
|
||||
if (!this._unsubLovelace) {
|
||||
const llColl = getLovelaceCollection(this.hass!.connection);
|
||||
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
|
||||
if (msg.urlPath === "lovelace") {
|
||||
msg.urlPath = null;
|
||||
}
|
||||
this._urlPath = msg.urlPath;
|
||||
if (this._unsubLovelace) {
|
||||
this._unsubLovelace();
|
||||
}
|
||||
const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
|
||||
? getLovelaceCollection(this.hass!.connection, msg.urlPath)
|
||||
: getLegacyLovelaceCollection(this.hass!.connection);
|
||||
// We first do a single refresh because we need to check if there is LL
|
||||
// configuration.
|
||||
try {
|
||||
@@ -183,6 +200,15 @@ export class HcMain extends HassElement {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!resourcesLoaded) {
|
||||
resourcesLoaded = true;
|
||||
const resources = atLeastVersion(this.hass.connection.haVersion, 0, 107)
|
||||
? await fetchResources(this.hass!.connection)
|
||||
: (this._lovelaceConfig as LegacyLovelaceConfig).resources;
|
||||
if (resources) {
|
||||
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
|
||||
}
|
||||
}
|
||||
this._showDemo = false;
|
||||
this._lovelacePath = msg.viewPath;
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
@@ -194,12 +220,6 @@ export class HcMain extends HassElement {
|
||||
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
|
||||
castContext.setApplicationState(lovelaceConfig.title!);
|
||||
this._lovelaceConfig = lovelaceConfig;
|
||||
if (lovelaceConfig.resources) {
|
||||
loadLovelaceResources(
|
||||
lovelaceConfig.resources,
|
||||
this.hass!.auth.data.hassUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleShowDemo(_msg: ShowDemoMessage) {
|
||||
|
@@ -6,6 +6,6 @@ const { isProdBuild } = require("../build-scripts/env.js");
|
||||
const latestBuild = true;
|
||||
|
||||
module.exports = createCastConfig({
|
||||
isProdBuild,
|
||||
isProdBuild: isProdBuild(),
|
||||
latestBuild,
|
||||
});
|
||||
|
Before Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 805 B After Width: | Height: | Size: 803 B |
BIN
demo/public/assets/arsaboo/images/camera.backyard.jpg
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
demo/public/assets/arsaboo/images/camera.driveway.jpg
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
demo/public/assets/arsaboo/images/camera.patio.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
demo/public/assets/arsaboo/images/camera.porch.jpg
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
demo/public/assets/arsaboo/images/media_player_family_room.jpg
Normal file
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
demo/public/stub_config/bedroom.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
demo/public/stub_config/floorplan.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
demo/public/stub_config/kitchen.png
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
demo/public/stub_config/t-shirt-promo.png
Normal file
After Width: | Height: | Size: 185 KiB |
@@ -291,16 +291,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
state: "13:21",
|
||||
attributes: {
|
||||
attribution: "Data provided by Ring.com",
|
||||
device_id: "e04f434dca02",
|
||||
firmware: "Up to Date",
|
||||
kind: "lpd_v2",
|
||||
timezone: "America/New_York",
|
||||
type: "doorbots",
|
||||
wifi_name: "RingOfSecurity-hUrGKNlhR",
|
||||
created_at: "2019-01-22T13:21:03-05:00",
|
||||
answered: false,
|
||||
recording_status: "ready",
|
||||
category: "motion",
|
||||
friendly_name: "Front Door Last Motion",
|
||||
icon: "hademo:history",
|
||||
},
|
||||
@@ -313,8 +304,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
"cbd8dfac9efb441f19168e271cb8629b0372d0c1f721353394b23ed0202013b0",
|
||||
motion_detection: true,
|
||||
friendly_name: "Patio",
|
||||
entity_picture:
|
||||
"/api/camera_proxy/camera.patio?token=cbd8dfac9efb441f19168e271cb8629b0372d0c1f721353394b23ed0202013b0",
|
||||
entity_picture: "/assets/arsaboo/images/camera.patio.jpg",
|
||||
supported_features: 0,
|
||||
},
|
||||
},
|
||||
@@ -326,8 +316,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
"479b332e0a7cad4c58e0fb98a1ecb7942e3b225190adb93a1341edfa7daf45b0",
|
||||
motion_detection: true,
|
||||
friendly_name: "Porch",
|
||||
entity_picture:
|
||||
"/api/camera_proxy/camera.porch?token=479b332e0a7cad4c58e0fb98a1ecb7942e3b225190adb93a1341edfa7daf45b0",
|
||||
entity_picture: "/assets/arsaboo/images/camera.porch.jpg",
|
||||
supported_features: 0,
|
||||
},
|
||||
},
|
||||
@@ -339,8 +328,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
"9381b2e4edd1bb21e868e2193f5d132a5fae153ce4f458451d979a02712b4642",
|
||||
motion_detection: true,
|
||||
friendly_name: "Backyard",
|
||||
entity_picture:
|
||||
"/api/camera_proxy/camera.backyard?token=9381b2e4edd1bb21e868e2193f5d132a5fae153ce4f458451d979a02712b4642",
|
||||
entity_picture: "/assets/arsaboo/images/camera.backyard.jpg",
|
||||
supported_features: 0,
|
||||
},
|
||||
},
|
||||
@@ -352,8 +340,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
"ac38bf88c2c5896eed66ae15739a3e726677f92d79e0d57f83f726ac28bda746",
|
||||
motion_detection: true,
|
||||
friendly_name: "Driveway",
|
||||
entity_picture:
|
||||
"/api/camera_proxy/camera.driveway?token=ac38bf88c2c5896eed66ae15739a3e726677f92d79e0d57f83f726ac28bda746",
|
||||
entity_picture: "/assets/arsaboo/images/camera.driveway.jpg",
|
||||
supported_features: 0,
|
||||
},
|
||||
},
|
||||
@@ -477,8 +464,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
friendly_name: localize(
|
||||
"ui.panel.page-demo.config.arsaboo.names.family_room"
|
||||
),
|
||||
entity_picture:
|
||||
"/api/media_player_proxy/media_player.family_room_2?token=be41a86e2a360761d67c36a010b09654b730deec092016ee92aafef79b1978ff&cache=e03d22fb103202e7",
|
||||
entity_picture: "/assets/arsaboo/images/media_player_family_room.jpg",
|
||||
supported_features: 64063,
|
||||
},
|
||||
},
|
||||
@@ -487,16 +473,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
state: "06:44",
|
||||
attributes: {
|
||||
attribution: "Data provided by Ring.com",
|
||||
device_id: "e04f434dca02",
|
||||
firmware: "Up to Date",
|
||||
kind: "lpd_v2",
|
||||
timezone: "America/New_York",
|
||||
type: "doorbots",
|
||||
wifi_name: "RingOfSecurity-hUrGKNlhR",
|
||||
created_at: "2019-01-22T06:44:31-05:00",
|
||||
answered: false,
|
||||
recording_status: "ready",
|
||||
category: "ding",
|
||||
friendly_name: "Front Door Last Ding",
|
||||
icon: "hademo:history",
|
||||
},
|
||||
|
@@ -395,7 +395,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
cards: [
|
||||
{
|
||||
entity: "script.air_cleaner_quiet",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -408,7 +408,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
{
|
||||
entity: "script.air_cleaner_auto",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -421,7 +421,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
{
|
||||
entity: "script.air_cleaner_turbo",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC bed",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -434,7 +434,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
{
|
||||
entity: "script.ac_off",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -447,7 +447,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
{
|
||||
entity: "script.ac_on",
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
name: "AC",
|
||||
tap_action: {
|
||||
action: "call-service",
|
||||
@@ -658,7 +658,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
action: "call-service",
|
||||
service: "script.goodnight",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:weather-night",
|
||||
},
|
||||
{
|
||||
@@ -670,7 +670,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
service: "scene.turn_on",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:coffee-outline",
|
||||
},
|
||||
{
|
||||
@@ -682,7 +682,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
service: "scene.turn_on",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:television-classic",
|
||||
},
|
||||
],
|
||||
@@ -743,7 +743,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
service: "light.toggle",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:page-layout-footer",
|
||||
},
|
||||
{
|
||||
@@ -755,7 +755,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
},
|
||||
service: "light.toggle",
|
||||
},
|
||||
type: "entity-button",
|
||||
type: "button",
|
||||
icon: "mdi:page-layout-header",
|
||||
},
|
||||
],
|
||||
|
@@ -6,7 +6,7 @@ const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
|
||||
const latestBuild = true;
|
||||
|
||||
module.exports = createDemoConfig({
|
||||
isProdBuild,
|
||||
isStatsBuild,
|
||||
isProdBuild: isProdBuild(),
|
||||
isStatsBuild: isStatsBuild(),
|
||||
latestBuild,
|
||||
});
|
||||
|
BIN
gallery/public/images/album_cover.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
gallery/public/images/album_cover_2.jpg
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
gallery/public/images/frenck.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
gallery/public/images/netflix.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
@@ -16,7 +16,8 @@ class DemoCard extends PolymerElement {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
#card {
|
||||
width: 400px;
|
||||
max-width: 400px;
|
||||
width: 100vw;
|
||||
}
|
||||
pre {
|
||||
width: 400px;
|
||||
|
84
gallery/src/data/media_players.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
|
||||
export const createMediaPlayerEntities = () => [
|
||||
getEntity("media_player", "music_paused", "paused", {
|
||||
friendly_name: "Pausing The Music",
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 64063,
|
||||
entity_picture: "/images/album_cover_2.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 50,
|
||||
media_position_updated_at: new Date(
|
||||
// 23 seconds in
|
||||
new Date().getTime() - 23000
|
||||
).toISOString(),
|
||||
}),
|
||||
getEntity("media_player", "music_playing", "playing", {
|
||||
friendly_name: "Playing The Music",
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 64063,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 0,
|
||||
media_position_updated_at: new Date(
|
||||
// 23 seconds in
|
||||
new Date().getTime() - 23000
|
||||
).toISOString(),
|
||||
}),
|
||||
getEntity("media_player", "stream_playing", "playing", {
|
||||
friendly_name: "Playing the Stream",
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
entity_picture: "/images/frenck.jpg",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "living_room", "playing", {
|
||||
friendly_name: "Pause, No skip, tvshow",
|
||||
media_content_type: "tvshow",
|
||||
media_title: "Chapter 1",
|
||||
media_series_title: "House of Cards",
|
||||
app_name: "Netflix",
|
||||
entity_picture: "/images/netflix.jpg",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "sonos_idle", "idle", {
|
||||
friendly_name: "Sonos Idle",
|
||||
supported_features: 64063,
|
||||
}),
|
||||
getEntity("media_player", "theater", "off", {
|
||||
friendly_name: "TV Off",
|
||||
supported_features: 161,
|
||||
}),
|
||||
getEntity("media_player", "android_cast", "playing", {
|
||||
friendly_name: "Casting App",
|
||||
media_title: "Android Screen Casting",
|
||||
app_name: "Screen Mirroring",
|
||||
// supported_features: 21437,
|
||||
}),
|
||||
getEntity("media_player", "unavailable", "unavailable", {
|
||||
friendly_name: "Player Unavailable",
|
||||
supported_features: 21437,
|
||||
}),
|
||||
getEntity("media_player", "unknown", "unknown", {
|
||||
friendly_name: "Player Unknown",
|
||||
supported_features: 21437,
|
||||
}),
|
||||
getEntity("media_player", "receiver_on", "on", {
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
volume_level: 0.63,
|
||||
is_volume_muted: false,
|
||||
source: "TV",
|
||||
friendly_name: "Receiver",
|
||||
supported_features: 84364,
|
||||
}),
|
||||
getEntity("media_player", "receiver_off", "off", {
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
friendly_name: "Receiver",
|
||||
supported_features: 84364,
|
||||
}),
|
||||
];
|
@@ -15,14 +15,14 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Basic example",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "With Name",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
name: Bedroom
|
||||
entity: light.bed_light
|
||||
`,
|
||||
@@ -30,7 +30,7 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "With Icon",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
icon: mdi:hotel
|
||||
`,
|
||||
@@ -38,7 +38,7 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Without State",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
show_state: false
|
||||
`,
|
||||
@@ -46,7 +46,7 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Custom Tap Action (toggle)",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
tap_action:
|
||||
action: toggle
|
||||
@@ -55,7 +55,7 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Running Service",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
service: light.toggle
|
||||
`,
|
||||
@@ -63,13 +63,13 @@ const CONFIGS = [
|
||||
{
|
||||
heading: "Invalid Entity",
|
||||
config: `
|
||||
- type: entity-button
|
||||
- type: button
|
||||
entity: sensor.invalid_entity
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoEntityButtonEntity extends PolymerElement {
|
||||
class DemoButtonEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
@@ -97,4 +97,4 @@ class DemoEntityButtonEntity extends PolymerElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-entity-button-card", DemoEntityButtonEntity);
|
||||
customElements.define("demo-hui-button-card", DemoButtonEntity);
|
||||
|
117
gallery/src/demos/demo-hui-media-control-card.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
import { createMediaPlayerEntities } from "../data/media_players";
|
||||
import "../../../src/panels/lovelace/cards/hui-media-control-card";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "Paused music",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.music_paused
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Playing music",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.music_playing
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Playing stream",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.stream_playing
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Pause, No skip, tvshow",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.living_room
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Screen casting",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.android_cast
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Sonos Idle",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.sonos_idle
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Off",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.theater
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Unavailable",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.unavailable
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Player Unknown",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.unknown
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Receiver On",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.receiver_on
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Receiver Off",
|
||||
config: `
|
||||
- type: media-control
|
||||
entity: media_player.receiver_off
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoHuiMediControlCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
hass: Object,
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(createMediaPlayerEntities());
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-media-control-card", DemoHuiMediControlCard);
|
@@ -1,54 +1,9 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("media_player", "bedroom", "playing", {
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
supported_features: 32,
|
||||
}),
|
||||
getEntity("media_player", "family_room", "paused", {
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 16417,
|
||||
}),
|
||||
getEntity("media_player", "family_room_no_play", "paused", {
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "living_room", "playing", {
|
||||
media_content_type: "tvshow",
|
||||
media_title: "Chapter 1",
|
||||
media_series_title: "House of Cards",
|
||||
app_name: "Netflix",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "lounge_room", "idle", {
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "theater", "off", {
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "android_cast", "playing", {
|
||||
media_title: "Android Screen Casting",
|
||||
app_name: "Screen Mirroring",
|
||||
supported_features: 21437,
|
||||
}),
|
||||
];
|
||||
import { createMediaPlayerEntities } from "../data/media_players";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
@@ -56,20 +11,24 @@ const CONFIGS = [
|
||||
config: `
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: media_player.bedroom
|
||||
name: Skip, no pause
|
||||
- entity: media_player.family_room
|
||||
name: Paused, music
|
||||
- entity: media_player.family_room_no_play
|
||||
- entity: media_player.music_paused
|
||||
name: Paused music
|
||||
- entity: media_player.music_playing
|
||||
name: Playing music
|
||||
- entity: media_player.stream_playing
|
||||
name: Paused, no play
|
||||
- entity: media_player.living_room
|
||||
name: Pause, No skip, tvshow
|
||||
- entity: media_player.android_cast
|
||||
name: Screen casting
|
||||
- entity: media_player.lounge_room
|
||||
- entity: media_player.sonos_idle
|
||||
name: Chromcast Idle
|
||||
- entity: media_player.theater
|
||||
name: 'Player Off'
|
||||
name: Player Off
|
||||
- entity: media_player.unavailable
|
||||
name: Player Unavailable
|
||||
- entity: media_player.unknown
|
||||
name: Player Unknown
|
||||
`,
|
||||
},
|
||||
];
|
||||
@@ -98,7 +57,7 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
hass.addEntities(createMediaPlayerEntities());
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { filterAndSort } from "../components/hassio-filter-addons";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
|
||||
class HassioAddonRepositoryEl extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@@ -42,75 +43,81 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
|
||||
if (this.filter && addons.length < 1) {
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
<div class="description">
|
||||
No results found in "${repo.name}"
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="description">
|
||||
No results found in "${repo.name}"
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
<div class="content">
|
||||
<h1>
|
||||
${repo.name}
|
||||
<div class="description">
|
||||
Maintained by ${repo.maintainer}<br />
|
||||
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
|
||||
</div>
|
||||
</h1>
|
||||
<p class="description">
|
||||
Maintained by ${repo.maintainer}<br />
|
||||
<a class="repo" href=${repo.url} target="_blank" rel="noreferrer">
|
||||
${repo.url}
|
||||
</a>
|
||||
</p>
|
||||
<div class="card-group">
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
<paper-card
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.icon=${addon.installed && addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle"}
|
||||
.iconTitle=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "New version available"
|
||||
: "Add-on is installed"
|
||||
: addon.available
|
||||
? "Add-on is not installed"
|
||||
: "Add-on is not available on your system"}
|
||||
.iconClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
.topbarClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "unavailable"
|
||||
: ""}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
<paper-card
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this.addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.icon=${this.computeIcon(addon)}
|
||||
.iconTitle=${this.computeIconTitle(addon)}
|
||||
.iconClass=${this.computeIconClass(addon)}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private computeIcon(addon) {
|
||||
return addon.installed && addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle";
|
||||
}
|
||||
|
||||
private computeIconTitle(addon) {
|
||||
if (addon.installed) {
|
||||
return addon.installed !== addon.version
|
||||
? "New version available"
|
||||
: "Add-on is installed";
|
||||
}
|
||||
return addon.available
|
||||
? "Add-on is not installed"
|
||||
: "Add-on is not available on your system";
|
||||
}
|
||||
|
||||
private computeIconClass(addon) {
|
||||
if (addon.installed) {
|
||||
return addon.installed !== addon.version ? "update" : "installed";
|
||||
}
|
||||
return !addon.available ? "not_available" : "";
|
||||
}
|
||||
|
||||
private addonTapped(ev) {
|
||||
private _addonTapped(ev) {
|
||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||
}
|
||||
|
||||
|
@@ -36,61 +36,63 @@ class HassioRepositoriesEditor extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const repos = this._sortedRepos(this.repos);
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
<div class="content">
|
||||
<h1>
|
||||
Repositories
|
||||
<div class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</div>
|
||||
</div>
|
||||
${// Use repeat so that the fade-out from call-service-api-button
|
||||
// stays with the correct repo after we add/delete one.
|
||||
repeat(
|
||||
repos,
|
||||
(repo) => repo.slug,
|
||||
(repo) => html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${repo.name}
|
||||
.description=${repo.url}
|
||||
icon="hassio:github-circle"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeRemoveRepoData(repos, repo.url)}
|
||||
class="warning"
|
||||
>
|
||||
Remove
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</h1>
|
||||
<p class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</p>
|
||||
<div class="card-group">
|
||||
${// Use repeat so that the fade-out from call-service-api-button
|
||||
// stays with the correct repo after we add/delete one.
|
||||
repeat(
|
||||
repos,
|
||||
(repo) => repo.slug,
|
||||
(repo) => html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${repo.name}
|
||||
.description=${repo.url}
|
||||
icon="hassio:github-circle"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeRemoveRepoData(repos, repo.url)}
|
||||
class="warning"
|
||||
>
|
||||
Remove
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input
|
||||
label="Add new repository by URL"
|
||||
.value=${this._repoUrl}
|
||||
@value-changed=${this._urlChanged}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeAddRepoData(repos, this._repoUrl)}
|
||||
>
|
||||
Add
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input
|
||||
label="Add new repository by URL"
|
||||
.value=${this._repoUrl}
|
||||
@value-changed=${this._urlChanged}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeAddRepoData(repos, this._repoUrl)}
|
||||
>
|
||||
Add
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@@ -51,7 +51,7 @@ class HassioAddonAudio extends LitElement {
|
||||
|
||||
<paper-dropdown-menu
|
||||
label="Input"
|
||||
@selected-item-changed=${this._setInputDevice}
|
||||
@iron-select=${this._setInputDevice}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
@@ -61,14 +61,16 @@ class HassioAddonAudio extends LitElement {
|
||||
${this._inputDevices &&
|
||||
this._inputDevices.map((item) => {
|
||||
return html`
|
||||
<paper-item device=${item.device}>${item.name}</paper-item>
|
||||
<paper-item device=${item.device || ""}
|
||||
>${item.name}</paper-item
|
||||
>
|
||||
`;
|
||||
})}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
<paper-dropdown-menu
|
||||
label="Output"
|
||||
@selected-item-changed=${this._setOutputDevice}
|
||||
@iron-select=${this._setOutputDevice}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
@@ -78,7 +80,9 @@ class HassioAddonAudio extends LitElement {
|
||||
${this._outputDevices &&
|
||||
this._outputDevices.map((item) => {
|
||||
return html`
|
||||
<paper-item device=${item.device}>${item.name}</paper-item>
|
||||
<paper-item device=${item.device || ""}
|
||||
>${item.name}</paper-item
|
||||
>
|
||||
`;
|
||||
})}
|
||||
</paper-listbox>
|
||||
@@ -123,33 +127,32 @@ class HassioAddonAudio extends LitElement {
|
||||
}
|
||||
|
||||
private _setInputDevice(ev): void {
|
||||
const device = ev.detail.device;
|
||||
if (device) {
|
||||
this._selectedInput = device;
|
||||
}
|
||||
const device = ev.detail.item.getAttribute("device");
|
||||
this._selectedInput = device;
|
||||
}
|
||||
|
||||
private _setOutputDevice(ev): void {
|
||||
const device = ev.detail.device;
|
||||
if (device) {
|
||||
this._selectedOutput = device;
|
||||
}
|
||||
const device = ev.detail.item.getAttribute("device");
|
||||
this._selectedOutput = device;
|
||||
}
|
||||
|
||||
private async _addonChanged(): Promise<void> {
|
||||
this._selectedInput = this.addon.audio_input;
|
||||
this._selectedOutput = this.addon.audio_output;
|
||||
this._selectedInput =
|
||||
this.addon.audio_input === null ? "default" : this.addon.audio_input;
|
||||
this._selectedOutput =
|
||||
this.addon.audio_output === null ? "default" : this.addon.audio_output;
|
||||
if (this._outputDevices) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noDevice: HassioHardwareAudioDevice[] = [
|
||||
{ device: undefined, name: "-" },
|
||||
];
|
||||
const noDevice: HassioHardwareAudioDevice = {
|
||||
device: "default",
|
||||
name: "Default",
|
||||
};
|
||||
|
||||
try {
|
||||
const { audio } = await fetchHassioHardwareAudio(this.hass);
|
||||
const inupt = Object.keys(audio.input).map((key) => ({
|
||||
const input = Object.keys(audio.input).map((key) => ({
|
||||
device: key,
|
||||
name: audio.input[key],
|
||||
}));
|
||||
@@ -158,20 +161,22 @@ class HassioAddonAudio extends LitElement {
|
||||
name: audio.output[key],
|
||||
}));
|
||||
|
||||
this._inputDevices = noDevice.concat(inupt);
|
||||
this._outputDevices = noDevice.concat(output);
|
||||
this._inputDevices = [noDevice, ...input];
|
||||
this._outputDevices = [noDevice, ...output];
|
||||
} catch {
|
||||
this._error = "Failed to fetch audio hardware";
|
||||
this._inputDevices = noDevice;
|
||||
this._outputDevices = noDevice;
|
||||
this._inputDevices = [noDevice];
|
||||
this._outputDevices = [noDevice];
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveSettings(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
audio_input: this._selectedInput || null,
|
||||
audio_output: this._selectedOutput || null,
|
||||
audio_input:
|
||||
this._selectedInput === "default" ? null : this._selectedInput,
|
||||
audio_output:
|
||||
this._selectedOutput === "default" ? null : this._selectedOutput,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
query,
|
||||
} from "lit-element";
|
||||
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
@@ -20,30 +21,42 @@ import {
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-yaml-editor";
|
||||
// tslint:disable-next-line: no-duplicate-imports
|
||||
import { HaYamlEditor } from "../../../src/components/ha-yaml-editor";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("hassio-addon-config")
|
||||
class HassioAddonConfig extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public addon!: HassioAddonDetails;
|
||||
@property() private _error?: string;
|
||||
@property() private _config!: string;
|
||||
@property({ type: Boolean }) private _configHasChanged = false;
|
||||
|
||||
@query("ha-yaml-editor") private _editor!: HaYamlEditor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const editor = this._editor;
|
||||
// If editor not rendered, don't show the error.
|
||||
const valid = editor ? editor.isValid : true;
|
||||
|
||||
return html`
|
||||
<paper-card heading="Config">
|
||||
<div class="card-content">
|
||||
<ha-yaml-editor
|
||||
@value-changed=${this._configChanged}
|
||||
></ha-yaml-editor>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="errors">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<iron-autogrow-textarea
|
||||
@value-changed=${this._configChanged}
|
||||
.value=${this._config}
|
||||
></iron-autogrow-textarea>
|
||||
${valid
|
||||
? ""
|
||||
: html`
|
||||
<div class="errors">Invalid YAML</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._resetTapped}>
|
||||
@@ -51,7 +64,7 @@ class HassioAddonConfig extends LitElement {
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this._configHasChanged}
|
||||
.disabled=${!this._configHasChanged || !valid}
|
||||
>
|
||||
Save
|
||||
</mwc-button>
|
||||
@@ -77,7 +90,7 @@ class HassioAddonConfig extends LitElement {
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
iron-autogrow-textarea {
|
||||
width: 100%;
|
||||
@@ -93,18 +106,26 @@ class HassioAddonConfig extends LitElement {
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._config = JSON.stringify(this.addon.options, null, 2);
|
||||
this._editor.setValue(this.addon.options);
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(ev: PolymerChangedEvent<string>): void {
|
||||
this._config =
|
||||
ev.detail.value || JSON.stringify(this.addon.options, null, 2);
|
||||
this._configHasChanged =
|
||||
this._config !== JSON.stringify(this.addon.options, null, 2);
|
||||
private _configChanged(): void {
|
||||
this._configHasChanged = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _resetTapped(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to reset all your options?",
|
||||
confirmText: "reset options",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
options: null,
|
||||
@@ -129,7 +150,7 @@ class HassioAddonConfig extends LitElement {
|
||||
this._error = undefined;
|
||||
try {
|
||||
data = {
|
||||
options: JSON.parse(this._config),
|
||||
options: this._editor.value,
|
||||
};
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
|
@@ -36,6 +36,7 @@ import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
|
||||
const PERMIS_DESC = {
|
||||
rating: {
|
||||
@@ -106,7 +107,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title="${this.addon.name} ${this.addon
|
||||
.last_version} is available"
|
||||
.version_latest} is available"
|
||||
.description="You are currently running version ${this.addon
|
||||
.version}"
|
||||
icon="hassio:arrow-up-bold-circle"
|
||||
@@ -178,21 +179,26 @@ class HassioAddonInfo extends LitElement {
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
${this.addon.last_version}
|
||||
${this.addon.version_latest}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
Visit
|
||||
<a href="${this.addon.url}" target="_blank">
|
||||
<a href="${this.addon.url}" target="_blank" rel="noreferrer">
|
||||
${this.addon.name} page</a
|
||||
>
|
||||
for details.
|
||||
</div>
|
||||
${this.addon.logo
|
||||
? html`
|
||||
<a href="${this.addon.url}" target="_blank" class="logo">
|
||||
<a
|
||||
href="${this.addon.url}"
|
||||
target="_blank"
|
||||
class="logo"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src="/api/hassio/addons/${this.addon.slug}/logo" />
|
||||
</a>
|
||||
`
|
||||
@@ -319,6 +325,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-switch
|
||||
@change=${this._startOnBootToggled}
|
||||
.checked=${this.addon.boot === "auto"}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="state">
|
||||
@@ -326,6 +333,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-switch
|
||||
@change=${this._autoUpdateToggled}
|
||||
.checked=${this.addon.auto_update}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
${this.addon.ingress
|
||||
@@ -336,6 +344,7 @@ class HassioAddonInfo extends LitElement {
|
||||
@change=${this._panelToggled}
|
||||
.checked=${this.addon.ingress_panel}
|
||||
.disabled=${this._computeCannotIngressSidebar}
|
||||
haptic
|
||||
></ha-switch>
|
||||
${this._computeCannotIngressSidebar
|
||||
? html`
|
||||
@@ -363,6 +372,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-switch
|
||||
@change=${this._protectionToggled}
|
||||
.checked=${this.addon.protected}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
`
|
||||
@@ -424,6 +434,7 @@ class HassioAddonInfo extends LitElement {
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
class="right"
|
||||
rel="noopener"
|
||||
>
|
||||
<mwc-button>
|
||||
Open web UI
|
||||
@@ -448,9 +459,8 @@ class HassioAddonInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<ha-progress-button
|
||||
.disabled=${!this.addon.available}
|
||||
.disabled=${!this.addon.available || this._installing}
|
||||
.progress=${this._installing}
|
||||
class="right"
|
||||
@click=${this._installClicked}
|
||||
>
|
||||
Install
|
||||
@@ -626,7 +636,7 @@ class HassioAddonInfo extends LitElement {
|
||||
this.addon &&
|
||||
!this.addon.detached &&
|
||||
this.addon.version &&
|
||||
this.addon.version !== this.addon.last_version
|
||||
this.addon.version !== this.addon.version_latest
|
||||
);
|
||||
}
|
||||
|
||||
@@ -650,7 +660,9 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private get _computeCannotIngressSidebar(): boolean {
|
||||
return !this.addon.ingress || !this._computeHA92plus;
|
||||
return (
|
||||
!this.addon.ingress || !atLeastVersion(this.hass.config.version, 0, 92)
|
||||
);
|
||||
}
|
||||
|
||||
private get _computeUsesProtectedOptions(): boolean {
|
||||
@@ -659,11 +671,6 @@ class HassioAddonInfo extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private get _computeHA92plus(): boolean {
|
||||
const [major, minor] = this.hass.config.version.split(".", 2);
|
||||
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
|
||||
}
|
||||
|
||||
private async _startOnBootToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
|
@@ -17,21 +17,40 @@ class HassioCardContent extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public title!: string;
|
||||
@property() public description?: string;
|
||||
@property({ type: Boolean }) public available?: boolean;
|
||||
@property({ type: Boolean }) public available: boolean = true;
|
||||
@property({ type: Boolean }) public showTopbar: boolean = false;
|
||||
@property() public topbarClass?: string;
|
||||
@property() public datetime?: string;
|
||||
@property() public iconTitle?: string;
|
||||
@property() public iconClass?: string;
|
||||
@property() public icon = "hass:help-circle";
|
||||
@property() public iconImage?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<iron-icon
|
||||
class=${this.iconClass}
|
||||
.icon=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></iron-icon>
|
||||
${this.showTopbar
|
||||
? html`
|
||||
<div class="topbar ${this.topbarClass}"></div>
|
||||
`
|
||||
: ""}
|
||||
${this.iconImage
|
||||
? html`
|
||||
<div class="icon_image ${this.iconClass}">
|
||||
<img src="${this.iconImage}" title="${this.iconTitle}" />
|
||||
<div></div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<iron-icon
|
||||
class=${this.iconClass}
|
||||
.icon=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></iron-icon>
|
||||
`}
|
||||
<div>
|
||||
<div class="title">${this.title}</div>
|
||||
<div class="title">
|
||||
${this.title}
|
||||
</div>
|
||||
<div class="addition">
|
||||
${this.description}
|
||||
${/* treat as available when undefined */
|
||||
@@ -53,8 +72,9 @@ class HassioCardContent extends LitElement {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
iron-icon {
|
||||
margin-right: 16px;
|
||||
margin-top: 16px;
|
||||
margin-right: 24px;
|
||||
margin-left: 8px;
|
||||
margin-top: 12px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@@ -88,6 +108,44 @@ class HassioCardContent extends LitElement {
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
}
|
||||
.icon_image img {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
margin-top: 4px;
|
||||
margin-right: 16px;
|
||||
float: left;
|
||||
}
|
||||
.icon_image.stopped,
|
||||
.icon_image.not_available {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.dot {
|
||||
position: absolute;
|
||||
background-color: var(--paper-orange-400);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.topbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.topbar.installed {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
.topbar.update {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.topbar.unavailable {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|