Compare commits
1094 Commits
v0.1.0-alp
...
1.3.1
Author | SHA1 | Date | |
---|---|---|---|
b8ff80680b | |||
e633449578 | |||
18c48ad067 | |||
07dafa7039 | |||
8a1f539668 | |||
19325b5b7f | |||
b68d6539b6 | |||
aa8c402f97 | |||
343d39d157 | |||
b056de5230 | |||
f7e139b963 | |||
8f4edd5448 | |||
10f4c71a49 | |||
a663838283 | |||
3d27f20798 | |||
f7d8bf9ae7 | |||
9b0b90dba1 | |||
5ab34cd32f | |||
f4b6d7a332 | |||
f87cff7a7e | |||
d2d037e468 | |||
bf7983d72e | |||
40eb564c29 | |||
bc72956365 | |||
595406bb7e | |||
01b8ec9b97 | |||
2126bc4f02 | |||
d94cdf0d1b | |||
3215556440 | |||
a8e7ab6f06 | |||
5aa6171c50 | |||
2fb296b7a8 | |||
e5fe853f0b | |||
819dfc725d | |||
ccbc2eec47 | |||
dce93966e3 | |||
3a7f3db6cd | |||
f39dbe3b24 | |||
2ec549af7e | |||
7ebaf736b5 | |||
71dcf16d70 | |||
6ba1e88b09 | |||
08e1327a29 | |||
85693ac5ce | |||
da8efcd43f | |||
7ebf5f7c18 | |||
f84728b948 | |||
26a62d341e | |||
772bddeb9a | |||
5b55940ccf | |||
7683d18e81 | |||
d09afc37b5 | |||
1c686402d0 | |||
5f4a3fbdfc | |||
312ed99e9f | |||
25e6d51c17 | |||
b501574bab | |||
53b31d8e90 | |||
6d80420a9b | |||
e977054139 | |||
6367d38524 | |||
f9b2d7d84c | |||
44c28ad106 | |||
fec3c525e1 | |||
b1bbed6d80 | |||
13878cfc51 | |||
be49180205 | |||
c4a0b16553 | |||
caacd5e9f4 | |||
5fa28abb6c | |||
e0a28c0b59 | |||
096e714a04 | |||
78893ea01f | |||
90efb29be5 | |||
fca323c56b | |||
e5fe6af5f3 | |||
f0090d522d | |||
edbfd8359b | |||
2702bb254a | |||
ca7b6ed550 | |||
fb00b5d9ff | |||
7ffba397ce | |||
1080076e3b | |||
e295a36465 | |||
9a09a83dc6 | |||
95ca80949f | |||
80b5763530 | |||
9a5e35b024 | |||
4493975676 | |||
141a68faf7 | |||
a8efe7dbb6 | |||
9608983994 | |||
8eb15ab9a4 | |||
aac0cfbb56 | |||
343494ece0 | |||
b1e5e73278 | |||
9b5a0068fd | |||
aa26212ddd | |||
1c45f96706 | |||
c2d5192c51 | |||
88ae80507c | |||
55868d1dfe | |||
92a1230267 | |||
d3f99fb262 | |||
3fdf016c39 | |||
8ce0e8aafa | |||
54f6fb28ef | |||
d53825f140 | |||
ea7e0f04ce | |||
cf75989447 | |||
f27d55869b | |||
a287f597ad | |||
0698950f3d | |||
aa58559ba6 | |||
94acc67383 | |||
701e6a46df | |||
1ed56ce8f1 | |||
a6d1baca77 | |||
dbeda6ea68 | |||
3dca28a7da | |||
da4264a409 | |||
302451e118 | |||
d19dbd389b | |||
05ae954b30 | |||
840e266381 | |||
01525a2929 | |||
2eb2596f37 | |||
699cab3498 | |||
353c80b6bc | |||
e4d1a4f823 | |||
78d6b38b92 | |||
1499a91ef7 | |||
9b7f7aa380 | |||
5683ab5158 | |||
a20dfaf05e | |||
24d42c9597 | |||
9078ad81e8 | |||
7cba6c8a10 | |||
c1f9c8c16d | |||
1d1d132b33 | |||
e258b3bc2c | |||
13508ee92f | |||
4fbf58e707 | |||
a3442f84ca | |||
6a6ab3b2cb | |||
d9fa553e2f | |||
cde5d9b912 | |||
3468446b5b | |||
326434273a | |||
470d3be946 | |||
d1032be6a6 | |||
cffac8e1f8 | |||
870bc25dd9 | |||
de713024f6 | |||
4d4add4581 | |||
1670c8e505 | |||
55eb1b5125 | |||
dbeaaaf91e | |||
8166d8ce6d | |||
35bcf0c1fa | |||
9c1d240962 | |||
a76652b552 | |||
a140f993d0 | |||
ded60a2867 | |||
b86602bcdb | |||
02ea45469f | |||
90105c3b09 | |||
3d828914cc | |||
8cd5776bc6 | |||
17ec73b176 | |||
e7cce01ca9 | |||
6c73f5d979 | |||
f59cb5afbf | |||
5629215229 | |||
45fb637d48 | |||
7c473eb1ca | |||
b40880c85a | |||
30329ea3ba | |||
ca10401bee | |||
814e0a8b00 | |||
b5fbe7b86f | |||
fc9b6f05c0 | |||
eadae4374b | |||
711cb04dcf | |||
1d39b7fc7d | |||
2fa640433a | |||
2445dc7869 | |||
e3e114fe94 | |||
7a1603b423 | |||
4b831821da | |||
1ec54953d7 | |||
61571600d1 | |||
07a097aa50 | |||
ce1cebaf64 | |||
faf6f73b2a | |||
db3b5d941e | |||
cc60dc2b21 | |||
8aa0e03187 | |||
4492a08d6b | |||
792c0d0c84 | |||
8221eceb78 | |||
12ba5598e4 | |||
536cbf9445 | |||
a87943da27 | |||
3fddc3b5a7 | |||
5bc0b0868a | |||
e9ad612fec | |||
c62e045dae | |||
725ec9291d | |||
96c8338890 | |||
0996fb94da | |||
5de2431a0f | |||
163338ea75 | |||
f28e5493dc | |||
01c0a08fa8 | |||
1c461d2449 | |||
915e8045a3 | |||
f10fc7eec1 | |||
320bc677e0 | |||
46ca1948e2 | |||
7a0ce93cfd | |||
3c0bd68b0a | |||
b4ad3061e4 | |||
d6b1fbec24 | |||
cacdd0d304 | |||
e3e1fa3499 | |||
58842d1ebb | |||
101569d6ee | |||
8a180c4c0e | |||
ba343fbd98 | |||
1d528df341 | |||
51ea0b0afa | |||
9dbb697e58 | |||
947558bb3d | |||
8ba4cc85d8 | |||
0f604a6ce6 | |||
7e48c6535f | |||
1d2a8b613b | |||
89e833eb33 | |||
b65a68e0c4 | |||
bfb24b9d11 | |||
0792cae2b1 | |||
a85fb3d03b | |||
ddb9a9d4e9 | |||
29ee360ec4 | |||
c0faaafd04 | |||
bc045344a5 | |||
7d746fd546 | |||
3ff55f181e | |||
187e12dd79 | |||
10daf2d952 | |||
31c6509d13 | |||
cb74108814 | |||
9efded2139 | |||
96b3e7c739 | |||
b029146bf3 | |||
d715aaf5e5 | |||
0dc12963f0 | |||
4da3b40d55 | |||
f7d05a57ad | |||
df01599fe0 | |||
2c3335ebf3 | |||
05c1427aa8 | |||
02bfaf7db6 | |||
f488c0810b | |||
8dbfb91234 | |||
aee99e3925 | |||
50d3280803 | |||
a90eb5c4db | |||
16c06a2d48 | |||
513bf85cae | |||
82d7aeba02 | |||
12f7cb86de | |||
b65c885467 | |||
2a828a1289 | |||
291f12ba97 | |||
6afbd37d71 | |||
0e8869878f | |||
7c2cfe3215 | |||
c376c0e952 | |||
da5f663396 | |||
0e92418a33 | |||
2eef7cfe5e | |||
de4e0bfb3a | |||
8bf2d31e72 | |||
2125c46143 | |||
5402eb84df | |||
ad5aa0898f | |||
040d40b614 | |||
8e58f22c56 | |||
c91695fbe5 | |||
c43741da49 | |||
f2563a0397 | |||
fba4017819 | |||
5f23e108a1 | |||
68d14bd13d | |||
022622522f | |||
89513ca4e5 | |||
a934ee2335 | |||
49aeea634f | |||
e18b9ebe14 | |||
08ee3f3d80 | |||
62d07bf8b9 | |||
ab398cbdc3 | |||
007d12719c | |||
524d195800 | |||
405de64249 | |||
f53554702e | |||
379e1a4a7e | |||
d6f7096055 | |||
37c721e4f6 | |||
d94235ef6d | |||
eb4184713f | |||
a0a0cb4612 | |||
f448a20784 | |||
36eff26862 | |||
5b2a1163b9 | |||
e627a8b963 | |||
4432124e8c | |||
b8ba3c59e9 | |||
c40a496b6b | |||
a7c3b46061 | |||
dfbaaeb06b | |||
f6ab20c6e8 | |||
7625099d74 | |||
32c8e76855 | |||
0aa2c974d5 | |||
9524c8587b | |||
c075db8b1a | |||
d0b7cc1929 | |||
d8df32f140 | |||
293b5e0242 | |||
2f517a3ad5 | |||
56d8e389db | |||
1377843350 | |||
8e31eaf8bb | |||
5ced01463f | |||
a3548455eb | |||
c40fceea4f | |||
6ad3938a91 | |||
bc642f81ad | |||
14ce608bbb | |||
c4c67747c5 | |||
5b3ceecb0e | |||
bf53e4b9df | |||
7e09d92fdf | |||
1ba9106d0b | |||
d727a29991 | |||
c5d617477f | |||
244a1984cc | |||
b00b745f27 | |||
959ff21b9b | |||
e6a7fd2dfe | |||
216276e5f3 | |||
3e6229cf3e | |||
fc4cb80b74 | |||
b907ff1e82 | |||
7536a52771 | |||
73a8c111d1 | |||
86a19eeec2 | |||
fba4459977 | |||
06f994a827 | |||
35d8607484 | |||
2f4c06e9b5 | |||
92e008a380 | |||
14c272af92 | |||
710de9f2b8 | |||
d9ad3b3083 | |||
b2686cb105 | |||
959e89de2b | |||
6e448d3458 | |||
6695756727 | |||
ed732e9b77 | |||
f495a6affc | |||
c8d7e1a95f | |||
e1ca2638e3 | |||
01226cb9eb | |||
8a80d0c5d1 | |||
f26f3e87c7 | |||
b750417415 | |||
2c35dd7c21 | |||
cff4a4feed | |||
62174b0651 | |||
d3ea4210c1 | |||
1c782bf64d | |||
bc96dab339 | |||
0f7179b944 | |||
1e3bfa8ff7 | |||
2bce86f905 | |||
0be00acc3a | |||
4e61adaeb1 | |||
49a8f08153 | |||
ce15658462 | |||
16d73ba7dd | |||
9f3e3c1917 | |||
f29e382a19 | |||
073562373a | |||
4298ebcd66 | |||
a121295bef | |||
9303e4c0a5 | |||
831fc98ab1 | |||
2003005e56 | |||
fda8fb7182 | |||
cf6039b279 | |||
41e552dce5 | |||
90043b5806 | |||
9eb74b5a8d | |||
9cc60a136b | |||
78eb1e779c | |||
8db2d8508e | |||
3f1ece26ec | |||
d1912a44c6 | |||
36a05eb390 | |||
4f39ea1ad8 | |||
a241cc1d61 | |||
8b4df98cb9 | |||
7d30c2f9d5 | |||
44acabadfe | |||
6f3a2bb78d | |||
dd5f8b155d | |||
cd81fc72fd | |||
890da650dc | |||
9897b6a44b | |||
7969f54d3b | |||
7c18454de3 | |||
dcf5efddd1 | |||
a6541134e0 | |||
90504047b4 | |||
ca1eec6602 | |||
edc01d14b7 | |||
6cb5463b13 | |||
63a789ebfb | |||
a0994e9a60 | |||
8d1b728194 | |||
1a9fec8b98 | |||
e634253282 | |||
64b23ec7cc | |||
afe207a878 | |||
4bac0c092f | |||
74c8ae35a1 | |||
7856637456 | |||
965f80a6ca | |||
198c2ba49a | |||
4b9ec5ca6e | |||
5792652619 | |||
2c900333a5 | |||
1f782d7cd3 | |||
89cc1833de | |||
1262d8c9aa | |||
85b0c4f814 | |||
551a8dfa31 | |||
139533d2ca | |||
889682f771 | |||
f16c98057f | |||
26ec807c25 | |||
45af6cbe3c | |||
5dd9cde12d | |||
472fb1d367 | |||
8b372fbc0b | |||
40d72eb6e1 | |||
ced008a7c1 | |||
d1f652282a | |||
f656528d5b | |||
bcdb2a648c | |||
8a78745aa7 | |||
2a3eaabbe4 | |||
bcd175fbfb | |||
f9f013636d | |||
b34cc97080 | |||
327f623ef7 | |||
4d0877e5ae | |||
0eac217399 | |||
9c42ad687d | |||
5cda98da46 | |||
958f545f65 | |||
44165993b4 | |||
283ae6cfd4 | |||
4068b295bd | |||
e36b33dcec | |||
4b12912697 | |||
49a21967cc | |||
cf36406f2a | |||
872ad044f1 | |||
345301c03a | |||
117923413d | |||
24ccbc58c4 | |||
89c91b4b01 | |||
4494da1f4f | |||
c263542c54 | |||
c70f52a73d | |||
423813d6fb | |||
ec6a86f4b0 | |||
64cf18cb23 | |||
e0e064bc67 | |||
5cee6cbd9c | |||
43659b26f7 | |||
98e15ad429 | |||
90728cdf8b | |||
d1ec4f36cc | |||
079070071e | |||
520fd6bc38 | |||
085aead36b | |||
fcbaf298cc | |||
eedc0c9b22 | |||
f70c1e12ff | |||
ec094a4362 | |||
11646c840e | |||
86987c57c9 | |||
e4d6e842f5 | |||
cfe4dd1c59 | |||
3387ab2850 | |||
abd23e27ea | |||
2f110b20bb | |||
f88e6f9b61 | |||
2836973dca | |||
a4477e9f83 | |||
96fa7ece25 | |||
b84caa4cc3 | |||
49c212632e | |||
92165aa7ed | |||
cbbdb754aa | |||
7e3fe0608d | |||
781f39f281 | |||
bfb80f6f8c | |||
801b8f9288 | |||
b988fcfcdd | |||
dff6457cb2 | |||
f50f68f318 | |||
c869ad41d9 | |||
cd41f9a236 | |||
1dbe162bf0 | |||
1a52203bd7 | |||
753df3c724 | |||
dc62a08da3 | |||
0c26aff498 | |||
6323f8f2e6 | |||
885c0b1316 | |||
14958d9165 | |||
bf6a52e0b9 | |||
72aad5cc16 | |||
340e8569cc | |||
8fc7d0b61e | |||
5dcb27ada7 | |||
db1a076132 | |||
6707201e23 | |||
b8b92171a8 | |||
3dd7069292 | |||
7177419472 | |||
c37313cf07 | |||
a65f42d0fd | |||
78dd7df686 | |||
2ea7d9440c | |||
abdcd49368 | |||
6da7a5ab90 | |||
20ffe03139 | |||
a71213c589 | |||
d61103ac42 | |||
298a64b7ae | |||
9e2c673966 | |||
092469d668 | |||
bcf3dab0e2 | |||
7ecfc8a9ff | |||
ecf0a696f7 | |||
dc5db28e01 | |||
555f305c22 | |||
76bf07cfcd | |||
c4663576d1 | |||
a64aa73aae | |||
a3a60dd707 | |||
9c28b0085b | |||
d5baabdd53 | |||
56a333a852 | |||
c5922368de | |||
8c2316a51a | |||
e2e6c015de | |||
0a6ff4586d | |||
fc228d85ae | |||
61823cb43b | |||
127e0b8182 | |||
38c37fa212 | |||
dfaf2a2924 | |||
c90c40c046 | |||
d2049b726a | |||
6508f109f7 | |||
37e63637a7 | |||
6650c5c145 | |||
9160dbf7f2 | |||
243fcd7c49 | |||
c114bcfb35 | |||
83defb08f1 | |||
57ebdbbe85 | |||
c6aceed623 | |||
ba4c88ec5d | |||
ee1685e981 | |||
996fbf7bba | |||
56cd8963d7 | |||
5759aad0cb | |||
02717332f7 | |||
8d1b159f56 | |||
fb335e1100 | |||
5f0bc83d67 | |||
6a8cee2cc2 | |||
0d2f1cf9aa | |||
8efeb3da8a | |||
620aa3b8d8 | |||
ab5bf3b807 | |||
6663bcad72 | |||
113cd29f74 | |||
f2fdfb0a32 | |||
691e48a36b | |||
2036cc117f | |||
389d28a1e1 | |||
27e6198d83 | |||
de762a4878 | |||
e8efefe25d | |||
21f3e8985a | |||
622543d405 | |||
abdc0fc1c8 | |||
1ecb839042 | |||
cece4d1e16 | |||
623634cb6e | |||
f9c37f5084 | |||
3e12f4f8a4 | |||
b07ff6fe71 | |||
5a3b57c28e | |||
e858eee83b | |||
73f00d3bd7 | |||
eea59cf11b | |||
61b459ed8a | |||
dca8c309aa | |||
be53500104 | |||
bc1a791608 | |||
b112ff980a | |||
7beab9ae93 | |||
8c0d1f90a3 | |||
05c05ba768 | |||
67e885e76a | |||
594bce0b8d | |||
7f6569e0db | |||
1c829c8364 | |||
7ca4b02e6d | |||
fadfefd836 | |||
37155901ef | |||
fbbb96409d | |||
5126c54914 | |||
916d0b7e3c | |||
0815840a9c | |||
bc237796b2 | |||
7f44800f64 | |||
85ac746e9d | |||
8215175098 | |||
39ee8b1799 | |||
c76d3d68c8 | |||
cde257922b | |||
be0c9d3372 | |||
66cd7ea307 | |||
b704ce6984 | |||
247c856a41 | |||
9afaebfa12 | |||
929abea5d3 | |||
13102a6b04 | |||
57c3083f9f | |||
5c31ddd00f | |||
8f55be187d | |||
1fe82d8b0d | |||
cbc56a8105 | |||
b63cddfa46 | |||
91db82f730 | |||
0c4d1b78ff | |||
5af2fd0562 | |||
2375543ebf | |||
de187f3ed5 | |||
9266ffacf3 | |||
3c0ca5d16d | |||
caabf25260 | |||
0af2afbb80 | |||
12d226509d | |||
3417c38426 | |||
c7fc5afbb8 | |||
11f565a9dc | |||
53240faac3 | |||
95d4878785 | |||
ef15026203 | |||
ad6355503b | |||
491c2b0dc0 | |||
5b99ade088 | |||
e1d9d9f304 | |||
209ccd4f7f | |||
5a8a207f2e | |||
19c85d9c16 | |||
a916ddfa50 | |||
8c1ad9c7f9 | |||
93af1eca7e | |||
cabf836fa3 | |||
15b3d31a6f | |||
9b98689012 | |||
84ebd0c33c | |||
ccd7774931 | |||
b2773635f5 | |||
8b046b7313 | |||
885a516676 | |||
921b0e09b0 | |||
277c67fc6f | |||
2a01ff8a03 | |||
b246b7bc1d | |||
e1868b9a14 | |||
125f3ac16c | |||
be502b5668 | |||
6f33fdca9f | |||
a7cda2a35e | |||
102b10ade0 | |||
4e96b9adbb | |||
b9581d3762 | |||
7c010359c3 | |||
4a75243994 | |||
d29d7e5b3b | |||
5ebd25e0d1 | |||
b7d5a53e86 | |||
20d3498bfd | |||
67d7bb45f5 | |||
6a03105d01 | |||
5ae580ecf1 | |||
0efef33e53 | |||
ccb88884a7 | |||
d70ba0a55a | |||
5140840d3a | |||
14759fd3c9 | |||
fed35be517 | |||
db77cc43aa | |||
b2269cc96d | |||
8b28bb2e9e | |||
fb456878bc | |||
8b961ebd69 | |||
9bd3a41cf5 | |||
491ae55a2a | |||
e1d2981782 | |||
74572168ae | |||
92d0b5c055 | |||
3504d3276c | |||
736b38b64c | |||
cb118b599a | |||
a08a056cff | |||
0ef2ebfe31 | |||
4f4ac3b574 | |||
7064cb0e30 | |||
91a99e17e0 | |||
2e9b7d20b9 | |||
b8aa808de4 | |||
2cfa92a42b | |||
146efef72d | |||
8c9804e16f | |||
a4736bfb5a | |||
15c54df629 | |||
32ffef21e9 | |||
848d3cb510 | |||
8a4caeebba | |||
aa923f0fba | |||
4d8f50ddd5 | |||
fe06b21a6c | |||
efed7fb1b5 | |||
df2cbb7d13 | |||
03edaa9ca2 | |||
1a7457abf9 | |||
00889b13e0 | |||
0615073ec4 | |||
eb7d17d147 | |||
24f80feeee | |||
4b6dda5a9c | |||
4099fa0c83 | |||
76057e8797 | |||
538d3603dc | |||
bc0e72ca52 | |||
f25a47beb2 | |||
cc3c6b0087 | |||
6cf80c0bfd | |||
8ce9bdb7a5 | |||
31e50150b1 | |||
e359150d97 | |||
93680c981c | |||
e06b66c523 | |||
3dea844e1e | |||
62b1af30e0 | |||
e006c4e403 | |||
983573388e | |||
bdd1dc7e17 | |||
9c1970ee14 | |||
d0e0bf3571 | |||
b399357517 | |||
0290cd3a32 | |||
d8a1d03179 | |||
216fad3cb9 | |||
fead6ea348 | |||
8814687be6 | |||
71c0e2caa0 | |||
1531c41542 | |||
bc90d013e8 | |||
2adfaca0c4 | |||
6cc1a37d9d | |||
4bb616b327 | |||
38219618ba | |||
6774b53758 | |||
29a94c882f | |||
5897fa3a99 | |||
7af92c2dc9 | |||
1094177a42 | |||
5e814e8109 | |||
24c7675fa4 | |||
dc3ca38c78 | |||
96b528e055 | |||
3858036631 | |||
19d42ceeb3 | |||
a2836a3603 | |||
2a45758a6d | |||
dc1bf4d878 | |||
e82ba60c4e | |||
09199d30e8 | |||
724d32dbe2 | |||
949c8ee44e | |||
1a446d34c7 | |||
22a5847285 | |||
1c8f770f10 | |||
be5ea55f6b | |||
c65ade9827 | |||
d3c1422b9e | |||
b6ac9f985f | |||
a59de4b6dc | |||
f507d5df0c | |||
f77e46de37 | |||
cda17b1217 | |||
be560769ef | |||
3815800e32 | |||
a3226311a2 | |||
79669243c2 | |||
fdc81f6ea4 | |||
7fe44459e7 | |||
a8500d44e1 | |||
b4d4c5abec | |||
c19a3f272a | |||
b264534858 | |||
ab53f77f9e | |||
c73956720c | |||
051041e794 | |||
5c83be9fee | |||
4bece42693 | |||
4ae107fe4c | |||
9523ed2562 | |||
9c403480e2 | |||
20b1b90e39 | |||
5633e30448 | |||
4492fb9f0c | |||
36410752e4 | |||
0219f7bfbb | |||
5f3c77f4b9 | |||
a36c7a9ca3 | |||
56ce6dfeeb | |||
67c214454f | |||
73398378c4 | |||
215871ce9e | |||
fd8ea6befd | |||
809a1a1c8c | |||
fc8f2f200f | |||
f41c9f9197 | |||
cdf55ce68b | |||
12088d9516 | |||
a0235ee385 | |||
67fbdb13c6 | |||
c5960de0be | |||
da15e880ec | |||
efbe33f4e3 | |||
af84c99a2d | |||
438449cad8 | |||
d9ca55c3b7 | |||
f248268984 | |||
8ee096595c | |||
a8e79c289b | |||
2cd8533882 | |||
0a21d9c690 | |||
e77bb533b1 | |||
96f1211395 | |||
1e4cb03470 | |||
ab67b557ca | |||
82c9bd26d1 | |||
1bd04abd37 | |||
c5942d22b3 | |||
37ad5e81cf | |||
26187e6233 | |||
b8f6fda8d3 | |||
62b4e99810 | |||
25bf10a64e | |||
874410964d | |||
57c30917b3 | |||
87f89b63e1 | |||
3190b45db3 | |||
f5434e26e5 | |||
86b6ad6bba | |||
8a9641fbed | |||
5142391da2 | |||
01090dc3b1 | |||
0a7bbb5a38 | |||
c347eee9f0 | |||
90f197ba54 | |||
e09917c687 | |||
a69da832cb | |||
c1708fd980 | |||
c85a9bbe27 | |||
d9790dedbb | |||
30e4eaa023 | |||
54e00c3403 | |||
0e3474bbcb | |||
efd06ca547 | |||
69fd37d4fe | |||
4a49372410 | |||
478f58e2d8 | |||
a87aff67ac | |||
644f5e7fc6 | |||
3cddac3dc6 | |||
ab30c64eab | |||
6d79487219 | |||
9f7444eae0 | |||
788d682f2f | |||
66f84952f0 | |||
5d95c3702d | |||
1f0bd8059b | |||
a7830df628 | |||
790446d592 | |||
bb17885b4a | |||
04d8681656 | |||
71c4ac7fed | |||
3f7e21e97e | |||
e24c47b041 | |||
73b32b30a8 | |||
5b6155057c | |||
ff4185effe | |||
b2da9fc04d | |||
f281fab744 | |||
3b99f4feeb | |||
efab8b60b1 | |||
0e96406573 | |||
ed8757c08d | |||
813770329c | |||
1853bd466e | |||
07258477b3 | |||
a3adb72cf8 | |||
e25162f7b5 | |||
d30c9d574b | |||
efa5a1958c | |||
37f20fae5a | |||
91db34badb | |||
c20200b609 | |||
fcd4ac7292 | |||
e16338c3f2 | |||
6e038b0685 | |||
052cd3894e | |||
809c7d6355 | |||
9edfec7dff | |||
df56f6ceda | |||
5e834b0645 | |||
8fb0d61a84 | |||
54979b583b | |||
4e955e98d8 | |||
88cfcb4382 | |||
5338e45ddc | |||
24d071e2f8 | |||
988cd4a72f | |||
d1ea916781 | |||
ce9f25b86c | |||
f29762c931 | |||
30e4496ef1 | |||
7f9dc5dd3a | |||
0f6babc243 | |||
6a43e04b31 | |||
36fa5a50c4 | |||
9ad6d92ccd | |||
fafa8f43f4 | |||
9b490d33d5 | |||
33f9a1075e | |||
b83006e2c3 | |||
ba09c36bd2 | |||
c71ee568b0 | |||
75041f5c23 | |||
14da471774 | |||
369b44f1c8 | |||
8284bb6e76 | |||
9b3b4dfbbc | |||
5ca4424933 | |||
a308aa29a4 | |||
9e80b0eaaf | |||
85379cf491 | |||
758376a891 | |||
2ebba364e3 | |||
6e604440c0 | |||
c23034688e | |||
69f45b52cf | |||
ffc053fbe6 | |||
b5f9ecf601 | |||
948d1d4e23 | |||
136297c18b | |||
164800951d | |||
84d283de2b | |||
2fa35d771a | |||
326cd073b9 | |||
e99c3f5742 | |||
16a9392fa6 | |||
5bf063969b | |||
c19a0511a6 | |||
a4ac40b366 | |||
ce69f044fb | |||
70b6469bd1 | |||
253316fb1f | |||
ec71200ab0 | |||
bc1f4eab2e | |||
4085006446 | |||
b7fb821abe | |||
284e7ba451 | |||
17a3bd8d35 | |||
c2b88c8a12 | |||
c975af4c79 | |||
debf1b71f1 | |||
4725953b32 | |||
e7ca1209e2 | |||
f9afa663f5 | |||
5068cbbcf4 | |||
043d3a9905 | |||
77c5f80c13 | |||
e0d35d07dc | |||
285447a5b7 | |||
ed3e4ba272 | |||
908563063a | |||
7f2611b410 | |||
648750655c | |||
8a0d5581d9 | |||
98d716109b | |||
ebb2f2b4e5 | |||
d910e4dd43 | |||
95d80fbbfc | |||
41297150c2 | |||
b14b248f2f | |||
13fc1bff27 | |||
eee8f21e76 | |||
8ce3560d8d | |||
9e97bac85b | |||
4a0b447f00 | |||
bc4969dae8 | |||
5025b3d384 | |||
0d7e7eb6f7 | |||
062392b38c | |||
acd468ae75 | |||
60f216df13 | |||
9de8a659d3 | |||
7dd8f65af7 | |||
9e83a3e447 | |||
2f135169a9 | |||
76d2750ad6 | |||
571778fbd4 | |||
b89b5dfb98 | |||
a196b0d8d4 | |||
95f7c14296 | |||
2fcd27d240 | |||
6834f2ca34 | |||
c0a9b89d40 | |||
067ccfde02 | |||
4b4fc338f6 | |||
08c07e8398 | |||
df04d000b2 | |||
d0d1ab2740 | |||
af3a5bc611 | |||
b935a0e372 | |||
49444ab3df | |||
098a556279 | |||
375ae36884 | |||
0b42019ef3 | |||
516d38a8a9 | |||
fb886a4622 | |||
662b44d443 | |||
f9c48e6cc7 | |||
88d6e1008f | |||
4540fadf1e | |||
bd13d3693d | |||
5db9d6005f | |||
7e4f744598 | |||
772b569da5 | |||
0e11c1a146 | |||
60793dbf89 | |||
2b622cff04 | |||
94bcc30421 | |||
94f43ded6f | |||
7f7be8aa78 | |||
c0e0059487 | |||
23d3d1839f | |||
aa0d7ee8fd | |||
36d727b454 | |||
7cad0141c7 | |||
86738a0515 | |||
4dc211f2f7 |
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help improve HA Client
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**HA Client version:** [Main menu -> About HA Client]
|
||||
|
||||
**Home Assistant version:**
|
||||
|
||||
**Device name:**
|
||||
|
||||
**Android version:**
|
||||
|
||||
**Description**
|
||||
[Replace with description]
|
||||
|
||||
**Screenshots**
|
||||
[Replace with screenshots]
|
12
.github/ISSUE_TEMPLATE/entity-support-request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Entity support request
|
||||
about: Suggest to add support of any entity type
|
||||
title: ''
|
||||
labels: ENTITY, feature/improvement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Entity type:**
|
||||
|
||||
**Link to documentation:**
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for HA Client if it is not a card or entity support
|
||||
title: ''
|
||||
labels: feature/improvement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
12
.github/ISSUE_TEMPLATE/lovelace-card-support-request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Lovelace Card support request
|
||||
about: Suggest to add any Lovelace card support
|
||||
title: ''
|
||||
labels: CARD, feature/improvement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Card name:**
|
||||
|
||||
**Link to card repository or web page:**
|
11
.github/no-response.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# Configuration for probot-no-response - https://github.com/probot/no-response
|
||||
|
||||
# Number of days of inactivity before an Issue is closed for lack of response
|
||||
daysUntilClose: 14
|
||||
# Label requiring a response
|
||||
responseRequiredLabel: more info needed
|
||||
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there has been no response
|
||||
to our request for more information from the original author. If the issue still relevant
|
||||
feel free to reopen this issue and add more information, or report a new one.
|
12
.gitignore
vendored
@ -9,5 +9,15 @@ build/
|
||||
.flutter-plugins
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
.theia/
|
||||
.project/
|
||||
.settings/
|
||||
|
||||
key.properties
|
||||
flutter_export_environment.sh
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
key.properties
|
||||
.secrets.dart
|
||||
pubspec.lock
|
||||
google-services.json
|
||||
|
8
.gitpod.dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM gitpod/workspace-full:latest
|
||||
|
||||
ENV ANDROID_HOME=/workspace/android-sdk \
|
||||
FLUTTER_ROOT=/workspace/flutter \
|
||||
FLUTTER_HOME=/workspace/flutter
|
||||
|
||||
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
|
||||
&& sdk install java 8.0.242.j9-adpt"
|
26
.gitpod.yml
Normal file
@ -0,0 +1,26 @@
|
||||
image:
|
||||
file: .gitpod.dockerfile
|
||||
|
||||
tasks:
|
||||
- before: |
|
||||
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||
mkdir -p /home/gitpod/.android
|
||||
touch /home/gitpod/.android/repositories.cfg
|
||||
init: |
|
||||
echo "Installing Flutter SDK..."
|
||||
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.12.13+hotfix.7-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
|
||||
echo "Installing Android SDK..."
|
||||
mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip
|
||||
/workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
|
||||
echo "Init Flutter..."
|
||||
cd /workspace/ha_client
|
||||
flutter upgrade
|
||||
flutter doctor --android-licenses
|
||||
flutter pub get
|
||||
command: |
|
||||
echo "Ready to go!"
|
||||
flutter doctor
|
||||
vscode:
|
||||
extensions:
|
||||
- Dart-Code.dart-code@3.5.0-beta.1:Wg2nTABftVR/Dry4tqeY1w==
|
||||
- Dart-Code.flutter@3.5.0:/kOacEWdiDRLyN/idUiM4A==
|
BIN
.gradle/6.0.1/fileChanges/last-build.bin
Normal file
BIN
.gradle/6.0.1/fileHashes/fileHashes.lock
Normal file
0
.gradle/6.0.1/gc.properties
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at vyalov.egor@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
24
README.md
@ -1,3 +1,23 @@
|
||||
# Android client for Home Assistant
|
||||
# HA Client
|
||||
| # IMPORTANT! |
|
||||
|:-:|
|
||||
| As you may know official Home Assistant android app continue to grow and improve. Today is the day when it has so much more and better functionality that I see no any reason not to move from HA Client to official Home Assistant app. You need to make the same as it has more device sensors, better location tracking, widgets etc.
|
||||
| **I'm stopping an active HA Client development as now the only advantage it has over the official app is native UI that is very far from completeness.** |
|
||||
| I need to say a HUGE THANK YOU all for a grate support you all made for HA Client. I really appreciate each and every issue reported to GitHub, each and every message from each of you and each donation made to support development. Now it is time to cancel subscription if any and migrate your HA setup to an official Home Assistant app. You can find it in [Google Play](https://play.google.com/store/apps/details?id=io.homeassistant.companion.android). |
|
||||
## Native Android client for Home Assistant
|
||||
### With actionable notifications, location tracking and Lovelace UI support
|
||||
|
||||
Home Assistant Android client using Flutter and Dart.
|
||||
Visit [ha-client.app](http://ha-client.app/) for more info.
|
||||
|
||||
Download the app from [Google Play](https://play.google.com/store/apps/details?id=com.keyboardcrumbs.haclient)
|
||||
|
||||
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||
|
||||
#### Last release build status
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||
|
||||
#### Projects used
|
||||
- [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski)
|
||||
- [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE)
|
||||
|
1
android/.gitignore
vendored
@ -8,3 +8,4 @@
|
||||
/build
|
||||
/captures
|
||||
GeneratedPluginRegistrant.java
|
||||
.project/
|
17
android/.project
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>android</name>
|
||||
<comment>Project android created by Buildship.</comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
6
android/app/.classpath
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
|
||||
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||
<classpathentry kind="output" path="bin/default"/>
|
||||
</classpath>
|
23
android/app/.project
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>app</name>
|
||||
<comment>Project app created by Buildship.</comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
@ -29,7 +29,12 @@ def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
compileSdkVersion 28
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
@ -38,13 +43,21 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "com.keyboardcrumbs.haclient"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
versionCode 18
|
||||
versionName "0.1.0-alpha"
|
||||
targetSdkVersion 28
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (!System.getenv()["CI"]) {
|
||||
debug {
|
||||
keyAlias keystoreProperties['debugKeyAlias']
|
||||
keyPassword keystoreProperties['debugKeyPassword']
|
||||
storeFile file(keystoreProperties['debugStoreFile'])
|
||||
storePassword keystoreProperties['debugStorePassword']
|
||||
}
|
||||
}
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
@ -65,7 +78,15 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.2'
|
||||
implementation 'com.google.firebase:firebase-messaging:20.2.0'
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.3.4'
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
||||
|
||||
apply plugin: 'io.fabric'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
@ -1,11 +1,15 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.keyboardcrumbs.hassclient">
|
||||
package="com.keyboardcrumbs.hassclient"
|
||||
android:installLocation="auto">
|
||||
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
@ -13,9 +17,19 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="ha_notify" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
@ -23,17 +37,64 @@
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- This keeps the window background of the activity showing
|
||||
until Flutter renders its first frame. It can be removed if
|
||||
there is no splash screen (such as the default splash screen
|
||||
defined in @style/LaunchTheme). -->
|
||||
<meta-data
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="true" />
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".MessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".LocationUpdatesService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".LocationRequestService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver android:name=".NotificationActionReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".NextAlarmBroadcastReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".RestartLocationUpdate">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
@ -0,0 +1,146 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
|
||||
public class LocationRequestService extends Service {
|
||||
|
||||
private static final String TAG = LocationRequestService.class.getSimpleName();
|
||||
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
private LocationRequest mLocationRequest;
|
||||
|
||||
private FusedLocationProviderClient mFusedLocationClient;
|
||||
|
||||
private LocationCallback mLocationCallback;
|
||||
|
||||
private Handler mServiceHandler;
|
||||
|
||||
public LocationRequestService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
||||
|
||||
mLocationCallback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
onNewLocation(locationResult.getLastLocation());
|
||||
}
|
||||
};
|
||||
|
||||
mLocationRequest = new LocationRequest();
|
||||
|
||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
mServiceHandler = new Handler(handlerThread.getLooper());
|
||||
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location requests";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.i(TAG, "Service started. startId="+startId);
|
||||
|
||||
requestLocationUpdates();
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
try {
|
||||
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
|
||||
} catch (SecurityException unlikely) {
|
||||
//When we lost permission
|
||||
Log.i(TAG, "No location permission");
|
||||
}
|
||||
mServiceHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void requestLocationUpdates() {
|
||||
Log.i(TAG, "Requesting location update in 5 seconds.");
|
||||
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
|
||||
mLocationRequest.setInterval(5000);
|
||||
|
||||
startForeground(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(this, null, LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID));
|
||||
try {
|
||||
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
|
||||
mLocationCallback, Looper.myLooper());
|
||||
} catch (SecurityException unlikely) {
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private void onNewLocation(Location location) {
|
||||
Log.i(TAG, "New location: " + location);
|
||||
|
||||
mNotificationManager.notify(LocationUtils.ONETIME_NOTIFICATION_ID, LocationUtils.getRequestNotification(
|
||||
this,
|
||||
location,
|
||||
LocationUtils.ONETIME_NOTIFICATION_CHANNEL_ID
|
||||
));
|
||||
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data locationData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
||||
.putDouble("Lat", location.getLatitude())
|
||||
.putDouble("Long", location.getLongitude())
|
||||
.putFloat("Acc", location.getAccuracy())
|
||||
.build();
|
||||
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setInputData(locationData)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(getApplicationContext())
|
||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||
stopSelf();
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
|
||||
public class LocationUpdatesService extends Service {
|
||||
|
||||
private static final String TAG = LocationUpdatesService.class.getSimpleName();
|
||||
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
private LocationRequest mLocationRequest;
|
||||
|
||||
private FusedLocationProviderClient mFusedLocationClient;
|
||||
|
||||
private LocationCallback mLocationCallback;
|
||||
|
||||
private Handler mServiceHandler;
|
||||
|
||||
public LocationUpdatesService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
||||
|
||||
mLocationCallback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
onNewLocation(locationResult.getLastLocation());
|
||||
}
|
||||
};
|
||||
|
||||
mLocationRequest = new LocationRequest();
|
||||
|
||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
mServiceHandler = new Handler(handlerThread.getLooper());
|
||||
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location updates";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.i(TAG, "Service started. startId="+startId);
|
||||
|
||||
requestLocationUpdates();
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
try {
|
||||
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
|
||||
} catch (SecurityException unlikely) {
|
||||
//When we lost permission
|
||||
Log.i(TAG, "No location permission");
|
||||
}
|
||||
mServiceHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void requestLocationUpdates() {
|
||||
long requestInterval = LocationUtils.getLocationUpdateIntervals(getApplicationContext());
|
||||
int priority;
|
||||
if (requestInterval >= 600000) {
|
||||
mLocationRequest.setFastestInterval(60000);
|
||||
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
|
||||
} else {
|
||||
priority = LocationRequest.PRIORITY_HIGH_ACCURACY;
|
||||
}
|
||||
Log.i(TAG, "Requesting location updates. Every " + requestInterval + "ms with priority of " + priority);
|
||||
mLocationRequest.setPriority(priority);
|
||||
mLocationRequest.setInterval(requestInterval);
|
||||
|
||||
startForeground(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(this, null, LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID));
|
||||
try {
|
||||
mFusedLocationClient.requestLocationUpdates(mLocationRequest,
|
||||
mLocationCallback, Looper.myLooper());
|
||||
} catch (SecurityException unlikely) {
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private void onNewLocation(Location location) {
|
||||
Log.i(TAG, "New location: " + location);
|
||||
|
||||
mNotificationManager.notify(LocationUtils.SERVICE_NOTIFICATION_ID, LocationUtils.getNotification(
|
||||
this,
|
||||
location,
|
||||
LocationUtils.SERVICE_NOTIFICATION_CHANNEL_ID
|
||||
));
|
||||
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data locationData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
||||
.putDouble("Lat", location.getLatitude())
|
||||
.putDouble("Long", location.getLongitude())
|
||||
.putFloat("Acc", location.getAccuracy())
|
||||
.build();
|
||||
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setInputData(locationData)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(getApplicationContext())
|
||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.ListenableWorker;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
import com.google.android.gms.location.LocationCallback;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.android.gms.location.LocationResult;
|
||||
import com.google.android.gms.location.LocationServices;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocationUpdatesWorker extends ListenableWorker {
|
||||
|
||||
private Context currentContext;
|
||||
private LocationCallback callback;
|
||||
private FusedLocationProviderClient fusedLocationClient;
|
||||
|
||||
public LocationUpdatesWorker(Context context, WorkerParameters params) {
|
||||
super(context, params);
|
||||
currentContext = context;
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
fusedLocationClient.removeLocationUpdates(callback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListenableFuture<Result> startWork() {
|
||||
return CallbackToFutureAdapter.getFuture(completer -> {
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(currentContext);
|
||||
|
||||
callback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(LocationResult locationResult) {
|
||||
super.onLocationResult(locationResult);
|
||||
Location location = locationResult.getLastLocation();
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data locationData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_LOCATION)
|
||||
.putDouble("Lat", location.getLatitude())
|
||||
.putDouble("Long", location.getLongitude())
|
||||
.putFloat("Acc", location.getAccuracy())
|
||||
.build();
|
||||
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setConstraints(constraints)
|
||||
.setInputData(locationData)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(getApplicationContext())
|
||||
.enqueueUniqueWork("SendLocationUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||
if (LocationUtils.showNotification(currentContext)) {
|
||||
NotificationManager notificationManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
notificationManager = currentContext.getSystemService(NotificationManager.class);
|
||||
} else {
|
||||
notificationManager = (NotificationManager)currentContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Location updates";
|
||||
NotificationChannel mChannel =
|
||||
new NotificationChannel(LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
notificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
notificationManager.notify(LocationUtils.WORKER_NOTIFICATION_ID, LocationUtils.getNotification(currentContext, location, LocationUtils.WORKER_NOTIFICATION_CHANNEL_ID));
|
||||
}
|
||||
finish();
|
||||
completer.set(Result.success());
|
||||
}
|
||||
};
|
||||
|
||||
LocationRequest locationRequest = new LocationRequest();
|
||||
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
|
||||
locationRequest.setInterval(5000);
|
||||
try {
|
||||
fusedLocationClient.requestLocationUpdates(locationRequest,
|
||||
callback, Looper.myLooper());
|
||||
} catch (SecurityException e) {
|
||||
completer.setException(e);
|
||||
}
|
||||
return callback;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class LocationUtils {
|
||||
|
||||
static final String KEY_REQUESTING_LOCATION_UPDATES = "flutter.location-updates-state";
|
||||
static final String KEY_LOCATION_UPDATE_INTERVAL = "flutter.location-updates-interval";
|
||||
static final String KEY_LOCATION_SHOW_NOTIFICATION = "flutter.location-updates-show-notification";
|
||||
|
||||
static final String WORKER_NOTIFICATION_CHANNEL_ID = "location_worker";
|
||||
static final int WORKER_NOTIFICATION_ID = 954322;
|
||||
static final String SERVICE_NOTIFICATION_CHANNEL_ID = "location_service";
|
||||
static final int SERVICE_NOTIFICATION_ID = 954311;
|
||||
static final String ONETIME_NOTIFICATION_CHANNEL_ID = "location_request";
|
||||
static final int ONETIME_NOTIFICATION_ID = 954333;
|
||||
|
||||
static final String REQUEST_LOCATION_NOTIFICATION = "request_location_update";
|
||||
|
||||
static final String LOCATION_WORK_NAME = "HALocationWorker";
|
||||
static final String LOCATION_REQUEST_NAME = "HALocationRequest";
|
||||
|
||||
static final int LOCATION_UPDATES_DISABLED = 0;
|
||||
static final int LOCATION_UPDATES_SERVICE = 1;
|
||||
static final int LOCATION_UPDATES_WORKER = 2;
|
||||
|
||||
static final int DEFAULT_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
|
||||
static final long MIN_WORKER_LOCATION_UPDATE_INTERVAL_MS = 900000; //15 minutes
|
||||
|
||||
static int getLocationUpdatesState(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getInt(KEY_REQUESTING_LOCATION_UPDATES, LOCATION_UPDATES_DISABLED);
|
||||
}
|
||||
|
||||
static long getLocationUpdateIntervals(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getLong(KEY_LOCATION_UPDATE_INTERVAL, DEFAULT_LOCATION_UPDATE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
static boolean showNotification(Context context) {
|
||||
return context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).getBoolean(KEY_LOCATION_SHOW_NOTIFICATION, true);
|
||||
}
|
||||
|
||||
static void setLocationUpdatesState(Context context, int locationUpdatesState) {
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putInt(KEY_REQUESTING_LOCATION_UPDATES, locationUpdatesState)
|
||||
.apply();
|
||||
}
|
||||
|
||||
static void setLocationUpdatesSettings(Context context, long interval, boolean showNotification) {
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putBoolean(KEY_LOCATION_SHOW_NOTIFICATION, showNotification)
|
||||
.putLong(KEY_LOCATION_UPDATE_INTERVAL, interval)
|
||||
.apply();
|
||||
}
|
||||
|
||||
static void startService(Context context) {
|
||||
Intent myService = new Intent(context, LocationUpdatesService.class);
|
||||
context.startService(myService);
|
||||
}
|
||||
|
||||
static void startServiceFromBroadcast(Context context) {
|
||||
Intent serviceIntent = new Intent(context, LocationUpdatesService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(serviceIntent);
|
||||
} else {
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
}
|
||||
|
||||
static void startWorker(Context context, long interval) {
|
||||
PeriodicWorkRequest periodicWork = new PeriodicWorkRequest.Builder(LocationUpdatesWorker.class, interval, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(LocationUtils.LOCATION_WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, periodicWork);
|
||||
}
|
||||
|
||||
static void requestLocationOnce(Context context) {
|
||||
Intent myService = new Intent(context, LocationRequestService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(myService);
|
||||
} else {
|
||||
context.startService(myService);
|
||||
}
|
||||
//OneTimeWorkRequest oneTimeWork = new OneTimeWorkRequest.Builder(LocationUpdatesWorker.class)
|
||||
// .build();
|
||||
//WorkManager.getInstance(context).enqueueUniqueWork(LocationUtils.LOCATION_REQUEST_NAME, ExistingWorkPolicy.REPLACE, oneTimeWork);
|
||||
}
|
||||
|
||||
static Notification getNotification(Context context, Location location, String channelId) {
|
||||
CharSequence title = "Location tracking";
|
||||
CharSequence text = location == null ? "Accuracy: unknown" : "Accuracy: " + location.getAccuracy() + " m";
|
||||
CharSequence bigText = location == null ? "Waiting for location..." : "Time: " + DateFormat.getDateTimeInstance().format(new Date(location.getTime())) +
|
||||
System.getProperty("line.separator") + "Accuracy: " + location.getAccuracy() + " m" +
|
||||
System.getProperty("line.separator") + "Location: " + location.getLatitude() + ", " + location.getLongitude();
|
||||
|
||||
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
|
||||
new Intent(context, MainActivity.class), 0);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||
.setContentIntent(activityPendingIntent)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(bigText))
|
||||
.setPriority(-1)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.mini_icon_location)
|
||||
.setWhen(System.currentTimeMillis());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
static Notification getRequestNotification(Context context, Location location, String channelId) {
|
||||
CharSequence title = "Updating location...";
|
||||
CharSequence text = location == null ? "Waiting for location..." : "Accuracy: " + location.getAccuracy() + " m";
|
||||
|
||||
PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0,
|
||||
new Intent(context, MainActivity.class), 0);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||
.setContentIntent(activityPendingIntent)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setPriority(-1)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.mini_icon_location)
|
||||
.setWhen(System.currentTimeMillis());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
@ -1,13 +1,184 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.os.Bundle;
|
||||
import io.flutter.app.FlutterActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
GeneratedPluginRegistrant.registerWith(this);
|
||||
}
|
||||
|
||||
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
|
||||
|
||||
private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34;
|
||||
|
||||
private int locationUpdatesType = LocationUtils.LOCATION_UPDATES_DISABLED;
|
||||
private long locationUpdatesInterval = LocationUtils.DEFAULT_LOCATION_UPDATE_INTERVAL_MS;
|
||||
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
|
||||
(call, result) -> {
|
||||
Context context = getActivity();
|
||||
switch (call.method) {
|
||||
case "getFCMToken":
|
||||
try {
|
||||
if (checkPlayServices()) {
|
||||
FirebaseInstanceId.getInstance().getInstanceId()
|
||||
.addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
String token = task.getResult().getToken();
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
||||
result.success(token);
|
||||
} else {
|
||||
Exception ex = task.getException();
|
||||
if (ex != null) {
|
||||
result.error("fcm_error", ex.getMessage(), null);
|
||||
} else {
|
||||
result.error("fcm_error", "Unknown", null);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
result.error("google_play_service_error", "Google Play Services unavailable", null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.error("get_token_exception", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
case "startLocationService":
|
||||
try {
|
||||
locationUpdatesInterval = ((Number)call.argument("location-updates-interval")).longValue();
|
||||
boolean useForegroundService = (boolean)call.argument("foreground-location-tracking");
|
||||
|
||||
if (useForegroundService) {
|
||||
locationUpdatesType = LocationUtils.LOCATION_UPDATES_SERVICE;
|
||||
} else {
|
||||
locationUpdatesType = LocationUtils.LOCATION_UPDATES_WORKER;
|
||||
}
|
||||
LocationUtils.setLocationUpdatesSettings(this, locationUpdatesInterval, (boolean)call.argument("location-updates-show-notification"));
|
||||
if (isNoLocationPermissions()) {
|
||||
requestLocationPermissions();
|
||||
} else {
|
||||
startLocationUpdates();
|
||||
}
|
||||
result.success("");
|
||||
} catch (Exception e) {
|
||||
result.error("location_error", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
case "stopLocationService":
|
||||
try {
|
||||
stopLocationUpdates();
|
||||
result.success("");
|
||||
} catch (Exception e) {
|
||||
result.error("location_error", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
case "cancelOldLocationWorker":
|
||||
WorkManager.getInstance(this).cancelAllWorkByTag("haclocation");
|
||||
result.success("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private boolean checkPlayServices() {
|
||||
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
|
||||
}
|
||||
|
||||
private void startLocationUpdates() {
|
||||
if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_SERVICE) {
|
||||
LocationUtils.startService(this);
|
||||
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
|
||||
} else if (locationUpdatesType == LocationUtils.LOCATION_UPDATES_WORKER) {
|
||||
LocationUtils.startWorker(this, locationUpdatesInterval);
|
||||
LocationUtils.setLocationUpdatesState(this, locationUpdatesType);
|
||||
} else {
|
||||
stopLocationUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopLocationUpdates() {
|
||||
Intent myService = new Intent(MainActivity.this, LocationUpdatesService.class);
|
||||
stopService(myService);
|
||||
WorkManager.getInstance(this).cancelUniqueWork(LocationUtils.LOCATION_WORK_NAME);
|
||||
NotificationManager notificationManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
notificationManager = getSystemService(NotificationManager.class);
|
||||
} else {
|
||||
notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
notificationManager.cancel(LocationUtils.WORKER_NOTIFICATION_ID);
|
||||
LocationUtils.setLocationUpdatesState(this, LocationUtils.LOCATION_UPDATES_DISABLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private boolean isNoLocationPermissions() {
|
||||
return PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
private void requestLocationPermissions() {
|
||||
ActivityCompat.requestPermissions(MainActivity.this,
|
||||
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
||||
REQUEST_PERMISSIONS_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
startLocationUpdates();
|
||||
} else {
|
||||
stopLocationUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,165 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import java.util.Map;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
|
||||
public class MessagingService extends FirebaseMessagingService {
|
||||
|
||||
private static final String TAG = MessagingService.class.getSimpleName();
|
||||
|
||||
public static final String NOTIFICATION_ACTION_BROADCAST = "com.keyboardcrumbs.hassclient.haNotificationAction";
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||
Map<String, String> data = remoteMessage.getData();
|
||||
if (data.size() > 0) {
|
||||
if (data.containsKey("body") || data.containsKey("title")) {
|
||||
sendNotification(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewToken(@NonNull String token) {
|
||||
getApplicationContext().getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit().putString("flutter.npush-token", token).apply();
|
||||
}
|
||||
|
||||
private void sendNotification(Map<String, String> data) {
|
||||
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
|
||||
boolean autoCancel;
|
||||
if (!data.containsKey("body")) {
|
||||
messageBody = "";
|
||||
} else {
|
||||
messageBody = data.get("body");
|
||||
}
|
||||
if (messageBody != null && messageBody.equals(LocationUtils.REQUEST_LOCATION_NOTIFICATION)) {
|
||||
Log.d(TAG, "Location update request received");
|
||||
LocationUtils.requestLocationOnce(this);
|
||||
return;
|
||||
}
|
||||
String customChannelId = data.get("channelId");
|
||||
if (customChannelId == null) {
|
||||
channelId = "ha_notify";
|
||||
channelDescription = "Default notification channel";
|
||||
} else {
|
||||
channelId = customChannelId;
|
||||
channelDescription = channelId;
|
||||
}
|
||||
if (!data.containsKey("title")) {
|
||||
messageTitle = "HA Client";
|
||||
} else {
|
||||
messageTitle = data.get("title");
|
||||
}
|
||||
if (!data.containsKey("tag")) {
|
||||
nTag = String.valueOf(System.currentTimeMillis());
|
||||
} else {
|
||||
nTag = data.get("tag");
|
||||
}
|
||||
if (data.containsKey("dismiss")) {
|
||||
try {
|
||||
boolean dismiss = Boolean.parseBoolean(data.get("dismiss"));
|
||||
if (dismiss) {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(nTag, 0);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//nope
|
||||
}
|
||||
}
|
||||
if (data.containsKey("autoDismiss")) {
|
||||
try {
|
||||
autoCancel = Boolean.parseBoolean(data.get("autoDismiss"));
|
||||
} catch (Exception e) {
|
||||
autoCancel = true;
|
||||
}
|
||||
} else {
|
||||
autoCancel = true;
|
||||
}
|
||||
imageUrl = data.get("image");
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
|
||||
PendingIntent.FLAG_ONE_SHOT);
|
||||
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
NotificationCompat.Builder notificationBuilder =
|
||||
new NotificationCompat.Builder(this, channelId)
|
||||
.setSmallIcon(R.drawable.mini_icon)
|
||||
.setContentTitle(messageTitle)
|
||||
.setContentText(messageBody)
|
||||
.setAutoCancel(autoCancel)
|
||||
.setSound(defaultSoundUri)
|
||||
.setContentIntent(pendingIntent);
|
||||
Bitmap image = null;
|
||||
if (URLUtil.isValidUrl(imageUrl)) {
|
||||
image = getBitmapFromURL(imageUrl);
|
||||
}
|
||||
if (image != null) {
|
||||
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
|
||||
notificationBuilder.setLargeIcon(image);
|
||||
} else {
|
||||
notificationBuilder.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(messageBody));
|
||||
}
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
if (data.containsKey("action" + i)) {
|
||||
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class).setAction(NOTIFICATION_ACTION_BROADCAST);
|
||||
if (autoCancel) {
|
||||
broadcastIntent.putExtra("tag", nTag);
|
||||
}
|
||||
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
|
||||
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
notificationBuilder.addAction(R.drawable.blank_icon, data.get("action" + i), actionIntent);
|
||||
}
|
||||
}
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
// Since android Oreo notification channel is needed.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(channelId,
|
||||
channelDescription,
|
||||
NotificationManager.IMPORTANCE_HIGH);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
notificationManager.notify(nTag, 0 /* ID of notification */, notificationBuilder.build());
|
||||
}
|
||||
|
||||
private Bitmap getBitmapFromURL(String imageUrl) {
|
||||
try {
|
||||
URL url = new URL(imageUrl);
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.connect();
|
||||
InputStream input = connection.getInputStream();
|
||||
return BitmapFactory.decodeStream(input);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean isBootIntent = Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction());
|
||||
final boolean isNextAlarmIntent = AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equalsIgnoreCase(intent.getAction());
|
||||
if (!isBootIntent && !isNextAlarmIntent) {
|
||||
return;
|
||||
}
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
Data workerData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NEXT_ALARM)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.EXPONENTIAL,
|
||||
10,
|
||||
TimeUnit.SECONDS)
|
||||
.setInputData(workerData)
|
||||
.setConstraints(constraints)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
|
||||
import androidx.work.BackoffPolicy;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class NotificationActionReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
String intentAction = intent.getAction();
|
||||
if (intentAction == null || !intentAction.equalsIgnoreCase(MessagingService.NOTIFICATION_ACTION_BROADCAST)) {
|
||||
return;
|
||||
}
|
||||
String rawActionData = intent.getStringExtra("actionData");
|
||||
if (intent.hasExtra("tag")) {
|
||||
String notificationTag = intent.getStringExtra("tag");
|
||||
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationTag, 0);
|
||||
}
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
Data workerData = new Data.Builder()
|
||||
.putInt(SendDataHomeWorker.DATA_TYPE_KEY, SendDataHomeWorker.DATA_TYPE_NOTIFICATION_ACTION)
|
||||
.putString("rawActionData", rawActionData)
|
||||
.build();
|
||||
|
||||
OneTimeWorkRequest uploadWorkRequest =
|
||||
new OneTimeWorkRequest.Builder(SendDataHomeWorker.class)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.EXPONENTIAL,
|
||||
10,
|
||||
TimeUnit.SECONDS)
|
||||
.setInputData(workerData)
|
||||
.setConstraints(constraints)
|
||||
.build();
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueueUniqueWork("NotificationAction", ExistingWorkPolicy.APPEND, uploadWorkRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class RestartLocationUpdate extends BroadcastReceiver {
|
||||
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (LocationUtils.getLocationUpdatesState(context) == LocationUtils.LOCATION_UPDATES_SERVICE &&
|
||||
(Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction()) || Intent.ACTION_MY_PACKAGE_REPLACED.equalsIgnoreCase(intent.getAction()))) {
|
||||
LocationUtils.startServiceFromBroadcast(context);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
package com.keyboardcrumbs.hassclient;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.BatteryManager;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SendDataHomeWorker extends Worker {
|
||||
public static final String DATA_TYPE_KEY = "dataType";
|
||||
|
||||
public static final int DATA_TYPE_LOCATION = 1;
|
||||
public static final int DATA_TYPE_NEXT_ALARM = 2;
|
||||
public static final int DATA_TYPE_NOTIFICATION_ACTION = 3;
|
||||
|
||||
private Context currentContext;
|
||||
private static final String TAG = "SendDataHomeWorker";
|
||||
|
||||
public static final String KEY_LAT_ARG = "Lat";
|
||||
public static final String KEY_LONG_ARG = "Long";
|
||||
public static final String KEY_ACC_ARG = "Acc";
|
||||
|
||||
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
|
||||
|
||||
public SendDataHomeWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
currentContext = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
Log.d(TAG, "Start sending data home");
|
||||
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
|
||||
String webhookId = prefs.getString("flutter.app-webhook-id", null);
|
||||
if (webhookId != null) {
|
||||
try {
|
||||
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
|
||||
"://" +
|
||||
prefs.getString("flutter.hassio-domain", "") +
|
||||
":" +
|
||||
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
|
||||
if (URLUtil.isValidUrl(requestUrl)) {
|
||||
int dataType = getInputData().getInt(DATA_TYPE_KEY, 0);
|
||||
String stringRequest;
|
||||
if (dataType == DATA_TYPE_LOCATION) {
|
||||
Log.d(TAG, "Location data");
|
||||
stringRequest = getLocationDataToSend();
|
||||
} else if (dataType == DATA_TYPE_NEXT_ALARM) {
|
||||
Log.d(TAG, "Next alarm data");
|
||||
stringRequest = getNextAlarmDataToSend();
|
||||
} else if (dataType == DATA_TYPE_NOTIFICATION_ACTION) {
|
||||
Log.d(TAG, "Notification action data");
|
||||
stringRequest = getNotificationActionData();
|
||||
} else {
|
||||
Log.e(TAG, "doWork() unknown data type: " + dataType);
|
||||
return Result.failure();
|
||||
}
|
||||
try {
|
||||
URL url = new URL(requestUrl);
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setDoOutput(true);
|
||||
assert stringRequest != null;
|
||||
byte[] outputBytes = stringRequest.getBytes(StandardCharsets.UTF_8);
|
||||
OutputStream os = urlConnection.getOutputStream();
|
||||
os.write(outputBytes);
|
||||
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
urlConnection.disconnect();
|
||||
if (responseCode >= 300) {
|
||||
return Result.retry();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error sending data", e);
|
||||
return Result.retry();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Invalid HA url");
|
||||
return Result.failure();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error =(", e);
|
||||
return Result.failure();
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Webhook id not found");
|
||||
return Result.failure();
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private String getLocationDataToSend() {
|
||||
try {
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
dataToSend.put("type", "update_location");
|
||||
JSONObject dataObject = new JSONObject();
|
||||
|
||||
JSONArray gps = new JSONArray();
|
||||
gps.put(0, getInputData().getDouble(KEY_LAT_ARG, 0));
|
||||
gps.put(1, getInputData().getDouble(KEY_LONG_ARG, 0));
|
||||
|
||||
dataObject.put("gps", gps);
|
||||
dataObject.put("gps_accuracy", getInputData().getFloat(KEY_ACC_ARG, 0));
|
||||
|
||||
BatteryManager bm;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
bm = currentContext.getSystemService(BatteryManager.class);
|
||||
} else {
|
||||
bm = (BatteryManager)currentContext.getSystemService(Context.BATTERY_SERVICE);
|
||||
}
|
||||
int batLevel = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||
|
||||
dataObject.put("battery", batLevel);
|
||||
|
||||
dataToSend.put("data", dataObject);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getLocationDataToSend", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getNotificationActionData() {
|
||||
try {
|
||||
String rawActionData = getInputData().getString("rawActionData");
|
||||
if (rawActionData == null || rawActionData.length() == 0) {
|
||||
Log.e(TAG,"getNotificationActionData rawAction data is empty");
|
||||
return null;
|
||||
}
|
||||
JSONObject actionData = new JSONObject(rawActionData);
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
JSONObject requestData = new JSONObject();
|
||||
if (actionData.getString("action").equals("call-service")) {
|
||||
dataToSend.put("type", "call_service");
|
||||
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
|
||||
requestData.put("service", actionData.getString("service").split("\\.")[1]);
|
||||
if (actionData.has("service_data")) {
|
||||
requestData.put("service_data", actionData.get("service_data"));
|
||||
}
|
||||
} else {
|
||||
dataToSend.put("type", "fire_event");
|
||||
requestData.put("event_type", "ha_client_event");
|
||||
JSONObject eventData = new JSONObject();
|
||||
eventData.put("action", actionData.getString("action"));
|
||||
requestData.put("event_data", eventData);
|
||||
}
|
||||
dataToSend.put("data", requestData);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getNotificationActionData", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getNextAlarmDataToSend() {
|
||||
try {
|
||||
final AlarmManager alarmManager;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
alarmManager = currentContext.getSystemService(AlarmManager.class);
|
||||
} else {
|
||||
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
|
||||
}
|
||||
|
||||
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
|
||||
|
||||
JSONObject dataToSend = new JSONObject();
|
||||
dataToSend.put("type", "update_sensor_states");
|
||||
JSONArray dataArray = new JSONArray();
|
||||
JSONObject sensorData = new JSONObject();
|
||||
JSONObject sensorAttrs = new JSONObject();
|
||||
sensorData.put("unique_id", "next_alarm");
|
||||
sensorData.put("type", "sensor");
|
||||
final long triggerTimestamp;
|
||||
if (alarmClockInfo != null) {
|
||||
triggerTimestamp = alarmClockInfo.getTriggerTime();
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(triggerTimestamp);
|
||||
Date date = calendar.getTime();
|
||||
sensorData.put("state", DATE_TIME_FORMAT.format(date));
|
||||
sensorAttrs.put("date", DATE_FORMAT.format(date));
|
||||
sensorAttrs.put("time", TIME_FORMAT.format(date));
|
||||
sensorAttrs.put("timestamp", triggerTimestamp);
|
||||
} else {
|
||||
sensorData.put("state", "");
|
||||
sensorAttrs.put("date", "");
|
||||
sensorAttrs.put("time", "");
|
||||
sensorAttrs.put("timestamp", 0);
|
||||
}
|
||||
sensorData.put("icon", "mdi:alarm");
|
||||
sensorData.put("attributes", sensorAttrs);
|
||||
dataArray.put(0, sensorData);
|
||||
dataToSend.put("data", dataArray);
|
||||
return dataToSend.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getNextAlarmDataToSend", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
BIN
android/app/src/main/res/drawable/blank_icon.png
Normal file
After Width: | Height: | Size: 461 B |
27
android/app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
|
||||
<path
|
||||
android:fillColor="#709ac1"
|
||||
android:fillAlpha="0"
|
||||
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.12"
|
||||
android:pathData="M 70.506 38.389 L 108 72.466 L 108 108 L 77 108 L 35.066 72.466 L 38.373 63.769 L 36.268 50.216 L 43.335 44.523 L 51.841 34.578 L 63.096 42.478 L 68.586 42.478 L 70.506 38.389 Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.12"
|
||||
android:pathData="M 28.979 53.708 L 47.736 67.31 L 38.373 58.563 L 36.268 51.52 L 28.979 53.708 Z" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M 77.131 54.24 L 72.878 54.24 L 72.878 72.415 L 56.339 72.415 L 56.339 64.85 L 62.931 58.511 L 64.609 58.784 C 67.349 58.784 69.57 56.649 69.57 54.013 C 69.57 51.378 67.349 49.242 64.609 49.242 C 61.868 49.242 59.647 51.378 59.647 54.013 L 59.883 55.626 L 56.339 59.079 L 56.339 46.63 C 57.898 45.812 58.938 44.244 58.938 42.427 C 58.938 39.792 56.717 37.656 53.976 37.656 C 51.236 37.656 49.015 39.792 49.015 42.427 C 49.015 44.244 50.054 45.812 51.614 46.63 L 51.614 59.079 L 48.07 55.626 L 48.306 54.013 C 48.306 51.378 46.084 49.242 43.344 49.242 C 40.604 49.242 38.383 51.378 38.383 54.013 C 38.383 56.648 40.604 58.784 43.344 58.784 L 45.022 58.511 L 51.614 64.85 L 51.614 72.415 L 35.075 72.415 L 35.075 54.24 L 30.94 54.24 C 29.948 54.24 28.979 54.24 28.979 53.763 C 29.003 53.263 29.995 52.309 31.011 51.332 L 51.614 31.522 C 52.393 30.772 53.197 30 53.976 30 C 54.756 30 55.559 30.772 56.339 31.522 L 65.79 40.609 L 65.79 38.338 L 70.515 38.338 L 70.515 45.153 L 77.084 51.469 C 78.029 52.377 78.997 53.309 79.021 53.786 C 79.021 54.24 78.076 54.24 77.131 54.24 Z M 43.344 51.969 C 43.908 51.969 44.449 52.184 44.848 52.567 C 45.247 52.951 45.471 53.471 45.471 54.013 C 45.471 54.555 45.247 55.076 44.848 55.459 C 44.449 55.842 43.908 56.058 43.344 56.058 C 42.78 56.058 42.239 55.842 41.841 55.459 C 41.442 55.076 41.218 54.555 41.218 54.013 C 41.218 53.471 41.442 52.951 41.841 52.567 C 42.239 52.184 42.78 51.969 43.344 51.969 Z M 64.609 51.969 C 65.79 51.969 66.735 52.877 66.735 54.013 C 66.735 55.149 65.79 56.058 64.609 56.058 C 64.045 56.058 63.504 55.842 63.105 55.459 C 62.706 55.076 62.482 54.555 62.482 54.013 C 62.482 53.471 62.706 52.951 63.105 52.567 C 63.504 52.184 64.045 51.969 64.609 51.969 Z M 53.976 40.382 C 55.158 40.382 56.103 41.291 56.103 42.427 C 56.103 43.563 55.158 44.472 53.976 44.472 C 52.795 44.472 51.85 43.563 51.85 42.427 C 51.85 41.291 52.795 40.382 53.976 40.382 Z" />
|
||||
</group>
|
||||
</vector>
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
<item android:drawable="@color/main_color" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
|
BIN
android/app/src/main/res/drawable/mini_icon.png
Normal file
After Width: | Height: | Size: 612 B |
BIN
android/app/src/main/res/drawable/mini_icon_location.png
Normal file
After Width: | Height: | Size: 571 B |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/main_color"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/main_color"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.8 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 9.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 20 KiB |
@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="main_color">#709AC1</color>
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -2,10 +2,15 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
classpath 'io.fabric.tools:gradle:1.26.1'
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +18,9 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx512m
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableR8=true
|
||||
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
|
0
android/gradlew
vendored
Normal file → Executable file
1
android/settings_aar.gradle
Normal file
@ -0,0 +1 @@
|
||||
include ':app'
|
61
assets/html/cameraLiveView.html
Normal file
@ -0,0 +1,61 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
widows: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var messageChannel = '{{message_channel}}';
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<video id="screen" width="100%" controls></video>
|
||||
<script>
|
||||
if (Hls.isSupported()) {
|
||||
var video = document.getElementById('screen');
|
||||
var hls = new Hls();
|
||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||
if (data.fatal) {
|
||||
switch(data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
// try to recover network error
|
||||
console.log("fatal network error encountered, try to recover");
|
||||
hls.startLoad();
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.log("fatal media error encountered, try to recover");
|
||||
hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
// cannot recover
|
||||
hls.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
// bind them together
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
|
||||
console.log("video and hls.js are now bound together !");
|
||||
hls.loadSource("{{stream_url}}");
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
|
||||
console.log("manifest loaded, found " + data.levels.length + " quality level");
|
||||
video.play();
|
||||
video.onloadedmetadata = function() {
|
||||
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
28
assets/html/cameraView.html
Normal file
@ -0,0 +1,28 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
widows: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var messageChannel = '{{message_channel}}';
|
||||
window.onload = function() {
|
||||
var img = document.getElementById('screen');
|
||||
if (img) {
|
||||
window[messageChannel].postMessage(document.body.clientWidth / img.offsetHeight);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<img id="screen" src="{{stream_url}}">
|
||||
</body>
|
||||
</html>
|
37
assets/js/externalAuth.js
Normal file
@ -0,0 +1,37 @@
|
||||
window.externalApp = {};
|
||||
window.externalApp.getExternalAuth = function(options) {
|
||||
console.log("Starting external auth");
|
||||
var options = JSON.parse(options);
|
||||
if (options && options.callback) {
|
||||
var responseData = {
|
||||
access_token: "[token]",
|
||||
expires_in: 1800
|
||||
};
|
||||
console.log("Waiting for callback to be added");
|
||||
setTimeout(function(){
|
||||
console.log("Calling a callback");
|
||||
window[options.callback](true, responseData);
|
||||
}, 900);
|
||||
}
|
||||
};
|
||||
/*
|
||||
window.externalApp.externalBus = function(message) {
|
||||
console.log("External bus message: " + message);
|
||||
var messageObj = JSON.parse(message);
|
||||
if (messageObj.type == "config/get") {
|
||||
var responseData = {
|
||||
id: messageObj.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: {
|
||||
hasSettingsScreen: true
|
||||
}
|
||||
};
|
||||
setTimeout(function(){
|
||||
window.externalBus(responseData);
|
||||
}, 500);
|
||||
} else if (messageObj.type == "config_screen/show") {
|
||||
HAClient.postMessage('show-settings');
|
||||
}
|
||||
};
|
||||
*/
|
1
docs/empty
Normal file
@ -0,0 +1 @@
|
||||
|
BIN
docs/ha_access_tokens.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
docs/ha_profile-300x247.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/settings-869x1024.png
Normal file
After Width: | Height: | Size: 102 KiB |
41
flutter_01.log
Normal file
@ -0,0 +1,41 @@
|
||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||
|
||||
## command
|
||||
|
||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||
|
||||
## exception
|
||||
|
||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/.packages, isolateId: isolates/68989666}}, details: Isolate must be runnable before this request is made.}}
|
||||
|
||||
```
|
||||
null```
|
||||
|
||||
## flutter doctor
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||
• Engine revision fee001c93f
|
||||
• Dart version 2.4.0
|
||||
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||
• Android SDK at /home/estevez/Android/Sdk
|
||||
• Android NDK location not configured (optional; useful for native profiling support)
|
||||
• Platform android-29, build-tools 29.0.2
|
||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
• All Android licenses accepted.
|
||||
|
||||
[✓] Android Studio (version 3.5)
|
||||
• Android Studio at /home/estevez/bin/android-studio
|
||||
• Flutter plugin version 38.2.3
|
||||
• Dart plugin version 191.8423
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
|
||||
[✓] Connected device (1 available)
|
||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||
|
||||
• No issues found!
|
||||
```
|
41
flutter_02.log
Normal file
@ -0,0 +1,41 @@
|
||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||
|
||||
## command
|
||||
|
||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||
|
||||
## exception
|
||||
|
||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/.packages, isolateId: isolates/289688365}}, details: Isolate must be runnable before this request is made.}}
|
||||
|
||||
```
|
||||
null```
|
||||
|
||||
## flutter doctor
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||
• Engine revision fee001c93f
|
||||
• Dart version 2.4.0
|
||||
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||
• Android SDK at /home/estevez/Android/Sdk
|
||||
• Android NDK location not configured (optional; useful for native profiling support)
|
||||
• Platform android-29, build-tools 29.0.2
|
||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
• All Android licenses accepted.
|
||||
|
||||
[✓] Android Studio (version 3.5)
|
||||
• Android Studio at /home/estevez/bin/android-studio
|
||||
• Flutter plugin version 38.2.3
|
||||
• Dart plugin version 191.8423
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
|
||||
[✓] Connected device (1 available)
|
||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||
|
||||
• No issues found!
|
||||
```
|
41
flutter_03.log
Normal file
@ -0,0 +1,41 @@
|
||||
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||
|
||||
## command
|
||||
|
||||
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||
|
||||
## exception
|
||||
|
||||
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/.packages, isolateId: isolates/866521062}}, details: Isolate must be runnable before this request is made.}}
|
||||
|
||||
```
|
||||
null```
|
||||
|
||||
## flutter doctor
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||
• Engine revision fee001c93f
|
||||
• Dart version 2.4.0
|
||||
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||
• Android SDK at /home/estevez/Android/Sdk
|
||||
• Android NDK location not configured (optional; useful for native profiling support)
|
||||
• Platform android-29, build-tools 29.0.2
|
||||
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
• All Android licenses accepted.
|
||||
|
||||
[✓] Android Studio (version 3.5)
|
||||
• Android Studio at /home/estevez/bin/android-studio
|
||||
• Flutter plugin version 38.2.3
|
||||
• Dart plugin version 191.8423
|
||||
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||
|
||||
[✓] Connected device (1 available)
|
||||
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||
|
||||
• No issues found!
|
||||
```
|
BIN
fonts/materialdesignicons-webfont-5.3.45.ttf
Normal file
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 715 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.3 KiB |
62
lib/cards/alarm_panel_card.dart
Normal file
@ -0,0 +1,62 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class AlarmPanelCard extends StatelessWidget {
|
||||
final AlarmPanelCardData card;
|
||||
|
||||
const AlarmPanelCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeader(
|
||||
name: card.name ?? "",
|
||||
subtitle: Text("${card.entity.entity.displayState}",
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
EntityIcon(
|
||||
size: 50.0,
|
||||
),
|
||||
Container(
|
||||
width: 26.0,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
alignment: Alignment.centerRight,
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: card.entity.entity.entityId))
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
));
|
||||
body.add(
|
||||
AlarmControlPanelControlsWidget(
|
||||
extended: true,
|
||||
states: card.states,
|
||||
)
|
||||
);
|
||||
return CardWrapper(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
handleTap: null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: body
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
196
lib/cards/badges.dart
Normal file
@ -0,0 +1,196 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Badges extends StatelessWidget {
|
||||
final BadgesData badges;
|
||||
|
||||
const Badges({Key key, this.badges}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow();
|
||||
|
||||
if (entitiesToShow.isNotEmpty) {
|
||||
if (AppSettings().scrollBadges) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(height: 112),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: entitiesToShow.map((entity) =>
|
||||
EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||
child: BadgeWidget(),
|
||||
),
|
||||
handleTap: true,
|
||||
)).toList()
|
||||
),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10.0,
|
||||
runSpacing: 5,
|
||||
children: entitiesToShow.map((entity) =>
|
||||
EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: BadgeWidget(),
|
||||
handleTap: true,
|
||||
)).toList(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
class BadgeWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
Widget badgeIcon;
|
||||
String onBadgeTextValue;
|
||||
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||
switch (entityModel.entityWrapper.entity.domain) {
|
||||
case "sun":
|
||||
{
|
||||
IconData iconData;
|
||||
if (entityModel.entityWrapper.entity.state == "below_horizon") {
|
||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc);
|
||||
} else {
|
||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8);
|
||||
}
|
||||
badgeIcon = Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(
|
||||
iconData,
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "camera":
|
||||
case "media_player":
|
||||
case "binary_sensor":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
imagePadding: EdgeInsets.all(0.0),
|
||||
iconPadding: EdgeInsets.all(10),
|
||||
color: Theme.of(context).textTheme.body2.color
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
imagePadding: EdgeInsets.all(0.0),
|
||||
iconPadding: EdgeInsets.all(10),
|
||||
color: Theme.of(context).textTheme.body2.color
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.body1
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||
} else {
|
||||
onBadgeText = Container(
|
||||
constraints: BoxConstraints(maxWidth: 50),
|
||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||
child: Text("$onBadgeTextValue",
|
||||
style: Theme.of(context).textTheme.overline.copyWith(
|
||||
color: HAClientTheme().getOnBadgeTextColor()
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis
|
||||
),
|
||||
decoration: new BoxDecoration(
|
||||
color: iconColor,
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
)
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
overflow: Overflow.visible,
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).cardColor,
|
||||
// The border you want
|
||||
border: Border.all(
|
||||
width: 2.0,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 41,
|
||||
height: 41,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.center,
|
||||
child: badgeIcon,
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
bottom: -6,
|
||||
child: onBadgeText
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 45),
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.displayName}",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
fontSize: 10
|
||||
),
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
onTap: () => entityModel.entityWrapper.handleTap(),
|
||||
onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(),
|
||||
onLongPress: () => entityModel.entityWrapper.handleHold(),
|
||||
);
|
||||
}
|
||||
}
|
730
lib/cards/card.class.dart
Normal file
@ -0,0 +1,730 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class CardData {
|
||||
|
||||
String type;
|
||||
List<EntityWrapper> entities = [];
|
||||
List conditions;
|
||||
bool showEmpty;
|
||||
List stateFilter;
|
||||
bool stateColor = true;
|
||||
|
||||
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
||||
|
||||
factory CardData.parse(rawData) {
|
||||
try {
|
||||
if (rawData['type'] == null) {
|
||||
rawData['type'] = CardType.ENTITIES;
|
||||
} else if (!(rawData['type'] is String)) {
|
||||
return CardData(null);
|
||||
}
|
||||
switch (rawData['type']) {
|
||||
case CardType.ENTITIES:
|
||||
case CardType.HISTORY_GRAPH:
|
||||
case CardType.PICTURE_GLANCE:
|
||||
case CardType.SENSOR:
|
||||
case CardType.ENTITY:
|
||||
case CardType.WEATHER_FORECAST:
|
||||
case CardType.PLANT_STATUS:
|
||||
if (rawData['entity'] != null) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return EntitiesCardData(rawData);
|
||||
break;
|
||||
case CardType.ALARM_PANEL:
|
||||
return AlarmPanelCardData(rawData);
|
||||
break;
|
||||
case CardType.LIGHT:
|
||||
return LightCardData(rawData);
|
||||
break;
|
||||
case CardType.PICTURE_ELEMENTS:
|
||||
//TODO temporary solution
|
||||
if (rawData.containsKey('camera_image')) {
|
||||
rawData['entity'] = rawData['camera_image'];
|
||||
return ButtonCardData(rawData);
|
||||
} else {
|
||||
return CardData(null);
|
||||
}
|
||||
break;
|
||||
case CardType.MAP:
|
||||
return MapCardData(rawData);
|
||||
break;
|
||||
case CardType.ENTITY_BUTTON:
|
||||
case CardType.BUTTON:
|
||||
case CardType.PICTURE_ENTITY:
|
||||
return ButtonCardData(rawData);
|
||||
break;
|
||||
case CardType.CONDITIONAL:
|
||||
return CardData.parse(rawData['card']);
|
||||
break;
|
||||
case CardType.ENTITY_FILTER:
|
||||
Map cardData = Map.from(rawData);
|
||||
cardData.remove('type');
|
||||
if (rawData.containsKey('card')) {
|
||||
cardData.addAll(rawData['card']);
|
||||
}
|
||||
cardData['type'] ??= CardType.ENTITIES;
|
||||
return CardData.parse(cardData);
|
||||
break;
|
||||
case CardType.GAUGE:
|
||||
return GaugeCardData(rawData);
|
||||
break;
|
||||
case CardType.GLANCE:
|
||||
case CardType.THERMOSTAT:
|
||||
if (rawData['entity'] != null) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return GlanceCardData(rawData);
|
||||
break;
|
||||
case CardType.HORIZONTAL_STACK:
|
||||
return HorizontalStackCardData(rawData);
|
||||
break;
|
||||
case CardType.VERTICAL_STACK:
|
||||
return VerticalStackCardData(rawData);
|
||||
break;
|
||||
case CardType.MARKDOWN:
|
||||
return MarkdownCardData(rawData);
|
||||
break;
|
||||
case CardType.MEDIA_CONTROL:
|
||||
return MediaControlCardData(rawData);
|
||||
break;
|
||||
case CardType.BADGES:
|
||||
return BadgesData(rawData);
|
||||
break;
|
||||
default:
|
||||
if (rawData.containsKey('entity')) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
if (rawData.containsKey('entities') && rawData['entities'] is List) {
|
||||
return EntitiesCardData(rawData);
|
||||
}
|
||||
return CardData(null);
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
|
||||
return ErrorCardData(rawData);
|
||||
}
|
||||
}
|
||||
|
||||
CardData(rawData) {
|
||||
if (rawData != null && rawData is Map) {
|
||||
type = rawData['type'];
|
||||
conditions = rawData['conditions'] ?? [];
|
||||
showEmpty = rawData['show_empty'] ?? true;
|
||||
if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) {
|
||||
stateFilter = rawData['state_filter'];
|
||||
} else {
|
||||
stateFilter = [];
|
||||
}
|
||||
} else {
|
||||
type = CardType.UNKNOWN;
|
||||
conditions = [];
|
||||
showEmpty = true;
|
||||
stateFilter = [];
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildCardWidget() {
|
||||
return UnsupportedCard(card: this);
|
||||
}
|
||||
|
||||
List<EntityWrapper> getEntitiesToShow() {
|
||||
return entities.where((entityWrapper) {
|
||||
if (entityWrapper.entity.isHidden) {
|
||||
return false;
|
||||
}
|
||||
List currentStateFilter;
|
||||
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
|
||||
currentStateFilter = entityWrapper.stateFilter;
|
||||
} else {
|
||||
currentStateFilter = stateFilter;
|
||||
}
|
||||
bool showByFilter = currentStateFilter.isEmpty;
|
||||
for (var allowedState in currentStateFilter) {
|
||||
if (allowedState is String && allowedState == entityWrapper.entity.state) {
|
||||
showByFilter = true;
|
||||
break;
|
||||
} else if (allowedState is Map) {
|
||||
try {
|
||||
var tmpVal = allowedState['attribute'] != null ? entityWrapper.entity.getAttribute(allowedState['attribute']) : entityWrapper.entity.state;
|
||||
var valToCompareWith = allowedState['value'];
|
||||
var valToCompare;
|
||||
if (valToCompareWith is! String && tmpVal is String) {
|
||||
valToCompare = double.tryParse(tmpVal);
|
||||
} else {
|
||||
valToCompare = tmpVal;
|
||||
}
|
||||
if (valToCompare != null) {
|
||||
bool result;
|
||||
switch (allowedState['operator']) {
|
||||
case '<=': { result = valToCompare <= valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '<': { result = valToCompare < valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '>=': { result = valToCompare >= valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '>': { result = valToCompare > valToCompareWith;}
|
||||
break;
|
||||
|
||||
case '!=': { result = valToCompare != valToCompareWith;}
|
||||
break;
|
||||
|
||||
case 'regex': {
|
||||
RegExp regExp = RegExp(valToCompareWith.toString());
|
||||
result = regExp.hasMatch(valToCompare.toString());
|
||||
}
|
||||
break;
|
||||
|
||||
default: {
|
||||
result = valToCompare == valToCompareWith;
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
showByFilter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState: $e', stacktrace: stacktrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
return showByFilter;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class BadgesData extends CardData {
|
||||
|
||||
String title;
|
||||
String icon;
|
||||
bool showHeaderToggle;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return Badges(badges: this);
|
||||
}
|
||||
|
||||
BadgesData(rawData) : super(rawData) {
|
||||
if (rawData['badges'] is List) {
|
||||
rawData['badges'].forEach((dynamic rawBadge) {
|
||||
if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge)));
|
||||
} else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) {
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawBadge['entity']),
|
||||
overrideName: rawBadge["name"]?.toString(),
|
||||
overrideIcon: rawBadge["icon"],
|
||||
)
|
||||
);
|
||||
} else if (rawBadge is Map && rawBadge.containsKey('entities')) {
|
||||
_parseEntities(rawBadge);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parseEntities(rawData) {
|
||||
var rawEntities = rawData['entities'] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawEntity),
|
||||
stateFilter: rawData['state_filter'] ?? [],
|
||||
));
|
||||
}
|
||||
} else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntitiesCardData extends CardData {
|
||||
|
||||
String title;
|
||||
String icon;
|
||||
bool showHeaderToggle;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return EntitiesCard(card: this);
|
||||
}
|
||||
|
||||
EntitiesCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData['title']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
stateColor = rawData['state_color'] ?? false;
|
||||
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
||||
//Parsing entities
|
||||
var rawEntities = rawData['entities'] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (rawEntity["type"] == "divider") {
|
||||
entities.add(EntityWrapper(entity: Entity.divider()));
|
||||
} else if (rawEntity["type"] == "section") {
|
||||
entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||
} else if (rawEntity["type"] == "call-service") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.callService,
|
||||
"service": rawEntity["service"],
|
||||
"service_data": rawEntity["service_data"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: Entity.callService(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"]?.toString(),
|
||||
service: rawEntity["service"],
|
||||
actionName: rawEntity["action_name"]
|
||||
),
|
||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (rawEntity["type"] == "weblink") {
|
||||
Map uiActionData = {
|
||||
"tap_action": {
|
||||
"action": EntityUIAction.navigate,
|
||||
"service": rawEntity["url"]
|
||||
},
|
||||
"hold_action": EntityUIAction.none
|
||||
};
|
||||
entities.add(EntityWrapper(
|
||||
entity: Entity.weblink(
|
||||
icon: rawEntity["icon"],
|
||||
name: rawEntity["name"]?.toString(),
|
||||
url: rawEntity["url"]
|
||||
),
|
||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||
)
|
||||
);
|
||||
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AlarmPanelCardData extends CardData {
|
||||
|
||||
String name;
|
||||
List<dynamic> states;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return AlarmPanelCard(card: this);
|
||||
}
|
||||
|
||||
AlarmPanelCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name']?.toString();
|
||||
states = rawData['states'];
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
stateColor: true,
|
||||
overrideName: name
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LightCardData extends CardData {
|
||||
|
||||
String name;
|
||||
String icon;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
if (this.entity != null && this.entity.entity is LightEntity) {
|
||||
return LightCard(card: this);
|
||||
}
|
||||
return ErrorCard(
|
||||
errorText: 'Specify an entity from within the light domain.',
|
||||
showReportButton: false,
|
||||
);
|
||||
}
|
||||
|
||||
LightCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
overrideName: name,
|
||||
overrideIcon: icon,
|
||||
uiAction: EntityUIAction()..tapAction = EntityUIAction.toggle
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonCardData extends CardData {
|
||||
|
||||
String name;
|
||||
String icon;
|
||||
bool showName;
|
||||
bool showIcon;
|
||||
double iconHeightPx = 0;
|
||||
double iconHeightRem = 0;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return EntityButtonCard(card: this);
|
||||
}
|
||||
|
||||
ButtonCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name']?.toString();
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
showName = rawData['show_name'] ?? true;
|
||||
showIcon = rawData['show_icon'] ?? true;
|
||||
stateColor = rawData['state_color'] ?? true;
|
||||
var rawHeight = rawData['icon_height'];
|
||||
if (rawHeight != null && rawHeight is String) {
|
||||
if (rawHeight.contains('px')) {
|
||||
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
|
||||
} else if (rawHeight.contains('rem')) {
|
||||
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
|
||||
} else if (rawHeight.contains('em')) {
|
||||
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
|
||||
}
|
||||
}
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
overrideName: name,
|
||||
overrideIcon: icon,
|
||||
stateColor: stateColor,
|
||||
uiAction: EntityUIAction(
|
||||
rawEntityData: rawData
|
||||
)
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
} else if (entitiId == null) {
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: Entity.ghost(
|
||||
name,
|
||||
icon,
|
||||
),
|
||||
stateColor: stateColor,
|
||||
uiAction: EntityUIAction(
|
||||
rawEntityData: rawData
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GaugeCardData extends CardData {
|
||||
|
||||
String name;
|
||||
String unit;
|
||||
double min;
|
||||
double max;
|
||||
Map severity;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return GaugeCard(card: this);
|
||||
}
|
||||
|
||||
GaugeCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name']?.toString();
|
||||
unit = rawData['unit'];
|
||||
if (rawData['min'] is int) {
|
||||
min = rawData['min'].toDouble();
|
||||
} else if (rawData['min'] is double) {
|
||||
min = rawData['min'];
|
||||
} else {
|
||||
min = 0;
|
||||
}
|
||||
if (rawData['max'] is int) {
|
||||
max = rawData['max'].toDouble();
|
||||
} else if (rawData['max'] is double) {
|
||||
max = rawData['max'];
|
||||
} else {
|
||||
max = 100;
|
||||
}
|
||||
severity = rawData['severity'];
|
||||
//Parsing entity
|
||||
var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
overrideName: name
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GlanceCardData extends CardData {
|
||||
|
||||
String title;
|
||||
bool showName;
|
||||
bool showIcon;
|
||||
bool showState;
|
||||
bool stateColor;
|
||||
int columnsCount;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return GlanceCard(card: this);
|
||||
}
|
||||
|
||||
GlanceCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData["title"]?.toString();
|
||||
showName = rawData['show_name'] ?? true;
|
||||
showIcon = rawData['show_icon'] ?? true;
|
||||
showState = rawData['show_state'] ?? true;
|
||||
stateColor = rawData['state_color'] ?? true;
|
||||
columnsCount = rawData['columns'] ?? 4;
|
||||
//Parsing entities
|
||||
var rawEntities = rawData["entities"] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
stateColor: stateColor,
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class HorizontalStackCardData extends CardData {
|
||||
|
||||
List<CardData> childCards;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return HorizontalStackCard(card: this);
|
||||
}
|
||||
|
||||
HorizontalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
}).toList();
|
||||
} else {
|
||||
childCards = [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class VerticalStackCardData extends CardData {
|
||||
|
||||
List<CardData> childCards;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return VerticalStackCard(card: this);
|
||||
}
|
||||
|
||||
VerticalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
}).toList();
|
||||
} else {
|
||||
childCards = [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MarkdownCardData extends CardData {
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return MarkdownCard(card: this);
|
||||
}
|
||||
|
||||
MarkdownCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData['title'];
|
||||
content = rawData['content'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MapCardData extends CardData {
|
||||
|
||||
String title;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return MapCard(card: this);
|
||||
}
|
||||
|
||||
MapCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData['title'];
|
||||
List<dynamic> geoLocationSources = rawData['geo_location_sources'] ?? [];
|
||||
if (geoLocationSources.isNotEmpty) {
|
||||
//TODO add entities by source
|
||||
}
|
||||
var rawEntities = rawData["entities"] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||
}
|
||||
} else {
|
||||
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
stateColor: stateColor,
|
||||
overrideName: rawEntity["name"]?.toString(),
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? [],
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MediaControlCardData extends CardData {
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return MediaControlsCard(card: this);
|
||||
}
|
||||
|
||||
MediaControlCardData(rawData) : super(rawData) {
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(entitiId),
|
||||
));
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ErrorCardData extends CardData {
|
||||
|
||||
String cardConfig;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return ErrorCard(card: this);
|
||||
}
|
||||
|
||||
ErrorCardData(rawData) : super(rawData) {
|
||||
cardConfig = '$rawData';
|
||||
}
|
||||
|
||||
}
|
79
lib/cards/entities_card.dart
Normal file
@ -0,0 +1,79 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class EntitiesCard extends StatelessWidget {
|
||||
final EntitiesCardData card;
|
||||
|
||||
const EntitiesCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
Widget headerSwitch;
|
||||
if (card.showHeaderToggle) {
|
||||
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
|
||||
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
|
||||
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
|
||||
}).map((EntityWrapper en) {
|
||||
return en.entity.entityId;
|
||||
}).toList();
|
||||
headerSwitch = Switch(
|
||||
value: headerToggleVal,
|
||||
onChanged: (val) {
|
||||
if (entitiesToToggle.isNotEmpty) {
|
||||
ConnectionManager().callService(
|
||||
domain: "homeassistant",
|
||||
service: val ? "turn_on" : "turn_off",
|
||||
entityId: entitiesToToggle
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
body.add(
|
||||
CardHeader(
|
||||
name: card.title,
|
||||
trailing: headerSwitch,
|
||||
emptyPadding: Sizes.rowPadding,
|
||||
leading: card.icon != null ? Icon(
|
||||
MaterialDesignIcons.getIconDataFromIconName(card.icon),
|
||||
size: Sizes.iconSize,
|
||||
color: Theme.of(context).textTheme.headline.color
|
||||
) : null,
|
||||
)
|
||||
);
|
||||
body.addAll(
|
||||
entitiesToShow.map((EntityWrapper entity) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
handleTap: true,
|
||||
child: entity.entity.buildDefaultWidget(context)
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
return CardWrapper(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: Sizes.rightWidgetPadding,
|
||||
left: Sizes.leftWidgetPadding,
|
||||
bottom: Sizes.rowPadding,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: body
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
96
lib/cards/entity_button_card.dart
Normal file
@ -0,0 +1,96 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class EntityButtonCard extends StatelessWidget {
|
||||
|
||||
final ButtonCardData card;
|
||||
|
||||
EntityButtonCard({
|
||||
Key key, this.card
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = card.entity;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
|
||||
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
|
||||
|
||||
Widget buttonIcon;
|
||||
if (!card.showIcon) {
|
||||
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
|
||||
} else if (iconSize > 0) {
|
||||
buttonIcon = SizedBox(
|
||||
height: iconSize,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: EntityIcon(
|
||||
//padding: EdgeInsets.only(top: 6),
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
} else {
|
||||
buttonIcon = AspectRatio(
|
||||
aspectRatio: 2,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
child: EntityIcon(
|
||||
//padding: EdgeInsets.only(top: 6),
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return CardWrapper(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buttonIcon,
|
||||
_buildName(context)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName(BuildContext context) {
|
||||
if (card.showName) {
|
||||
return EntityName(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
textStyle: Theme.of(context).textTheme.subhead,
|
||||
wordsWrap: true,
|
||||
textAlign: TextAlign.center
|
||||
);
|
||||
}
|
||||
return Container(width: 0, height: 0);
|
||||
}
|
||||
}
|
51
lib/cards/error_card.dart
Normal file
@ -0,0 +1,51 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ErrorCard extends StatelessWidget {
|
||||
final ErrorCardData card;
|
||||
final String errorText;
|
||||
final bool showReportButton;
|
||||
|
||||
const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String error;
|
||||
if (errorText == null) {
|
||||
error = 'There was an error showing ${card?.type}';
|
||||
} else {
|
||||
error = errorText;
|
||||
}
|
||||
return CardWrapper(
|
||||
color: Theme.of(context).errorColor,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
error,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
card != null ?
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
||||
},
|
||||
child: Text('Copy card config'),
|
||||
) :
|
||||
Container(width: 0, height: 0),
|
||||
showReportButton ?
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
||||
},
|
||||
child: Text('Report issue'),
|
||||
) :
|
||||
Container(width: 0, height: 0)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
201
lib/cards/gauge_card.dart
Normal file
@ -0,0 +1,201 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class GaugeCard extends StatelessWidget {
|
||||
|
||||
final GaugeCardData card;
|
||||
|
||||
GaugeCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = card.entity;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
entityWrapper.overrideName = card.name ??
|
||||
entityWrapper.displayName;
|
||||
entityWrapper.unitOfMeasurementOverride = card.unit ??
|
||||
entityWrapper.unitOfMeasurement;
|
||||
double fixedValue;
|
||||
double value = entityWrapper.entity.doubleState;
|
||||
if (value > card.max) {
|
||||
fixedValue = card.max.toDouble();
|
||||
} else if (value < card.min) {
|
||||
fixedValue = card.min.toDouble();
|
||||
} else {
|
||||
fixedValue = value;
|
||||
}
|
||||
|
||||
List<GaugeRange> ranges;
|
||||
Color currentColor;
|
||||
if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) {
|
||||
List<RangeContainer> rangesList = <RangeContainer>[
|
||||
RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()),
|
||||
RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()),
|
||||
RangeContainer(card.severity["yellow"], HAClientTheme().getYellowGaugeColor())
|
||||
];
|
||||
rangesList.sort((current, next) {
|
||||
if (current.startFrom > next.startFrom) {
|
||||
return 1;
|
||||
}
|
||||
if (current.startFrom < next.startFrom) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (fixedValue < rangesList[1].startFrom) {
|
||||
currentColor = rangesList[0].color;
|
||||
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
|
||||
currentColor = rangesList[1].color;
|
||||
} else {
|
||||
currentColor = rangesList[2].color;
|
||||
}
|
||||
|
||||
ranges = [
|
||||
GaugeRange(
|
||||
startValue: rangesList[0].startFrom.toDouble(),
|
||||
endValue: rangesList[1].startFrom.toDouble(),
|
||||
color: rangesList[0].color.withOpacity(0.1),
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
endWidth: 0.3,
|
||||
startWidth: 0.3
|
||||
),
|
||||
GaugeRange(
|
||||
startValue: rangesList[1].startFrom.toDouble(),
|
||||
endValue: rangesList[2].startFrom.toDouble(),
|
||||
color: rangesList[1].color.withOpacity(0.1),
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
endWidth: 0.3,
|
||||
startWidth: 0.3
|
||||
),
|
||||
GaugeRange(
|
||||
startValue: rangesList[2].startFrom.toDouble(),
|
||||
endValue: card.max.toDouble(),
|
||||
color: rangesList[2].color.withOpacity(0.1),
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
endWidth: 0.3,
|
||||
startWidth: 0.3
|
||||
)
|
||||
];
|
||||
}
|
||||
if (ranges == null) {
|
||||
currentColor = Theme.of(context).primaryColorDark;
|
||||
ranges = <GaugeRange>[
|
||||
GaugeRange(
|
||||
startValue: card.min.toDouble(),
|
||||
endValue: card.max.toDouble(),
|
||||
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
endWidth: 0.3,
|
||||
startWidth: 0.3,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return CardWrapper(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: EntityModel(
|
||||
entityWrapper: entityWrapper,
|
||||
child: InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.8,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: <Widget>[
|
||||
IgnorePointer(
|
||||
ignoring: true,
|
||||
child: SfRadialGauge(
|
||||
axes: <RadialAxis>[
|
||||
RadialAxis(
|
||||
maximum: card.max.toDouble(),
|
||||
minimum: card.min.toDouble(),
|
||||
showLabels: false,
|
||||
useRangeColorForAxis: true,
|
||||
showTicks: false,
|
||||
canScaleToFit: true,
|
||||
ranges: ranges,
|
||||
axisLineStyle: AxisLineStyle(
|
||||
thickness: 0.3,
|
||||
thicknessUnit: GaugeSizeUnit.factor,
|
||||
color: Colors.transparent
|
||||
),
|
||||
startAngle: 180,
|
||||
endAngle: 0,
|
||||
pointers: <GaugePointer>[
|
||||
RangePointer(
|
||||
value: fixedValue,
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
width: 0.3,
|
||||
color: currentColor,
|
||||
enableAnimation: true,
|
||||
animationType: AnimationType.bounceOut,
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
flex: 8,
|
||||
fit: FlexFit.tight,
|
||||
child: Container()
|
||||
),
|
||||
Flexible(
|
||||
flex: 6,
|
||||
fit: FlexFit.tight,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SimpleEntityState(
|
||||
padding: EdgeInsets.all(0),
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
fit: FlexFit.tight,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: EntityName(
|
||||
padding: EdgeInsets.all(0),
|
||||
textStyle: Theme.of(context).textTheme.subhead
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RangeContainer {
|
||||
final int startFrom;
|
||||
Color color;
|
||||
|
||||
RangeContainer(this.startFrom, this.color);
|
||||
}
|
126
lib/cards/glance_card.dart
Normal file
@ -0,0 +1,126 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class GlanceCard extends StatelessWidget {
|
||||
final GlanceCardData card;
|
||||
|
||||
const GlanceCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
|
||||
if (entitiesToShow.isEmpty && !card.showEmpty) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
int length = entitiesToShow.length;
|
||||
int rowsCount;
|
||||
int columnsCount;
|
||||
if (length == 0) {
|
||||
columnsCount = 0;
|
||||
rowsCount = 0;
|
||||
} else {
|
||||
columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||
rowsCount = (length / columnsCount).round();
|
||||
}
|
||||
List<TableRow> rows = [];
|
||||
for (int i = 0; i < rowsCount; i++) {
|
||||
int start = i*columnsCount;
|
||||
int end = start + math.min(columnsCount, length - start);
|
||||
List<Widget> rowChildren = [];
|
||||
rowChildren.addAll(entitiesToShow.sublist(
|
||||
start, end
|
||||
).map(
|
||||
(EntityWrapper entity){
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: _buildEntityContainer(context, entity),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
).toList()
|
||||
);
|
||||
while (rowChildren.length < columnsCount) {
|
||||
rowChildren.add(
|
||||
Container()
|
||||
);
|
||||
}
|
||||
rows.add(
|
||||
TableRow(
|
||||
children: rowChildren
|
||||
)
|
||||
);
|
||||
}
|
||||
return CardWrapper(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CardHeader(
|
||||
name: card.title,
|
||||
emptyPadding: Sizes.rowPadding,
|
||||
),
|
||||
Table(
|
||||
children: rows
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) {
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return MissedEntityWidget();
|
||||
} else if (entityWrapper.entity.statelessType != StatelessEntityType.none) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
List<Widget> result = [];
|
||||
if (card.showName) {
|
||||
result.add(_buildName(context));
|
||||
}
|
||||
result.add(
|
||||
EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: Sizes.iconSize,
|
||||
)
|
||||
);
|
||||
if (card.showState) {
|
||||
result.add(_buildState());
|
||||
}
|
||||
|
||||
return InkResponse(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: result,
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName(BuildContext context) {
|
||||
return EntityName(
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
wordsWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
textStyle: Theme.of(context).textTheme.body1,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildState() {
|
||||
return SimpleEntityState(
|
||||
textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
30
lib/cards/horizontal_srack_card.dart
Normal file
@ -0,0 +1,30 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class HorizontalStackCard extends StatelessWidget {
|
||||
final HorizontalStackCardData card;
|
||||
|
||||
const HorizontalStackCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
children = card.childCards.map((childCard) => Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: childCard.buildCardWidget()
|
||||
)
|
||||
).toList();
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
|
||||
}
|
161
lib/cards/light_card.dart
Normal file
@ -0,0 +1,161 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class LightCard extends StatefulWidget {
|
||||
|
||||
final LightCardData card;
|
||||
|
||||
LightCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _LightCardState();
|
||||
}
|
||||
}
|
||||
|
||||
class _LightCardState extends State<LightCard> {
|
||||
|
||||
double _actualBrightness;
|
||||
double _newBrightness;
|
||||
bool _changedHere = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setBrightness(double value, LightEntity entity) {
|
||||
setState((){
|
||||
_newBrightness = value;
|
||||
_changedHere = true;
|
||||
});
|
||||
ConnectionManager().callService(
|
||||
domain: entity.domain,
|
||||
service: "turn_on",
|
||||
entityId: entity.entityId,
|
||||
data: {"brightness": value.round()}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = widget.card.entity;
|
||||
LightEntity entity = entityWrapper.entity;
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: widget.card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
entityWrapper.overrideName = widget.card.name ??
|
||||
entityWrapper.displayName;
|
||||
entityWrapper.overrideIcon = widget.card.icon ??
|
||||
entityWrapper.icon;
|
||||
|
||||
if (!_changedHere) {
|
||||
_actualBrightness = (entity.brightness ?? 0).toDouble();
|
||||
_newBrightness = _actualBrightness;
|
||||
} else {
|
||||
_changedHere = false;
|
||||
}
|
||||
Color lightColor = entity.color?.toColor();
|
||||
Color color;
|
||||
if (lightColor != null && lightColor != Colors.white) {
|
||||
color = lightColor;
|
||||
} else {
|
||||
color = Theme.of(context).accentColor;
|
||||
}
|
||||
|
||||
return CardWrapper(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: EntityModel(
|
||||
entityWrapper: entityWrapper,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(Size(200, 200)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
SfRadialGauge(
|
||||
axes: <RadialAxis>[
|
||||
RadialAxis(
|
||||
onAxisTapped: (val) {
|
||||
_setBrightness(val, entity);
|
||||
},
|
||||
maximum: 255,
|
||||
minimum: 0,
|
||||
showLabels: false,
|
||||
showTicks: false,
|
||||
axisLineStyle: AxisLineStyle(
|
||||
thickness: 0.05,
|
||||
thicknessUnit: GaugeSizeUnit.factor,
|
||||
color: HAClientTheme().getDisabledStateColor(context)
|
||||
),
|
||||
pointers: <GaugePointer>[
|
||||
RangePointer(
|
||||
value: _actualBrightness,
|
||||
sizeUnit: GaugeSizeUnit.factor,
|
||||
width: 0.05,
|
||||
color: color,
|
||||
enableAnimation: true,
|
||||
animationType: AnimationType.bounceOut,
|
||||
),
|
||||
MarkerPointer(
|
||||
value: _newBrightness,
|
||||
markerType: MarkerType.circle,
|
||||
markerHeight: 20,
|
||||
markerWidth: 20,
|
||||
enableDragging: true,
|
||||
onValueChangeEnd: (val) {
|
||||
_setBrightness(val, entity);
|
||||
},
|
||||
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
||||
//enableAnimation: true,
|
||||
//animationType: AnimationType.bounceOut,
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
FractionallySizedBox(
|
||||
heightFactor: 0.4,
|
||||
widthFactor: 0.4,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: InkResponse(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: EntityIcon(
|
||||
showBadge: false,
|
||||
padding: EdgeInsets.all(0)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
EntityName(
|
||||
padding: EdgeInsets.all(0),
|
||||
wordsWrap: true,
|
||||
maxLines: 3,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
88
lib/cards/map_card.dart
Normal file
@ -0,0 +1,88 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MapCard extends StatefulWidget {
|
||||
final MapCardData card;
|
||||
|
||||
const MapCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MapCardState createState() => _MapCardState();
|
||||
}
|
||||
|
||||
class _MapCardState extends State<MapCard> {
|
||||
|
||||
void _openMap(BuildContext context) {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (bc) {
|
||||
return Scaffold(
|
||||
primary: false,
|
||||
/*appBar: new AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.fullscreen),
|
||||
onPressed: () {},
|
||||
)
|
||||
],
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: new Text("${widget.card.title ?? ""}"),
|
||||
),*/
|
||||
body: Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
EntitiesMap(
|
||||
entities: widget.card.entities,
|
||||
interactive: true
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
})
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardWrapper(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
CardHeader(name: widget.card.title),
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () => _openMap(context),
|
||||
child: EntitiesMap(
|
||||
aspectRatio: 1,
|
||||
interactive: false,
|
||||
entities: widget.card.entities,
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
child: Text('Tap to open interactive map', style: Theme.of(context).textTheme.caption)
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
33
lib/cards/markdown_card.dart
Normal file
@ -0,0 +1,33 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MarkdownCard extends StatelessWidget {
|
||||
final MarkdownCardData card;
|
||||
|
||||
const MarkdownCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.content == null) {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
} else if (card.content == '***') {
|
||||
return Container(height: Sizes.rowPadding, width: 0.0,);
|
||||
}
|
||||
return CardWrapper(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
CardHeader(name: card.title),
|
||||
MarkdownBody(
|
||||
data: card.content,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
35
lib/cards/media_control_card.dart
Normal file
@ -0,0 +1,35 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class MediaControlsCard extends StatelessWidget {
|
||||
final MediaControlCardData card;
|
||||
|
||||
const MediaControlsCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: MissedEntityWidget(),
|
||||
handleTap: false,
|
||||
);
|
||||
} else if (card.entity.entity.domain == null || card.entity.entity.domain != 'media_player') {
|
||||
return EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
child: ErrorEntityWidget(
|
||||
text: '${card.entity.entity?.entityId} is not a media_player',
|
||||
),
|
||||
handleTap: false,
|
||||
);
|
||||
}
|
||||
return CardWrapper(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.entity,
|
||||
handleTap: null,
|
||||
child: MediaPlayerWidget()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
12
lib/cards/unsupported_card.dart
Normal file
@ -0,0 +1,12 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class UnsupportedCard extends StatelessWidget {
|
||||
final CardData card;
|
||||
|
||||
const UnsupportedCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(height: 20);
|
||||
}
|
||||
}
|
23
lib/cards/vertical_stack_card.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class VerticalStackCard extends StatelessWidget {
|
||||
final VerticalStackCardData card;
|
||||
|
||||
const VerticalStackCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: card.childCards.map<Widget>(
|
||||
(childCard) => childCard.buildCardWidget()
|
||||
).toList(),
|
||||
);
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
|
||||
}
|
32
lib/cards/widgets/card_header.widget.dart
Normal file
@ -0,0 +1,32 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class CardHeader extends StatelessWidget {
|
||||
|
||||
final String name;
|
||||
final Widget trailing;
|
||||
final Widget leading;
|
||||
final Widget subtitle;
|
||||
final double emptyPadding;
|
||||
|
||||
const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var result;
|
||||
if ((name != null) && (name.trim().length > 0)) {
|
||||
result = new ListTile(
|
||||
trailing: trailing,
|
||||
leading: leading,
|
||||
subtitle: subtitle,
|
||||
title: Text("$name",
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.headline),
|
||||
);
|
||||
} else {
|
||||
result = new Container(width: 0.0, height: emptyPadding);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
23
lib/cards/widgets/card_wrapper.widget.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class CardWrapper extends StatelessWidget {
|
||||
|
||||
final Widget child;
|
||||
final EdgeInsets padding;
|
||||
final Color color;
|
||||
|
||||
const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: color,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: child
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
73
lib/cards/widgets/entities_map.dart
Normal file
@ -0,0 +1,73 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
|
||||
class EntitiesMap extends StatelessWidget {
|
||||
|
||||
final List<EntityWrapper> entities;
|
||||
final bool interactive;
|
||||
final double aspectRatio;
|
||||
final LatLng center;
|
||||
final double zoom;
|
||||
|
||||
const EntitiesMap({Key key, this.entities: const [], this.aspectRatio, this.interactive: true, this.center, this.zoom}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Marker> markers = [];
|
||||
List<LatLng> points = [];
|
||||
entities.forEach((entityWrapper) {
|
||||
double lat = entityWrapper.entity._getDoubleAttributeValue("latitude");
|
||||
double long = entityWrapper.entity._getDoubleAttributeValue("longitude");
|
||||
if (lat != null && long != null) {
|
||||
points.add(LatLng(lat, long));
|
||||
markers.add(
|
||||
Marker(
|
||||
width: 36,
|
||||
height: 36,
|
||||
point: LatLng(lat, long),
|
||||
builder: (ctx) => EntityModel(
|
||||
handleTap: true,
|
||||
entityWrapper: entityWrapper,
|
||||
child: EntityIcon(
|
||||
size: 36,
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
MapOptions mapOptions;
|
||||
if (center != null) {
|
||||
mapOptions = MapOptions(
|
||||
interactive: interactive,
|
||||
center: center,
|
||||
zoom: zoom ?? 10,
|
||||
);
|
||||
} else {
|
||||
mapOptions = MapOptions(
|
||||
interactive: interactive,
|
||||
bounds: LatLngBounds.fromPoints(points),
|
||||
boundsOptions: FitBoundsOptions(padding: EdgeInsets.all(40)),
|
||||
);
|
||||
}
|
||||
Widget map = FlutterMap(
|
||||
options: mapOptions,
|
||||
layers: [
|
||||
new TileLayerOptions(
|
||||
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
subdomains: ['a', 'b', 'c']
|
||||
),
|
||||
new MarkerLayerOptions(
|
||||
markers: markers,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (aspectRatio != null) {
|
||||
return AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: map
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|