Compare commits
	
		
			960 Commits
		
	
	
		
			v0.1.0-alp
			...
			foreground
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c844e21e76 | ||
|  | 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.  | ||||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -9,5 +9,15 @@ build/ | |||||||
| .flutter-plugins | .flutter-plugins | ||||||
|  |  | ||||||
| .idea/ | .idea/ | ||||||
|  | .vscode/ | ||||||
|  | .theia/ | ||||||
|  | .project/ | ||||||
|  | .settings/ | ||||||
|  |  | ||||||
|  | flutter_export_environment.sh | ||||||
|  | .flutter-plugins-dependencies | ||||||
|  |  | ||||||
| key.properties | 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. | ||||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,3 +1,18 @@ | |||||||
| # Android client for Home Assistant | # HA Client | ||||||
|  | ## Native Android client for Home Assistant | ||||||
|  | ### With notifications 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/apps/testing/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)  | ||||||
|  |  | ||||||
|  | #### Stable CI build | ||||||
|  | [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/latest_build) | ||||||
|  | #### Beta CI build | ||||||
|  | [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) | ||||||
|  | #### Pre-release CI build | ||||||
|  | [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								android/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -8,3 +8,4 @@ | |||||||
| /build | /build | ||||||
| /captures | /captures | ||||||
| GeneratedPluginRegistrant.java | 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)) | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) | ||||||
|  |  | ||||||
| android { | android { | ||||||
|     compileSdkVersion 27 |     compileSdkVersion 28 | ||||||
|  |  | ||||||
|  |     compileOptions { | ||||||
|  |         sourceCompatibility JavaVersion.VERSION_1_8 | ||||||
|  |         targetCompatibility JavaVersion.VERSION_1_8 | ||||||
|  |     } | ||||||
|  |  | ||||||
|     lintOptions { |     lintOptions { | ||||||
|         disable 'InvalidPackage' |         disable 'InvalidPackage' | ||||||
| @@ -38,13 +43,21 @@ android { | |||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         applicationId "com.keyboardcrumbs.haclient" |         applicationId "com.keyboardcrumbs.haclient" | ||||||
|         minSdkVersion 21 |         minSdkVersion 21 | ||||||
|         targetSdkVersion 27 |         targetSdkVersion 28 | ||||||
|         versionCode 18 |         versionCode flutterVersionCode.toInteger() | ||||||
|         versionName "0.1.0-alpha" |         versionName flutterVersionName | ||||||
|         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" |         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     signingConfigs { |     signingConfigs { | ||||||
|  |         if (!System.getenv()["CI"]) { | ||||||
|  |             debug { | ||||||
|  |                 keyAlias keystoreProperties['debugKeyAlias'] | ||||||
|  |                 keyPassword keystoreProperties['debugKeyPassword'] | ||||||
|  |                 storeFile file(keystoreProperties['debugStoreFile']) | ||||||
|  |                 storePassword keystoreProperties['debugStorePassword'] | ||||||
|  |             } | ||||||
|  |         } | ||||||
|         release { |         release { | ||||||
|             keyAlias keystoreProperties['keyAlias'] |             keyAlias keystoreProperties['keyAlias'] | ||||||
|             keyPassword keystoreProperties['keyPassword'] |             keyPassword keystoreProperties['keyPassword'] | ||||||
| @@ -65,7 +78,11 @@ flutter { | |||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|  |     implementation 'com.google.firebase:firebase-analytics:17.2.2' | ||||||
|     testImplementation 'junit:junit:4.12' |     testImplementation 'junit:junit:4.12' | ||||||
|     androidTestImplementation 'com.android.support.test:runner:1.0.2' |     androidTestImplementation 'com.android.support.test:runner:1.0.2' | ||||||
|     androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.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,19 @@ | |||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     package="com.keyboardcrumbs.hassclient"> |     package="com.keyboardcrumbs.hassclient"> | ||||||
|  |  | ||||||
|     <!-- The INTERNET permission is required for development. Specifically, |     <uses-feature android:name="android.hardware.touchscreen" | ||||||
|          flutter needs it to communicate with the running application |         android:required="false" /> | ||||||
|          to allow setting breakpoints, to provide hot reload, etc. |  | ||||||
|     --> |  | ||||||
|     <uses-permission android:name="android.permission.INTERNET"/> |     <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.ACCESS_BACKGROUND_LOCATION" /> | ||||||
|  |     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||||
|  |     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> | ||||||
|  |     <!-- | ||||||
|  |     <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 |     <!-- io.flutter.app.FlutterApplication is an android.app.Application that | ||||||
|          calls FlutterMain.startInitialization(this); in its onCreate method. |          calls FlutterMain.startInitialization(this); in its onCreate method. | ||||||
| @@ -15,7 +23,18 @@ | |||||||
|     <application |     <application | ||||||
|         android:name="io.flutter.app.FlutterApplication" |         android:name="io.flutter.app.FlutterApplication" | ||||||
|         android:label="HA Client" |         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 |         <activity | ||||||
|             android:name=".MainActivity" |             android:name=".MainActivity" | ||||||
|             android:launchMode="singleTop" |             android:launchMode="singleTop" | ||||||
| @@ -23,17 +42,47 @@ | |||||||
|             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" |             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" | ||||||
|             android:hardwareAccelerated="true" |             android:hardwareAccelerated="true" | ||||||
|             android:windowSoftInputMode="adjustResize"> |             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 |             <meta-data | ||||||
|                 android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" |                 android:name="io.flutter.embedding.android.SplashScreenDrawable" | ||||||
|                 android:value="true" /> |                 android:resource="@drawable/launch_background" /> | ||||||
|  |             <meta-data | ||||||
|  |                 android:name="io.flutter.embedding.android.NormalTheme" | ||||||
|  |                 android:resource="@style/NormalTheme" /> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="FLUTTER_NOTIFICATION_CLICK" /> | ||||||
|  |                 <category android:name="android.intent.category.DEFAULT" /> | ||||||
|  |             </intent-filter> | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|                 <action android:name="android.intent.action.MAIN"/> |                 <action android:name="android.intent.action.MAIN"/> | ||||||
|                 <category android:name="android.intent.category.LAUNCHER"/> |                 <category android:name="android.intent.category.LAUNCHER"/> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </activity> |         </activity> | ||||||
|  |  | ||||||
|  |         <receiver android:name="rekab.app.background_locator.LocatorBroadcastReceiver" | ||||||
|  |             android:enabled="true" | ||||||
|  |             android:exported="true"/> | ||||||
|  |         <service android:name="rekab.app.background_locator.LocatorService" | ||||||
|  |             android:permission="android.permission.BIND_JOB_SERVICE" | ||||||
|  |             android:exported="true"/> | ||||||
|  |         <service android:name="rekab.app.background_locator.IsolateHolderService" | ||||||
|  |             android:permission="android.permission.FOREGROUND_SERVICE" | ||||||
|  |             android:exported="true"/> | ||||||
|  |  | ||||||
|  |         <!--  | ||||||
|  |         <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"></action> | ||||||
|  |             </intent-filter> | ||||||
|  |         </receiver> | ||||||
|  |     --> | ||||||
|     </application> |     </application> | ||||||
| </manifest> | </manifest> | ||||||
| @@ -1,13 +1,15 @@ | |||||||
| package com.keyboardcrumbs.hassclient; | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
| import android.os.Bundle; | import androidx.annotation.NonNull; | ||||||
| import io.flutter.app.FlutterActivity; | import io.flutter.embedding.android.FlutterActivity; | ||||||
|  | import io.flutter.embedding.engine.FlutterEngine; | ||||||
| import io.flutter.plugins.GeneratedPluginRegistrant; | import io.flutter.plugins.GeneratedPluginRegistrant; | ||||||
|  |  | ||||||
| public class MainActivity extends FlutterActivity { | public class MainActivity extends FlutterActivity { | ||||||
|  |    | ||||||
|     @Override |     @Override | ||||||
|   protected void onCreate(Bundle savedInstanceState) { |     public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { | ||||||
|     super.onCreate(savedInstanceState); |         GeneratedPluginRegistrant.registerWith(flutterEngine); | ||||||
|     GeneratedPluginRegistrant.registerWith(this); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <!-- Modify this file to customize your launch splash screen --> | <!-- Modify this file to customize your launch splash screen --> | ||||||
| <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> | <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 --> |     <!-- You can insert your own image assets here --> | ||||||
|     <!-- <item> |     <!-- <item> | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/mini_icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 612 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"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <resources> | <resources> | ||||||
|  |     <color name="main_color">#709AC1</color> | ||||||
|     <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> |     <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> | ||||||
|         <!-- Show a splash screen on the activity. Automatically removed when |         <!-- Show a splash screen on the activity. Automatically removed when | ||||||
|              Flutter draws its first frame --> |              Flutter draws its first frame --> | ||||||
|         <item name="android:windowBackground">@drawable/launch_background</item> |         <item name="android:windowBackground">@drawable/launch_background</item> | ||||||
|     </style> |     </style> | ||||||
|  |     <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> | ||||||
|  |         <item name="android:windowBackground">@drawable/launch_background</item> | ||||||
|  |     </style> | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
| @@ -2,10 +2,15 @@ buildscript { | |||||||
|     repositories { |     repositories { | ||||||
|         google() |         google() | ||||||
|         jcenter() |         jcenter() | ||||||
|  |         maven { | ||||||
|  |             url 'https://maven.fabric.io/public' | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     dependencies { |     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 { |     repositories { | ||||||
|         google() |         google() | ||||||
|         jcenter() |         jcenter() | ||||||
|  |         maven { | ||||||
|  |             url 'https://maven.fabric.io/public' | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1 +1,6 @@ | |||||||
| org.gradle.jvmargs=-Xmx1536M | org.gradle.jvmargs=-Xmx2g | ||||||
|  | org.gradle.daemon=true | ||||||
|  | org.gradle.caching=true | ||||||
|  | android.useAndroidX=true | ||||||
|  | android.enableJetifier=true | ||||||
|  | android.enableR8=true | ||||||
|   | |||||||
| @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME | |||||||
| distributionPath=wrapper/dists | distributionPath=wrapper/dists | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
| zipStorePath=wrapper/dists | 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-4.5.95.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 (ConnectionManager().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(), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										622
									
								
								lib/cards/card.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,622 @@ | |||||||
|  | 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.MAP: | ||||||
|  |           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.ENTITY_BUTTON: | ||||||
|  |           case CardType.LIGHT: | ||||||
|  |           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: | ||||||
|  |             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; | ||||||
|  |       stateFilter = rawData['state_filter']  ?? []; | ||||||
|  |     } 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"], | ||||||
|  |               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"], | ||||||
|  |               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']; | ||||||
|  |     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"], | ||||||
|  |                 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"], | ||||||
|  |                   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"], | ||||||
|  |                 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']; | ||||||
|  |     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 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']; | ||||||
|  |     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']; | ||||||
|  |     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"]; | ||||||
|  |     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"], | ||||||
|  |                 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 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); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								lib/cards/error_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,38 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class ErrorCard extends StatelessWidget { | ||||||
|  |   final ErrorCardData card; | ||||||
|  |  | ||||||
|  |   const ErrorCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return CardWrapper( | ||||||
|  |       child: Padding( | ||||||
|  |         padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |           children: <Widget>[ | ||||||
|  |             Text( | ||||||
|  |               'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!', | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |             ), | ||||||
|  |             RaisedButton( | ||||||
|  |               onPressed: () { | ||||||
|  |                 Clipboard.setData(new ClipboardData(text: card.cardConfig)); | ||||||
|  |               }, | ||||||
|  |               child: Text('Copy card config'), | ||||||
|  |             ), | ||||||
|  |             RaisedButton( | ||||||
|  |               onPressed: () { | ||||||
|  |                 Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title="); | ||||||
|  |               }, | ||||||
|  |               child: Text('Report issue'), | ||||||
|  |             ) | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   }   | ||||||
|  | } | ||||||
							
								
								
									
										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,); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |    | ||||||
|  | } | ||||||
							
								
								
									
										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(); | ||||||
|  |   }   | ||||||
|  | } | ||||||
							
								
								
									
										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; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								lib/cards/widgets/card_wrapper.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class CardWrapper extends StatelessWidget { | ||||||
|  |    | ||||||
|  |   final Widget child; | ||||||
|  |   final EdgeInsets padding; | ||||||
|  |  | ||||||
|  |   const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Card( | ||||||
|  |       child: Padding( | ||||||
|  |         padding: padding, | ||||||
|  |         child: child | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										87
									
								
								lib/const.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,87 @@ | |||||||
|  | part of 'main.dart'; | ||||||
|  |  | ||||||
|  | class EntityState { | ||||||
|  |   static const on = 'on'; | ||||||
|  |   static const off = 'off'; | ||||||
|  |   static const home = 'home'; | ||||||
|  |   static const not_home = 'not_home'; | ||||||
|  |   static const unknown = 'unknown'; | ||||||
|  |   static const open = 'open'; | ||||||
|  |   static const opening = 'opening'; | ||||||
|  |   static const closed = 'closed'; | ||||||
|  |   static const closing = 'closing'; | ||||||
|  |   static const playing = 'playing'; | ||||||
|  |   static const paused = 'paused'; | ||||||
|  |   static const idle = 'idle'; | ||||||
|  |   static const standby = 'standby'; | ||||||
|  |   static const alarm_disarmed = 'disarmed'; | ||||||
|  |   static const alarm_armed_home = 'armed_home'; | ||||||
|  |   static const alarm_armed_away = 'armed_away'; | ||||||
|  |   static const alarm_armed_night = 'armed_night'; | ||||||
|  |   static const alarm_armed_custom_bypass = 'armed_custom_bypass'; | ||||||
|  |   static const alarm_pending = 'pending'; | ||||||
|  |   static const alarm_arming = 'arming'; | ||||||
|  |   static const alarm_disarming = 'disarming'; | ||||||
|  |   static const alarm_triggered = 'triggered'; | ||||||
|  |   static const locked = 'locked'; | ||||||
|  |   static const unlocked = 'unlocked'; | ||||||
|  |   static const unavailable = 'unavailable'; | ||||||
|  |   static const ok = 'ok'; | ||||||
|  |   static const problem = 'problem'; | ||||||
|  |   static const active = 'active'; | ||||||
|  |   static const cleaning = 'cleaning'; | ||||||
|  |   static const docked = 'docked'; | ||||||
|  |   static const returning = 'returning'; | ||||||
|  |   static const error = 'error'; | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class CardType { | ||||||
|  |   static const HORIZONTAL_STACK = "horizontal-stack"; | ||||||
|  |   static const VERTICAL_STACK = "vertical-stack"; | ||||||
|  |   static const ENTITIES = "entities"; | ||||||
|  |   static const GLANCE = "glance"; | ||||||
|  |   static const MEDIA_CONTROL = "media-control"; | ||||||
|  |   static const WEATHER_FORECAST = "weather-forecast"; | ||||||
|  |   static const THERMOSTAT = "thermostat"; | ||||||
|  |   static const SENSOR = "sensor"; | ||||||
|  |   static const PLANT_STATUS = "plant-status"; | ||||||
|  |   static const PICTURE_ENTITY = "picture-entity"; | ||||||
|  |   static const PICTURE_ELEMENTS = "picture-elements"; | ||||||
|  |   static const PICTURE = "picture"; | ||||||
|  |   static const MAP = "map"; | ||||||
|  |   static const IFRAME = "iframe"; | ||||||
|  |   static const GAUGE = "gauge"; | ||||||
|  |   static const ENTITY_BUTTON = "entity-button"; | ||||||
|  |   static const ENTITY = "entity"; | ||||||
|  |   static const BUTTON = "button"; | ||||||
|  |   static const CONDITIONAL = "conditional"; | ||||||
|  |   static const ALARM_PANEL = "alarm-panel"; | ||||||
|  |   static const MARKDOWN = "markdown"; | ||||||
|  |   static const LIGHT = "light"; | ||||||
|  |   static const ENTITY_FILTER = "entity-filter"; | ||||||
|  |   static const UNKNOWN = "unknown"; | ||||||
|  |   static const HISTORY_GRAPH = "history-graph"; | ||||||
|  |   static const PICTURE_GLANCE = "picture-glance"; | ||||||
|  |   static const BADGES = "badges"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class Sizes { | ||||||
|  |   static const rightWidgetPadding = 10.0; | ||||||
|  |   static const leftWidgetPadding = 10.0; | ||||||
|  |   static const buttonPadding = 4.0; | ||||||
|  |   static const extendedWidgetHeight = 50.0; | ||||||
|  |   static const iconSize = 28.0; | ||||||
|  |   static const largeIconSize = 46.0; | ||||||
|  |   //static const stateFontSize = 15.0; | ||||||
|  |   //static const nameFontSize = 15.0; | ||||||
|  |   //static const smallFontSize = 14.0; | ||||||
|  |   //static const largeFontSize = 24.0; | ||||||
|  |   static const inputWidth = 160.0; | ||||||
|  |   static const rowPadding = 10.0; | ||||||
|  |   static const doubleRowPadding = rowPadding*2; | ||||||
|  |   static const minViewColumnWidth = 350; | ||||||
|  |   static const entityPageMaxWidth = 400.0; | ||||||
|  |   static const mainPageScreenSeparatorWidth = 5.0; | ||||||
|  |   static const tabletMinWidth = minViewColumnWidth + entityPageMaxWidth + 5; | ||||||
|  | } | ||||||
							
								
								
									
										3265
									
								
								lib/data_model.dart
									
									
									
									
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class AlarmControlPanelEntity extends Entity { | ||||||
|  |   AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||||
|  |     return AlarmControlPanelControlsWidget( | ||||||
|  |       extended: false, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,271 @@ | |||||||
|  | part of '../../../main.dart'; | ||||||
|  |  | ||||||
|  | class AlarmControlPanelControlsWidget extends StatefulWidget { | ||||||
|  |  | ||||||
|  |   final bool extended; | ||||||
|  |   final List states; | ||||||
|  |  | ||||||
|  |   const AlarmControlPanelControlsWidget({Key key, @required this.extended, this.states}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _AlarmControlPanelControlsWidgetWidgetState createState() => _AlarmControlPanelControlsWidgetWidgetState(); | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPanelControlsWidget> { | ||||||
|  |  | ||||||
|  |   String code = ""; | ||||||
|  |   List supportedStates; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     supportedStates = widget.states ?? ["arm_home", "arm_away"]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   void _callService(AlarmControlPanelEntity entity, String service) { | ||||||
|  |     ConnectionManager().callService( | ||||||
|  |       domain: entity.domain, | ||||||
|  |       service: service, | ||||||
|  |       entityId: entity.entityId, | ||||||
|  |       data: {"code": "$code"} | ||||||
|  |     ); | ||||||
|  |     setState(() { | ||||||
|  |       code = ""; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _pinPadHandler(value) { | ||||||
|  |     setState(() { | ||||||
|  |       code += "$value"; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _pinPadClear() { | ||||||
|  |     setState(() { | ||||||
|  |       code = ""; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _askToTrigger(AlarmControlPanelEntity entity) { | ||||||
|  |     // flutter defined function | ||||||
|  |     showDialog( | ||||||
|  |       context: context, | ||||||
|  |       builder: (BuildContext context) { | ||||||
|  |         // return object of type Dialog | ||||||
|  |         return AlertDialog( | ||||||
|  |           title: new Text("Are you sure?"), | ||||||
|  |           content: new Text("Are you sure want to trigger alarm ${entity.displayName}?"), | ||||||
|  |           actions: <Widget>[ | ||||||
|  |             FlatButton( | ||||||
|  |               child: new Text("Yes"), | ||||||
|  |               onPressed: () { | ||||||
|  |                 ConnectionManager().callService( | ||||||
|  |                   domain: entity.domain, | ||||||
|  |                   service: "alarm_trigger", | ||||||
|  |                   entityId: entity.entityId | ||||||
|  |                 ); | ||||||
|  |                 Navigator.of(context).pop(); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |             FlatButton( | ||||||
|  |               child: new Text("No"), | ||||||
|  |               onPressed: () { | ||||||
|  |                 Navigator.of(context).pop(); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final entityModel = EntityModel.of(context); | ||||||
|  |     final AlarmControlPanelEntity entity = entityModel.entityWrapper.entity; | ||||||
|  |     List<Widget> buttons = []; | ||||||
|  |     if (entity.state == EntityState.alarm_disarmed) { | ||||||
|  |       if (supportedStates.contains("arm_home")) { | ||||||
|  |         buttons.add( | ||||||
|  |           RaisedButton( | ||||||
|  |             onPressed: () => _callService(entity, "alarm_arm_home"), | ||||||
|  |             child: Text("ARM HOME"), | ||||||
|  |           ) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       if (supportedStates.contains("arm_away")) { | ||||||
|  |         buttons.add( | ||||||
|  |             RaisedButton( | ||||||
|  |               onPressed: () => _callService(entity, "alarm_arm_away"), | ||||||
|  |               child: Text("ARM AWAY"), | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       if (widget.extended) { | ||||||
|  |         if (supportedStates.contains("arm_night")) { | ||||||
|  |           buttons.add( | ||||||
|  |               RaisedButton( | ||||||
|  |                 onPressed: () => _callService(entity, "alarm_arm_night"), | ||||||
|  |                 child: Text("ARM NIGHT"), | ||||||
|  |               ) | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |         if (supportedStates.contains("arm_custom_bypass")) { | ||||||
|  |           buttons.add( | ||||||
|  |               RaisedButton( | ||||||
|  |                 onPressed: () => | ||||||
|  |                     _callService(entity, "alarm_arm_custom_bypass"), | ||||||
|  |                 child: Text("ARM CUSTOM BYPASS"), | ||||||
|  |               ) | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       buttons.add( | ||||||
|  |         RaisedButton( | ||||||
|  |           onPressed: () => _callService(entity, "alarm_disarm"), | ||||||
|  |           child: Text("DISARM"), | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     Widget pinPad; | ||||||
|  |     if (entity.attributes["code_format"] == null) { | ||||||
|  |       pinPad = Container(width: 0.0, height: 0.0,); | ||||||
|  |     } else { | ||||||
|  |       pinPad = Padding( | ||||||
|  |           padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||||
|  |           child: Column( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             children: <Widget>[ | ||||||
|  |               Wrap( | ||||||
|  |                 spacing: 5.0, | ||||||
|  |                 children: <Widget>[ | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("1"), | ||||||
|  |                     child: Text("1"), | ||||||
|  |                   ), | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("2"), | ||||||
|  |                     child: Text("2"), | ||||||
|  |                   ), | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("3"), | ||||||
|  |                     child: Text("3"), | ||||||
|  |                   ) | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               Wrap( | ||||||
|  |                 spacing: 5.0, | ||||||
|  |                 children: <Widget>[ | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("4"), | ||||||
|  |                     child: Text("4"), | ||||||
|  |                   ), | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("5"), | ||||||
|  |                     child: Text("5"), | ||||||
|  |                   ), | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("6"), | ||||||
|  |                     child: Text("6"), | ||||||
|  |                   ) | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               Wrap( | ||||||
|  |                 spacing: 5.0, | ||||||
|  |                 children: <Widget>[ | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("7"), | ||||||
|  |                     child: Text("7"), | ||||||
|  |                   ), | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("8"), | ||||||
|  |                     child: Text("8"), | ||||||
|  |                   ), | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("9"), | ||||||
|  |                     child: Text("9"), | ||||||
|  |                   ) | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               Wrap( | ||||||
|  |                 spacing: 5.0, | ||||||
|  |                 alignment: WrapAlignment.end, | ||||||
|  |                 children: <Widget>[ | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadHandler("0"), | ||||||
|  |                     child: Text("0"), | ||||||
|  |                   ), | ||||||
|  |                   RaisedButton( | ||||||
|  |                     onPressed: () => _pinPadClear(), | ||||||
|  |                     child: Text("CLEAR"), | ||||||
|  |                   ) | ||||||
|  |                 ], | ||||||
|  |               ) | ||||||
|  |             ], | ||||||
|  |           ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     Widget inputWrapper; | ||||||
|  |     if (entity.attributes["code_format"] == null) { | ||||||
|  |       inputWrapper = Container(width: 0.0, height: 0.0,); | ||||||
|  |     } else { | ||||||
|  |       inputWrapper = Container( | ||||||
|  |           width: 150.0, | ||||||
|  |           child: TextField( | ||||||
|  |               decoration: InputDecoration( | ||||||
|  |                   labelText: "Alarm Code" | ||||||
|  |               ), | ||||||
|  |               //focusNode: _focusNode, | ||||||
|  |               obscureText: true, | ||||||
|  |               controller: new TextEditingController.fromValue( | ||||||
|  |                   new TextEditingValue( | ||||||
|  |                       text: code, | ||||||
|  |                       selection: | ||||||
|  |                       new TextSelection.collapsed(offset: code.length) | ||||||
|  |                   ) | ||||||
|  |               ), | ||||||
|  |               onChanged: (value) { | ||||||
|  |                 code = value; | ||||||
|  |               } | ||||||
|  |           ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     Widget buttonsWrapper = Padding( | ||||||
|  |         padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||||
|  |         child: Wrap( | ||||||
|  |             alignment: WrapAlignment.center, | ||||||
|  |             spacing: 15.0, | ||||||
|  |             runSpacing: Sizes.rowPadding, | ||||||
|  |             children: buttons | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  |     Widget triggerButton = Row( | ||||||
|  |       mainAxisAlignment: MainAxisAlignment.end, | ||||||
|  |       children: [ | ||||||
|  |         FlatButton( | ||||||
|  |           child: Text( | ||||||
|  |             "TRIGGER", | ||||||
|  |             style: Theme.of(context).textTheme.subhead.copyWith( | ||||||
|  |               color: Theme.of(context).errorColor | ||||||
|  |             ) | ||||||
|  |           ), | ||||||
|  |           onPressed: () => _askToTrigger(entity), | ||||||
|  |         ) | ||||||
|  |       ] | ||||||
|  |     ); | ||||||
|  |     return Column( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |       children: <Widget>[ | ||||||
|  |         widget.extended ? buttonsWrapper : inputWrapper, | ||||||
|  |         widget.extended ? inputWrapper : buttonsWrapper, | ||||||
|  |         widget.extended ? pinPad : triggerButton | ||||||
|  |       ] | ||||||
|  |  | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								lib/entities/automation/automation_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class AutomationEntity extends Entity { | ||||||
|  |   AutomationEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget _buildStatePart(BuildContext context) { | ||||||
|  |     return SwitchStateWidget(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||||
|  |     return Row( | ||||||
|  |       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |       mainAxisSize: MainAxisSize.max, | ||||||
|  |       children: <Widget>[ | ||||||
|  |         FlatServiceButton( | ||||||
|  |           serviceDomain: domain, | ||||||
|  |           entityId: entityId, | ||||||
|  |           text: "TRIGGER", | ||||||
|  |           serviceName: "trigger", | ||||||
|  |         ) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								lib/entities/button/button_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class ButtonEntity extends Entity { | ||||||
|  |   ButtonEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget _buildStatePart(BuildContext context) { | ||||||
|  |     return FlatServiceButton( | ||||||
|  |       entityId: entityId, | ||||||
|  |       serviceDomain: domain, | ||||||
|  |       serviceName: 'turn_on', | ||||||
|  |       text: domain == "scene" ? "ACTIVATE" : "EXECUTE", | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								lib/entities/camera/camera_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class CameraEntity extends Entity { | ||||||
|  |  | ||||||
|  |   static const SUPPORT_ON_OFF = 1; | ||||||
|  |   static const SUPPORT_STREAM = 2; | ||||||
|  |  | ||||||
|  |   CameraEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|  |   bool get supportOnOff => ((supportedFeatures & | ||||||
|  |   CameraEntity.SUPPORT_ON_OFF) == | ||||||
|  |       CameraEntity.SUPPORT_ON_OFF); | ||||||
|  |   bool get supportStream => ((supportedFeatures & | ||||||
|  |   CameraEntity.SUPPORT_STREAM) == | ||||||
|  |       CameraEntity.SUPPORT_STREAM); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||||
|  |     return CameraStreamView(); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										190
									
								
								lib/entities/camera/widgets/camera_stream_view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,190 @@ | |||||||
|  | part of '../../../main.dart'; | ||||||
|  |  | ||||||
|  | class CameraStreamView extends StatefulWidget { | ||||||
|  |  | ||||||
|  |   final bool withControls; | ||||||
|  |  | ||||||
|  |   CameraStreamView({Key key, this.withControls: true}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _CameraStreamViewState createState() => _CameraStreamViewState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _CameraStreamViewState extends State<CameraStreamView> { | ||||||
|  |  | ||||||
|  |   CameraEntity _entity; | ||||||
|  |   String _streamUrl = ""; | ||||||
|  |   bool _isLoaded = false; | ||||||
|  |   double _aspectRatio = 1.33; | ||||||
|  |   String _webViewHtml; | ||||||
|  |   String _jsMessageChannelName = 'unknown'; | ||||||
|  |   Completer _loading; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future _loadResources() { | ||||||
|  |     if (_loading != null && !_loading.isCompleted) { | ||||||
|  |       Logger.d("[Camera Player] Resources loading is not finished yet"); | ||||||
|  |       return _loading.future;   | ||||||
|  |     } | ||||||
|  |     Logger.d("[Camera Player] Loading resources"); | ||||||
|  |     _loading = Completer(); | ||||||
|  |     _entity = EntityModel | ||||||
|  |           .of(context) | ||||||
|  |           .entityWrapper | ||||||
|  |           .entity; | ||||||
|  |     if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) { | ||||||
|  |       HomeAssistant().getCameraStream(_entity.entityId) | ||||||
|  |         .then((data) { | ||||||
|  |           _jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}'; | ||||||
|  |             rootBundle.loadString('assets/html/cameraLiveView.html').then((file) { | ||||||
|  |               _webViewHtml = Uri.dataFromString( | ||||||
|  |                   file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName), | ||||||
|  |                   mimeType: 'text/html', | ||||||
|  |                   encoding: Encoding.getByName('utf-8') | ||||||
|  |               ).toString(); | ||||||
|  |               _loading.complete(); | ||||||
|  |             }); | ||||||
|  |         }) | ||||||
|  |         .catchError((e) { | ||||||
|  |           if (e == 'start_stream_failed') { | ||||||
|  |             Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e"); | ||||||
|  |             _loadMJPEG().then((_) { | ||||||
|  |               _loading.complete(); | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             _loading.completeError(e); | ||||||
|  |             Logger.e("[Camera Player] Error loading stream: $e"); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     } else { | ||||||
|  |       _loadMJPEG().then((_) { | ||||||
|  |         _loading.complete(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     return _loading.future; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future _loadMJPEG() async { | ||||||
|  |     _streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity | ||||||
|  |         .entityId}?token=${_entity.attributes['access_token']}'; | ||||||
|  |     _jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}'; | ||||||
|  |     var file = await rootBundle.loadString('assets/html/cameraView.html'); | ||||||
|  |     _webViewHtml = Uri.dataFromString( | ||||||
|  |         file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName), | ||||||
|  |         mimeType: 'text/html', | ||||||
|  |         encoding: Encoding.getByName('utf-8') | ||||||
|  |     ).toString(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildScreen() { | ||||||
|  |     Widget screenWidget; | ||||||
|  |     if (!_isLoaded) { | ||||||
|  |       screenWidget = Center( | ||||||
|  |         child: EntityPicture( | ||||||
|  |           fit: BoxFit.contain, | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       screenWidget = WebView( | ||||||
|  |         initialUrl: _webViewHtml, | ||||||
|  |         initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, | ||||||
|  |         debuggingEnabled: Logger.isInDebugMode, | ||||||
|  |         gestureNavigationEnabled: false, | ||||||
|  |         javascriptMode: JavascriptMode.unrestricted, | ||||||
|  |         javascriptChannels: { | ||||||
|  |           JavascriptChannel( | ||||||
|  |             name: _jsMessageChannelName, | ||||||
|  |             onMessageReceived: ((message) { | ||||||
|  |               Logger.d('[Camera Player] Message from page: $message'); | ||||||
|  |               setState((){ | ||||||
|  |                 _aspectRatio = double.tryParse(message.message) ?? 1.33; | ||||||
|  |               }); | ||||||
|  |             }) | ||||||
|  |           ) | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return AspectRatio( | ||||||
|  |       aspectRatio: _aspectRatio, | ||||||
|  |       child: screenWidget | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildControls() { | ||||||
|  |       return Row( | ||||||
|  |         mainAxisSize: MainAxisSize.max, | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |         mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |         children: <Widget>[ | ||||||
|  |           IconButton( | ||||||
|  |             icon: Icon(Icons.refresh), | ||||||
|  |             iconSize: 40, | ||||||
|  |             color: Theme.of(context).accentColor, | ||||||
|  |             onPressed: _isLoaded ? () { | ||||||
|  |               setState(() { | ||||||
|  |                 _isLoaded = false;   | ||||||
|  |               }); | ||||||
|  |             } : null, | ||||||
|  |           ), | ||||||
|  |           Expanded( | ||||||
|  |             child: Container(), | ||||||
|  |           ), | ||||||
|  |           IconButton( | ||||||
|  |             icon: Icon(Icons.fullscreen), | ||||||
|  |             iconSize: 40, | ||||||
|  |             color: Theme.of(context).accentColor, | ||||||
|  |             onPressed: _isLoaded ? () { | ||||||
|  |               Navigator.of(context).pushReplacement( | ||||||
|  |                 MaterialPageRoute( | ||||||
|  |                   builder: (conext) => FullScreenPage( | ||||||
|  |                     child: EntityModel( | ||||||
|  |                       child: CameraStreamView( | ||||||
|  |                         withControls: false | ||||||
|  |                       ), | ||||||
|  |                       handleTap: false, | ||||||
|  |                       entityWrapper: EntityWrapper( | ||||||
|  |                         entity: _entity | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   fullscreenDialog: true | ||||||
|  |                 ) | ||||||
|  |               ).then((_){ | ||||||
|  |                 eventBus.fire(ShowEntityPageEvent(entityId: _entity.entityId)); | ||||||
|  |               }); | ||||||
|  |             } : null, | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (!_isLoaded && (_loading == null || _loading.isCompleted)) { | ||||||
|  |       _loadResources().then((_) => setState((){ _isLoaded = true; })); | ||||||
|  |     } | ||||||
|  |     if (widget.withControls) { | ||||||
|  |       return Card( | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           children: <Widget>[ | ||||||
|  |             _buildScreen(), | ||||||
|  |             _buildControls() | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return _buildScreen(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										114
									
								
								lib/entities/climate/climate_entity.class.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,114 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class ClimateEntity extends Entity { | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   EntityHistoryConfig historyConfig = EntityHistoryConfig( | ||||||
|  |     chartType: EntityHistoryWidgetType.numericAttributes, | ||||||
|  |     numericState: false, | ||||||
|  |     numericAttributesToShow: ["current_temperature"] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   static const SUPPORT_TARGET_TEMPERATURE = 1; | ||||||
|  |   static const SUPPORT_TARGET_TEMPERATURE_RANGE = 2; | ||||||
|  |   static const SUPPORT_TARGET_HUMIDITY = 4; | ||||||
|  |   static const SUPPORT_FAN_MODE = 8; | ||||||
|  |   static const SUPPORT_PRESET_MODE = 16; | ||||||
|  |   static const SUPPORT_SWING_MODE = 32; | ||||||
|  |   static const SUPPORT_AUX_HEAT = 64; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   //static const SUPPORT_OPERATION_MODE = 16; | ||||||
|  |   //static const SUPPORT_HOLD_MODE = 256; | ||||||
|  |   //static const SUPPORT_AWAY_MODE = 1024; | ||||||
|  |   //static const SUPPORT_ON_OFF = 4096; | ||||||
|  |  | ||||||
|  |   ClimateEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|  |   bool get supportTargetTemperature => ((supportedFeatures & | ||||||
|  |   ClimateEntity.SUPPORT_TARGET_TEMPERATURE) == | ||||||
|  |       ClimateEntity.SUPPORT_TARGET_TEMPERATURE); | ||||||
|  |   bool get supportTargetTemperatureRange => ((supportedFeatures & | ||||||
|  |   ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE) == | ||||||
|  |       ClimateEntity.SUPPORT_TARGET_TEMPERATURE_RANGE); | ||||||
|  |   bool get supportTargetHumidity => ((supportedFeatures & | ||||||
|  |   ClimateEntity.SUPPORT_TARGET_HUMIDITY) == | ||||||
|  |       ClimateEntity.SUPPORT_TARGET_HUMIDITY); | ||||||
|  |   bool get supportFanMode => | ||||||
|  |       ((supportedFeatures & ClimateEntity.SUPPORT_FAN_MODE) == | ||||||
|  |           ClimateEntity.SUPPORT_FAN_MODE); | ||||||
|  |   bool get supportSwingMode => | ||||||
|  |       ((supportedFeatures & ClimateEntity.SUPPORT_SWING_MODE) == | ||||||
|  |           ClimateEntity.SUPPORT_SWING_MODE); | ||||||
|  |   bool get supportPresetMode => | ||||||
|  |       ((supportedFeatures & ClimateEntity.SUPPORT_PRESET_MODE) == | ||||||
|  |           ClimateEntity.SUPPORT_PRESET_MODE); | ||||||
|  |   bool get supportAuxHeat => | ||||||
|  |       ((supportedFeatures & ClimateEntity.SUPPORT_AUX_HEAT) == | ||||||
|  |           ClimateEntity.SUPPORT_AUX_HEAT); | ||||||
|  |  | ||||||
|  |   List<String> get hvacModes => attributes["hvac_modes"] != null | ||||||
|  |       ? (attributes["hvac_modes"] as List).cast<String>() | ||||||
|  |       : null; | ||||||
|  |   List<String> get fanModes => attributes["fan_modes"] != null | ||||||
|  |       ? (attributes["fan_modes"] as List).cast<String>() | ||||||
|  |       : null; | ||||||
|  |   List<String> get presetModes => attributes["preset_modes"] != null | ||||||
|  |       ? (attributes["preset_modes"] as List).cast<String>() | ||||||
|  |       : null; | ||||||
|  |   List<String> get swingModes => attributes["swing_modes"] != null | ||||||
|  |       ? (attributes["swing_modes"] as List).cast<String>() | ||||||
|  |       : null; | ||||||
|  |   double get temperature => _getDoubleAttributeValue('temperature'); | ||||||
|  |   double get currentTemperature => _getDoubleAttributeValue('current_temperature'); | ||||||
|  |   double get targetHigh => _getDoubleAttributeValue('target_temp_high'); | ||||||
|  |   double get targetLow => _getDoubleAttributeValue('target_temp_low'); | ||||||
|  |   double get maxTemp => _getDoubleAttributeValue('max_temp') ?? 100.0; | ||||||
|  |   double get minTemp => _getDoubleAttributeValue('min_temp') ?? -100.0; | ||||||
|  |   double get targetHumidity => _getDoubleAttributeValue('humidity'); | ||||||
|  |   double get maxHumidity => _getDoubleAttributeValue('max_humidity'); | ||||||
|  |   double get minHumidity => _getDoubleAttributeValue('min_humidity'); | ||||||
|  |   double get temperatureStep => _getDoubleAttributeValue('target_temp_step') ?? 0.5; | ||||||
|  |   String get hvacAction => attributes['hvac_action']; | ||||||
|  |   String get fanMode => attributes['fan_mode']; | ||||||
|  |   String get presetMode => attributes['preset_mode']; | ||||||
|  |   String get swingMode => attributes['swing_mode']; | ||||||
|  |   bool get awayMode => attributes['away_mode'] == "on"; | ||||||
|  |   //bool get isOff => state == EntityState.off; | ||||||
|  |   bool get auxHeat => attributes['aux_heat'] == "on"; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void update(Map rawData, String webHost) { | ||||||
|  |     super.update(rawData, webHost); | ||||||
|  |     if (supportTargetTemperature) { | ||||||
|  |       historyConfig.numericAttributesToShow.add("temperature"); | ||||||
|  |     } | ||||||
|  |     if (supportTargetTemperatureRange) { | ||||||
|  |       historyConfig.numericAttributesToShow.add("target_temp_high"); | ||||||
|  |       historyConfig.numericAttributesToShow.add("target_temp_low"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget _buildStatePart(BuildContext context) { | ||||||
|  |     return ClimateStateWidget(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||||
|  |     return ClimateControlWidget(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   double _getDoubleAttributeValue(String attributeName) { | ||||||
|  |     var temp1 = attributes["$attributeName"]; | ||||||
|  |     if (temp1 is int) { | ||||||
|  |       return temp1.toDouble(); | ||||||
|  |     } else if (temp1 is double) { | ||||||
|  |       return temp1; | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										398
									
								
								lib/entities/climate/widgets/climate_controls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,398 @@ | |||||||
|  | part of '../../../main.dart'; | ||||||
|  |  | ||||||
|  | class ClimateControlWidget extends StatefulWidget { | ||||||
|  |  | ||||||
|  |   ClimateControlWidget({Key key}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   _ClimateControlWidgetState createState() => _ClimateControlWidgetState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||||
|  |  | ||||||
|  |   bool _temperaturePending = false; | ||||||
|  |   bool _changedHere = false; | ||||||
|  |   Timer _tempThrottleTimer; | ||||||
|  |   Timer _targetTempThrottleTimer; | ||||||
|  |   double _tmpTemperature = 0.0; | ||||||
|  |   double _tmpTargetLow = 0.0; | ||||||
|  |   double _tmpTargetHigh = 0.0; | ||||||
|  |   double _tmpTargetHumidity = 0.0; | ||||||
|  |   String _tmpHVACMode; | ||||||
|  |   String _tmpFanMode; | ||||||
|  |   String _tmpSwingMode; | ||||||
|  |   String _tmpPresetMode; | ||||||
|  |   //bool _tmpIsOff = false; | ||||||
|  |   bool _tmpAuxHeat = false; | ||||||
|  |  | ||||||
|  |   void _resetVars(ClimateEntity entity) { | ||||||
|  |     if (!_temperaturePending) { | ||||||
|  |       _tmpTemperature = entity.temperature; | ||||||
|  |       _tmpTargetHigh = entity.targetHigh; | ||||||
|  |       _tmpTargetLow = entity.targetLow; | ||||||
|  |     } | ||||||
|  |     _tmpHVACMode = entity.state; | ||||||
|  |     _tmpFanMode = entity.fanMode; | ||||||
|  |     _tmpSwingMode = entity.swingMode; | ||||||
|  |     _tmpPresetMode = entity.presetMode; | ||||||
|  |     //_tmpIsOff = entity.isOff; | ||||||
|  |     _tmpAuxHeat = entity.auxHeat; | ||||||
|  |     _tmpTargetHumidity = entity.targetHumidity; | ||||||
|  |  | ||||||
|  |     _changedHere = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _temperatureUp(ClimateEntity entity) { | ||||||
|  |     _tmpTemperature = ((_tmpTemperature + entity.temperatureStep) <= entity.maxTemp) ? _tmpTemperature + entity.temperatureStep : entity.maxTemp; | ||||||
|  |     _setTemperature(entity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _temperatureDown(ClimateEntity entity) { | ||||||
|  |     _tmpTemperature = ((_tmpTemperature - entity.temperatureStep) >= entity.minTemp) ? _tmpTemperature - entity.temperatureStep : entity.minTemp; | ||||||
|  |     _setTemperature(entity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _targetLowUp(ClimateEntity entity) { | ||||||
|  |     _tmpTargetLow = ((_tmpTargetLow + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetLow + entity.temperatureStep : entity.maxTemp; | ||||||
|  |     _setTargetTemp(entity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _targetLowDown(ClimateEntity entity) { | ||||||
|  |     _tmpTargetLow = ((_tmpTargetLow - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetLow - entity.temperatureStep : entity.minTemp; | ||||||
|  |     _setTargetTemp(entity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _targetHighUp(ClimateEntity entity) { | ||||||
|  |     _tmpTargetHigh = ((_tmpTargetHigh + entity.temperatureStep) <= entity.maxTemp) ? _tmpTargetHigh + entity.temperatureStep : entity.maxTemp; | ||||||
|  |     _setTargetTemp(entity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _targetHighDown(ClimateEntity entity) { | ||||||
|  |     _tmpTargetHigh = ((_tmpTargetHigh - entity.temperatureStep) >= entity.minTemp) ? _tmpTargetHigh - entity.temperatureStep : entity.minTemp; | ||||||
|  |     _setTargetTemp(entity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setTemperature(ClimateEntity entity) { | ||||||
|  |     _tempThrottleTimer?.cancel(); | ||||||
|  |     setState(() { | ||||||
|  |       _changedHere = true; | ||||||
|  |       _temperaturePending = true; | ||||||
|  |       _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1)); | ||||||
|  |     }); | ||||||
|  |     _tempThrottleTimer = Timer(Duration(seconds: 2), () { | ||||||
|  |       setState(() { | ||||||
|  |         _changedHere = true; | ||||||
|  |         _temperaturePending = false; | ||||||
|  |         ConnectionManager().callService( | ||||||
|  |           domain: entity.domain, | ||||||
|  |           service: "set_temperature", | ||||||
|  |           entityId: entity.entityId, | ||||||
|  |           data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"} | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setTargetTemp(ClimateEntity entity) { | ||||||
|  |     _targetTempThrottleTimer?.cancel(); | ||||||
|  |     setState(() { | ||||||
|  |       _changedHere = true; | ||||||
|  |       _temperaturePending = true; | ||||||
|  |       _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); | ||||||
|  |       _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); | ||||||
|  |     }); | ||||||
|  |     _targetTempThrottleTimer = Timer(Duration(seconds: 2), () { | ||||||
|  |       setState(() { | ||||||
|  |         _changedHere = true; | ||||||
|  |         _temperaturePending = false; | ||||||
|  |         ConnectionManager().callService( | ||||||
|  |           domain: entity.domain, | ||||||
|  |           service: "set_temperature", | ||||||
|  |           entityId: entity.entityId, | ||||||
|  |           data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"} | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setTargetHumidity(ClimateEntity entity, double value) { | ||||||
|  |     setState(() { | ||||||
|  |       _tmpTargetHumidity = value.roundToDouble(); | ||||||
|  |       _changedHere = true; | ||||||
|  |       ConnectionManager().callService( | ||||||
|  |           domain: entity.domain, | ||||||
|  |           service: "set_humidity", | ||||||
|  |           entityId: entity.entityId, | ||||||
|  |           data: {"humidity": "$_tmpTargetHumidity"} | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setHVACMode(ClimateEntity entity, value) { | ||||||
|  |     setState(() { | ||||||
|  |       _tmpHVACMode = value; | ||||||
|  |       _changedHere = true; | ||||||
|  |       ConnectionManager().callService( | ||||||
|  |           domain: entity.domain, | ||||||
|  |           service: "set_hvac_mode", | ||||||
|  |           entityId: entity.entityId, | ||||||
|  |           data: {"hvac_mode": "$_tmpHVACMode"} | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setSwingMode(ClimateEntity entity, value) { | ||||||
|  |     setState(() { | ||||||
|  |       _tmpSwingMode = value; | ||||||
|  |       _changedHere = true; | ||||||
|  |       ConnectionManager().callService( | ||||||
|  |           domain: entity.domain, | ||||||
|  |           service: "set_swing_mode", | ||||||
|  |           entityId: entity.entityId, | ||||||
|  |           data: {"swing_mode": "$_tmpSwingMode"} | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setFanMode(ClimateEntity entity, value) { | ||||||
|  |     setState(() { | ||||||
|  |       _tmpFanMode = value; | ||||||
|  |       _changedHere = true; | ||||||
|  |       ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"}); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setPresetMode(ClimateEntity entity, value) { | ||||||
|  |     setState(() { | ||||||
|  |       _tmpPresetMode = value; | ||||||
|  |       _changedHere = true; | ||||||
|  |       ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"}); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /*void _setOnOf(ClimateEntity entity, value) { | ||||||
|  |     setState(() { | ||||||
|  |       _tmpIsOff = !value; | ||||||
|  |       _changedHere = true; | ||||||
|  |       eventBus.fire(new ServiceCallEvent(entity.domain, "${_tmpIsOff ? 'turn_off' : 'turn_on'}", entity.entityId, null)); | ||||||
|  |       _resetStateTimer(entity); | ||||||
|  |     }); | ||||||
|  |   }*/ | ||||||
|  |  | ||||||
|  |   void _setAuxHeat(ClimateEntity entity, value) { | ||||||
|  |     setState(() { | ||||||
|  |       _tmpAuxHeat = value; | ||||||
|  |       _changedHere = true; | ||||||
|  |       ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"}); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final entityModel = EntityModel.of(context); | ||||||
|  |     final ClimateEntity entity = entityModel.entityWrapper.entity; | ||||||
|  |     if (_changedHere) { | ||||||
|  |       //_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow); | ||||||
|  |       _changedHere = false; | ||||||
|  |     } else { | ||||||
|  |       _resetVars(entity); | ||||||
|  |     } | ||||||
|  |     return Padding( | ||||||
|  |       padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: <Widget>[ | ||||||
|  |           //_buildOnOffControl(entity), | ||||||
|  |           _buildTemperatureControls(entity, context), | ||||||
|  |           _buildTargetTemperatureControls(entity, context), | ||||||
|  |           _buildHumidityControls(entity, context), | ||||||
|  |           _buildOperationControl(entity, context), | ||||||
|  |           _buildFanControl(entity, context), | ||||||
|  |           _buildSwingControl(entity, context), | ||||||
|  |           _buildPresetModeControl(entity, context), | ||||||
|  |           _buildAuxHeatControl(entity, context) | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) { | ||||||
|  |     if (entity.supportPresetMode) { | ||||||
|  |       return ModeSelectorWidget( | ||||||
|  |         options: entity.presetModes, | ||||||
|  |         onChange: (mode) => _setPresetMode(entity, mode), | ||||||
|  |         caption: "Preset", | ||||||
|  |         value: _tmpPresetMode, | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container(height: 0.0, width: 0.0,); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /*Widget _buildOnOffControl(ClimateEntity entity) { | ||||||
|  |     if (entity.supportOnOff) { | ||||||
|  |       return ModeSwitchWidget( | ||||||
|  |           onChange: (value) => _setOnOf(entity, value), | ||||||
|  |           caption: "On / Off", | ||||||
|  |           value: !_tmpIsOff | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container(height: 0.0, width: 0.0,); | ||||||
|  |     } | ||||||
|  |   }*/ | ||||||
|  |  | ||||||
|  |   Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) { | ||||||
|  |     if (entity.supportAuxHeat ) { | ||||||
|  |       return ModeSwitchWidget( | ||||||
|  |           caption: "Aux heat", | ||||||
|  |           onChange: (value) => _setAuxHeat(entity, value), | ||||||
|  |           value: _tmpAuxHeat | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container(height: 0.0, width: 0.0,); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildOperationControl(ClimateEntity entity, BuildContext context) { | ||||||
|  |     if (entity.hvacModes != null) { | ||||||
|  |       return ModeSelectorWidget( | ||||||
|  |         onChange: (mode) => _setHVACMode(entity, mode), | ||||||
|  |         options: entity.hvacModes, | ||||||
|  |         caption: "Operation", | ||||||
|  |         value: _tmpHVACMode, | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container(height: 0.0, width: 0.0); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildFanControl(ClimateEntity entity, BuildContext context) { | ||||||
|  |     if (entity.supportFanMode) { | ||||||
|  |       return ModeSelectorWidget( | ||||||
|  |         options: entity.fanModes, | ||||||
|  |         onChange: (mode) => _setFanMode(entity, mode), | ||||||
|  |         caption: "Fan mode", | ||||||
|  |         value: _tmpFanMode, | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container(height: 0.0, width: 0.0); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildSwingControl(ClimateEntity entity, BuildContext context) { | ||||||
|  |     if (entity.supportSwingMode) { | ||||||
|  |       return ModeSelectorWidget( | ||||||
|  |           onChange: (mode) => _setSwingMode(entity, mode), | ||||||
|  |           options: entity.swingModes, | ||||||
|  |           value: _tmpSwingMode, | ||||||
|  |           caption: "Swing mode" | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container(height: 0.0, width: 0.0); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) { | ||||||
|  |     if ((entity.supportTargetTemperature) && (entity.temperature != null)) { | ||||||
|  |       return Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: <Widget>[ | ||||||
|  |           Text("Target temperature", style: Theme.of(context).textTheme.body1), | ||||||
|  |           TemperatureControlWidget( | ||||||
|  |             value: _tmpTemperature, | ||||||
|  |             active: _temperaturePending, | ||||||
|  |             onDec: () => _temperatureDown(entity), | ||||||
|  |             onInc: () => _temperatureUp(entity), | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container(width: 0.0, height: 0.0,); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) { | ||||||
|  |     List<Widget> controls = []; | ||||||
|  |     if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) { | ||||||
|  |       controls.addAll(<Widget>[ | ||||||
|  |         TemperatureControlWidget( | ||||||
|  |           value: _tmpTargetLow, | ||||||
|  |           active: _temperaturePending, | ||||||
|  |           onDec: () => _targetLowDown(entity), | ||||||
|  |           onInc: () => _targetLowUp(entity), | ||||||
|  |         ), | ||||||
|  |         Expanded( | ||||||
|  |           child: Container(height: 10.0), | ||||||
|  |         ) | ||||||
|  |       ]); | ||||||
|  |     } | ||||||
|  |     if ((entity.supportTargetTemperatureRange) && (entity.targetHigh != null)) { | ||||||
|  |       controls.add( | ||||||
|  |           TemperatureControlWidget( | ||||||
|  |             value: _tmpTargetHigh, | ||||||
|  |             active: _temperaturePending, | ||||||
|  |             onDec: () => _targetHighDown(entity), | ||||||
|  |             onInc: () => _targetHighUp(entity), | ||||||
|  |           ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     if (controls.isNotEmpty) { | ||||||
|  |       return Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: <Widget>[ | ||||||
|  |           Text("Target temperature range", style: Theme.of(context).textTheme.body1), | ||||||
|  |           Row( | ||||||
|  |             children: controls, | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container(width: 0.0, height: 0.0); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) { | ||||||
|  |     if (entity.supportTargetHumidity) { | ||||||
|  |       return Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: <Widget>[ | ||||||
|  |           Padding( | ||||||
|  |             padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||||
|  |             child: Text("Target humidity", style: Theme.of(context).textTheme.body1), | ||||||
|  |           ), | ||||||
|  |           UniversalSlider( | ||||||
|  |             leading: Text( | ||||||
|  |               "$_tmpTargetHumidity%", | ||||||
|  |               style: Theme.of(context).textTheme.display1, | ||||||
|  |             ), | ||||||
|  |             value: _tmpTargetHumidity, | ||||||
|  |             max: entity.maxHumidity, | ||||||
|  |             min: entity.minHumidity, | ||||||
|  |             onChanged: ((double val) { | ||||||
|  |               setState(() { | ||||||
|  |                 _changedHere = true; | ||||||
|  |                 _tmpTargetHumidity = val.roundToDouble(); | ||||||
|  |               }); | ||||||
|  |             }), | ||||||
|  |             onChangeEnd: (double v) => _setTargetHumidity(entity, v), | ||||||
|  |           ), | ||||||
|  |           Container( | ||||||
|  |             height: Sizes.rowPadding, | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return Container( | ||||||
|  |         width: 0.0, | ||||||
|  |         height: 0.0, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								lib/entities/climate/widgets/climate_state.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | |||||||
|  | part of '../../../main.dart'; | ||||||
|  |  | ||||||
|  | class ClimateStateWidget extends StatelessWidget { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final entityModel = EntityModel.of(context); | ||||||
|  |     final ClimateEntity entity = entityModel.entityWrapper.entity; | ||||||
|  |     String targetTemp = "-"; | ||||||
|  |     if ((entity.supportTargetTemperature) && (entity.temperature != null)) { | ||||||
|  |       targetTemp = "${entity.temperature}"; | ||||||
|  |     } else if ((entity.supportTargetTemperatureRange) && | ||||||
|  |         (entity.targetLow != null) && | ||||||
|  |         (entity.targetHigh != null)) { | ||||||
|  |       targetTemp = "${entity.targetLow} - ${entity.targetHigh}"; | ||||||
|  |     } | ||||||
|  |     String displayState = ''; | ||||||
|  |     if (entity.hvacAction != null) { | ||||||
|  |       displayState = "${entity.hvacAction} (${entity.displayState})"; | ||||||
|  |     } else { | ||||||
|  |       displayState = "${entity.displayState}"; | ||||||
|  |     } | ||||||
|  |     if (entity.presetMode != null) { | ||||||
|  |       displayState += " - ${entity.presetMode}"; | ||||||
|  |     } | ||||||
|  |     return Padding( | ||||||
|  |         padding: EdgeInsets.fromLTRB( | ||||||
|  |             0.0, 0.0, Sizes.rightWidgetPadding, 0.0), | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |           children: <Widget>[ | ||||||
|  |             Row( | ||||||
|  |               children: <Widget>[ | ||||||
|  |                 Text("$displayState", | ||||||
|  |                     textAlign: TextAlign.right, | ||||||
|  |                     style: Theme.of(context).textTheme.body2), | ||||||
|  |                 Text(" $targetTemp", | ||||||
|  |                     textAlign: TextAlign.right, | ||||||
|  |                     style: Theme.of(context).textTheme.body1) | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |             entity.currentTemperature != null ? | ||||||
|  |             Text("Currently: ${entity.currentTemperature}", | ||||||
|  |                 textAlign: TextAlign.right, | ||||||
|  |                 style: Theme.of(context).textTheme.subtitle | ||||||
|  |             ) : | ||||||
|  |             Container(height: 0.0,) | ||||||
|  |           ], | ||||||
|  |         )); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								lib/entities/climate/widgets/mode_selector.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | |||||||
|  | part of '../../../main.dart'; | ||||||
|  |  | ||||||
|  | class ModeSelectorWidget extends StatelessWidget { | ||||||
|  |  | ||||||
|  |   final String caption; | ||||||
|  |   final List options; | ||||||
|  |   final String value; | ||||||
|  |   final onChange; | ||||||
|  |   final EdgeInsets padding; | ||||||
|  |  | ||||||
|  |   ModeSelectorWidget({ | ||||||
|  |     Key key, | ||||||
|  |     @required this.caption, | ||||||
|  |     this.options: const [], | ||||||
|  |     this.value, | ||||||
|  |     @required this.onChange, | ||||||
|  |     this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Padding( | ||||||
|  |       padding: padding, | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: <Widget>[ | ||||||
|  |           Text("$caption", style: Theme.of(context).textTheme.body1), | ||||||
|  |           Row( | ||||||
|  |             children: <Widget>[ | ||||||
|  |               Expanded( | ||||||
|  |                 child: ButtonTheme( | ||||||
|  |                   alignedDropdown: true, | ||||||
|  |                   child: DropdownButton<String>( | ||||||
|  |                     value: value, | ||||||
|  |                     iconSize: 30.0, | ||||||
|  |                     isExpanded: true, | ||||||
|  |                     style: Theme.of(context).textTheme.title, | ||||||
|  |                     hint: Text("Select ${caption.toLowerCase()}"), | ||||||
|  |                     items: options.map((value) { | ||||||
|  |                       return new DropdownMenuItem<String>( | ||||||
|  |                         value: '$value', | ||||||
|  |                         child: Text('$value'), | ||||||
|  |                       ); | ||||||
|  |                     }).toList(), | ||||||
|  |                     onChanged: (mode) => onChange(mode), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ) | ||||||
|  |             ], | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								lib/entities/climate/widgets/mode_swicth.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | |||||||
|  | part of '../../../main.dart'; | ||||||
|  |  | ||||||
|  | class ModeSwitchWidget extends StatelessWidget { | ||||||
|  |  | ||||||
|  |   final String caption; | ||||||
|  |   final onChange; | ||||||
|  |   final bool value; | ||||||
|  |   final bool expanded; | ||||||
|  |   final EdgeInsets padding; | ||||||
|  |  | ||||||
|  |   ModeSwitchWidget({ | ||||||
|  |     Key key, | ||||||
|  |     @required this.caption, | ||||||
|  |     @required this.onChange, | ||||||
|  |     this.value, | ||||||
|  |     this.expanded: true, | ||||||
|  |     this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding) | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Padding( | ||||||
|  |       padding: this.padding, | ||||||
|  |       child: Row( | ||||||
|  |         children: <Widget>[ | ||||||
|  |           _buildCaption(context), | ||||||
|  |           Switch( | ||||||
|  |             onChanged: (value) => onChange(value), | ||||||
|  |             value: value ?? false, | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildCaption(BuildContext context) { | ||||||
|  |     Widget captionWidget = Text( | ||||||
|  |       "$caption", | ||||||
|  |       style: Theme.of(context).textTheme.body1, | ||||||
|  |     ); | ||||||
|  |     if (expanded) { | ||||||
|  |       return Expanded( | ||||||
|  |         child: captionWidget, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return captionWidget; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								lib/entities/climate/widgets/temperature_control_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | |||||||
|  | part of '../../../main.dart'; | ||||||
|  |  | ||||||
|  | class TemperatureControlWidget extends StatelessWidget { | ||||||
|  |   final double value; | ||||||
|  |   final bool active; | ||||||
|  |   final onInc; | ||||||
|  |   final onDec; | ||||||
|  |  | ||||||
|  |   TemperatureControlWidget( | ||||||
|  |       {Key key, | ||||||
|  |         @required this.value, | ||||||
|  |         @required this.onInc, | ||||||
|  |         @required this.onDec, | ||||||
|  |         //this.fontSize, | ||||||
|  |         this.active: false | ||||||
|  |       }) | ||||||
|  |       : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Row( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |       children: <Widget>[ | ||||||
|  |         Text( | ||||||
|  |           "$value", | ||||||
|  |           style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1, | ||||||
|  |         ), | ||||||
|  |         Column( | ||||||
|  |           children: <Widget>[ | ||||||
|  |             IconButton( | ||||||
|  |               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||||
|  |                   'mdi:chevron-up')), | ||||||
|  |               iconSize: 30.0, | ||||||
|  |               onPressed: () => onInc(), | ||||||
|  |             ), | ||||||
|  |             IconButton( | ||||||
|  |               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||||
|  |                   'mdi:chevron-down')), | ||||||
|  |               iconSize: 30.0, | ||||||
|  |               onPressed: () => onDec(), | ||||||
|  |             ) | ||||||
|  |           ], | ||||||
|  |         ) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||