Compare commits
	
		
			361 Commits
		
	
	
		
			beta/0.7.7
			...
			1.2.0-beta
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | f84728b948 | ||
|  | 26a62d341e | ||
|  | 772bddeb9a | ||
|  | 5b55940ccf | ||
|  | 7683d18e81 | ||
|  | d09afc37b5 | ||
|  | 1c686402d0 | ||
|  | 5f4a3fbdfc | ||
|  | 312ed99e9f | ||
|  | 25e6d51c17 | ||
|  | b501574bab | ||
|  | 53b31d8e90 | ||
|  | 6d80420a9b | ||
|  | e977054139 | ||
|  | 6367d38524 | ||
|  | f9b2d7d84c | ||
|  | 44c28ad106 | ||
|  | fec3c525e1 | ||
|  | b1bbed6d80 | ||
|  | 13878cfc51 | ||
|  | be49180205 | ||
|  | c4a0b16553 | ||
|  | caacd5e9f4 | ||
|  | 5fa28abb6c | ||
|  | e0a28c0b59 | ||
|  | 096e714a04 | ||
|  | 78893ea01f | ||
|  | 90efb29be5 | ||
|  | fca323c56b | ||
|  | e5fe6af5f3 | ||
|  | f0090d522d | ||
|  | edbfd8359b | ||
|  | 2702bb254a | ||
|  | ca7b6ed550 | ||
|  | fb00b5d9ff | ||
|  | 7ffba397ce | ||
|  | 1080076e3b | ||
|  | e295a36465 | ||
|  | 9a09a83dc6 | ||
|  | 95ca80949f | ||
|  | 80b5763530 | ||
|  | 9a5e35b024 | ||
|  | 4493975676 | ||
|  | 141a68faf7 | ||
|  | a8efe7dbb6 | ||
|  | 9608983994 | ||
|  | 8eb15ab9a4 | ||
|  | aac0cfbb56 | ||
|  | 343494ece0 | ||
|  | b1e5e73278 | ||
|  | 9b5a0068fd | ||
|  | aa26212ddd | ||
|  | 1c45f96706 | ||
|  | c2d5192c51 | ||
|  | 88ae80507c | ||
|  | 55868d1dfe | ||
|  | 92a1230267 | ||
|  | d3f99fb262 | ||
|  | 3fdf016c39 | ||
|  | 8ce0e8aafa | ||
|  | 54f6fb28ef | ||
|  | d53825f140 | ||
|  | ea7e0f04ce | ||
|  | cf75989447 | ||
|  | f27d55869b | ||
|  | a287f597ad | ||
|  | 0698950f3d | ||
|  | aa58559ba6 | ||
|  | 94acc67383 | ||
|  | 701e6a46df | ||
|  | 1ed56ce8f1 | ||
|  | a6d1baca77 | ||
|  | dbeda6ea68 | ||
|  | 3dca28a7da | ||
|  | da4264a409 | ||
|  | 302451e118 | ||
|  | d19dbd389b | ||
|  | 05ae954b30 | ||
|  | 840e266381 | ||
|  | 01525a2929 | ||
|  | 2eb2596f37 | ||
|  | 699cab3498 | ||
|  | 353c80b6bc | ||
|  | e4d1a4f823 | ||
|  | 78d6b38b92 | ||
|  | 1499a91ef7 | ||
|  | 9b7f7aa380 | ||
|  | 5683ab5158 | ||
|  | a20dfaf05e | ||
|  | 24d42c9597 | ||
|  | 9078ad81e8 | ||
|  | 7cba6c8a10 | ||
|  | c1f9c8c16d | ||
|  | 1d1d132b33 | ||
|  | e258b3bc2c | ||
|  | 13508ee92f | ||
|  | 4fbf58e707 | ||
|  | a3442f84ca | ||
|  | 6a6ab3b2cb | ||
|  | d9fa553e2f | ||
|  | cde5d9b912 | ||
|  | 3468446b5b | ||
|  | 326434273a | ||
|  | 470d3be946 | ||
|  | d1032be6a6 | ||
|  | cffac8e1f8 | ||
|  | 870bc25dd9 | ||
|  | de713024f6 | ||
|  | 4d4add4581 | ||
|  | 1670c8e505 | ||
|  | 55eb1b5125 | ||
|  | dbeaaaf91e | ||
|  | 8166d8ce6d | ||
|  | 35bcf0c1fa | ||
|  | 9c1d240962 | ||
|  | a76652b552 | ||
|  | a140f993d0 | ||
|  | ded60a2867 | ||
|  | b86602bcdb | ||
|  | 02ea45469f | ||
|  | 90105c3b09 | ||
|  | 3d828914cc | ||
|  | 8cd5776bc6 | ||
|  | 17ec73b176 | ||
|  | e7cce01ca9 | ||
|  | 6c73f5d979 | ||
|  | f59cb5afbf | ||
|  | 5629215229 | ||
|  | 45fb637d48 | ||
|  | 7c473eb1ca | ||
|  | b40880c85a | ||
|  | 30329ea3ba | ||
|  | ca10401bee | ||
|  | 814e0a8b00 | ||
|  | b5fbe7b86f | ||
|  | fc9b6f05c0 | ||
|  | eadae4374b | ||
|  | 711cb04dcf | ||
|  | 1d39b7fc7d | ||
|  | 2fa640433a | ||
|  | 2445dc7869 | ||
|  | e3e114fe94 | ||
|  | 7a1603b423 | ||
|  | 4b831821da | ||
|  | 1ec54953d7 | ||
|  | 61571600d1 | ||
|  | 07a097aa50 | ||
|  | ce1cebaf64 | ||
|  | faf6f73b2a | ||
|  | db3b5d941e | ||
|  | cc60dc2b21 | ||
|  | 8aa0e03187 | ||
|  | 4492a08d6b | ||
|  | 792c0d0c84 | ||
|  | 8221eceb78 | ||
|  | 12ba5598e4 | ||
|  | 536cbf9445 | ||
|  | a87943da27 | ||
|  | 3fddc3b5a7 | ||
|  | 5bc0b0868a | ||
|  | e9ad612fec | ||
|  | c62e045dae | ||
|  | 725ec9291d | ||
|  | 96c8338890 | ||
|  | 0996fb94da | ||
|  | 5de2431a0f | ||
|  | 163338ea75 | ||
|  | f28e5493dc | ||
|  | 01c0a08fa8 | ||
|  | 1c461d2449 | ||
|  | 915e8045a3 | ||
|  | f10fc7eec1 | ||
|  | 320bc677e0 | ||
|  | 46ca1948e2 | ||
|  | 7a0ce93cfd | ||
|  | 3c0bd68b0a | ||
|  | b4ad3061e4 | ||
|  | d6b1fbec24 | ||
|  | cacdd0d304 | ||
|  | e3e1fa3499 | ||
|  | 58842d1ebb | ||
|  | 101569d6ee | ||
|  | 8a180c4c0e | ||
|  | ba343fbd98 | ||
|  | 1d528df341 | ||
|  | 51ea0b0afa | ||
|  | 9dbb697e58 | ||
|  | 947558bb3d | ||
|  | 8ba4cc85d8 | ||
|  | 0f604a6ce6 | ||
|  | 7e48c6535f | ||
|  | 1d2a8b613b | ||
|  | 89e833eb33 | ||
|  | b65a68e0c4 | ||
|  | bfb24b9d11 | ||
|  | 0792cae2b1 | ||
|  | a85fb3d03b | ||
|  | ddb9a9d4e9 | ||
|  | 29ee360ec4 | ||
|  | c0faaafd04 | ||
|  | bc045344a5 | ||
|  | 7d746fd546 | ||
|  | 3ff55f181e | ||
|  | 187e12dd79 | ||
|  | 10daf2d952 | ||
|  | 31c6509d13 | ||
|  | cb74108814 | ||
|  | 9efded2139 | ||
|  | 96b3e7c739 | ||
|  | b029146bf3 | ||
|  | d715aaf5e5 | ||
|  | 0dc12963f0 | ||
|  | 4da3b40d55 | ||
|  | f7d05a57ad | ||
|  | df01599fe0 | ||
|  | 2c3335ebf3 | ||
|  | 05c1427aa8 | ||
|  | 02bfaf7db6 | ||
|  | f488c0810b | ||
|  | 8dbfb91234 | ||
|  | aee99e3925 | ||
|  | 50d3280803 | ||
|  | a90eb5c4db | ||
|  | 16c06a2d48 | ||
|  | 513bf85cae | ||
|  | 82d7aeba02 | ||
|  | 12f7cb86de | ||
|  | b65c885467 | ||
|  | 2a828a1289 | ||
|  | 291f12ba97 | ||
|  | 6afbd37d71 | ||
|  | 0e8869878f | ||
|  | 7c2cfe3215 | ||
|  | c376c0e952 | ||
|  | da5f663396 | ||
|  | 0e92418a33 | ||
|  | 2eef7cfe5e | ||
|  | de4e0bfb3a | ||
|  | 8bf2d31e72 | ||
|  | 2125c46143 | ||
|  | 5402eb84df | ||
|  | ad5aa0898f | ||
|  | 040d40b614 | ||
|  | 8e58f22c56 | ||
|  | c91695fbe5 | ||
|  | c43741da49 | ||
|  | f2563a0397 | ||
|  | fba4017819 | ||
|  | 5f23e108a1 | ||
|  | 68d14bd13d | ||
|  | 022622522f | ||
|  | 89513ca4e5 | ||
|  | a934ee2335 | ||
|  | 49aeea634f | ||
|  | e18b9ebe14 | ||
|  | 08ee3f3d80 | ||
|  | 62d07bf8b9 | ||
|  | ab398cbdc3 | ||
|  | 007d12719c | ||
|  | 524d195800 | ||
|  | 405de64249 | ||
|  | f53554702e | ||
|  | 379e1a4a7e | ||
|  | d6f7096055 | ||
|  | 37c721e4f6 | ||
|  | d94235ef6d | ||
|  | eb4184713f | ||
|  | a0a0cb4612 | ||
|  | f448a20784 | ||
|  | 36eff26862 | ||
|  | 5b2a1163b9 | ||
|  | e627a8b963 | ||
|  | 4432124e8c | ||
|  | b8ba3c59e9 | ||
|  | c40a496b6b | ||
|  | a7c3b46061 | ||
|  | dfbaaeb06b | ||
|  | f6ab20c6e8 | ||
|  | 7625099d74 | ||
|  | 32c8e76855 | ||
|  | 0aa2c974d5 | ||
|  | 9524c8587b | ||
|  | c075db8b1a | ||
|  | d0b7cc1929 | ||
|  | d8df32f140 | ||
|  | 293b5e0242 | ||
|  | 2f517a3ad5 | ||
|  | 56d8e389db | ||
|  | 1377843350 | ||
|  | 8e31eaf8bb | ||
|  | 5ced01463f | ||
|  | a3548455eb | ||
|  | c40fceea4f | ||
|  | 6ad3938a91 | ||
|  | bc642f81ad | ||
|  | 14ce608bbb | ||
|  | c4c67747c5 | ||
|  | 5b3ceecb0e | ||
|  | bf53e4b9df | ||
|  | 7e09d92fdf | ||
|  | 1ba9106d0b | ||
|  | d727a29991 | ||
|  | c5d617477f | ||
|  | 244a1984cc | ||
|  | b00b745f27 | ||
|  | 959ff21b9b | ||
|  | e6a7fd2dfe | ||
|  | 216276e5f3 | ||
|  | 3e6229cf3e | ||
|  | fc4cb80b74 | ||
|  | b907ff1e82 | ||
|  | 7536a52771 | ||
|  | 73a8c111d1 | ||
|  | 86a19eeec2 | ||
|  | fba4459977 | ||
|  | 06f994a827 | ||
|  | 35d8607484 | ||
|  | 2f4c06e9b5 | ||
|  | 92e008a380 | ||
|  | 14c272af92 | ||
|  | 710de9f2b8 | ||
|  | d9ad3b3083 | ||
|  | b2686cb105 | ||
|  | 959e89de2b | ||
|  | 6e448d3458 | ||
|  | 6695756727 | ||
|  | ed732e9b77 | ||
|  | f495a6affc | ||
|  | c8d7e1a95f | ||
|  | e1ca2638e3 | ||
|  | 01226cb9eb | ||
|  | 8a80d0c5d1 | ||
|  | f26f3e87c7 | ||
|  | b750417415 | ||
|  | 2c35dd7c21 | ||
|  | cff4a4feed | ||
|  | 62174b0651 | ||
|  | d3ea4210c1 | ||
|  | 1c782bf64d | ||
|  | bc96dab339 | ||
|  | 0f7179b944 | ||
|  | 1e3bfa8ff7 | ||
|  | 2bce86f905 | ||
|  | 0be00acc3a | ||
|  | 4e61adaeb1 | ||
|  | 49a8f08153 | ||
|  | ce15658462 | ||
|  | 16d73ba7dd | ||
|  | 9f3e3c1917 | ||
|  | f29e382a19 | ||
|  | 073562373a | ||
|  | 4298ebcd66 | ||
|  | a121295bef | ||
|  | 9303e4c0a5 | ||
|  | 831fc98ab1 | ||
|  | 2003005e56 | ||
|  | fda8fb7182 | ||
|  | cf6039b279 | ||
|  | 41e552dce5 | ||
|  | 90043b5806 | ||
|  | 9eb74b5a8d | 
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -15,7 +15,9 @@ build/ | |||||||
| .settings/ | .settings/ | ||||||
|  |  | ||||||
| flutter_export_environment.sh | flutter_export_environment.sh | ||||||
|  | .flutter-plugins-dependencies | ||||||
|  |  | ||||||
| key.properties | key.properties | ||||||
| premium_features_manager.class.dart | .secrets.dart | ||||||
| pubspec.lock | pubspec.lock | ||||||
|  | google-services.json | ||||||
|   | |||||||
| @@ -4,9 +4,5 @@ ENV ANDROID_HOME=/workspace/android-sdk \ | |||||||
|     FLUTTER_ROOT=/workspace/flutter \ |     FLUTTER_ROOT=/workspace/flutter \ | ||||||
|     FLUTTER_HOME=/workspace/flutter |     FLUTTER_HOME=/workspace/flutter | ||||||
|  |  | ||||||
| USER root | RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \ | ||||||
|  |              && sdk install java 8.0.242.j9-adpt" | ||||||
| RUN apt-get update && \ |  | ||||||
|     apt-get -y install build-essential libkrb5-dev gcc make gradle openjdk-8-jdk && \ |  | ||||||
|     apt-get clean && \ |  | ||||||
|     apt-get -y autoremove |  | ||||||
| @@ -3,12 +3,12 @@ image: | |||||||
|  |  | ||||||
| tasks: | tasks: | ||||||
| - before: | | - before: | | ||||||
|     export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH |     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 |     mkdir -p /home/gitpod/.android | ||||||
|     touch /home/gitpod/.android/repositories.cfg |     touch /home/gitpod/.android/repositories.cfg | ||||||
|   init: | |   init: | | ||||||
|     echo "Installing Flutter SDK..." |     echo "Installing Flutter SDK..." | ||||||
|     cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.4-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz |     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..." |     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 |     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" |     /workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3" | ||||||
| @@ -18,7 +18,6 @@ tasks: | |||||||
|     flutter doctor --android-licenses |     flutter doctor --android-licenses | ||||||
|     flutter pub get |     flutter pub get | ||||||
|   command: | |   command: | | ||||||
|     flutter pub upgrade |  | ||||||
|     echo "Ready to go!" |     echo "Ready to go!" | ||||||
|     flutter doctor |     flutter doctor | ||||||
| vscode: | vscode: | ||||||
|   | |||||||
							
								
								
									
										
											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
									
								
							
							
						
						
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,15 +1,18 @@ | |||||||
| [](https://gitpod.io/#https://github.com/estevez-dev/ha_client) |  | ||||||
| # HA Client | # HA Client | ||||||
| ## Native Android client for Home Assistant | ## Native Android client for Home Assistant | ||||||
| ### With notifications and Lovelace UI support | ### With actionable notifications, location tracking and Lovelace UI support | ||||||
|  |  | ||||||
| Visit [homemade.systems](http://ha-client.homemade.systems/) for more info. | 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) | Download the app from [Google Play](https://play.google.com/store/apps/details?id=com.keyboardcrumbs.haclient) | ||||||
|  |  | ||||||
| Discuss it on [Spectrum.chat](https://spectrum.chat/ha-client) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android) | Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android) | ||||||
|  |  | ||||||
| #### Pre-release CI build | [](https://gitpod.io/#https://github.com/estevez-dev/ha_client)  | ||||||
| [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build) |  | ||||||
| #### Beta CI build | #### Last release build status | ||||||
| [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) | [](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) | ||||||
|  |  | ||||||
|  | #### Projects used | ||||||
|  | - [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski) | ||||||
|  | - [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE) | ||||||
|   | |||||||
| @@ -78,10 +78,13 @@ flutter { | |||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|     implementation 'com.google.firebase:firebase-core:16.0.8' |     implementation 'com.google.firebase:firebase-analytics:17.2.2' | ||||||
|  |     implementation 'com.google.firebase:firebase-messaging:20.2.0' | ||||||
|  |     implementation 'androidx.work:work-runtime:2.3.4' | ||||||
|     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' | apply plugin: 'com.google.gms.google-services' | ||||||
|   | |||||||
| @@ -1,64 +0,0 @@ | |||||||
| { |  | ||||||
|   "project_info": { |  | ||||||
|     "project_number": "441874387819", |  | ||||||
|     "firebase_url": "https://ha-client-c73c4.firebaseio.com", |  | ||||||
|     "project_id": "ha-client-c73c4", |  | ||||||
|     "storage_bucket": "ha-client-c73c4.appspot.com" |  | ||||||
|   }, |  | ||||||
|   "client": [ |  | ||||||
|     { |  | ||||||
|       "client_info": { |  | ||||||
|         "mobilesdk_app_id": "1:441874387819:android:92c7efc892dc3d45", |  | ||||||
|         "android_client_info": { |  | ||||||
|           "package_name": "com.keyboardcrumbs.haclient" |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       "oauth_client": [ |  | ||||||
|         { |  | ||||||
|           "client_id": "441874387819-uqmkibhf361828od1982o2jhl0n3m0ov.apps.googleusercontent.com", |  | ||||||
|           "client_type": 1, |  | ||||||
|           "android_info": { |  | ||||||
|             "package_name": "com.keyboardcrumbs.haclient", |  | ||||||
|             "certificate_hash": "bebe4d970fbebf0bff2c93244fdc7fcbcefb3470" |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "client_id": "441874387819-5q7vmimci4s2jl3v0ncugv1ocp4m48nb.apps.googleusercontent.com", |  | ||||||
|           "client_type": 1, |  | ||||||
|           "android_info": { |  | ||||||
|             "package_name": "com.keyboardcrumbs.haclient", |  | ||||||
|             "certificate_hash": "0ea12348468be44bc2aa5792ee7e8924c633da81" |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "client_id": "441874387819-joi8plo5345ebt8i1dug27u2aenv5tg7.apps.googleusercontent.com", |  | ||||||
|           "client_type": 1, |  | ||||||
|           "android_info": { |  | ||||||
|             "package_name": "com.keyboardcrumbs.haclient", |  | ||||||
|             "certificate_hash": "fcbc805d965ccf6a4d5417398d191edc9c9890b0" |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           "client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com", |  | ||||||
|           "client_type": 3 |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "api_key": [ |  | ||||||
|         { |  | ||||||
|           "current_key": "AIzaSyBsl9cjBY633IrdrTyCsLFlO9lfsYJ0OJU" |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       "services": { |  | ||||||
|         "appinvite_service": { |  | ||||||
|           "other_platform_oauth_client": [ |  | ||||||
|             { |  | ||||||
|               "client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com", |  | ||||||
|               "client_type": 3 |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   "configuration_version": "1" |  | ||||||
| } |  | ||||||
| @@ -1,12 +1,12 @@ | |||||||
| <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" | ||||||
|  |     android:installLocation="auto"> | ||||||
|  |  | ||||||
|     <uses-feature android:name="android.hardware.touchscreen" |     <uses-feature android:name="android.hardware.touchscreen" | ||||||
|         android:required="false" /> |         android:required="false" /> | ||||||
|     <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.VIBRATE" /> | ||||||
|     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> |     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||||
|     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> |  | ||||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> |     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | ||||||
|     <uses-permission android:name="android.permission.WAKE_LOCK"/> |     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||||
|  |  | ||||||
| @@ -17,11 +17,15 @@ | |||||||
|          additional functionality it is fine to subclass or reimplement |          additional functionality it is fine to subclass or reimplement | ||||||
|          FlutterApplication and put your custom class here. --> |          FlutterApplication and put your custom class here. --> | ||||||
|     <application |     <application | ||||||
|         android:name=".Application" |  | ||||||
|         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"> |         android:usesCleartextTraffic="true"> | ||||||
|  |  | ||||||
|  |         <meta-data | ||||||
|  |             android:name="flutterEmbedding" | ||||||
|  |             android:value="2" /> | ||||||
|  |  | ||||||
|         <meta-data |         <meta-data | ||||||
|             android:name="com.google.firebase.messaging.default_notification_channel_id" |             android:name="com.google.firebase.messaging.default_notification_channel_id" | ||||||
|             android:value="ha_notify" /> |             android:value="ha_notify" /> | ||||||
| @@ -33,31 +37,37 @@ | |||||||
|             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" /> | ||||||
|             <intent-filter> |             <meta-data | ||||||
|                 <action android:name="FLUTTER_NOTIFICATION_CLICK" /> |                 android:name="io.flutter.embedding.android.NormalTheme" | ||||||
|                 <category android:name="android.intent.category.DEFAULT" /> |                 android:resource="@style/NormalTheme" /> | ||||||
|             </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> | ||||||
|             <intent-filter android:autoVerify="true"> |  | ||||||
|                 <action android:name="android.intent.action.VIEW" /> |  | ||||||
|                 <category android:name="android.intent.category.DEFAULT" /> |  | ||||||
|                 <category android:name="android.intent.category.BROWSABLE" /> |  | ||||||
|                 <data |  | ||||||
|                 android:scheme="haclient" |  | ||||||
|                 android:host="auth" /> |  | ||||||
|             </intent-filter> |  | ||||||
|         </activity> |         </activity> | ||||||
|  |  | ||||||
|  |         <service | ||||||
|  |             android:name=".MessagingService" | ||||||
|  |             android:exported="false"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="com.google.firebase.MESSAGING_EVENT" /> | ||||||
|  |             </intent-filter> | ||||||
|  |         </service> | ||||||
|  |         <receiver android:name=".NotificationActionReceiver"  android:exported="true"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.intent.action.BOOT_COMPLETED"/> | ||||||
|  |                 <action android:name="android.intent.action.INPUT_METHOD_CHANGED" /> | ||||||
|  |             </intent-filter> | ||||||
|  |         </receiver> | ||||||
|  |         <receiver android:name=".NextAlarmBroadcastReceiver"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.intent.action.BOOT_COMPLETED" /> | ||||||
|  |                 <action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" /> | ||||||
|  |             </intent-filter> | ||||||
|  |         </receiver> | ||||||
|         <service |         <service | ||||||
|             android:name="io.flutter.plugins.androidalarmmanager.AlarmService" |             android:name="io.flutter.plugins.androidalarmmanager.AlarmService" | ||||||
|             android:permission="android.permission.BIND_JOB_SERVICE" |             android:permission="android.permission.BIND_JOB_SERVICE" | ||||||
| @@ -69,7 +79,7 @@ | |||||||
|             android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" |             android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" | ||||||
|             android:enabled="false"> |             android:enabled="false"> | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|                 <action android:name="android.intent.action.BOOT_COMPLETED"></action> |                 <action android:name="android.intent.action.BOOT_COMPLETED"/> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </receiver> |         </receiver> | ||||||
|     </application> |     </application> | ||||||
|   | |||||||
| @@ -1,20 +0,0 @@ | |||||||
| package com.keyboardcrumbs.hassclient; |  | ||||||
|  |  | ||||||
| import io.flutter.app.FlutterApplication; |  | ||||||
| import io.flutter.plugin.common.PluginRegistry; |  | ||||||
| import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback; |  | ||||||
| import io.flutter.plugins.GeneratedPluginRegistrant; |  | ||||||
| import be.tramckrijte.workmanager.WorkmanagerPlugin; |  | ||||||
|  |  | ||||||
| public class Application extends FlutterApplication implements PluginRegistrantCallback { |  | ||||||
|     @Override |  | ||||||
|     public void onCreate() { |  | ||||||
|         super.onCreate(); |  | ||||||
|         WorkmanagerPlugin.setPluginRegistrantCallback(this); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void registerWith(PluginRegistry registry) { |  | ||||||
|         GeneratedPluginRegistrant.registerWith(registry); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,15 +1,71 @@ | |||||||
| 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; | ||||||
| import io.flutter.plugins.share.FlutterShareReceiverActivity; |  | ||||||
|  |  | ||||||
| public class MainActivity extends FlutterShareReceiverActivity { | import android.content.Context; | ||||||
|  |  | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.content.SharedPreferences.Editor; | ||||||
|  | import android.os.Bundle; | ||||||
|  |  | ||||||
|  | import io.flutter.plugin.common.MethodCall; | ||||||
|  | import io.flutter.plugin.common.MethodChannel; | ||||||
|  |  | ||||||
|  | import com.google.android.gms.tasks.OnCompleteListener; | ||||||
|  | import com.google.android.gms.tasks.Task; | ||||||
|  | import com.google.android.gms.common.GoogleApiAvailability; | ||||||
|  | import com.google.android.gms.common.ConnectionResult; | ||||||
|  | import com.google.firebase.iid.FirebaseInstanceId; | ||||||
|  | import com.google.firebase.iid.InstanceIdResult; | ||||||
|  | import com.google.firebase.messaging.FirebaseMessaging; | ||||||
|  |  | ||||||
|  | public class MainActivity extends FlutterActivity { | ||||||
|  |  | ||||||
|  |     private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native"; | ||||||
|  |    | ||||||
|  |     @Override | ||||||
|  |     public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { | ||||||
|  |         GeneratedPluginRegistrant.registerWith(flutterEngine); | ||||||
|  |         new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler( | ||||||
|  |             new MethodChannel.MethodCallHandler() { | ||||||
|  |                 @Override | ||||||
|  |                 public void onMethodCall(MethodCall call, MethodChannel.Result result) { | ||||||
|  |                     Context context = getActivity(); | ||||||
|  |                     if (call.method.equals("getFCMToken")) { | ||||||
|  |                         if (checkPlayServices()) { | ||||||
|  |                             FirebaseInstanceId.getInstance().getInstanceId() | ||||||
|  |                             .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() { | ||||||
|  |                                 @Override | ||||||
|  |                                 public void onComplete(@NonNull Task<InstanceIdResult> task) { | ||||||
|  |                                     if (task.isSuccessful()) { | ||||||
|  |                                         String token = task.getResult().getToken(); | ||||||
|  |                                         UpdateTokenTask updateTokenTask = new UpdateTokenTask(context); | ||||||
|  |                                         updateTokenTask.execute(token); | ||||||
|  |                                         result.success(token); | ||||||
|  |                                     } else { | ||||||
|  |                                         result.error("fcm_error", task.getException().getMessage(), null); | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             }); | ||||||
|  |                         } else { | ||||||
|  |                             result.error("google_play_service_error", "Google Play Services unavailable", null); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean checkPlayServices() { | ||||||
|  |         return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|     GeneratedPluginRegistrant.registerWith(this); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,152 @@ | |||||||
|  | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
|  | import java.util.Map; | ||||||
|  | import java.net.URL; | ||||||
|  | import java.net.URLConnection; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.InputStream; | ||||||
|  |  | ||||||
|  | import android.app.NotificationChannel; | ||||||
|  | import android.app.NotificationManager; | ||||||
|  | import android.app.PendingIntent; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.media.RingtoneManager; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Build; | ||||||
|  | import androidx.core.app.NotificationCompat; | ||||||
|  |  | ||||||
|  | import com.google.firebase.messaging.FirebaseMessagingService; | ||||||
|  | import com.google.firebase.messaging.RemoteMessage; | ||||||
|  |  | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.graphics.BitmapFactory; | ||||||
|  | import android.webkit.URLUtil; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | public class MessagingService extends FirebaseMessagingService { | ||||||
|  |  | ||||||
|  |     private static final String TAG = "MessagingService"; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onMessageReceived(RemoteMessage remoteMessage) { | ||||||
|  |         Map<String, String> data = remoteMessage.getData(); | ||||||
|  |         if (data.size() > 0) { | ||||||
|  |            if (data.containsKey("body") || data.containsKey("title")) { | ||||||
|  |                 sendNotification(data); | ||||||
|  |            } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onNewToken(String token) { | ||||||
|  |         UpdateTokenTask updateTokenTask = new UpdateTokenTask(this); | ||||||
|  |         updateTokenTask.execute(token); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void sendNotification(Map<String, String> data) { | ||||||
|  |         String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription; | ||||||
|  |         boolean autoCancel; | ||||||
|  |         if (!data.containsKey("channelId")) { | ||||||
|  |             channelId = "ha_notify"; | ||||||
|  |             channelDescription = "Default notification channel"; | ||||||
|  |         } else { | ||||||
|  |             channelId = data.get("channelId"); | ||||||
|  |             channelDescription = channelId; | ||||||
|  |         } | ||||||
|  |         if (!data.containsKey("body")) { | ||||||
|  |             messageBody = ""; | ||||||
|  |         } else { | ||||||
|  |             messageBody = data.get("body"); | ||||||
|  |         } | ||||||
|  |         if (!data.containsKey("title")) { | ||||||
|  |             messageTitle = "HA Client"; | ||||||
|  |         } else { | ||||||
|  |             messageTitle = data.get("title"); | ||||||
|  |         } | ||||||
|  |         if (!data.containsKey("tag")) { | ||||||
|  |             nTag = String.valueOf(System.currentTimeMillis()); | ||||||
|  |         } else { | ||||||
|  |             nTag = data.get("tag"); | ||||||
|  |         } | ||||||
|  |         if (data.containsKey("dismiss")) { | ||||||
|  |             try { | ||||||
|  |                 boolean dismiss = Boolean.parseBoolean(data.get("dismiss")); | ||||||
|  |                 if (dismiss) { | ||||||
|  |                     NotificationManager notificationManager = | ||||||
|  |                         (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | ||||||
|  |                     notificationManager.cancel(nTag, 0); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 //nope | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (data.containsKey("autoDismiss")) { | ||||||
|  |             try { | ||||||
|  |                 autoCancel = Boolean.parseBoolean(data.get("autoDismiss")); | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 autoCancel = true; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             autoCancel = true; | ||||||
|  |         } | ||||||
|  |         imageUrl = data.get("image"); | ||||||
|  |         Intent intent = new Intent(this, MainActivity.class); | ||||||
|  |         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); | ||||||
|  |         PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, | ||||||
|  |                 PendingIntent.FLAG_ONE_SHOT); | ||||||
|  |         Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); | ||||||
|  |         NotificationCompat.Builder notificationBuilder = | ||||||
|  |                 new NotificationCompat.Builder(this, channelId) | ||||||
|  |                         .setSmallIcon(R.drawable.mini_icon) | ||||||
|  |                         .setContentTitle(messageTitle) | ||||||
|  |                         .setContentText(messageBody) | ||||||
|  |                         .setAutoCancel(autoCancel) | ||||||
|  |                         .setSound(defaultSoundUri) | ||||||
|  |                         .setContentIntent(pendingIntent); | ||||||
|  |         if (URLUtil.isValidUrl(imageUrl)) { | ||||||
|  |             Bitmap image = getBitmapFromURL(imageUrl); | ||||||
|  |             if (image != null) { | ||||||
|  |                 notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon))); | ||||||
|  |                 notificationBuilder.setLargeIcon(image); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         for (int i = 1; i <= 3; i++) { | ||||||
|  |             if (data.containsKey("action" + i)) { | ||||||
|  |                 Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class); | ||||||
|  |                 if (autoCancel) { | ||||||
|  |                     broadcastIntent.putExtra("tag", nTag); | ||||||
|  |                 } | ||||||
|  |                 broadcastIntent.putExtra("actionData", data.get("action" + i + "_data")); | ||||||
|  |                 PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT); | ||||||
|  |                 notificationBuilder.addAction(R.drawable.mini_icon, data.get("action" + i), actionIntent); | ||||||
|  |             }    | ||||||
|  |         } | ||||||
|  |         NotificationManager notificationManager = | ||||||
|  |                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | ||||||
|  |  | ||||||
|  |         // Since android Oreo notification channel is needed. | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||||
|  |             NotificationChannel channel = new NotificationChannel(channelId, | ||||||
|  |                     channelDescription, | ||||||
|  |                     NotificationManager.IMPORTANCE_HIGH); | ||||||
|  |             notificationManager.createNotificationChannel(channel); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         notificationManager.notify(nTag, 0 /* ID of notification */, notificationBuilder.build()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Bitmap getBitmapFromURL(String imageUrl) { | ||||||
|  |         try { | ||||||
|  |             URL url = new URL(imageUrl); | ||||||
|  |             URLConnection connection = url.openConnection(); | ||||||
|  |             connection.setDoInput(true); | ||||||
|  |             connection.connect(); | ||||||
|  |             InputStream input = connection.getInputStream(); | ||||||
|  |             return BitmapFactory.decodeStream(input); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,51 @@ | |||||||
|  | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
|  | import android.app.AlarmManager; | ||||||
|  | import android.content.BroadcastReceiver; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  |  | ||||||
|  | import androidx.work.BackoffPolicy; | ||||||
|  | import androidx.work.Constraints; | ||||||
|  | import androidx.work.ExistingWorkPolicy; | ||||||
|  | import androidx.work.NetworkType; | ||||||
|  | import androidx.work.OneTimeWorkRequest; | ||||||
|  | import androidx.work.WorkManager; | ||||||
|  | import androidx.work.WorkRequest; | ||||||
|  |  | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | public class NextAlarmBroadcastReceiver extends BroadcastReceiver { | ||||||
|  |  | ||||||
|  |     private static final String TAG = "NextAlarmReceiver"; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onReceive(Context context, Intent intent) { | ||||||
|  |         if (intent == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final boolean isBootIntent = Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction()); | ||||||
|  |         final boolean isNextAlarmIntent = AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equalsIgnoreCase(intent.getAction()); | ||||||
|  |         if (!isBootIntent && !isNextAlarmIntent) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         Constraints constraints = new Constraints.Builder() | ||||||
|  |                 .setRequiredNetworkType(NetworkType.CONNECTED) | ||||||
|  |                 .build(); | ||||||
|  |  | ||||||
|  |         OneTimeWorkRequest uploadWorkRequest = | ||||||
|  |                 new OneTimeWorkRequest.Builder(UpdateNextAlarmWorker.class) | ||||||
|  |                         .setBackoffCriteria( | ||||||
|  |                                 BackoffPolicy.EXPONENTIAL, | ||||||
|  |                                 10, | ||||||
|  |                                 TimeUnit.SECONDS) | ||||||
|  |                         .setConstraints(constraints) | ||||||
|  |                         .build(); | ||||||
|  |  | ||||||
|  |         WorkManager | ||||||
|  |                 .getInstance(context) | ||||||
|  |                 .enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,73 @@ | |||||||
|  | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
|  | import android.app.AlarmManager; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.content.BroadcastReceiver; | ||||||
|  | import android.content.Intent; | ||||||
|  |  | ||||||
|  | import android.app.NotificationManager; | ||||||
|  |  | ||||||
|  | import android.webkit.URLUtil; | ||||||
|  |  | ||||||
|  | import org.json.JSONObject; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  |  | ||||||
|  | public class NotificationActionReceiver extends BroadcastReceiver { | ||||||
|  |  | ||||||
|  |     private static final String TAG = "NotificationAction"; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onReceive(Context context, Intent intent) { | ||||||
|  |         if (intent == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         String rawActionData = intent.getStringExtra("actionData"); | ||||||
|  |         if (intent.hasExtra("tag")) { | ||||||
|  |             String notificationTag = intent.getStringExtra("tag"); | ||||||
|  |             NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); | ||||||
|  |             notificationManager.cancel(notificationTag, 0); | ||||||
|  |         } | ||||||
|  |         SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE); | ||||||
|  |         String webhookId = prefs.getString("flutter.app-webhook-id", null); | ||||||
|  |         if (webhookId != null) { | ||||||
|  |             try { | ||||||
|  |                 String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") + | ||||||
|  |                     "://" + | ||||||
|  |                     prefs.getString("flutter.hassio-domain", "") + | ||||||
|  |                     ":" + | ||||||
|  |                     prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId; | ||||||
|  |                 JSONObject actionData = new JSONObject(rawActionData); | ||||||
|  |                 if (URLUtil.isValidUrl(requestUrl)) { | ||||||
|  |                     JSONObject dataToSend = new JSONObject(); | ||||||
|  |                     JSONObject requestData = new JSONObject(); | ||||||
|  |                     if (actionData.getString("action").equals("call-service")) { | ||||||
|  |                         dataToSend.put("type", "call_service"); | ||||||
|  |                         requestData.put("domain", actionData.getString("service").split("\\.")[0]); | ||||||
|  |                         requestData.put("service", actionData.getString("service").split("\\.")[1]); | ||||||
|  |                         if (actionData.has("service_data")) { | ||||||
|  |                             requestData.put("service_data", actionData.get("service_data")); | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         dataToSend.put("type", "fire_event"); | ||||||
|  |                         requestData.put("event_type", "ha_client_event"); | ||||||
|  |                         JSONObject eventData = new JSONObject(); | ||||||
|  |                         eventData.put("action", actionData.getString("action")); | ||||||
|  |                         requestData.put("event_data", eventData); | ||||||
|  |                     } | ||||||
|  |                     dataToSend.put("data", requestData); | ||||||
|  |                     String stringRequest = dataToSend.toString(); | ||||||
|  |                     SendTask sendTask = new SendTask(); | ||||||
|  |                     sendTask.execute(requestUrl, stringRequest); | ||||||
|  |                 } else { | ||||||
|  |                     Log.w(TAG, "Invalid HA url"); | ||||||
|  |                 } | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 Log.e(TAG, "Error handling notification action", e);     | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Log.w(TAG, "Webhook id not found"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
|  | import android.util.Log; | ||||||
|  | import android.os.AsyncTask; | ||||||
|  |  | ||||||
|  | import java.net.URL; | ||||||
|  | import java.net.HttpURLConnection; | ||||||
|  | import java.io.OutputStream; | ||||||
|  |  | ||||||
|  | public class SendTask extends AsyncTask<String, String, String> { | ||||||
|  |  | ||||||
|  |     private static final String TAG = "SendTask"; | ||||||
|  |  | ||||||
|  |     public SendTask(){ | ||||||
|  |         //set context variables if required | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onPreExecute() { | ||||||
|  |         super.onPreExecute(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected String doInBackground(String... params) { | ||||||
|  |         String urlString = params[0]; | ||||||
|  |         String data = params[1]; | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             URL url = new URL(urlString); | ||||||
|  |             HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); | ||||||
|  |             urlConnection.setRequestMethod("POST"); | ||||||
|  |             urlConnection.setRequestProperty("Content-Type", "application/json"); | ||||||
|  |             urlConnection.setDoOutput(true); | ||||||
|  |             byte[] outputBytes = data.getBytes("UTF-8"); | ||||||
|  |             OutputStream os = urlConnection.getOutputStream(); | ||||||
|  |             os.write(outputBytes); | ||||||
|  |  | ||||||
|  |             int responseCode = urlConnection.getResponseCode(); | ||||||
|  |  | ||||||
|  |             urlConnection.disconnect(); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Log.e(TAG, "Error sending data", e);  | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,119 @@ | |||||||
|  | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
|  | import android.app.AlarmManager; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.webkit.URLUtil; | ||||||
|  |  | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.work.Worker; | ||||||
|  | import androidx.work.WorkerParameters; | ||||||
|  |  | ||||||
|  | import org.json.JSONArray; | ||||||
|  | import org.json.JSONObject; | ||||||
|  |  | ||||||
|  | import java.io.OutputStream; | ||||||
|  | import java.net.HttpURLConnection; | ||||||
|  | import java.net.URL; | ||||||
|  | import java.text.SimpleDateFormat; | ||||||
|  | import java.util.Calendar; | ||||||
|  | import java.util.Locale; | ||||||
|  |  | ||||||
|  | public class UpdateNextAlarmWorker extends Worker { | ||||||
|  |  | ||||||
|  |     private Context currentContext; | ||||||
|  |     private static final String TAG = "NextAlarmWorker"; | ||||||
|  |     private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH); | ||||||
|  |     private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); | ||||||
|  |     private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH); | ||||||
|  |  | ||||||
|  |     public UpdateNextAlarmWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { | ||||||
|  |         super(context, workerParams); | ||||||
|  |         currentContext = context; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Result doWork() { | ||||||
|  |         final AlarmManager alarmManager; | ||||||
|  |         if (android.os.Build.VERSION.SDK_INT >= 23) { | ||||||
|  |             alarmManager = currentContext.getSystemService(AlarmManager.class); | ||||||
|  |         } else { | ||||||
|  |             alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock(); | ||||||
|  |  | ||||||
|  |         SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE); | ||||||
|  |         String webhookId = prefs.getString("flutter.app-webhook-id", null); | ||||||
|  |         if (webhookId != null) { | ||||||
|  |             try { | ||||||
|  |                 String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") + | ||||||
|  |                         "://" + | ||||||
|  |                         prefs.getString("flutter.hassio-domain", "") + | ||||||
|  |                         ":" + | ||||||
|  |                         prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId; | ||||||
|  |                 JSONObject dataToSend = new JSONObject(); | ||||||
|  |                 if (URLUtil.isValidUrl(requestUrl)) { | ||||||
|  |                     dataToSend.put("type", "update_sensor_states"); | ||||||
|  |                     JSONArray dataArray = new JSONArray(); | ||||||
|  |                     JSONObject sensorData = new JSONObject(); | ||||||
|  |                     JSONObject sensorAttrs = new JSONObject(); | ||||||
|  |                     sensorData.put("unique_id", "next_alarm"); | ||||||
|  |                     sensorData.put("type", "sensor"); | ||||||
|  |                     final long triggerTimestamp; | ||||||
|  |                     if (alarmClockInfo != null) { | ||||||
|  |                         triggerTimestamp = alarmClockInfo.getTriggerTime(); | ||||||
|  |                         final Calendar calendar = Calendar.getInstance(); | ||||||
|  |                         calendar.setTimeInMillis(triggerTimestamp); | ||||||
|  |                         sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime())); | ||||||
|  |                         sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime())); | ||||||
|  |                         sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime())); | ||||||
|  |                         sensorAttrs.put("timestamp", triggerTimestamp); | ||||||
|  |                     } else { | ||||||
|  |                         sensorData.put("state", ""); | ||||||
|  |                         sensorAttrs.put("date", ""); | ||||||
|  |                         sensorAttrs.put("time", ""); | ||||||
|  |                         sensorAttrs.put("timestamp", 0); | ||||||
|  |                     } | ||||||
|  |                     sensorData.put("icon", "mdi:alarm"); | ||||||
|  |                     sensorData.put("attributes", sensorAttrs); | ||||||
|  |                     dataArray.put(0, sensorData); | ||||||
|  |                     dataToSend.put("data", dataArray); | ||||||
|  |  | ||||||
|  |                     String stringRequest = dataToSend.toString(); | ||||||
|  |                     try { | ||||||
|  |                         URL url = new URL(requestUrl); | ||||||
|  |                         HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); | ||||||
|  |                         urlConnection.setRequestMethod("POST"); | ||||||
|  |                         urlConnection.setRequestProperty("Content-Type", "application/json"); | ||||||
|  |                         urlConnection.setDoOutput(true); | ||||||
|  |                         byte[] outputBytes = stringRequest.getBytes("UTF-8"); | ||||||
|  |                         OutputStream os = urlConnection.getOutputStream(); | ||||||
|  |                         os.write(outputBytes); | ||||||
|  |  | ||||||
|  |                         int responseCode = urlConnection.getResponseCode(); | ||||||
|  |                         urlConnection.disconnect(); | ||||||
|  |                         if (responseCode >= 300) { | ||||||
|  |                             return Result.retry(); | ||||||
|  |                         } | ||||||
|  |                     } catch (Exception e) { | ||||||
|  |                         Log.e(TAG, "Error sending data", e); | ||||||
|  |                         return Result.retry(); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Log.w(TAG, "Invalid HA url"); | ||||||
|  |                     return Result.failure(); | ||||||
|  |                 } | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 Log.e(TAG, "Error setting next alarm", e); | ||||||
|  |                 return Result.failure(); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Log.w(TAG, "Webhook id not found"); | ||||||
|  |             return Result.failure(); | ||||||
|  |         } | ||||||
|  |         return Result.success(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | package com.keyboardcrumbs.hassclient; | ||||||
|  |  | ||||||
|  | import android.util.Log; | ||||||
|  | import android.os.AsyncTask; | ||||||
|  |  | ||||||
|  | import java.net.URL; | ||||||
|  | import java.net.HttpURLConnection; | ||||||
|  | import java.io.OutputStream; | ||||||
|  |  | ||||||
|  | import android.webkit.URLUtil; | ||||||
|  |  | ||||||
|  | import org.json.JSONObject; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.content.Context; | ||||||
|  | import java.lang.ref.WeakReference; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | public class UpdateTokenTask extends AsyncTask<String, String, String> { | ||||||
|  |  | ||||||
|  |     private static final String TAG = "UpdateTokenTask"; | ||||||
|  |  | ||||||
|  |     private WeakReference<Context> contextRef; | ||||||
|  |  | ||||||
|  |     public UpdateTokenTask(Context context){ | ||||||
|  |         contextRef = new WeakReference<>(context); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onPreExecute() { | ||||||
|  |         super.onPreExecute(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected String doInBackground(String... params) { | ||||||
|  |         Log.d(TAG, "Updating push token"); | ||||||
|  |         Context context = contextRef.get(); | ||||||
|  |         if (context != null) { | ||||||
|  |             String token = params[0]; | ||||||
|  |             SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE); | ||||||
|  |             SharedPreferences.Editor editor = prefs.edit(); | ||||||
|  |             editor.putString("flutter.npush-token", token); | ||||||
|  |             editor.commit(); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/blank_icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 461 B | 
							
								
								
									
										27
									
								
								android/app/src/main/res/drawable/ic_launcher_foreground.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="108dp" | ||||||
|  |     android:height="108dp" | ||||||
|  |     android:viewportWidth="108" | ||||||
|  |     android:viewportHeight="108"> | ||||||
|  |  | ||||||
|  |     <group> | ||||||
|  |         <clip-path | ||||||
|  |             android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" /> | ||||||
|  |         <path | ||||||
|  |             android:fillColor="#709ac1" | ||||||
|  |             android:fillAlpha="0" | ||||||
|  |             android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" /> | ||||||
|  |         <path | ||||||
|  |             android:fillColor="#000000" | ||||||
|  |             android:fillAlpha="0.12" | ||||||
|  |             android:pathData="M 70.506 38.389 L 108 72.466 L 108 108 L 77 108 L 35.066 72.466 L 38.373 63.769 L 36.268 50.216 L 43.335 44.523 L 51.841 34.578 L 63.096 42.478 L 68.586 42.478 L 70.506 38.389 Z" /> | ||||||
|  |         <path | ||||||
|  |             android:fillColor="#000000" | ||||||
|  |             android:fillAlpha="0.12" | ||||||
|  |             android:pathData="M 28.979 53.708 L 47.736 67.31 L 38.373 58.563 L 36.268 51.52 L 28.979 53.708 Z" /> | ||||||
|  |         <path | ||||||
|  |             android:fillColor="#ffffff" | ||||||
|  |             android:pathData="M 77.131 54.24 L 72.878 54.24 L 72.878 72.415 L 56.339 72.415 L 56.339 64.85 L 62.931 58.511 L 64.609 58.784 C 67.349 58.784 69.57 56.649 69.57 54.013 C 69.57 51.378 67.349 49.242 64.609 49.242 C 61.868 49.242 59.647 51.378 59.647 54.013 L 59.883 55.626 L 56.339 59.079 L 56.339 46.63 C 57.898 45.812 58.938 44.244 58.938 42.427 C 58.938 39.792 56.717 37.656 53.976 37.656 C 51.236 37.656 49.015 39.792 49.015 42.427 C 49.015 44.244 50.054 45.812 51.614 46.63 L 51.614 59.079 L 48.07 55.626 L 48.306 54.013 C 48.306 51.378 46.084 49.242 43.344 49.242 C 40.604 49.242 38.383 51.378 38.383 54.013 C 38.383 56.648 40.604 58.784 43.344 58.784 L 45.022 58.511 L 51.614 64.85 L 51.614 72.415 L 35.075 72.415 L 35.075 54.24 L 30.94 54.24 C 29.948 54.24 28.979 54.24 28.979 53.763 C 29.003 53.263 29.995 52.309 31.011 51.332 L 51.614 31.522 C 52.393 30.772 53.197 30 53.976 30 C 54.756 30 55.559 30.772 56.339 31.522 L 65.79 40.609 L 65.79 38.338 L 70.515 38.338 L 70.515 45.153 L 77.084 51.469 C 78.029 52.377 78.997 53.309 79.021 53.786 C 79.021 54.24 78.076 54.24 77.131 54.24 Z M 43.344 51.969 C 43.908 51.969 44.449 52.184 44.848 52.567 C 45.247 52.951 45.471 53.471 45.471 54.013 C 45.471 54.555 45.247 55.076 44.848 55.459 C 44.449 55.842 43.908 56.058 43.344 56.058 C 42.78 56.058 42.239 55.842 41.841 55.459 C 41.442 55.076 41.218 54.555 41.218 54.013 C 41.218 53.471 41.442 52.951 41.841 52.567 C 42.239 52.184 42.78 51.969 43.344 51.969 Z M 64.609 51.969 C 65.79 51.969 66.735 52.877 66.735 54.013 C 66.735 55.149 65.79 56.058 64.609 56.058 C 64.045 56.058 63.504 55.842 63.105 55.459 C 62.706 55.076 62.482 54.555 62.482 54.013 C 62.482 53.471 62.706 52.951 63.105 52.567 C 63.504 52.184 64.045 51.969 64.609 51.969 Z M 53.976 40.382 C 55.158 40.382 56.103 41.291 56.103 42.427 C 56.103 43.563 55.158 44.472 53.976 44.472 C 52.795 44.472 51.85 43.563 51.85 42.427 C 51.85 41.291 52.795 40.382 53.976 40.382 Z" /> | ||||||
|  |     </group> | ||||||
|  | </vector> | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?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> | ||||||
|   | |||||||
| @@ -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: 4.0 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: 2.6 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: 5.4 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: 8.0 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: 11 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,11 +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.3.2' |         classpath 'com.android.tools.build:gradle:3.3.2' | ||||||
|         classpath 'com.google.gms:google-services:4.2.0' |         classpath 'com.google.gms:google-services:4.3.3' | ||||||
|  |         classpath 'io.fabric.tools:gradle:1.26.1' | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -14,6 +18,9 @@ allprojects { | |||||||
|     repositories { |     repositories { | ||||||
|         google() |         google() | ||||||
|         jcenter() |         jcenter() | ||||||
|  |         maven { | ||||||
|  |             url 'https://maven.fabric.io/public' | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| org.gradle.jvmargs=-Xmx2g | org.gradle.jvmargs=-Xmx512m | ||||||
| org.gradle.daemon=true |  | ||||||
| org.gradle.caching=true |  | ||||||
| android.useAndroidX=true | android.useAndroidX=true | ||||||
| android.enableJetifier=true | android.enableJetifier=true | ||||||
|  | android.enableR8=true | ||||||
|   | |||||||
							
								
								
									
										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> | ||||||
| @@ -11,6 +11,27 @@ window.externalApp.getExternalAuth = function(options) { | |||||||
|         setTimeout(function(){ |         setTimeout(function(){ | ||||||
|             console.log("Calling a callback"); |             console.log("Calling a callback"); | ||||||
|             window[options.callback](true, responseData); |             window[options.callback](true, responseData); | ||||||
|         }, 500); |         }, 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'); | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | */ | ||||||
| Before Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 24 KiB | 
							
								
								
									
										62
									
								
								lib/cards/alarm_panel_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class AlarmPanelCard extends StatelessWidget { | ||||||
|  |   final AlarmPanelCardData card; | ||||||
|  |  | ||||||
|  |   const AlarmPanelCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (card.entity.entity.statelessType == StatelessEntityType.missed) { | ||||||
|  |       return EntityModel( | ||||||
|  |         entityWrapper: card.entity, | ||||||
|  |         child: MissedEntityWidget(), | ||||||
|  |         handleTap: false, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     List<Widget> body = []; | ||||||
|  |     body.add(CardHeader( | ||||||
|  |       name: card.name ?? "", | ||||||
|  |       subtitle: Text("${card.entity.entity.displayState}", | ||||||
|  |     ), | ||||||
|  |     trailing: Row( | ||||||
|  |       mainAxisSize: MainAxisSize.min, | ||||||
|  |       mainAxisAlignment: MainAxisAlignment.end, | ||||||
|  |       children: [ | ||||||
|  |         EntityIcon( | ||||||
|  |           size: 50.0, | ||||||
|  |         ), | ||||||
|  |         Container( | ||||||
|  |             width: 26.0, | ||||||
|  |             child: IconButton( | ||||||
|  |                 padding: EdgeInsets.all(0.0), | ||||||
|  |                 alignment: Alignment.centerRight, | ||||||
|  |                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||||
|  |                     "mdi:dots-vertical")), | ||||||
|  |                 onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: card.entity.entity.entityId)) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |       ] | ||||||
|  |     ), | ||||||
|  |     )); | ||||||
|  |     body.add( | ||||||
|  |         AlarmControlPanelControlsWidget( | ||||||
|  |           extended: true, | ||||||
|  |           states: card.states, | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  |     return CardWrapper( | ||||||
|  |         child: EntityModel( | ||||||
|  |             entityWrapper: card.entity, | ||||||
|  |             handleTap: null, | ||||||
|  |             child: Column( | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                 mainAxisSize: MainAxisSize.min, | ||||||
|  |                 children: body | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |    | ||||||
|  | } | ||||||
							
								
								
									
										196
									
								
								lib/cards/badges.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,196 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class Badges extends StatelessWidget { | ||||||
|  |   final BadgesData badges; | ||||||
|  |  | ||||||
|  |   const Badges({Key key, this.badges}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow(); | ||||||
|  |      | ||||||
|  |     if (entitiesToShow.isNotEmpty) { | ||||||
|  |       if (AppSettings().scrollBadges) { | ||||||
|  |         return ConstrainedBox( | ||||||
|  |           constraints: BoxConstraints.tightFor(height: 112), | ||||||
|  |           child: SingleChildScrollView( | ||||||
|  |             scrollDirection: Axis.horizontal, | ||||||
|  |             child: Row( | ||||||
|  |               mainAxisSize: MainAxisSize.min, | ||||||
|  |               mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |               children: entitiesToShow.map((entity) => | ||||||
|  |                 EntityModel( | ||||||
|  |                   entityWrapper: entity, | ||||||
|  |                   child: Padding( | ||||||
|  |                     padding: EdgeInsets.fromLTRB(5, 10, 5, 10), | ||||||
|  |                     child: BadgeWidget(), | ||||||
|  |                   ), | ||||||
|  |                   handleTap: true, | ||||||
|  |                 )).toList() | ||||||
|  |             ), | ||||||
|  |           ) | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         return Padding( | ||||||
|  |           padding: EdgeInsets.fromLTRB(5, 10, 5, 10), | ||||||
|  |           child: Wrap( | ||||||
|  |             alignment: WrapAlignment.center, | ||||||
|  |             spacing: 10.0, | ||||||
|  |             runSpacing: 5, | ||||||
|  |             children: entitiesToShow.map((entity) => | ||||||
|  |                 EntityModel( | ||||||
|  |                   entityWrapper: entity, | ||||||
|  |                   child: BadgeWidget(), | ||||||
|  |                   handleTap: true, | ||||||
|  |                 )).toList(), | ||||||
|  |           ) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return Container(height: 0.0, width: 0.0,); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class BadgeWidget extends StatelessWidget { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final entityModel = EntityModel.of(context); | ||||||
|  |     Widget badgeIcon; | ||||||
|  |     String onBadgeTextValue; | ||||||
|  |     Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain); | ||||||
|  |     switch (entityModel.entityWrapper.entity.domain) { | ||||||
|  |       case "sun": | ||||||
|  |         { | ||||||
|  |           IconData iconData; | ||||||
|  |           if (entityModel.entityWrapper.entity.state == "below_horizon") { | ||||||
|  |             iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc); | ||||||
|  |           } else { | ||||||
|  |             iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8); | ||||||
|  |           } | ||||||
|  |           badgeIcon = Padding( | ||||||
|  |             padding: EdgeInsets.all(10), | ||||||
|  |             child: Icon( | ||||||
|  |               iconData, | ||||||
|  |             ) | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       case "camera": | ||||||
|  |       case "media_player": | ||||||
|  |       case "binary_sensor": | ||||||
|  |         { | ||||||
|  |           badgeIcon = EntityIcon( | ||||||
|  |             imagePadding: EdgeInsets.all(0.0), | ||||||
|  |             iconPadding: EdgeInsets.all(10), | ||||||
|  |             color: Theme.of(context).textTheme.body2.color | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       case "device_tracker": | ||||||
|  |       case "person": | ||||||
|  |         { | ||||||
|  |           badgeIcon = EntityIcon( | ||||||
|  |             imagePadding: EdgeInsets.all(0.0), | ||||||
|  |             iconPadding: EdgeInsets.all(10), | ||||||
|  |             color: Theme.of(context).textTheme.body2.color | ||||||
|  |           ); | ||||||
|  |           onBadgeTextValue = entityModel.entityWrapper.entity.displayState; | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       default: | ||||||
|  |         { | ||||||
|  |           onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement; | ||||||
|  |           badgeIcon = Padding( | ||||||
|  |             padding: EdgeInsets.all(4), | ||||||
|  |             child: Text( | ||||||
|  |               "${entityModel.entityWrapper.entity.displayState}", | ||||||
|  |               overflow: TextOverflow.fade, | ||||||
|  |               softWrap: false, | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |               style: Theme.of(context).textTheme.body1 | ||||||
|  |             ) | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     Widget onBadgeText; | ||||||
|  |     if (onBadgeTextValue == null || onBadgeTextValue.length == 0) { | ||||||
|  |       onBadgeText = Container(width: 0.0, height: 0.0); | ||||||
|  |     } else { | ||||||
|  |       onBadgeText = Container( | ||||||
|  |         constraints: BoxConstraints(maxWidth: 50), | ||||||
|  |         padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0), | ||||||
|  |         child: Text("$onBadgeTextValue", | ||||||
|  |             style: Theme.of(context).textTheme.overline.copyWith( | ||||||
|  |               color: HAClientTheme().getOnBadgeTextColor() | ||||||
|  |             ), | ||||||
|  |             textAlign: TextAlign.center, | ||||||
|  |             softWrap: false, | ||||||
|  |             overflow: TextOverflow.ellipsis | ||||||
|  |           ), | ||||||
|  |         decoration: new BoxDecoration( | ||||||
|  |           color: iconColor, | ||||||
|  |           borderRadius: BorderRadius.circular(9.0), | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return GestureDetector( | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           children: <Widget>[ | ||||||
|  |             Stack( | ||||||
|  |               overflow: Overflow.visible, | ||||||
|  |               alignment: Alignment.center, | ||||||
|  |               children: <Widget>[ | ||||||
|  |                 Container( | ||||||
|  |                   width: 45, | ||||||
|  |                   height: 45, | ||||||
|  |                   decoration: new BoxDecoration( | ||||||
|  |                     // Circle shape | ||||||
|  |                     shape: BoxShape.circle, | ||||||
|  |                     color: Theme.of(context).cardColor, | ||||||
|  |                     // The border you want | ||||||
|  |                     border: Border.all( | ||||||
|  |                       width: 2.0, | ||||||
|  |                       color: iconColor, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 SizedBox( | ||||||
|  |                   width: 41, | ||||||
|  |                   height: 41, | ||||||
|  |                   child: FittedBox( | ||||||
|  |                     fit: BoxFit.contain, | ||||||
|  |                     alignment: Alignment.center, | ||||||
|  |                     child: badgeIcon, | ||||||
|  |                   ) | ||||||
|  |                 ), | ||||||
|  |                 Positioned( | ||||||
|  |                   bottom: -6, | ||||||
|  |                   child: onBadgeText | ||||||
|  |                 ) | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |             Container( | ||||||
|  |               constraints: BoxConstraints(maxWidth: 45), | ||||||
|  |               padding: EdgeInsets.only(top: 10), | ||||||
|  |               child: Text( | ||||||
|  |                 "${entityModel.entityWrapper.displayName}", | ||||||
|  |                 textAlign: TextAlign.center, | ||||||
|  |                 style: Theme.of(context).textTheme.caption.copyWith( | ||||||
|  |                   fontSize: 10 | ||||||
|  |                 ), | ||||||
|  |                 softWrap: true, | ||||||
|  |                 maxLines: 3, | ||||||
|  |                 overflow: TextOverflow.ellipsis, | ||||||
|  |               ), | ||||||
|  |             ) | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |         onTap: () => entityModel.entityWrapper.handleTap(), | ||||||
|  |         onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(), | ||||||
|  |         onLongPress: () => entityModel.entityWrapper.handleHold(), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,64 +1,682 @@ | |||||||
| part of '../main.dart'; | part of '../main.dart'; | ||||||
|  |  | ||||||
| class HACard { | class CardData { | ||||||
|   List<EntityWrapper> entities = []; |  | ||||||
|   List<HACard> childCards = []; |  | ||||||
|   EntityWrapper linkedEntityWrapper; |  | ||||||
|   String name; |  | ||||||
|   String id; |  | ||||||
|   String type; |  | ||||||
|   bool showName; |  | ||||||
|   bool showState; |  | ||||||
|   bool showEmpty; |  | ||||||
|   int columnsCount; |  | ||||||
|   List stateFilter; |  | ||||||
|   List states; |  | ||||||
|   List conditions; |  | ||||||
|   String content; |  | ||||||
|   String unit; |  | ||||||
|   int min; |  | ||||||
|   int max; |  | ||||||
|   Map severity; |  | ||||||
|  |  | ||||||
|   HACard({ |   String type; | ||||||
|     this.name, |   List<EntityWrapper> entities = []; | ||||||
|     this.id, |   List conditions; | ||||||
|     this.linkedEntityWrapper, |   bool showEmpty; | ||||||
|     this.columnsCount: 4, |   List stateFilter; | ||||||
|     this.showName: true, |   bool stateColor = true; | ||||||
|     this.showState: true, |  | ||||||
|     this.stateFilter: const [], |   EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null; | ||||||
|     this.showEmpty: true, |  | ||||||
|     this.content, |   factory CardData.parse(rawData) { | ||||||
|     this.states, |     try { | ||||||
|     this.conditions: const [], |       if (rawData['type'] == null) { | ||||||
|     this.unit, |         rawData['type'] = CardType.ENTITIES; | ||||||
|     this.min, |       } else if (!(rawData['type'] is String)) { | ||||||
|     this.max, |         return CardData(null); | ||||||
|     this.severity, |  | ||||||
|     @required this.type |  | ||||||
|   }) { |  | ||||||
|     if (this.columnsCount <= 0) { |  | ||||||
|       this.columnsCount = 4; |  | ||||||
|       } |       } | ||||||
|  |       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.LIGHT: | ||||||
|  |             return LightCardData(rawData); | ||||||
|  |             break; | ||||||
|  |           case CardType.PICTURE_ELEMENTS: | ||||||
|  |             //TODO temporary solution  | ||||||
|  |             if (rawData.containsKey('camera_image')) { | ||||||
|  |               rawData['entity'] = rawData['camera_image']; | ||||||
|  |               return ButtonCardData(rawData); | ||||||
|  |             } else { | ||||||
|  |               return CardData(null); | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  |           case CardType.ENTITY_BUTTON: | ||||||
|  |           case CardType.BUTTON: | ||||||
|  |           case CardType.PICTURE_ENTITY: | ||||||
|  |             return ButtonCardData(rawData); | ||||||
|  |             break; | ||||||
|  |           case CardType.CONDITIONAL: | ||||||
|  |             return CardData.parse(rawData['card']); | ||||||
|  |             break; | ||||||
|  |           case CardType.ENTITY_FILTER: | ||||||
|  |             Map cardData = Map.from(rawData); | ||||||
|  |             cardData.remove('type'); | ||||||
|  |             if (rawData.containsKey('card')) { | ||||||
|  |               cardData.addAll(rawData['card']); | ||||||
|  |             } | ||||||
|  |             cardData['type'] ??= CardType.ENTITIES; | ||||||
|  |             return CardData.parse(cardData); | ||||||
|  |             break; | ||||||
|  |           case CardType.GAUGE: | ||||||
|  |             return GaugeCardData(rawData); | ||||||
|  |             break; | ||||||
|  |           case CardType.GLANCE: | ||||||
|  |           case CardType.THERMOSTAT: | ||||||
|  |             if (rawData['entity'] != null) { | ||||||
|  |               rawData['entities'] = [rawData['entity']]; | ||||||
|  |             } | ||||||
|  |             return GlanceCardData(rawData); | ||||||
|  |             break; | ||||||
|  |           case CardType.HORIZONTAL_STACK: | ||||||
|  |             return HorizontalStackCardData(rawData); | ||||||
|  |             break; | ||||||
|  |           case CardType.VERTICAL_STACK: | ||||||
|  |             return VerticalStackCardData(rawData); | ||||||
|  |             break; | ||||||
|  |           case CardType.MARKDOWN: | ||||||
|  |             return MarkdownCardData(rawData); | ||||||
|  |             break; | ||||||
|  |           case CardType.MEDIA_CONTROL: | ||||||
|  |             return MediaControlCardData(rawData); | ||||||
|  |             break; | ||||||
|  |           case CardType.BADGES: | ||||||
|  |             return BadgesData(rawData); | ||||||
|  |             break; | ||||||
|  |           default: | ||||||
|  |             if (rawData.containsKey('entity')) { | ||||||
|  |               rawData['entities'] = [rawData['entity']];  | ||||||
|  |             } | ||||||
|  |             if (rawData.containsKey('entities') && rawData['entities'] is List) { | ||||||
|  |               return EntitiesCardData(rawData);  | ||||||
|  |             } | ||||||
|  |             return CardData(null); | ||||||
|  |         } | ||||||
|  |     } catch (error, stacktrace) { | ||||||
|  |       Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace); | ||||||
|  |       return ErrorCardData(rawData); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   CardData(rawData) { | ||||||
|  |     if (rawData != null && rawData is Map) { | ||||||
|  |       type = rawData['type']; | ||||||
|  |       conditions = rawData['conditions'] ?? []; | ||||||
|  |       showEmpty = rawData['show_empty'] ?? true; | ||||||
|  |       if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) { | ||||||
|  |         stateFilter = rawData['state_filter']; | ||||||
|  |       } else { | ||||||
|  |         stateFilter = []; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       type = CardType.UNKNOWN; | ||||||
|  |       conditions = []; | ||||||
|  |       showEmpty = true; | ||||||
|  |       stateFilter = []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return UnsupportedCard(card: this); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   List<EntityWrapper> getEntitiesToShow() { |   List<EntityWrapper> getEntitiesToShow() { | ||||||
|     return entities.where((entityWrapper) { |     return entities.where((entityWrapper) { | ||||||
|       if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) { |       if (entityWrapper.entity.isHidden) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|       if (stateFilter.isNotEmpty) { |       List currentStateFilter; | ||||||
|         return stateFilter.contains(entityWrapper.entity.state); |       if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) { | ||||||
|  |         currentStateFilter = entityWrapper.stateFilter; | ||||||
|  |       } else { | ||||||
|  |         currentStateFilter = stateFilter; | ||||||
|       } |       } | ||||||
|       return true; |       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(); |     }).toList(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget build(BuildContext context) { | } | ||||||
|     return CardWidget( |  | ||||||
|       card: this, | class BadgesData extends CardData { | ||||||
|  |  | ||||||
|  |   String title; | ||||||
|  |   String icon; | ||||||
|  |   bool showHeaderToggle; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return Badges(badges: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   BadgesData(rawData) : super(rawData) { | ||||||
|  |     if (rawData['badges'] is List) { | ||||||
|  |       rawData['badges'].forEach((dynamic rawBadge) { | ||||||
|  |         if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {   | ||||||
|  |           entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge))); | ||||||
|  |         } else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) { | ||||||
|  |           entities.add( | ||||||
|  |             EntityWrapper( | ||||||
|  |               entity: HomeAssistant().entities.get(rawBadge['entity']), | ||||||
|  |               overrideName: rawBadge["name"]?.toString(), | ||||||
|  |               overrideIcon: rawBadge["icon"], | ||||||
|  |             ) | ||||||
|           ); |           ); | ||||||
|  |         } else if (rawBadge is Map && rawBadge.containsKey('entities')) { | ||||||
|  |           _parseEntities(rawBadge); | ||||||
|  |         } | ||||||
|  |       });     | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _parseEntities(rawData) { | ||||||
|  |     var rawEntities = rawData['entities'] ?? []; | ||||||
|  |     rawEntities.forEach((rawEntity) { | ||||||
|  |       if (rawEntity is String) { | ||||||
|  |         if (HomeAssistant().entities.isExist(rawEntity)) { | ||||||
|  |           entities.add(EntityWrapper( | ||||||
|  |             entity: HomeAssistant().entities.get(rawEntity), | ||||||
|  |             stateFilter: rawData['state_filter'] ?? [], | ||||||
|  |           )); | ||||||
|  |         } | ||||||
|  |       } else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) { | ||||||
|  |         Entity e = HomeAssistant().entities.get(rawEntity["entity"]); | ||||||
|  |         entities.add( | ||||||
|  |           EntityWrapper( | ||||||
|  |               entity: e, | ||||||
|  |               overrideName: rawEntity["name"]?.toString(), | ||||||
|  |               overrideIcon: rawEntity["icon"], | ||||||
|  |               stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []), | ||||||
|  |               uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||||
|  |           ) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class EntitiesCardData extends CardData { | ||||||
|  |  | ||||||
|  |   String title; | ||||||
|  |   String icon; | ||||||
|  |   bool showHeaderToggle; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return EntitiesCard(card: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   EntitiesCardData(rawData) : super(rawData) { | ||||||
|  |     //Parsing card data | ||||||
|  |     title = rawData['title']?.toString(); | ||||||
|  |     icon = rawData['icon'] is String ? rawData['icon'] : null; | ||||||
|  |     stateColor = rawData['state_color'] ?? false; | ||||||
|  |     showHeaderToggle = rawData['show_header_toggle'] ?? false; | ||||||
|  |     //Parsing entities | ||||||
|  |     var rawEntities = rawData['entities'] ?? []; | ||||||
|  |     rawEntities.forEach((rawEntity) { | ||||||
|  |       if (rawEntity is String) { | ||||||
|  |         if (HomeAssistant().entities.isExist(rawEntity)) { | ||||||
|  |           entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity))); | ||||||
|  |         } else { | ||||||
|  |           entities.add(EntityWrapper(entity: Entity.missed(rawEntity))); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         if (rawEntity["type"] == "divider") { | ||||||
|  |           entities.add(EntityWrapper(entity: Entity.divider())); | ||||||
|  |         } else if (rawEntity["type"] == "section") { | ||||||
|  |           entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? ""))); | ||||||
|  |         } else if (rawEntity["type"] == "call-service") { | ||||||
|  |           Map uiActionData = { | ||||||
|  |             "tap_action": { | ||||||
|  |               "action": EntityUIAction.callService, | ||||||
|  |               "service": rawEntity["service"], | ||||||
|  |               "service_data": rawEntity["service_data"] | ||||||
|  |             }, | ||||||
|  |             "hold_action": EntityUIAction.none | ||||||
|  |           }; | ||||||
|  |           entities.add( | ||||||
|  |             EntityWrapper( | ||||||
|  |               entity: Entity.callService( | ||||||
|  |                 icon: rawEntity["icon"], | ||||||
|  |                 name: rawEntity["name"]?.toString(), | ||||||
|  |                 service: rawEntity["service"], | ||||||
|  |                 actionName: rawEntity["action_name"] | ||||||
|  |               ), | ||||||
|  |               stateColor: rawEntity["state_color"] ?? stateColor, | ||||||
|  |               uiAction: EntityUIAction(rawEntityData: uiActionData) | ||||||
|  |             ) | ||||||
|  |           ); | ||||||
|  |         } else if (rawEntity["type"] == "weblink") { | ||||||
|  |           Map uiActionData = { | ||||||
|  |             "tap_action": { | ||||||
|  |               "action": EntityUIAction.navigate, | ||||||
|  |               "service": rawEntity["url"] | ||||||
|  |             }, | ||||||
|  |             "hold_action": EntityUIAction.none | ||||||
|  |           }; | ||||||
|  |           entities.add(EntityWrapper( | ||||||
|  |               entity: Entity.weblink( | ||||||
|  |                   icon: rawEntity["icon"], | ||||||
|  |                   name: rawEntity["name"]?.toString(), | ||||||
|  |                   url: rawEntity["url"] | ||||||
|  |               ), | ||||||
|  |               stateColor: rawEntity["state_color"] ?? stateColor, | ||||||
|  |               uiAction: EntityUIAction(rawEntityData: uiActionData) | ||||||
|  |           ) | ||||||
|  |           ); | ||||||
|  |         } else if (HomeAssistant().entities.isExist(rawEntity["entity"])) { | ||||||
|  |           Entity e = HomeAssistant().entities.get(rawEntity["entity"]); | ||||||
|  |           entities.add( | ||||||
|  |             EntityWrapper( | ||||||
|  |                 entity: e, | ||||||
|  |                 stateColor: rawEntity["state_color"] ?? stateColor, | ||||||
|  |                 overrideName: rawEntity["name"]?.toString(), | ||||||
|  |                 overrideIcon: rawEntity["icon"], | ||||||
|  |                 stateFilter: rawEntity['state_filter'] ?? [], | ||||||
|  |                 uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||||
|  |             ) | ||||||
|  |           ); | ||||||
|  |         } else { | ||||||
|  |           entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"]))); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class AlarmPanelCardData extends CardData { | ||||||
|  |  | ||||||
|  |   String name; | ||||||
|  |   List<dynamic> states; | ||||||
|  |    | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return AlarmPanelCard(card: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   AlarmPanelCardData(rawData) : super(rawData) { | ||||||
|  |     //Parsing card data | ||||||
|  |     name = rawData['name']?.toString(); | ||||||
|  |     states = rawData['states']; | ||||||
|  |     //Parsing entity | ||||||
|  |     var entitiId = rawData["entity"]; | ||||||
|  |     if (entitiId != null && entitiId is String) { | ||||||
|  |       if (HomeAssistant().entities.isExist(entitiId)) { | ||||||
|  |         entities.add(EntityWrapper( | ||||||
|  |             entity: HomeAssistant().entities.get(entitiId), | ||||||
|  |             stateColor: true, | ||||||
|  |             overrideName: name | ||||||
|  |         )); | ||||||
|  |       } else { | ||||||
|  |         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class LightCardData extends CardData { | ||||||
|  |  | ||||||
|  |   String name; | ||||||
|  |   String icon; | ||||||
|  |    | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     if (this.entity != null && this.entity.entity is LightEntity) { | ||||||
|  |       return LightCard(card: this); | ||||||
|  |     } | ||||||
|  |     return ErrorCard( | ||||||
|  |       errorText: 'Specify an entity from within the light domain.', | ||||||
|  |       showReportButton: false, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   LightCardData(rawData) : super(rawData) { | ||||||
|  |     //Parsing card data | ||||||
|  |     name = rawData['name']?.toString(); | ||||||
|  |     icon = rawData['icon'] is String ? rawData['icon'] : null; | ||||||
|  |     //Parsing entity | ||||||
|  |     var entitiId = rawData["entity"]; | ||||||
|  |     if (entitiId != null && entitiId is String) { | ||||||
|  |       if (HomeAssistant().entities.isExist(entitiId)) { | ||||||
|  |         entities.add(EntityWrapper( | ||||||
|  |             entity: HomeAssistant().entities.get(entitiId), | ||||||
|  |             overrideName: name, | ||||||
|  |             overrideIcon: icon, | ||||||
|  |             uiAction: EntityUIAction()..tapAction = EntityUIAction.toggle | ||||||
|  |         )); | ||||||
|  |       } else { | ||||||
|  |         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       entities.add(EntityWrapper(entity: Entity.missed('$entitiId'))); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class ButtonCardData extends CardData { | ||||||
|  |  | ||||||
|  |   String name; | ||||||
|  |   String icon; | ||||||
|  |   bool showName; | ||||||
|  |   bool showIcon; | ||||||
|  |   double iconHeightPx = 0; | ||||||
|  |   double iconHeightRem = 0; | ||||||
|  |    | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return EntityButtonCard(card: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   ButtonCardData(rawData) : super(rawData) { | ||||||
|  |     //Parsing card data | ||||||
|  |     name = rawData['name']?.toString(); | ||||||
|  |     icon = rawData['icon'] is String ? rawData['icon'] : null; | ||||||
|  |     showName = rawData['show_name'] ?? true; | ||||||
|  |     showIcon = rawData['show_icon'] ?? true; | ||||||
|  |     stateColor = rawData['state_color'] ?? true; | ||||||
|  |     var rawHeight = rawData['icon_height']; | ||||||
|  |     if (rawHeight != null && rawHeight is String) { | ||||||
|  |       if (rawHeight.contains('px')) { | ||||||
|  |         iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0; | ||||||
|  |       } else if (rawHeight.contains('rem')) { | ||||||
|  |         iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;  | ||||||
|  |       } else if (rawHeight.contains('em')) { | ||||||
|  |         iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     //Parsing entity | ||||||
|  |     var entitiId = rawData["entity"]; | ||||||
|  |     if (entitiId != null && entitiId is String) { | ||||||
|  |       if (HomeAssistant().entities.isExist(entitiId)) { | ||||||
|  |         entities.add(EntityWrapper( | ||||||
|  |             entity: HomeAssistant().entities.get(entitiId), | ||||||
|  |             overrideName: name, | ||||||
|  |             overrideIcon: icon, | ||||||
|  |             stateColor: stateColor, | ||||||
|  |             uiAction: EntityUIAction( | ||||||
|  |               rawEntityData: rawData | ||||||
|  |             ) | ||||||
|  |         )); | ||||||
|  |       } else { | ||||||
|  |         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||||
|  |       } | ||||||
|  |     } else if (entitiId == null) { | ||||||
|  |       entities.add( | ||||||
|  |         EntityWrapper( | ||||||
|  |           entity: Entity.ghost( | ||||||
|  |             name, | ||||||
|  |             icon, | ||||||
|  |           ), | ||||||
|  |           stateColor: stateColor, | ||||||
|  |           uiAction: EntityUIAction( | ||||||
|  |             rawEntityData: rawData | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class GaugeCardData extends CardData { | ||||||
|  |  | ||||||
|  |   String name; | ||||||
|  |   String unit; | ||||||
|  |   double min; | ||||||
|  |   double max; | ||||||
|  |   Map severity; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return GaugeCard(card: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   GaugeCardData(rawData) : super(rawData) { | ||||||
|  |     //Parsing card data | ||||||
|  |     name = rawData['name']?.toString(); | ||||||
|  |     unit = rawData['unit']; | ||||||
|  |     if (rawData['min'] is int) { | ||||||
|  |       min = rawData['min'].toDouble();   | ||||||
|  |     } else if (rawData['min'] is double) { | ||||||
|  |       min = rawData['min']; | ||||||
|  |     } else { | ||||||
|  |       min = 0; | ||||||
|  |     } | ||||||
|  |     if (rawData['max'] is int) { | ||||||
|  |       max = rawData['max'].toDouble();   | ||||||
|  |     } else if (rawData['max'] is double) { | ||||||
|  |       max = rawData['max']; | ||||||
|  |     } else { | ||||||
|  |       max = 100; | ||||||
|  |     } | ||||||
|  |     severity = rawData['severity']; | ||||||
|  |     //Parsing entity | ||||||
|  |     var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"]; | ||||||
|  |     if (entitiId != null && entitiId is String) { | ||||||
|  |       if (HomeAssistant().entities.isExist(entitiId)) { | ||||||
|  |         entities.add(EntityWrapper( | ||||||
|  |             entity: HomeAssistant().entities.get(entitiId), | ||||||
|  |             overrideName: name | ||||||
|  |         )); | ||||||
|  |       } else { | ||||||
|  |         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       entities.add(EntityWrapper(entity: Entity.missed('$entitiId'))); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class GlanceCardData extends CardData { | ||||||
|  |  | ||||||
|  |   String title; | ||||||
|  |   bool showName; | ||||||
|  |   bool showIcon; | ||||||
|  |   bool showState; | ||||||
|  |   bool stateColor; | ||||||
|  |   int columnsCount; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return GlanceCard(card: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   GlanceCardData(rawData) : super(rawData) { | ||||||
|  |     //Parsing card data | ||||||
|  |     title = rawData["title"]?.toString(); | ||||||
|  |     showName = rawData['show_name'] ?? true; | ||||||
|  |     showIcon = rawData['show_icon'] ?? true; | ||||||
|  |     showState = rawData['show_state'] ?? true; | ||||||
|  |     stateColor = rawData['state_color'] ?? true; | ||||||
|  |     columnsCount = rawData['columns'] ?? 4; | ||||||
|  |     //Parsing entities | ||||||
|  |     var rawEntities = rawData["entities"] ?? []; | ||||||
|  |     rawEntities.forEach((rawEntity) { | ||||||
|  |       if (rawEntity is String) { | ||||||
|  |         if (HomeAssistant().entities.isExist(rawEntity)) { | ||||||
|  |           entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity))); | ||||||
|  |         } else { | ||||||
|  |           entities.add(EntityWrapper(entity: Entity.missed(rawEntity))); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         if (HomeAssistant().entities.isExist(rawEntity["entity"])) { | ||||||
|  |           Entity e = HomeAssistant().entities.get(rawEntity["entity"]); | ||||||
|  |           entities.add( | ||||||
|  |             EntityWrapper( | ||||||
|  |                 entity: e, | ||||||
|  |                 stateColor: stateColor, | ||||||
|  |                 overrideName: rawEntity["name"]?.toString(), | ||||||
|  |                 overrideIcon: rawEntity["icon"], | ||||||
|  |                 stateFilter: rawEntity['state_filter'] ?? [], | ||||||
|  |                 uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||||
|  |             ) | ||||||
|  |           ); | ||||||
|  |         } else { | ||||||
|  |           entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"]))); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class HorizontalStackCardData extends CardData { | ||||||
|  |  | ||||||
|  |   List<CardData> childCards; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return HorizontalStackCard(card: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   HorizontalStackCardData(rawData) : super(rawData) { | ||||||
|  |     if (rawData.containsKey('cards') && rawData['cards'] is List) { | ||||||
|  |       childCards = rawData['cards'].map<CardData>((childCard) { | ||||||
|  |         return CardData.parse(childCard); | ||||||
|  |       }).toList(); | ||||||
|  |     } else { | ||||||
|  |       childCards = []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class VerticalStackCardData extends CardData { | ||||||
|  |  | ||||||
|  |   List<CardData> childCards; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return VerticalStackCard(card: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   VerticalStackCardData(rawData) : super(rawData) { | ||||||
|  |     if (rawData.containsKey('cards') && rawData['cards'] is List) { | ||||||
|  |       childCards = rawData['cards'].map<CardData>((childCard) { | ||||||
|  |         return CardData.parse(childCard); | ||||||
|  |       }).toList(); | ||||||
|  |     } else { | ||||||
|  |       childCards = []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class MarkdownCardData extends CardData { | ||||||
|  |  | ||||||
|  |   String title; | ||||||
|  |   String content; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget buildCardWidget() { | ||||||
|  |     return MarkdownCard(card: this); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   MarkdownCardData(rawData) : super(rawData) { | ||||||
|  |     //Parsing card data | ||||||
|  |     title = rawData['title']; | ||||||
|  |     content = rawData['content']; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class 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'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -1,363 +0,0 @@ | |||||||
| part of '../main.dart'; |  | ||||||
|  |  | ||||||
| class CardWidget extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   final HACard card; |  | ||||||
|  |  | ||||||
|   const CardWidget({ |  | ||||||
|     Key key, |  | ||||||
|     this.card |  | ||||||
|   }) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     if (card.linkedEntityWrapper!= null) { |  | ||||||
|       if (card.linkedEntityWrapper.entity.isHidden) { |  | ||||||
|         return Container(width: 0.0, height: 0.0,); |  | ||||||
|       } |  | ||||||
|       if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) { |  | ||||||
|         return EntityModel( |  | ||||||
|           entityWrapper: card.linkedEntityWrapper, |  | ||||||
|           child: MissedEntityWidget(), |  | ||||||
|           handleTap: false, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (card.conditions.isNotEmpty) { |  | ||||||
|       bool showCardByConditions = true; |  | ||||||
|       for (var condition in card.conditions) { |  | ||||||
|         Entity conditionEntity = HomeAssistant().entities.get(condition['entity']); |  | ||||||
|         if (conditionEntity != null && |  | ||||||
|             ((condition['state'] != null && conditionEntity.state != condition['state']) || |  | ||||||
|             (condition['state_not'] != null && conditionEntity.state == condition['state_not'])) |  | ||||||
|           ) { |  | ||||||
|           showCardByConditions = false; |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (!showCardByConditions) { |  | ||||||
|         return Container(width: 0.0, height: 0.0,); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     switch (card.type) { |  | ||||||
|  |  | ||||||
|       case CardType.ENTITIES: { |  | ||||||
|         return _buildEntitiesCard(context); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       case CardType.GLANCE: { |  | ||||||
|         return _buildGlanceCard(context); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       case CardType.MEDIA_CONTROL: { |  | ||||||
|         return _buildMediaControlsCard(context); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       case CardType.ENTITY_BUTTON: { |  | ||||||
|         return _buildEntityButtonCard(context); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       case CardType.GAUGE: { |  | ||||||
|         return _buildGaugeCard(context); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
| /*      case CardType.LIGHT: { |  | ||||||
|         return _buildLightCard(context); |  | ||||||
|       }*/ |  | ||||||
|  |  | ||||||
|       case CardType.MARKDOWN: { |  | ||||||
|         return _buildMarkdownCard(context); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       case CardType.ALARM_PANEL: { |  | ||||||
|         return _buildAlarmPanelCard(context); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       case CardType.HORIZONTAL_STACK: { |  | ||||||
|         if (card.childCards.isNotEmpty) { |  | ||||||
|           List<Widget> children = []; |  | ||||||
|           card.childCards.forEach((card) { |  | ||||||
|             if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) { |  | ||||||
|               children.add( |  | ||||||
|                   Flexible( |  | ||||||
|                     fit: FlexFit.tight, |  | ||||||
|                     child: card.build(context), |  | ||||||
|                   ) |  | ||||||
|               ); |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|           return Row( |  | ||||||
|             mainAxisSize: MainAxisSize.max, |  | ||||||
|             mainAxisAlignment: MainAxisAlignment.center, |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|             children: children, |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|         return Container(height: 0.0, width: 0.0,); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       case CardType.VERTICAL_STACK: { |  | ||||||
|         if (card.childCards.isNotEmpty) { |  | ||||||
|           List<Widget> children = []; |  | ||||||
|           card.childCards.forEach((card) { |  | ||||||
|             children.add( |  | ||||||
|                 card.build(context) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|           return Column( |  | ||||||
|             mainAxisSize: MainAxisSize.min, |  | ||||||
|             mainAxisAlignment: MainAxisAlignment.start, |  | ||||||
|             children: children, |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|         return Container(height: 0.0, width: 0.0,); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       default: { |  | ||||||
|         if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) { |  | ||||||
|           return _buildEntitiesCard(context); |  | ||||||
|         } else { |  | ||||||
|           return _buildUnsupportedCard(context); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildEntitiesCard(BuildContext context) { |  | ||||||
|     List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); |  | ||||||
|     if (entitiesToShow.isEmpty && !card.showEmpty) { |  | ||||||
|       return Container(height: 0.0, width: 0.0,); |  | ||||||
|     } |  | ||||||
|     List<Widget> body = []; |  | ||||||
|     body.add(CardHeader(name: card.name)); |  | ||||||
|     entitiesToShow.forEach((EntityWrapper entity) { |  | ||||||
|       body.add( |  | ||||||
|           Padding( |  | ||||||
|             padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0), |  | ||||||
|             child: EntityModel( |  | ||||||
|                 entityWrapper: entity, |  | ||||||
|                 handleTap: true, |  | ||||||
|                 child: entity.entity.buildDefaultWidget(context) |  | ||||||
|             ), |  | ||||||
|           )); |  | ||||||
|     }); |  | ||||||
|     return Card( |  | ||||||
|         child: Padding( |  | ||||||
|           padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding), |  | ||||||
|           child: Column(mainAxisSize: MainAxisSize.min, children: body), |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildMarkdownCard(BuildContext context) { |  | ||||||
|     if (card.content == null) { |  | ||||||
|       return Container(height: 0.0, width: 0.0,); |  | ||||||
|     } |  | ||||||
|     List<Widget> body = []; |  | ||||||
|     body.add(CardHeader(name: card.name)); |  | ||||||
|     body.add(MarkdownBody(data: card.content)); |  | ||||||
|     return Card( |  | ||||||
|         child: Padding( |  | ||||||
|           padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), |  | ||||||
|           child: new Column(mainAxisSize: MainAxisSize.min, children: body), |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildAlarmPanelCard(BuildContext context) { |  | ||||||
|     List<Widget> body = []; |  | ||||||
|     body.add(CardHeader( |  | ||||||
|       name: card.name ?? "", |  | ||||||
|       subtitle: Text("${card.linkedEntityWrapper.entity.displayState}", |  | ||||||
|         style: TextStyle( |  | ||||||
|             color: Colors.grey |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       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(entity: card.linkedEntityWrapper.entity)) |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|           ] |  | ||||||
|       ), |  | ||||||
|     )); |  | ||||||
|     body.add( |  | ||||||
|         AlarmControlPanelControlsWidget( |  | ||||||
|           extended: true, |  | ||||||
|           states: card.states, |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|     return Card( |  | ||||||
|         child: EntityModel( |  | ||||||
|             entityWrapper: card.linkedEntityWrapper, |  | ||||||
|             handleTap: null, |  | ||||||
|             child: Column( |  | ||||||
|                 crossAxisAlignment: CrossAxisAlignment.center, |  | ||||||
|                 mainAxisSize: MainAxisSize.min, |  | ||||||
|                 children: body |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildGlanceCard(BuildContext context) { |  | ||||||
|     List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); |  | ||||||
|     if (entitiesToShow.isEmpty && !card.showEmpty) { |  | ||||||
|       return Container(height: 0.0, width: 0.0,); |  | ||||||
|     } |  | ||||||
|     List<Widget> rows = []; |  | ||||||
|     rows.add(CardHeader(name: card.name)); |  | ||||||
|  |  | ||||||
|     int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length; |  | ||||||
|  |  | ||||||
|     rows.add( |  | ||||||
|         Padding( |  | ||||||
|           padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding), |  | ||||||
|           child: FractionallySizedBox( |  | ||||||
|             widthFactor: 1, |  | ||||||
|             child: LayoutBuilder( |  | ||||||
|                 builder: (BuildContext context, BoxConstraints constraints) { |  | ||||||
|                   List<Widget> buttons = []; |  | ||||||
|                   double buttonWidth = constraints.maxWidth / columnsCount; |  | ||||||
|                   entitiesToShow.forEach((EntityWrapper entity) { |  | ||||||
|                     buttons.add( |  | ||||||
|                         SizedBox( |  | ||||||
|                           width: buttonWidth, |  | ||||||
|                           child: EntityModel( |  | ||||||
|                               entityWrapper: entity, |  | ||||||
|                               child: GlanceCardEntityContainer( |  | ||||||
|                                 showName: card.showName, |  | ||||||
|                                 showState: card.showState, |  | ||||||
|                               ), |  | ||||||
|                               handleTap: true |  | ||||||
|                           ), |  | ||||||
|                         ) |  | ||||||
|                     ); |  | ||||||
|                   }); |  | ||||||
|                   return Wrap( |  | ||||||
|                     //spacing: 5.0, |  | ||||||
|                     //alignment: WrapAlignment.spaceEvenly, |  | ||||||
|                     runSpacing: Sizes.doubleRowPadding, |  | ||||||
|                     children: buttons, |  | ||||||
|                   ); |  | ||||||
|                 } |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return Card( |  | ||||||
|         child: Column( |  | ||||||
|             mainAxisSize: MainAxisSize.min, |  | ||||||
|             children: rows |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildMediaControlsCard(BuildContext context) { |  | ||||||
|     return Card( |  | ||||||
|         child: EntityModel( |  | ||||||
|             entityWrapper: card.linkedEntityWrapper, |  | ||||||
|             handleTap: null, |  | ||||||
|             child: MediaPlayerWidget() |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildEntityButtonCard(BuildContext context) { |  | ||||||
|     card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ?? |  | ||||||
|         card.linkedEntityWrapper.displayName.toUpperCase(); |  | ||||||
|     return Card( |  | ||||||
|         child: EntityModel( |  | ||||||
|             entityWrapper: card.linkedEntityWrapper, |  | ||||||
|             child: EntityButtonCardBody( |  | ||||||
|               showName: card.showName, |  | ||||||
|             ), |  | ||||||
|             handleTap: true |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildGaugeCard(BuildContext context) { |  | ||||||
|     card.linkedEntityWrapper.displayName = card.name ?? |  | ||||||
|         card.linkedEntityWrapper.displayName; |  | ||||||
|     card.linkedEntityWrapper.unitOfMeasurement = card.unit ?? |  | ||||||
|         card.linkedEntityWrapper.unitOfMeasurement; |  | ||||||
|     return Card( |  | ||||||
|         child: EntityModel( |  | ||||||
|             entityWrapper: card.linkedEntityWrapper, |  | ||||||
|             child: GaugeCardBody( |  | ||||||
|               min: card.min, |  | ||||||
|               max: card.max, |  | ||||||
|               severity: card.severity, |  | ||||||
|             ), |  | ||||||
|             handleTap: true |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildLightCard(BuildContext context) { |  | ||||||
|     card.linkedEntityWrapper.displayName = card.name ?? |  | ||||||
|         card.linkedEntityWrapper.displayName; |  | ||||||
|     return Card( |  | ||||||
|         child: EntityModel( |  | ||||||
|             entityWrapper: card.linkedEntityWrapper, |  | ||||||
|             child: LightCardBody( |  | ||||||
|               min: card.min, |  | ||||||
|               max: card.max, |  | ||||||
|               severity: card.severity, |  | ||||||
|             ), |  | ||||||
|             handleTap: true |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildUnsupportedCard(BuildContext context) { |  | ||||||
|     List<Widget> body = []; |  | ||||||
|     body.add(CardHeader(name: card.name ?? "")); |  | ||||||
|     List<Widget> result = []; |  | ||||||
|     if (card.linkedEntityWrapper != null) { |  | ||||||
|       result.addAll(<Widget>[ |  | ||||||
|         Padding( |  | ||||||
|           padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), |  | ||||||
|           child: EntityModel( |  | ||||||
|               entityWrapper: card.linkedEntityWrapper, |  | ||||||
|               handleTap: true, |  | ||||||
|               child: card.linkedEntityWrapper.entity.buildDefaultWidget(context) |  | ||||||
|           ), |  | ||||||
|         ) |  | ||||||
|       ]); |  | ||||||
|     } else { |  | ||||||
|       result.addAll(<Widget>[ |  | ||||||
|         Padding( |  | ||||||
|           padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), |  | ||||||
|           child: Text("'${card.type}' card is not supported yet"), |  | ||||||
|         ), |  | ||||||
|       ]); |  | ||||||
|     } |  | ||||||
|     body.addAll(result); |  | ||||||
|     return Card( |  | ||||||
|         child: new Column( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|             mainAxisSize: MainAxisSize.min, |  | ||||||
|             children: body |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
							
								
								
									
										79
									
								
								lib/cards/entities_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,79 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class EntitiesCard extends StatelessWidget { | ||||||
|  |   final EntitiesCardData card; | ||||||
|  |  | ||||||
|  |   const EntitiesCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); | ||||||
|  |     if (entitiesToShow.isEmpty && !card.showEmpty) { | ||||||
|  |       return Container(height: 0.0, width: 0.0,); | ||||||
|  |     } | ||||||
|  |     List<Widget> body = []; | ||||||
|  |     Widget headerSwitch; | ||||||
|  |     if (card.showHeaderToggle) { | ||||||
|  |       bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; }); | ||||||
|  |       List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) { | ||||||
|  |         return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain); | ||||||
|  |       }).map((EntityWrapper en) { | ||||||
|  |           return en.entity.entityId; | ||||||
|  |       }).toList(); | ||||||
|  |       headerSwitch = Switch( | ||||||
|  |         value: headerToggleVal, | ||||||
|  |         onChanged: (val) { | ||||||
|  |           if (entitiesToToggle.isNotEmpty) { | ||||||
|  |             ConnectionManager().callService( | ||||||
|  |               domain: "homeassistant", | ||||||
|  |               service: val ? "turn_on" : "turn_off", | ||||||
|  |               entityId: entitiesToToggle | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     body.add( | ||||||
|  |       CardHeader( | ||||||
|  |         name: card.title, | ||||||
|  |         trailing: headerSwitch, | ||||||
|  |         emptyPadding: Sizes.rowPadding, | ||||||
|  |         leading: card.icon != null ? Icon( | ||||||
|  |           MaterialDesignIcons.getIconDataFromIconName(card.icon), | ||||||
|  |           size: Sizes.iconSize, | ||||||
|  |           color: Theme.of(context).textTheme.headline.color | ||||||
|  |         ) : null, | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |     body.addAll( | ||||||
|  |       entitiesToShow.map((EntityWrapper entity) { | ||||||
|  |         return Padding( | ||||||
|  |             padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0), | ||||||
|  |             child: EntityModel( | ||||||
|  |                 entityWrapper: entity, | ||||||
|  |                 handleTap: true, | ||||||
|  |                 child: entity.entity.buildDefaultWidget(context) | ||||||
|  |             ), | ||||||
|  |           ); | ||||||
|  |       })   | ||||||
|  |     ); | ||||||
|  |     return CardWrapper( | ||||||
|  |         child: Padding( | ||||||
|  |           padding: EdgeInsets.only( | ||||||
|  |             right: Sizes.rightWidgetPadding, | ||||||
|  |             left: Sizes.leftWidgetPadding, | ||||||
|  |             bottom: Sizes.rowPadding, | ||||||
|  |           ), | ||||||
|  |           child: Center( | ||||||
|  |             child: Column( | ||||||
|  |               mainAxisSize: MainAxisSize.min, | ||||||
|  |               mainAxisAlignment: MainAxisAlignment.start, | ||||||
|  |               children: body | ||||||
|  |             ) | ||||||
|  |           ), | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |    | ||||||
|  | } | ||||||
							
								
								
									
										96
									
								
								lib/cards/entity_button_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,96 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class EntityButtonCard extends StatelessWidget { | ||||||
|  |  | ||||||
|  |   final ButtonCardData card; | ||||||
|  |  | ||||||
|  |   EntityButtonCard({ | ||||||
|  |     Key key, this.card | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     EntityWrapper entityWrapper = card.entity; | ||||||
|  |     if (entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||||
|  |       return EntityModel( | ||||||
|  |         entityWrapper: card.entity, | ||||||
|  |         child: MissedEntityWidget(), | ||||||
|  |         handleTap: false, | ||||||
|  |       ); | ||||||
|  |     } else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) { | ||||||
|  |       return Container(width: 0.0, height: 0.0,); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize); | ||||||
|  |      | ||||||
|  |     Widget buttonIcon; | ||||||
|  |     if (!card.showIcon) { | ||||||
|  |       buttonIcon = Container(height: Sizes.rowPadding, width: 10); | ||||||
|  |     } else if (iconSize > 0) { | ||||||
|  |       buttonIcon = SizedBox( | ||||||
|  |         height: iconSize, | ||||||
|  |         child: FractionallySizedBox( | ||||||
|  |           widthFactor: 0.5, | ||||||
|  |           child: FittedBox( | ||||||
|  |             fit: BoxFit.contain, | ||||||
|  |             child: EntityIcon( | ||||||
|  |               //padding: EdgeInsets.only(top: 6), | ||||||
|  |             ), | ||||||
|  |           ) | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       buttonIcon = AspectRatio( | ||||||
|  |         aspectRatio: 2, | ||||||
|  |         child: FractionallySizedBox( | ||||||
|  |           widthFactor: 0.5, | ||||||
|  |           child: FittedBox( | ||||||
|  |             fit: BoxFit.fitWidth, | ||||||
|  |             child: EntityIcon( | ||||||
|  |               //padding: EdgeInsets.only(top: 6), | ||||||
|  |             ), | ||||||
|  |           ) | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return CardWrapper( | ||||||
|  |       child: EntityModel( | ||||||
|  |         entityWrapper: card.entity, | ||||||
|  |         child: InkWell( | ||||||
|  |           onTap: () => entityWrapper.handleTap(), | ||||||
|  |           onLongPress: () => entityWrapper.handleHold(), | ||||||
|  |           onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||||
|  |           child: Center( | ||||||
|  |             child: Padding( | ||||||
|  |               padding: EdgeInsets.only(top: 5), | ||||||
|  |               child: Column( | ||||||
|  |                 mainAxisSize: MainAxisSize.min, | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |                 children: <Widget>[ | ||||||
|  |                   buttonIcon, | ||||||
|  |                   _buildName(context) | ||||||
|  |                 ], | ||||||
|  |               ) | ||||||
|  |             ) | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         handleTap: true | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildName(BuildContext context) { | ||||||
|  |     if (card.showName) { | ||||||
|  |       return EntityName( | ||||||
|  |         padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding), | ||||||
|  |         textOverflow: TextOverflow.ellipsis, | ||||||
|  |         maxLines: 3, | ||||||
|  |         textStyle: Theme.of(context).textTheme.subhead, | ||||||
|  |         wordsWrap: true, | ||||||
|  |         textAlign: TextAlign.center | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return Container(width: 0, height: 0); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								lib/cards/error_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class ErrorCard extends StatelessWidget { | ||||||
|  |   final ErrorCardData card; | ||||||
|  |   final String errorText; | ||||||
|  |   final bool showReportButton; | ||||||
|  |  | ||||||
|  |   const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     String error; | ||||||
|  |     if (errorText == null) { | ||||||
|  |       error = 'There was an error showing ${card?.type}'; | ||||||
|  |     } else { | ||||||
|  |       error = errorText; | ||||||
|  |     } | ||||||
|  |     return CardWrapper( | ||||||
|  |       color: Theme.of(context).errorColor, | ||||||
|  |       child: Padding( | ||||||
|  |         padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |           children: <Widget>[ | ||||||
|  |             Text( | ||||||
|  |               error, | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |             ), | ||||||
|  |             card != null ? | ||||||
|  |             RaisedButton( | ||||||
|  |               onPressed: () { | ||||||
|  |                 Clipboard.setData(new ClipboardData(text: card.cardConfig)); | ||||||
|  |               }, | ||||||
|  |               child: Text('Copy card config'), | ||||||
|  |             ) : | ||||||
|  |             Container(width: 0, height: 0), | ||||||
|  |             showReportButton ? | ||||||
|  |             RaisedButton( | ||||||
|  |               onPressed: () { | ||||||
|  |                 Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title="); | ||||||
|  |               }, | ||||||
|  |               child: Text('Report issue'), | ||||||
|  |             ) : | ||||||
|  |             Container(width: 0, height: 0) | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   }   | ||||||
|  | } | ||||||
							
								
								
									
										201
									
								
								lib/cards/gauge_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,201 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class GaugeCard extends StatelessWidget { | ||||||
|  |  | ||||||
|  |   final GaugeCardData card; | ||||||
|  |  | ||||||
|  |   GaugeCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     EntityWrapper entityWrapper = card.entity; | ||||||
|  |     if (entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||||
|  |       return EntityModel( | ||||||
|  |         entityWrapper: card.entity, | ||||||
|  |         child: MissedEntityWidget(), | ||||||
|  |         handleTap: false, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     entityWrapper.overrideName = card.name ?? | ||||||
|  |         entityWrapper.displayName; | ||||||
|  |     entityWrapper.unitOfMeasurementOverride = card.unit ?? | ||||||
|  |         entityWrapper.unitOfMeasurement; | ||||||
|  |     double fixedValue; | ||||||
|  |     double value = entityWrapper.entity.doubleState; | ||||||
|  |     if (value > card.max) { | ||||||
|  |       fixedValue = card.max.toDouble(); | ||||||
|  |     } else if (value < card.min) { | ||||||
|  |       fixedValue = card.min.toDouble(); | ||||||
|  |     } else { | ||||||
|  |       fixedValue = value; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     List<GaugeRange> ranges; | ||||||
|  |     Color currentColor; | ||||||
|  |     if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) { | ||||||
|  |       List<RangeContainer> rangesList = <RangeContainer>[ | ||||||
|  |         RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()), | ||||||
|  |         RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()), | ||||||
|  |         RangeContainer(card.severity["yellow"], HAClientTheme().getYellowGaugeColor()) | ||||||
|  |       ]; | ||||||
|  |       rangesList.sort((current, next) { | ||||||
|  |         if (current.startFrom > next.startFrom) { | ||||||
|  |           return 1; | ||||||
|  |         } | ||||||
|  |         if (current.startFrom < next.startFrom) { | ||||||
|  |           return -1; | ||||||
|  |         } | ||||||
|  |         return 0; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (fixedValue < rangesList[1].startFrom) { | ||||||
|  |         currentColor = rangesList[0].color; | ||||||
|  |       } else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) { | ||||||
|  |         currentColor = rangesList[1].color; | ||||||
|  |       } else { | ||||||
|  |         currentColor = rangesList[2].color; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       ranges = [ | ||||||
|  |         GaugeRange( | ||||||
|  |           startValue: rangesList[0].startFrom.toDouble(), | ||||||
|  |           endValue: rangesList[1].startFrom.toDouble(), | ||||||
|  |           color: rangesList[0].color.withOpacity(0.1), | ||||||
|  |           sizeUnit: GaugeSizeUnit.factor, | ||||||
|  |           endWidth: 0.3, | ||||||
|  |           startWidth: 0.3 | ||||||
|  |         ), | ||||||
|  |         GaugeRange( | ||||||
|  |           startValue: rangesList[1].startFrom.toDouble(), | ||||||
|  |           endValue: rangesList[2].startFrom.toDouble(), | ||||||
|  |           color: rangesList[1].color.withOpacity(0.1), | ||||||
|  |           sizeUnit: GaugeSizeUnit.factor, | ||||||
|  |           endWidth: 0.3, | ||||||
|  |           startWidth: 0.3 | ||||||
|  |         ), | ||||||
|  |         GaugeRange( | ||||||
|  |           startValue: rangesList[2].startFrom.toDouble(), | ||||||
|  |           endValue: card.max.toDouble(), | ||||||
|  |           color: rangesList[2].color.withOpacity(0.1), | ||||||
|  |           sizeUnit: GaugeSizeUnit.factor, | ||||||
|  |           endWidth: 0.3, | ||||||
|  |           startWidth: 0.3 | ||||||
|  |         ) | ||||||
|  |       ]; | ||||||
|  |     } | ||||||
|  |     if (ranges == null) { | ||||||
|  |       currentColor = Theme.of(context).primaryColorDark; | ||||||
|  |       ranges = <GaugeRange>[ | ||||||
|  |         GaugeRange( | ||||||
|  |           startValue: card.min.toDouble(), | ||||||
|  |           endValue: card.max.toDouble(), | ||||||
|  |           color: Theme.of(context).primaryColorDark.withOpacity(0.1), | ||||||
|  |           sizeUnit: GaugeSizeUnit.factor, | ||||||
|  |           endWidth: 0.3, | ||||||
|  |           startWidth: 0.3, | ||||||
|  |         ) | ||||||
|  |       ]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return CardWrapper( | ||||||
|  |       padding: EdgeInsets.all(4), | ||||||
|  |       child: EntityModel( | ||||||
|  |         entityWrapper: entityWrapper, | ||||||
|  |         child: InkWell( | ||||||
|  |           onTap: () => entityWrapper.handleTap(), | ||||||
|  |           onLongPress: () => entityWrapper.handleHold(), | ||||||
|  |           onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||||
|  |           child: AspectRatio( | ||||||
|  |             aspectRatio: 1.8, | ||||||
|  |             child: Stack( | ||||||
|  |               alignment: Alignment.bottomCenter, | ||||||
|  |               children: <Widget>[ | ||||||
|  |                 IgnorePointer( | ||||||
|  |                   ignoring: true, | ||||||
|  |                   child: SfRadialGauge( | ||||||
|  |                     axes: <RadialAxis>[ | ||||||
|  |                       RadialAxis( | ||||||
|  |                         maximum: card.max.toDouble(), | ||||||
|  |                         minimum: card.min.toDouble(), | ||||||
|  |                         showLabels: false, | ||||||
|  |                         useRangeColorForAxis: true, | ||||||
|  |                         showTicks: false, | ||||||
|  |                         canScaleToFit: true, | ||||||
|  |                         ranges: ranges, | ||||||
|  |                         axisLineStyle: AxisLineStyle( | ||||||
|  |                           thickness: 0.3, | ||||||
|  |                           thicknessUnit: GaugeSizeUnit.factor, | ||||||
|  |                           color: Colors.transparent | ||||||
|  |                         ), | ||||||
|  |                         startAngle: 180, | ||||||
|  |                         endAngle: 0, | ||||||
|  |                         pointers: <GaugePointer>[ | ||||||
|  |                           RangePointer( | ||||||
|  |                             value: fixedValue, | ||||||
|  |                             sizeUnit: GaugeSizeUnit.factor, | ||||||
|  |                             width: 0.3, | ||||||
|  |                             color: currentColor, | ||||||
|  |                             enableAnimation: true, | ||||||
|  |                             animationType: AnimationType.bounceOut, | ||||||
|  |                           ) | ||||||
|  |                         ] | ||||||
|  |                       ) | ||||||
|  |                     ], | ||||||
|  |                   ) | ||||||
|  |                 ), | ||||||
|  |                 Column( | ||||||
|  |                   mainAxisSize: MainAxisSize.max, | ||||||
|  |                   crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                   children: <Widget>[ | ||||||
|  |                     Flexible( | ||||||
|  |                       flex: 8, | ||||||
|  |                       fit: FlexFit.tight, | ||||||
|  |                       child: Container() | ||||||
|  |                     ), | ||||||
|  |                     Flexible( | ||||||
|  |                       flex: 6, | ||||||
|  |                       fit: FlexFit.tight, | ||||||
|  |                       child: FractionallySizedBox( | ||||||
|  |                         widthFactor: 0.4, | ||||||
|  |                         child: FittedBox( | ||||||
|  |                           fit: BoxFit.contain, | ||||||
|  |                           alignment: Alignment.bottomCenter, | ||||||
|  |                           child: SimpleEntityState( | ||||||
|  |                             padding: EdgeInsets.all(0), | ||||||
|  |                             expanded: false, | ||||||
|  |                             maxLines: 1, | ||||||
|  |                             textAlign: TextAlign.center | ||||||
|  |                           ), | ||||||
|  |                         ) | ||||||
|  |                       ) | ||||||
|  |                     ), | ||||||
|  |                     Flexible( | ||||||
|  |                       flex: 3, | ||||||
|  |                       fit: FlexFit.tight, | ||||||
|  |                       child: FittedBox( | ||||||
|  |                         fit: BoxFit.contain, | ||||||
|  |                         child: EntityName( | ||||||
|  |                           padding: EdgeInsets.all(0), | ||||||
|  |                           textStyle: Theme.of(context).textTheme.subhead | ||||||
|  |                         ), | ||||||
|  |                       ) | ||||||
|  |                     ),   | ||||||
|  |                   ], | ||||||
|  |                 ) | ||||||
|  |               ], | ||||||
|  |             ) | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         handleTap: true | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class RangeContainer { | ||||||
|  |   final int startFrom; | ||||||
|  |   Color color; | ||||||
|  |  | ||||||
|  |   RangeContainer(this.startFrom, this.color); | ||||||
|  | } | ||||||
							
								
								
									
										126
									
								
								lib/cards/glance_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,126 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class GlanceCard extends StatelessWidget { | ||||||
|  |   final GlanceCardData card; | ||||||
|  |  | ||||||
|  |   const GlanceCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     List<EntityWrapper> entitiesToShow = card.getEntitiesToShow(); | ||||||
|  |     if (entitiesToShow.isEmpty && !card.showEmpty) { | ||||||
|  |       return Container(height: 0.0, width: 0.0,); | ||||||
|  |     } | ||||||
|  |     int length = entitiesToShow.length; | ||||||
|  |     int rowsCount; | ||||||
|  |     int columnsCount; | ||||||
|  |     if (length == 0) { | ||||||
|  |       columnsCount = 0; | ||||||
|  |       rowsCount = 0; | ||||||
|  |     } else { | ||||||
|  |       columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length; | ||||||
|  |       rowsCount = (length / columnsCount).round(); | ||||||
|  |     } | ||||||
|  |     List<TableRow> rows = []; | ||||||
|  |     for (int i = 0; i < rowsCount; i++) { | ||||||
|  |       int start = i*columnsCount; | ||||||
|  |       int end = start + math.min(columnsCount, length - start); | ||||||
|  |       List<Widget> rowChildren = []; | ||||||
|  |       rowChildren.addAll(entitiesToShow.sublist( | ||||||
|  |           start, end | ||||||
|  |         ).map( | ||||||
|  |           (EntityWrapper entity){ | ||||||
|  |             return Padding( | ||||||
|  |               padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||||
|  |               child: EntityModel( | ||||||
|  |                 entityWrapper: entity, | ||||||
|  |                 child: _buildEntityContainer(context, entity), | ||||||
|  |                 handleTap: true | ||||||
|  |               ) | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |         ).toList() | ||||||
|  |       ); | ||||||
|  |       while (rowChildren.length < columnsCount) { | ||||||
|  |         rowChildren.add( | ||||||
|  |           Container() | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       rows.add( | ||||||
|  |         TableRow( | ||||||
|  |           children: rowChildren | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return CardWrapper( | ||||||
|  |       child: Center( | ||||||
|  |         child: Padding( | ||||||
|  |           padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||||
|  |           child: Column( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             children: <Widget>[ | ||||||
|  |               CardHeader( | ||||||
|  |                 name: card.title, | ||||||
|  |                 emptyPadding: Sizes.rowPadding, | ||||||
|  |               ), | ||||||
|  |               Table( | ||||||
|  |                 children: rows | ||||||
|  |               ) | ||||||
|  |             ], | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) { | ||||||
|  |     if (entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||||
|  |       return MissedEntityWidget(); | ||||||
|  |     } else if (entityWrapper.entity.statelessType != StatelessEntityType.none) { | ||||||
|  |       return Container(width: 0.0, height: 0.0,); | ||||||
|  |     } | ||||||
|  |     List<Widget> result = []; | ||||||
|  |     if (card.showName) { | ||||||
|  |       result.add(_buildName(context)); | ||||||
|  |     } | ||||||
|  |     result.add( | ||||||
|  |         EntityIcon( | ||||||
|  |           padding: EdgeInsets.all(0.0), | ||||||
|  |           size: Sizes.iconSize, | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  |     if (card.showState) { | ||||||
|  |       result.add(_buildState()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return InkResponse( | ||||||
|  |       child: Column( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         children: result, | ||||||
|  |       ), | ||||||
|  |       onTap: () => entityWrapper.handleTap(), | ||||||
|  |       onLongPress: () => entityWrapper.handleHold(), | ||||||
|  |       onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildName(BuildContext context) { | ||||||
|  |     return EntityName( | ||||||
|  |       padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||||
|  |       textOverflow: TextOverflow.ellipsis, | ||||||
|  |       wordsWrap: false, | ||||||
|  |       textAlign: TextAlign.center, | ||||||
|  |       textStyle: Theme.of(context).textTheme.body1, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildState() { | ||||||
|  |     return SimpleEntityState( | ||||||
|  |       textAlign: TextAlign.center, | ||||||
|  |       expanded: false, | ||||||
|  |       maxLines: 1, | ||||||
|  |       padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								lib/cards/horizontal_srack_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class HorizontalStackCard extends StatelessWidget { | ||||||
|  |   final HorizontalStackCardData card; | ||||||
|  |  | ||||||
|  |   const HorizontalStackCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (card.childCards.isNotEmpty) { | ||||||
|  |       List<Widget> children = []; | ||||||
|  |       children = card.childCards.map((childCard) => Flexible( | ||||||
|  |           fit: FlexFit.tight, | ||||||
|  |           child: childCard.buildCardWidget() | ||||||
|  |         ) | ||||||
|  |       ).toList(); | ||||||
|  |       return IntrinsicHeight( | ||||||
|  |         child: Row( | ||||||
|  |           mainAxisSize: MainAxisSize.max, | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |           children: children, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return Container(height: 0.0, width: 0.0,); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |    | ||||||
|  | } | ||||||
							
								
								
									
										161
									
								
								lib/cards/light_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,161 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class LightCard extends StatefulWidget { | ||||||
|  |  | ||||||
|  |   final LightCardData card; | ||||||
|  |  | ||||||
|  |   LightCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<StatefulWidget> createState() { | ||||||
|  |     return _LightCardState(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _LightCardState extends State<LightCard> { | ||||||
|  |  | ||||||
|  |   double _actualBrightness; | ||||||
|  |   double _newBrightness; | ||||||
|  |   bool _changedHere = false; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _setBrightness(double value, LightEntity entity) { | ||||||
|  |     setState((){ | ||||||
|  |       _newBrightness = value; | ||||||
|  |       _changedHere = true; | ||||||
|  |     }); | ||||||
|  |     ConnectionManager().callService( | ||||||
|  |       domain: entity.domain, | ||||||
|  |       service: "turn_on", | ||||||
|  |       entityId: entity.entityId, | ||||||
|  |       data: {"brightness": value.round()} | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     EntityWrapper entityWrapper = widget.card.entity; | ||||||
|  |     LightEntity entity = entityWrapper.entity; | ||||||
|  |     if (entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||||
|  |       return EntityModel( | ||||||
|  |         entityWrapper: widget.card.entity, | ||||||
|  |         child: MissedEntityWidget(), | ||||||
|  |         handleTap: false, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     entityWrapper.overrideName = widget.card.name ?? | ||||||
|  |         entityWrapper.displayName; | ||||||
|  |     entityWrapper.overrideIcon = widget.card.icon ?? | ||||||
|  |         entityWrapper.icon; | ||||||
|  |      | ||||||
|  |     if (!_changedHere) { | ||||||
|  |       _actualBrightness = (entity.brightness ?? 0).toDouble(); | ||||||
|  |       _newBrightness = _actualBrightness; | ||||||
|  |     } else { | ||||||
|  |       _changedHere = false; | ||||||
|  |     } | ||||||
|  |     Color lightColor = entity.color?.toColor(); | ||||||
|  |     Color color; | ||||||
|  |     if (lightColor != null && lightColor != Colors.white) { | ||||||
|  |       color = lightColor; | ||||||
|  |     } else { | ||||||
|  |       color = Theme.of(context).accentColor; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return CardWrapper( | ||||||
|  |       padding: EdgeInsets.all(4), | ||||||
|  |       child: EntityModel( | ||||||
|  |         entityWrapper: entityWrapper, | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |           children: <Widget>[ | ||||||
|  |             ConstrainedBox( | ||||||
|  |               constraints: BoxConstraints.loose(Size(200, 200)), | ||||||
|  |               child: AspectRatio( | ||||||
|  |                 aspectRatio: 1, | ||||||
|  |                 child: Stack( | ||||||
|  |                   alignment: Alignment.center, | ||||||
|  |                   children: <Widget>[ | ||||||
|  |                     SfRadialGauge( | ||||||
|  |                       axes: <RadialAxis>[ | ||||||
|  |                         RadialAxis( | ||||||
|  |                           onAxisTapped: (val) { | ||||||
|  |                             _setBrightness(val, entity); | ||||||
|  |                           }, | ||||||
|  |                           maximum: 255, | ||||||
|  |                           minimum: 0, | ||||||
|  |                           showLabels: false, | ||||||
|  |                           showTicks: false, | ||||||
|  |                           axisLineStyle: AxisLineStyle( | ||||||
|  |                             thickness: 0.05, | ||||||
|  |                             thicknessUnit: GaugeSizeUnit.factor, | ||||||
|  |                             color: HAClientTheme().getDisabledStateColor(context) | ||||||
|  |                           ), | ||||||
|  |                           pointers: <GaugePointer>[ | ||||||
|  |                             RangePointer( | ||||||
|  |                               value: _actualBrightness, | ||||||
|  |                               sizeUnit: GaugeSizeUnit.factor, | ||||||
|  |                               width: 0.05, | ||||||
|  |                               color: color, | ||||||
|  |                               enableAnimation: true, | ||||||
|  |                               animationType: AnimationType.bounceOut, | ||||||
|  |                             ), | ||||||
|  |                             MarkerPointer( | ||||||
|  |                               value: _newBrightness, | ||||||
|  |                               markerType: MarkerType.circle, | ||||||
|  |                               markerHeight: 20, | ||||||
|  |                               markerWidth: 20, | ||||||
|  |                               enableDragging: true, | ||||||
|  |                               onValueChangeEnd: (val) { | ||||||
|  |                                 _setBrightness(val, entity); | ||||||
|  |                               }, | ||||||
|  |                               color: HAClientTheme().getColorByEntityState(entity.state, context) | ||||||
|  |                               //enableAnimation: true, | ||||||
|  |                               //animationType: AnimationType.bounceOut, | ||||||
|  |                             ) | ||||||
|  |                           ] | ||||||
|  |                         ) | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                     FractionallySizedBox( | ||||||
|  |                       heightFactor: 0.4, | ||||||
|  |                       widthFactor: 0.4, | ||||||
|  |                       child: AspectRatio( | ||||||
|  |                         aspectRatio: 1, | ||||||
|  |                         child: InkResponse( | ||||||
|  |                           onTap: () => entityWrapper.handleTap(), | ||||||
|  |                           onLongPress: () => entityWrapper.handleHold(), | ||||||
|  |                           child: FittedBox( | ||||||
|  |                             fit: BoxFit.contain, | ||||||
|  |                             child: EntityIcon( | ||||||
|  |                               showBadge: false, | ||||||
|  |                               padding: EdgeInsets.all(0) | ||||||
|  |                             ) | ||||||
|  |                           ) | ||||||
|  |                         )  | ||||||
|  |                       ) | ||||||
|  |                     ) | ||||||
|  |                   ], | ||||||
|  |                 ) | ||||||
|  |               ) | ||||||
|  |             ), | ||||||
|  |             EntityName( | ||||||
|  |               padding: EdgeInsets.all(0), | ||||||
|  |               wordsWrap: true, | ||||||
|  |               maxLines: 3, | ||||||
|  |               textOverflow: TextOverflow.ellipsis, | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |             ) | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |         handleTap: true | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |    | ||||||
							
								
								
									
										33
									
								
								lib/cards/markdown_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class MarkdownCard extends StatelessWidget { | ||||||
|  |   final MarkdownCardData card; | ||||||
|  |  | ||||||
|  |   const MarkdownCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (card.content == null) { | ||||||
|  |       return Container(height: 0.0, width: 0.0,); | ||||||
|  |     } else if (card.content == '***') { | ||||||
|  |       return Container(height: Sizes.rowPadding, width: 0.0,); | ||||||
|  |     } | ||||||
|  |     return CardWrapper( | ||||||
|  |         child: Padding( | ||||||
|  |           padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||||
|  |           child: Column( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |             children: <Widget>[ | ||||||
|  |               CardHeader(name: card.title), | ||||||
|  |               MarkdownBody( | ||||||
|  |                 data: card.content, | ||||||
|  |               ) | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |    | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								lib/cards/media_control_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class MediaControlsCard extends StatelessWidget { | ||||||
|  |   final MediaControlCardData card; | ||||||
|  |  | ||||||
|  |   const MediaControlsCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (card.entity.entity.statelessType == StatelessEntityType.missed) { | ||||||
|  |       return EntityModel( | ||||||
|  |         entityWrapper: card.entity, | ||||||
|  |         child: MissedEntityWidget(), | ||||||
|  |         handleTap: false, | ||||||
|  |       ); | ||||||
|  |     } else if (card.entity.entity.domain == null || card.entity.entity.domain != 'media_player') { | ||||||
|  |       return EntityModel( | ||||||
|  |         entityWrapper: card.entity, | ||||||
|  |         child: ErrorEntityWidget( | ||||||
|  |           text: '${card.entity.entity?.entityId} is not a media_player', | ||||||
|  |         ), | ||||||
|  |         handleTap: false, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return CardWrapper( | ||||||
|  |         child: EntityModel( | ||||||
|  |             entityWrapper: card.entity, | ||||||
|  |             handleTap: null, | ||||||
|  |             child: MediaPlayerWidget() | ||||||
|  |         ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |    | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								lib/cards/unsupported_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class UnsupportedCard extends StatelessWidget { | ||||||
|  |   final CardData card; | ||||||
|  |  | ||||||
|  |   const UnsupportedCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Container(height: 20); | ||||||
|  |   }   | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								lib/cards/vertical_stack_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class VerticalStackCard extends StatelessWidget { | ||||||
|  |   final VerticalStackCardData card; | ||||||
|  |  | ||||||
|  |   const VerticalStackCard({Key key, this.card}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (card.childCards.isNotEmpty) { | ||||||
|  |       return Column( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         mainAxisAlignment: MainAxisAlignment.start, | ||||||
|  |         children: card.childCards.map<Widget>( | ||||||
|  |           (childCard) => childCard.buildCardWidget() | ||||||
|  |         ).toList(), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return Container(height: 0.0, width: 0.0,); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |    | ||||||
|  | } | ||||||
| @@ -4,9 +4,11 @@ class CardHeader extends StatelessWidget { | |||||||
|  |  | ||||||
|   final String name; |   final String name; | ||||||
|   final Widget trailing; |   final Widget trailing; | ||||||
|  |   final Widget leading; | ||||||
|   final Widget subtitle; |   final Widget subtitle; | ||||||
|  |   final double emptyPadding; | ||||||
|  |  | ||||||
|   const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key); |   const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
| @@ -14,14 +16,15 @@ class CardHeader extends StatelessWidget { | |||||||
|     if ((name != null) && (name.trim().length > 0)) { |     if ((name != null) && (name.trim().length > 0)) { | ||||||
|       result = new ListTile( |       result = new ListTile( | ||||||
|         trailing: trailing, |         trailing: trailing, | ||||||
|  |         leading: leading, | ||||||
|         subtitle: subtitle, |         subtitle: subtitle, | ||||||
|         title: Text("$name", |         title: Text("$name", | ||||||
|             textAlign: TextAlign.left, |             textAlign: TextAlign.left, | ||||||
|             overflow: TextOverflow.ellipsis, |             overflow: TextOverflow.ellipsis, | ||||||
|             style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)), |             style: Theme.of(context).textTheme.headline), | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       result = new Container(width: 0.0, height: 0.0); |       result = new Container(width: 0.0, height: emptyPadding); | ||||||
|     } |     } | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								lib/cards/widgets/card_wrapper.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | part of '../../main.dart'; | ||||||
|  |  | ||||||
|  | class CardWrapper extends StatelessWidget { | ||||||
|  |    | ||||||
|  |   final Widget child; | ||||||
|  |   final EdgeInsets padding; | ||||||
|  |   final Color color; | ||||||
|  |  | ||||||
|  |   const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Card( | ||||||
|  |       color: color, | ||||||
|  |       child: Padding( | ||||||
|  |         padding: padding, | ||||||
|  |         child: child | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,56 +0,0 @@ | |||||||
| part of '../../main.dart'; |  | ||||||
|  |  | ||||||
| class EntityButtonCardBody extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   final bool showName; |  | ||||||
|  |  | ||||||
|   EntityButtonCardBody({ |  | ||||||
|     Key key, this.showName: true, |  | ||||||
|   }) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; |  | ||||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { |  | ||||||
|       return MissedEntityWidget(); |  | ||||||
|     } |  | ||||||
|     if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) { |  | ||||||
|       return Container(width: 0.0, height: 0.0,); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return InkWell( |  | ||||||
|       onTap: () => entityWrapper.handleTap(), |  | ||||||
|       onLongPress: () => entityWrapper.handleHold(), |  | ||||||
|       child: FractionallySizedBox( |  | ||||||
|         widthFactor: 1, |  | ||||||
|         child: Column( |  | ||||||
|           children: <Widget>[ |  | ||||||
|             LayoutBuilder( |  | ||||||
|                 builder: (BuildContext context, BoxConstraints constraints) { |  | ||||||
|                   return EntityIcon( |  | ||||||
|                     padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0), |  | ||||||
|                     size: constraints.maxWidth / 2.5, |  | ||||||
|                   ); |  | ||||||
|                 } |  | ||||||
|             ), |  | ||||||
|             _buildName() |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildName() { |  | ||||||
|     if (showName) { |  | ||||||
|       return EntityName( |  | ||||||
|         padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding), |  | ||||||
|         textOverflow: TextOverflow.ellipsis, |  | ||||||
|         maxLines: 3, |  | ||||||
|         wordsWrap: true, |  | ||||||
|         textAlign: TextAlign.center, |  | ||||||
|         fontSize: Sizes.nameFontSize, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|     return Container(width: 0, height: 0); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,153 +0,0 @@ | |||||||
| part of '../../main.dart'; |  | ||||||
|  |  | ||||||
| class GaugeCardBody extends StatefulWidget { |  | ||||||
|  |  | ||||||
|   final int min; |  | ||||||
|   final int max; |  | ||||||
|   final Map severity; |  | ||||||
|  |  | ||||||
|   GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   _GaugeCardBodyState createState() => _GaugeCardBodyState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _GaugeCardBodyState extends State<GaugeCardBody> { |  | ||||||
|  |  | ||||||
|   List<charts.Series> seriesList; |  | ||||||
|  |  | ||||||
|   List<charts.Series<GaugeSegment, String>> _createData(double value) { |  | ||||||
|     double fixedValue; |  | ||||||
|     if (value > widget.max) { |  | ||||||
|       fixedValue = widget.max.toDouble(); |  | ||||||
|     } else if (value < widget.min) { |  | ||||||
|       fixedValue = widget.min.toDouble(); |  | ||||||
|     } else { |  | ||||||
|       fixedValue = value; |  | ||||||
|     } |  | ||||||
|     double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100; |  | ||||||
|     Color mainColor; |  | ||||||
|     if (widget.severity != null) { |  | ||||||
|       if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) { |  | ||||||
|         mainColor = Colors.red; |  | ||||||
|       } else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) { |  | ||||||
|         mainColor = Colors.amber; |  | ||||||
|       } else { |  | ||||||
|         mainColor = Colors.green; |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       mainColor = Colors.green; |  | ||||||
|     } |  | ||||||
|     final data = [ |  | ||||||
|       GaugeSegment('Main', toShow, mainColor), |  | ||||||
|       GaugeSegment('Rest', 100 - toShow, Colors.black45), |  | ||||||
|     ]; |  | ||||||
|  |  | ||||||
|     return [ |  | ||||||
|       charts.Series<GaugeSegment, String>( |  | ||||||
|         id: 'Segments', |  | ||||||
|         domainFn: (GaugeSegment segment, _) => segment.segment, |  | ||||||
|         measureFn: (GaugeSegment segment, _) => segment.value, |  | ||||||
|         colorFn: (GaugeSegment segment, _) => segment.color, |  | ||||||
|         // Set a label accessor to control the text of the arc label. |  | ||||||
|         labelAccessorFn: (GaugeSegment segment, _) => |  | ||||||
|         segment.segment == 'Main' ? '${segment.value}' : null, |  | ||||||
|         data: data, |  | ||||||
|       ) |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; |  | ||||||
|  |  | ||||||
|     return InkWell( |  | ||||||
|       onTap: () => entityWrapper.handleTap(), |  | ||||||
|       onLongPress: () => entityWrapper.handleHold(), |  | ||||||
|       child: AspectRatio( |  | ||||||
|           aspectRatio: 1.5, |  | ||||||
|           child: Stack( |  | ||||||
|               fit: StackFit.expand, |  | ||||||
|               overflow: Overflow.clip, |  | ||||||
|               children: [ |  | ||||||
|                 LayoutBuilder( |  | ||||||
|                     builder: (BuildContext context, BoxConstraints constraints) { |  | ||||||
|                       double verticalOffset; |  | ||||||
|                       if(constraints.maxWidth > 150.0) { |  | ||||||
|                         verticalOffset = 0.2; |  | ||||||
|                       } else if (constraints.maxWidth > 100.0)  { |  | ||||||
|                         verticalOffset = 0.3; |  | ||||||
|                       } else { |  | ||||||
|                         verticalOffset = 0.3; |  | ||||||
|                       } |  | ||||||
|                       return FractionallySizedBox( |  | ||||||
|                         heightFactor: 2, |  | ||||||
|                         widthFactor: 1, |  | ||||||
|                         alignment: FractionalOffset(0,verticalOffset), |  | ||||||
|                         child: charts.PieChart( |  | ||||||
|                           _createData(entityWrapper.entity.doubleState), |  | ||||||
|                           animate: false, |  | ||||||
|                           defaultRenderer: charts.ArcRendererConfig( |  | ||||||
|                             arcRatio: 0.4, |  | ||||||
|                             startAngle: pi, |  | ||||||
|                             arcLength: pi, |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                       ); |  | ||||||
|                     } |  | ||||||
|                 ), |  | ||||||
|                 Align( |  | ||||||
|                   alignment: Alignment.bottomCenter, |  | ||||||
|                   child: LayoutBuilder( |  | ||||||
|                       builder: (BuildContext context, BoxConstraints constraints) { |  | ||||||
|                         double fontSize = constraints.maxHeight / 7; |  | ||||||
|                         return Padding( |  | ||||||
|                           padding: EdgeInsets.only(bottom: 2*fontSize), |  | ||||||
|                           child: SimpleEntityState( |  | ||||||
|                             //textAlign: TextAlign.center, |  | ||||||
|                             expanded: false, |  | ||||||
|                             maxLines: 1, |  | ||||||
|                             bold: true, |  | ||||||
|                             textAlign: TextAlign.center, |  | ||||||
|                             padding: EdgeInsets.all(0.0), |  | ||||||
|                             fontSize: fontSize, |  | ||||||
|                             //padding: EdgeInsets.only(top: Sizes.rowPadding), |  | ||||||
|                           ), |  | ||||||
|                         ); |  | ||||||
|                       } |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|                 Align( |  | ||||||
|                   alignment: Alignment.bottomCenter, |  | ||||||
|                   child: LayoutBuilder( |  | ||||||
|                       builder: (BuildContext context, BoxConstraints constraints) { |  | ||||||
|                         double fontSize = constraints.maxHeight / 7; |  | ||||||
|                         return Padding( |  | ||||||
|                           padding: EdgeInsets.only(bottom: fontSize), |  | ||||||
|                           child: EntityName( |  | ||||||
|                             fontSize: fontSize, |  | ||||||
|                             maxLines: 1, |  | ||||||
|                             padding: EdgeInsets.all(0.0), |  | ||||||
|                             textAlign: TextAlign.center, |  | ||||||
|                             textOverflow: TextOverflow.ellipsis, |  | ||||||
|                           ), |  | ||||||
|                         ); |  | ||||||
|                       } |  | ||||||
|                   ), |  | ||||||
|                 ) |  | ||||||
|               ] |  | ||||||
|           ) |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class GaugeSegment { |  | ||||||
|   final String segment; |  | ||||||
|   final double value; |  | ||||||
|   final charts.Color color; |  | ||||||
|  |  | ||||||
|   GaugeSegment(this.segment, this.value, Color color) |  | ||||||
|       : this.color = charts.Color( |  | ||||||
|       r: color.red, g: color.green, b: color.blue, a: color.alpha); |  | ||||||
| } |  | ||||||
| @@ -1,85 +0,0 @@ | |||||||
| part of '../../main.dart'; |  | ||||||
|  |  | ||||||
| class GlanceCardEntityContainer extends StatelessWidget { |  | ||||||
|  |  | ||||||
|   final bool showName; |  | ||||||
|   final bool showState; |  | ||||||
|   final bool nameInTheBottom; |  | ||||||
|   final double iconSize; |  | ||||||
|   final double nameFontSize; |  | ||||||
|   final bool wordsWrapInName; |  | ||||||
|  |  | ||||||
|   GlanceCardEntityContainer({ |  | ||||||
|     Key key, |  | ||||||
|     @required this.showName, |  | ||||||
|     @required this.showState, |  | ||||||
|     this.nameInTheBottom: false, |  | ||||||
|     this.iconSize: Sizes.iconSize, |  | ||||||
|     this.nameFontSize: Sizes.smallFontSize, |  | ||||||
|     this.wordsWrapInName: false |  | ||||||
|   }) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; |  | ||||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { |  | ||||||
|       return MissedEntityWidget(); |  | ||||||
|     } |  | ||||||
|     if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) { |  | ||||||
|       return Container(width: 0.0, height: 0.0,); |  | ||||||
|     } |  | ||||||
|     List<Widget> result = []; |  | ||||||
|     if (!nameInTheBottom) { |  | ||||||
|       if (showName) { |  | ||||||
|         result.add(_buildName()); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       if (showState) { |  | ||||||
|         result.add(_buildState()); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     result.add( |  | ||||||
|         EntityIcon( |  | ||||||
|           padding: EdgeInsets.all(0.0), |  | ||||||
|           size: iconSize, |  | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|     if (!nameInTheBottom) { |  | ||||||
|       if (showState) { |  | ||||||
|         result.add(_buildState()); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       result.add(_buildName()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return Center( |  | ||||||
|       child: InkResponse( |  | ||||||
|         child: Column( |  | ||||||
|           mainAxisSize: MainAxisSize.min, |  | ||||||
|           children: result, |  | ||||||
|         ), |  | ||||||
|         onTap: () => entityWrapper.handleTap(), |  | ||||||
|         onLongPress: () => entityWrapper.handleHold(), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildName() { |  | ||||||
|     return EntityName( |  | ||||||
|       padding: EdgeInsets.only(bottom: Sizes.rowPadding), |  | ||||||
|       textOverflow: TextOverflow.ellipsis, |  | ||||||
|       wordsWrap: wordsWrapInName, |  | ||||||
|       textAlign: TextAlign.center, |  | ||||||
|       fontSize: nameFontSize, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildState() { |  | ||||||
|     return SimpleEntityState( |  | ||||||
|       textAlign: TextAlign.center, |  | ||||||
|       expanded: false, |  | ||||||
|       maxLines: 1, |  | ||||||
|       padding: EdgeInsets.only(top: Sizes.rowPadding), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,90 +0,0 @@ | |||||||
| part of '../../main.dart'; |  | ||||||
|  |  | ||||||
| class LightCardBody extends StatefulWidget { |  | ||||||
|  |  | ||||||
|   final int min; |  | ||||||
|   final int max; |  | ||||||
|   final Map severity; |  | ||||||
|  |  | ||||||
|   LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   _LightCardBodyState createState() => _LightCardBodyState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _LightCardBodyState extends State<LightCardBody> { |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; |  | ||||||
|     LightEntity entity = entityWrapper.entity; |  | ||||||
|     Logger.d("Light brightness: ${entity.brightness}"); |  | ||||||
|  |  | ||||||
|     return FractionallySizedBox( |  | ||||||
|       widthFactor: 0.5, |  | ||||||
|       child: Container( |  | ||||||
|         //color: Colors.redAccent, |  | ||||||
|         child: SingleCircularSlider( |  | ||||||
|             255, |  | ||||||
|             entity.brightness ?? 0, |  | ||||||
|           baseColor: Colors.white, |  | ||||||
|           handlerColor: Colors.blue[200], |  | ||||||
|           selectionColor: Colors.blue[100], |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return InkWell( |  | ||||||
|       onTap: () => entityWrapper.handleTap(), |  | ||||||
|       onLongPress: () => entityWrapper.handleHold(), |  | ||||||
|       child: AspectRatio( |  | ||||||
|           aspectRatio: 1.5, |  | ||||||
|           child: Stack( |  | ||||||
|               fit: StackFit.expand, |  | ||||||
|               overflow: Overflow.clip, |  | ||||||
|               children: [ |  | ||||||
|                 Align( |  | ||||||
|                   alignment: Alignment.bottomCenter, |  | ||||||
|                   child: LayoutBuilder( |  | ||||||
|                       builder: (BuildContext context, BoxConstraints constraints) { |  | ||||||
|                         double fontSize = constraints.maxHeight / 7; |  | ||||||
|                         return Padding( |  | ||||||
|                           padding: EdgeInsets.only(bottom: 2*fontSize), |  | ||||||
|                           child: SimpleEntityState( |  | ||||||
|                             //textAlign: TextAlign.center, |  | ||||||
|                             expanded: false, |  | ||||||
|                             maxLines: 1, |  | ||||||
|                             bold: true, |  | ||||||
|                             textAlign: TextAlign.center, |  | ||||||
|                             padding: EdgeInsets.all(0.0), |  | ||||||
|                             fontSize: fontSize, |  | ||||||
|                             //padding: EdgeInsets.only(top: Sizes.rowPadding), |  | ||||||
|                           ), |  | ||||||
|                         ); |  | ||||||
|                       } |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|                 Align( |  | ||||||
|                   alignment: Alignment.bottomCenter, |  | ||||||
|                   child: LayoutBuilder( |  | ||||||
|                       builder: (BuildContext context, BoxConstraints constraints) { |  | ||||||
|                         double fontSize = constraints.maxHeight / 7; |  | ||||||
|                         return Padding( |  | ||||||
|                           padding: EdgeInsets.only(bottom: fontSize), |  | ||||||
|                           child: EntityName( |  | ||||||
|                             fontSize: fontSize, |  | ||||||
|                             maxLines: 1, |  | ||||||
|                             padding: EdgeInsets.all(0.0), |  | ||||||
|                             textAlign: TextAlign.center, |  | ||||||
|                             textOverflow: TextOverflow.ellipsis, |  | ||||||
|                           ), |  | ||||||
|                         ); |  | ||||||
|                       } |  | ||||||
|                   ), |  | ||||||
|                 ) |  | ||||||
|               ] |  | ||||||
|           ) |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -36,51 +36,6 @@ class EntityState { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| class EntityUIAction { |  | ||||||
|   static const moreInfo = 'more-info'; |  | ||||||
|   static const toggle = 'toggle'; |  | ||||||
|   static const callService = 'call-service'; |  | ||||||
|   static const navigate = 'navigate'; |  | ||||||
|   static const none = 'none'; |  | ||||||
|  |  | ||||||
|   String tapAction = EntityUIAction.moreInfo; |  | ||||||
|   String tapNavigationPath; |  | ||||||
|   String tapService; |  | ||||||
|   Map<String, dynamic> tapServiceData; |  | ||||||
|   String holdAction = EntityUIAction.none; |  | ||||||
|   String holdNavigationPath; |  | ||||||
|   String holdService; |  | ||||||
|   Map<String, dynamic> holdServiceData; |  | ||||||
|  |  | ||||||
|   EntityUIAction({rawEntityData}) { |  | ||||||
|     if (rawEntityData != null) { |  | ||||||
|       if (rawEntityData["tap_action"] != null) { |  | ||||||
|         if (rawEntityData["tap_action"] is String) { |  | ||||||
|           tapAction = rawEntityData["tap_action"]; |  | ||||||
|         } else { |  | ||||||
|           tapAction = |  | ||||||
|               rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo; |  | ||||||
|           tapNavigationPath = rawEntityData["tap_action"]["navigation_path"]; |  | ||||||
|           tapService = rawEntityData["tap_action"]["service"]; |  | ||||||
|           tapServiceData = rawEntityData["tap_action"]["service_data"]; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (rawEntityData["hold_action"] != null) { |  | ||||||
|         if (rawEntityData["hold_action"] is String) { |  | ||||||
|           holdAction = rawEntityData["hold_action"]; |  | ||||||
|         } else { |  | ||||||
|           holdAction = |  | ||||||
|               rawEntityData["hold_action"]["action"] ?? EntityUIAction.none; |  | ||||||
|           holdNavigationPath = rawEntityData["hold_action"]["navigation_path"]; |  | ||||||
|           holdService = rawEntityData["hold_action"]["service"]; |  | ||||||
|           holdServiceData = rawEntityData["hold_action"]["service_data"]; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class CardType { | class CardType { | ||||||
|   static const HORIZONTAL_STACK = "horizontal-stack"; |   static const HORIZONTAL_STACK = "horizontal-stack"; | ||||||
|   static const VERTICAL_STACK = "vertical-stack"; |   static const VERTICAL_STACK = "vertical-stack"; | ||||||
| @@ -98,10 +53,17 @@ class CardType { | |||||||
|   static const IFRAME = "iframe"; |   static const IFRAME = "iframe"; | ||||||
|   static const GAUGE = "gauge"; |   static const GAUGE = "gauge"; | ||||||
|   static const ENTITY_BUTTON = "entity-button"; |   static const ENTITY_BUTTON = "entity-button"; | ||||||
|  |   static const ENTITY = "entity"; | ||||||
|  |   static const BUTTON = "button"; | ||||||
|   static const CONDITIONAL = "conditional"; |   static const CONDITIONAL = "conditional"; | ||||||
|   static const ALARM_PANEL = "alarm-panel"; |   static const ALARM_PANEL = "alarm-panel"; | ||||||
|   static const MARKDOWN = "markdown"; |   static const MARKDOWN = "markdown"; | ||||||
|   static const LIGHT = "light"; |   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 { | class Sizes { | ||||||
| @@ -111,10 +73,10 @@ class Sizes { | |||||||
|   static const extendedWidgetHeight = 50.0; |   static const extendedWidgetHeight = 50.0; | ||||||
|   static const iconSize = 28.0; |   static const iconSize = 28.0; | ||||||
|   static const largeIconSize = 46.0; |   static const largeIconSize = 46.0; | ||||||
|   static const stateFontSize = 15.0; |   //static const stateFontSize = 15.0; | ||||||
|   static const nameFontSize = 15.0; |   //static const nameFontSize = 15.0; | ||||||
|   static const smallFontSize = 14.0; |   //static const smallFontSize = 14.0; | ||||||
|   static const largeFontSize = 24.0; |   //static const largeFontSize = 24.0; | ||||||
|   static const inputWidth = 160.0; |   static const inputWidth = 160.0; | ||||||
|   static const rowPadding = 10.0; |   static const rowPadding = 10.0; | ||||||
|   static const doubleRowPadding = rowPadding*2; |   static const doubleRowPadding = rowPadding*2; | ||||||
|   | |||||||
| @@ -248,7 +248,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane | |||||||
|         FlatButton( |         FlatButton( | ||||||
|           child: Text( |           child: Text( | ||||||
|             "TRIGGER", |             "TRIGGER", | ||||||
|             style: TextStyle(color: Colors.redAccent) |             style: Theme.of(context).textTheme.subhead.copyWith( | ||||||
|  |               color: Theme.of(context).errorColor | ||||||
|  |             ) | ||||||
|           ), |           ), | ||||||
|           onPressed: () => _askToTrigger(entity), |           onPressed: () => _askToTrigger(entity), | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -1,145 +0,0 @@ | |||||||
| part of '../main.dart'; |  | ||||||
|  |  | ||||||
| class BadgeWidget extends StatelessWidget { |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final entityModel = EntityModel.of(context); |  | ||||||
|     double iconSize = 26.0; |  | ||||||
|     Widget badgeIcon; |  | ||||||
|     String onBadgeTextValue; |  | ||||||
|     Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ?? |  | ||||||
|         EntityColor.badgeColors["default"]; |  | ||||||
|     switch (entityModel.entityWrapper.entity.domain) { |  | ||||||
|       case "sun": |  | ||||||
|         { |  | ||||||
|           badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon" |  | ||||||
|               ? Icon( |  | ||||||
|             MaterialDesignIcons.getIconDataFromIconCode(0xf0dc), |  | ||||||
|             size: iconSize, |  | ||||||
|           ) |  | ||||||
|               : Icon( |  | ||||||
|             MaterialDesignIcons.getIconDataFromIconCode(0xf5a8), |  | ||||||
|             size: iconSize, |  | ||||||
|           ); |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|       case "camera": |  | ||||||
|       case "media_player": |  | ||||||
|       case "binary_sensor": |  | ||||||
|         { |  | ||||||
|           badgeIcon = EntityIcon( |  | ||||||
|             padding: EdgeInsets.all(0.0), |  | ||||||
|             size: iconSize, |  | ||||||
|             color: Colors.black |  | ||||||
|           ); |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|       case "device_tracker": |  | ||||||
|       case "person": |  | ||||||
|         { |  | ||||||
|           badgeIcon = EntityIcon( |  | ||||||
|               padding: EdgeInsets.all(0.0), |  | ||||||
|               size: iconSize, |  | ||||||
|               color: Colors.black |  | ||||||
|           ); |  | ||||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.displayState; |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|       default: |  | ||||||
|         { |  | ||||||
|           double stateFontSize; |  | ||||||
|           if (entityModel.entityWrapper.entity.displayState.length <= 3) { |  | ||||||
|             stateFontSize = 18.0; |  | ||||||
|           } else if (entityModel.entityWrapper.entity.displayState.length <= 4) { |  | ||||||
|             stateFontSize = 15.0; |  | ||||||
|           } else if (entityModel.entityWrapper.entity.displayState.length <= 6) { |  | ||||||
|             stateFontSize = 10.0; |  | ||||||
|           } else if (entityModel.entityWrapper.entity.displayState.length <= 10) { |  | ||||||
|             stateFontSize = 8.0; |  | ||||||
|           } |  | ||||||
|           onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement; |  | ||||||
|           badgeIcon = Center( |  | ||||||
|             child: Text( |  | ||||||
|               "${entityModel.entityWrapper.entity.displayState}", |  | ||||||
|               overflow: TextOverflow.fade, |  | ||||||
|               softWrap: false, |  | ||||||
|               textAlign: TextAlign.center, |  | ||||||
|               style: TextStyle(fontSize: stateFontSize), |  | ||||||
|             ), |  | ||||||
|           ); |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     Widget onBadgeText; |  | ||||||
|     if (onBadgeTextValue == null || onBadgeTextValue.length == 0) { |  | ||||||
|       onBadgeText = Container(width: 0.0, height: 0.0); |  | ||||||
|     } else { |  | ||||||
|       onBadgeText = Container( |  | ||||||
|           padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0), |  | ||||||
|           child: Text("$onBadgeTextValue", |  | ||||||
|               style: TextStyle(fontSize: 12.0, color: Colors.white), |  | ||||||
|               textAlign: TextAlign.center, |  | ||||||
|               softWrap: false, |  | ||||||
|               overflow: TextOverflow.fade), |  | ||||||
|           decoration: new BoxDecoration( |  | ||||||
|             // Circle shape |  | ||||||
|             //shape: BoxShape.circle, |  | ||||||
|             color: iconColor, |  | ||||||
|             borderRadius: BorderRadius.circular(9.0), |  | ||||||
|           )); |  | ||||||
|     } |  | ||||||
|     return GestureDetector( |  | ||||||
|         child: Column( |  | ||||||
|           children: <Widget>[ |  | ||||||
|             Container( |  | ||||||
|               margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0), |  | ||||||
|               width: 50.0, |  | ||||||
|               height: 50.0, |  | ||||||
|               decoration: new BoxDecoration( |  | ||||||
|                 // Circle shape |  | ||||||
|                 shape: BoxShape.circle, |  | ||||||
|                 color: Colors.white, |  | ||||||
|                 // The border you want |  | ||||||
|                 border: new Border.all( |  | ||||||
|                   width: 2.0, |  | ||||||
|                   color: iconColor, |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|               child: Stack( |  | ||||||
|                 overflow: Overflow.visible, |  | ||||||
|                 children: <Widget>[ |  | ||||||
|                   Positioned( |  | ||||||
|                     width: 46.0, |  | ||||||
|                     height: 46.0, |  | ||||||
|                     top: 0.0, |  | ||||||
|                     left: 0.0, |  | ||||||
|                     child: badgeIcon, |  | ||||||
|                   ), |  | ||||||
|                   Positioned( |  | ||||||
|                     //width: 50.0, |  | ||||||
|                       bottom: -9.0, |  | ||||||
|                       left: -10.0, |  | ||||||
|                       right: -10.0, |  | ||||||
|                       child: Center( |  | ||||||
|                         child: onBadgeText, |  | ||||||
|                       )) |  | ||||||
|                 ], |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|             Container( |  | ||||||
|               width: 60.0, |  | ||||||
|               child: Text( |  | ||||||
|                 "${entityModel.entityWrapper.displayName}", |  | ||||||
|                 textAlign: TextAlign.center, |  | ||||||
|                 style: TextStyle(fontSize: 12.0), |  | ||||||
|                 softWrap: true, |  | ||||||
|                 maxLines: 3, |  | ||||||
|                 overflow: TextOverflow.ellipsis, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|         onTap: () => |  | ||||||
|             eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity))); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -3,12 +3,16 @@ part of '../../main.dart'; | |||||||
| class CameraEntity extends Entity { | class CameraEntity extends Entity { | ||||||
|  |  | ||||||
|   static const SUPPORT_ON_OFF = 1; |   static const SUPPORT_ON_OFF = 1; | ||||||
|  |   static const SUPPORT_STREAM = 2; | ||||||
|  |  | ||||||
|   CameraEntity(Map rawData, String webHost) : super(rawData, webHost); |   CameraEntity(Map rawData, String webHost) : super(rawData, webHost); | ||||||
|  |  | ||||||
|   bool get supportOnOff => ((supportedFeatures & |   bool get supportOnOff => ((supportedFeatures & | ||||||
|   CameraEntity.SUPPORT_ON_OFF) == |   CameraEntity.SUPPORT_ON_OFF) == | ||||||
|       CameraEntity.SUPPORT_ON_OFF); |       CameraEntity.SUPPORT_ON_OFF); | ||||||
|  |   bool get supportStream => ((supportedFeatures & | ||||||
|  |   CameraEntity.SUPPORT_STREAM) == | ||||||
|  |       CameraEntity.SUPPORT_STREAM); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget _buildAdditionalControlsForPage(BuildContext context) { |   Widget _buildAdditionalControlsForPage(BuildContext context) { | ||||||
|   | |||||||
| @@ -2,7 +2,9 @@ part of '../../../main.dart'; | |||||||
|  |  | ||||||
| class CameraStreamView extends StatefulWidget { | class CameraStreamView extends StatefulWidget { | ||||||
|  |  | ||||||
|   CameraStreamView({Key key}) : super(key: key); |   final bool withControls; | ||||||
|  |  | ||||||
|  |   CameraStreamView({Key key, this.withControls: true}) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   _CameraStreamViewState createState() => _CameraStreamViewState(); |   _CameraStreamViewState createState() => _CameraStreamViewState(); | ||||||
| @@ -10,45 +12,175 @@ class CameraStreamView extends StatefulWidget { | |||||||
|  |  | ||||||
| class _CameraStreamViewState extends State<CameraStreamView> { | class _CameraStreamViewState extends State<CameraStreamView> { | ||||||
|  |  | ||||||
|  |   CameraEntity _entity; | ||||||
|  |   String _streamUrl = ""; | ||||||
|  |   bool _isLoaded = false; | ||||||
|  |   double _aspectRatio = 1.33; | ||||||
|  |   String _webViewHtml; | ||||||
|  |   String _jsMessageChannelName = 'unknown'; | ||||||
|  |   Completer _loading; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   CameraEntity _entity; |   Future _loadResources() { | ||||||
|   bool started = false; |     if (_loading != null && !_loading.isCompleted) { | ||||||
|   String streamUrl = ""; |       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}}', '${AppSettings().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; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   launchStream() { |   Future _loadMJPEG() async { | ||||||
|     Launcher.launchURLInCustomTab( |     _streamUrl = '${AppSettings().httpWebHost}/api/camera_proxy_stream/${_entity | ||||||
|       context: context, |         .entityId}?token=${_entity.attributes['access_token']}'; | ||||||
|       url: streamUrl |     _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 |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     if (!started) { |     if (!_isLoaded && (_loading == null || _loading.isCompleted)) { | ||||||
|       _entity = EntityModel |       _loadResources().then((_) => setState((){ _isLoaded = true; })); | ||||||
|           .of(context) |  | ||||||
|           .entityWrapper |  | ||||||
|           .entity; |  | ||||||
|       started = true; |  | ||||||
|     } |     } | ||||||
|     streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity |     if (widget.withControls) { | ||||||
|         .entityId}?token=${_entity.attributes['access_token']}'; |       return Card( | ||||||
|     return Column( |         child: Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|           children: <Widget>[ |           children: <Widget>[ | ||||||
|         Container( |             _buildScreen(), | ||||||
|             padding: const EdgeInsets.all(20.0), |             _buildControls() | ||||||
|             child: IconButton( |  | ||||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber), |  | ||||||
|               iconSize: 50.0, |  | ||||||
|               onPressed: () => launchStream(), |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|           ], |           ], | ||||||
|  |         ), | ||||||
|       ); |       ); | ||||||
|  |     } else { | ||||||
|  |       return _buildScreen(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   | |||||||
| @@ -10,9 +10,8 @@ class ClimateControlWidget extends StatefulWidget { | |||||||
|  |  | ||||||
| class _ClimateControlWidgetState extends State<ClimateControlWidget> { | class _ClimateControlWidgetState extends State<ClimateControlWidget> { | ||||||
|  |  | ||||||
|   bool _showPending = false; |   bool _temperaturePending = false; | ||||||
|   bool _changedHere = false; |   bool _changedHere = false; | ||||||
|   Timer _resetTimer; |  | ||||||
|   Timer _tempThrottleTimer; |   Timer _tempThrottleTimer; | ||||||
|   Timer _targetTempThrottleTimer; |   Timer _targetTempThrottleTimer; | ||||||
|   double _tmpTemperature = 0.0; |   double _tmpTemperature = 0.0; | ||||||
| @@ -27,9 +26,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|   bool _tmpAuxHeat = false; |   bool _tmpAuxHeat = false; | ||||||
|  |  | ||||||
|   void _resetVars(ClimateEntity entity) { |   void _resetVars(ClimateEntity entity) { | ||||||
|  |     if (!_temperaturePending) { | ||||||
|       _tmpTemperature = entity.temperature; |       _tmpTemperature = entity.temperature; | ||||||
|       _tmpTargetHigh = entity.targetHigh; |       _tmpTargetHigh = entity.targetHigh; | ||||||
|       _tmpTargetLow = entity.targetLow; |       _tmpTargetLow = entity.targetLow; | ||||||
|  |     } | ||||||
|     _tmpHVACMode = entity.state; |     _tmpHVACMode = entity.state; | ||||||
|     _tmpFanMode = entity.fanMode; |     _tmpFanMode = entity.fanMode; | ||||||
|     _tmpSwingMode = entity.swingMode; |     _tmpSwingMode = entity.swingMode; | ||||||
| @@ -38,7 +39,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     _tmpAuxHeat = entity.auxHeat; |     _tmpAuxHeat = entity.auxHeat; | ||||||
|     _tmpTargetHumidity = entity.targetHumidity; |     _tmpTargetHumidity = entity.targetHumidity; | ||||||
|  |  | ||||||
|     _showPending = false; |  | ||||||
|     _changedHere = false; |     _changedHere = false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -73,46 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _setTemperature(ClimateEntity entity) { |   void _setTemperature(ClimateEntity entity) { | ||||||
|     if (_tempThrottleTimer!=null) { |     _tempThrottleTimer?.cancel(); | ||||||
|       _tempThrottleTimer.cancel(); |  | ||||||
|     } |  | ||||||
|     setState(() { |     setState(() { | ||||||
|       _changedHere = true; |       _changedHere = true; | ||||||
|  |       _temperaturePending = true; | ||||||
|       _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1)); |       _tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1)); | ||||||
|     }); |     }); | ||||||
|     _tempThrottleTimer = Timer(Duration(seconds: 2), () { |     _tempThrottleTimer = Timer(Duration(seconds: 2), () { | ||||||
|       setState(() { |       setState(() { | ||||||
|         _changedHere = true; |         _changedHere = true; | ||||||
|  |         _temperaturePending = false; | ||||||
|         ConnectionManager().callService( |         ConnectionManager().callService( | ||||||
|           domain: entity.domain, |           domain: entity.domain, | ||||||
|           service: "set_temperature", |           service: "set_temperature", | ||||||
|           entityId: entity.entityId, |           entityId: entity.entityId, | ||||||
|           data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"} |           data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"} | ||||||
|         ); |         ); | ||||||
|         _resetStateTimer(entity); |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _setTargetTemp(ClimateEntity entity) { |   void _setTargetTemp(ClimateEntity entity) { | ||||||
|     if (_targetTempThrottleTimer!=null) { |     _targetTempThrottleTimer?.cancel(); | ||||||
|       _targetTempThrottleTimer.cancel(); |  | ||||||
|     } |  | ||||||
|     setState(() { |     setState(() { | ||||||
|       _changedHere = true; |       _changedHere = true; | ||||||
|  |       _temperaturePending = true; | ||||||
|       _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); |       _tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1)); | ||||||
|       _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); |       _tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1)); | ||||||
|     }); |     }); | ||||||
|     _targetTempThrottleTimer = Timer(Duration(seconds: 2), () { |     _targetTempThrottleTimer = Timer(Duration(seconds: 2), () { | ||||||
|       setState(() { |       setState(() { | ||||||
|         _changedHere = true; |         _changedHere = true; | ||||||
|  |         _temperaturePending = false; | ||||||
|         ConnectionManager().callService( |         ConnectionManager().callService( | ||||||
|           domain: entity.domain, |           domain: entity.domain, | ||||||
|           service: "set_temperature", |           service: "set_temperature", | ||||||
|           entityId: entity.entityId, |           entityId: entity.entityId, | ||||||
|           data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"} |           data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"} | ||||||
|         ); |         ); | ||||||
|         _resetStateTimer(entity); |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @@ -127,7 +125,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|           entityId: entity.entityId, |           entityId: entity.entityId, | ||||||
|           data: {"humidity": "$_tmpTargetHumidity"} |           data: {"humidity": "$_tmpTargetHumidity"} | ||||||
|         ); |         ); | ||||||
|       _resetStateTimer(entity); |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -141,7 +138,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|           entityId: entity.entityId, |           entityId: entity.entityId, | ||||||
|           data: {"hvac_mode": "$_tmpHVACMode"} |           data: {"hvac_mode": "$_tmpHVACMode"} | ||||||
|         ); |         ); | ||||||
|       _resetStateTimer(entity); |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -155,7 +151,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|           entityId: entity.entityId, |           entityId: entity.entityId, | ||||||
|           data: {"swing_mode": "$_tmpSwingMode"} |           data: {"swing_mode": "$_tmpSwingMode"} | ||||||
|         ); |         ); | ||||||
|       _resetStateTimer(entity); |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -164,7 +159,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|       _tmpFanMode = value; |       _tmpFanMode = value; | ||||||
|       _changedHere = true; |       _changedHere = true; | ||||||
|       ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"}); |       ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"}); | ||||||
|       _resetStateTimer(entity); |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -173,7 +167,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|       _tmpPresetMode = value; |       _tmpPresetMode = value; | ||||||
|       _changedHere = true; |       _changedHere = true; | ||||||
|       ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"}); |       ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"}); | ||||||
|       _resetStateTimer(entity); |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -191,17 +184,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|       _tmpAuxHeat = value; |       _tmpAuxHeat = value; | ||||||
|       _changedHere = true; |       _changedHere = true; | ||||||
|       ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"}); |       ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"}); | ||||||
|       _resetStateTimer(entity); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   void _resetStateTimer(ClimateEntity entity) { |  | ||||||
|     if (_resetTimer!=null) { |  | ||||||
|       _resetTimer.cancel(); |  | ||||||
|     } |  | ||||||
|     _resetTimer = Timer(Duration(seconds: 3), () { |  | ||||||
|       setState(() {}); |  | ||||||
|       _resetVars(entity); |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -210,10 +192,9 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     final entityModel = EntityModel.of(context); |     final entityModel = EntityModel.of(context); | ||||||
|     final ClimateEntity entity = entityModel.entityWrapper.entity; |     final ClimateEntity entity = entityModel.entityWrapper.entity; | ||||||
|     if (_changedHere) { |     if (_changedHere) { | ||||||
|       _showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow); |       //_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow); | ||||||
|       _changedHere = false; |       _changedHere = false; | ||||||
|     } else { |     } else { | ||||||
|       _resetTimer?.cancel(); |  | ||||||
|       _resetVars(entity); |       _resetVars(entity); | ||||||
|     } |     } | ||||||
|     return Padding( |     return Padding( | ||||||
| @@ -222,20 +203,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           //_buildOnOffControl(entity), |           //_buildOnOffControl(entity), | ||||||
|           _buildTemperatureControls(entity), |           _buildTemperatureControls(entity, context), | ||||||
|           _buildTargetTemperatureControls(entity), |           _buildTargetTemperatureControls(entity, context), | ||||||
|           _buildHumidityControls(entity), |           _buildHumidityControls(entity, context), | ||||||
|           _buildOperationControl(entity), |           _buildOperationControl(entity, context), | ||||||
|           _buildFanControl(entity), |           _buildFanControl(entity, context), | ||||||
|           _buildSwingControl(entity), |           _buildSwingControl(entity, context), | ||||||
|           _buildPresetModeControl(entity), |           _buildPresetModeControl(entity, context), | ||||||
|           _buildAuxHeatControl(entity) |           _buildAuxHeatControl(entity, context) | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildPresetModeControl(ClimateEntity entity) { |   Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) { | ||||||
|     if (entity.supportPresetMode) { |     if (entity.supportPresetMode) { | ||||||
|       return ModeSelectorWidget( |       return ModeSelectorWidget( | ||||||
|         options: entity.presetModes, |         options: entity.presetModes, | ||||||
| @@ -260,7 +241,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     } |     } | ||||||
|   }*/ |   }*/ | ||||||
|  |  | ||||||
|   Widget _buildAuxHeatControl(ClimateEntity entity) { |   Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) { | ||||||
|     if (entity.supportAuxHeat ) { |     if (entity.supportAuxHeat ) { | ||||||
|       return ModeSwitchWidget( |       return ModeSwitchWidget( | ||||||
|           caption: "Aux heat", |           caption: "Aux heat", | ||||||
| @@ -272,7 +253,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildOperationControl(ClimateEntity entity) { |   Widget _buildOperationControl(ClimateEntity entity, BuildContext context) { | ||||||
|     if (entity.hvacModes != null) { |     if (entity.hvacModes != null) { | ||||||
|       return ModeSelectorWidget( |       return ModeSelectorWidget( | ||||||
|         onChange: (mode) => _setHVACMode(entity, mode), |         onChange: (mode) => _setHVACMode(entity, mode), | ||||||
| @@ -285,7 +266,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildFanControl(ClimateEntity entity) { |   Widget _buildFanControl(ClimateEntity entity, BuildContext context) { | ||||||
|     if (entity.supportFanMode) { |     if (entity.supportFanMode) { | ||||||
|       return ModeSelectorWidget( |       return ModeSelectorWidget( | ||||||
|         options: entity.fanModes, |         options: entity.fanModes, | ||||||
| @@ -298,7 +279,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildSwingControl(ClimateEntity entity) { |   Widget _buildSwingControl(ClimateEntity entity, BuildContext context) { | ||||||
|     if (entity.supportSwingMode) { |     if (entity.supportSwingMode) { | ||||||
|       return ModeSelectorWidget( |       return ModeSelectorWidget( | ||||||
|           onChange: (mode) => _setSwingMode(entity, mode), |           onChange: (mode) => _setSwingMode(entity, mode), | ||||||
| @@ -311,17 +292,15 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildTemperatureControls(ClimateEntity entity) { |   Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) { | ||||||
|     if ((entity.supportTargetTemperature) && (entity.temperature != null)) { |     if ((entity.supportTargetTemperature) && (entity.temperature != null)) { | ||||||
|       return Column( |       return Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           Text("Target temperature", style: TextStyle( |           Text("Target temperature", style: Theme.of(context).textTheme.body1), | ||||||
|               fontSize: Sizes.stateFontSize |  | ||||||
|           )), |  | ||||||
|           TemperatureControlWidget( |           TemperatureControlWidget( | ||||||
|             value: _tmpTemperature, |             value: _tmpTemperature, | ||||||
|             fontColor: _showPending ? Colors.red : Colors.black, |             active: _temperaturePending, | ||||||
|             onDec: () => _temperatureDown(entity), |             onDec: () => _temperatureDown(entity), | ||||||
|             onInc: () => _temperatureUp(entity), |             onInc: () => _temperatureUp(entity), | ||||||
|           ) |           ) | ||||||
| @@ -332,13 +311,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildTargetTemperatureControls(ClimateEntity entity) { |   Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) { | ||||||
|     List<Widget> controls = []; |     List<Widget> controls = []; | ||||||
|     if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) { |     if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) { | ||||||
|       controls.addAll(<Widget>[ |       controls.addAll(<Widget>[ | ||||||
|         TemperatureControlWidget( |         TemperatureControlWidget( | ||||||
|           value: _tmpTargetLow, |           value: _tmpTargetLow, | ||||||
|           fontColor: _showPending ? Colors.red : Colors.black, |           active: _temperaturePending, | ||||||
|           onDec: () => _targetLowDown(entity), |           onDec: () => _targetLowDown(entity), | ||||||
|           onInc: () => _targetLowUp(entity), |           onInc: () => _targetLowUp(entity), | ||||||
|         ), |         ), | ||||||
| @@ -351,7 +330,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|       controls.add( |       controls.add( | ||||||
|           TemperatureControlWidget( |           TemperatureControlWidget( | ||||||
|             value: _tmpTargetHigh, |             value: _tmpTargetHigh, | ||||||
|             fontColor: _showPending ? Colors.red : Colors.black, |             active: _temperaturePending, | ||||||
|             onDec: () => _targetHighDown(entity), |             onDec: () => _targetHighDown(entity), | ||||||
|             onInc: () => _targetHighUp(entity), |             onInc: () => _targetHighUp(entity), | ||||||
|           ) |           ) | ||||||
| @@ -361,9 +340,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|       return Column( |       return Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           Text("Target temperature range", style: TextStyle( |           Text("Target temperature range", style: Theme.of(context).textTheme.body1), | ||||||
|               fontSize: Sizes.stateFontSize |  | ||||||
|           )), |  | ||||||
|           Row( |           Row( | ||||||
|             children: controls, |             children: controls, | ||||||
|           ) |           ) | ||||||
| @@ -374,16 +351,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildHumidityControls(ClimateEntity entity) { |   Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) { | ||||||
|     List<Widget> result = []; |  | ||||||
|     if (entity.supportTargetHumidity) { |     if (entity.supportTargetHumidity) { | ||||||
|       result.addAll(<Widget>[ |       return Column( | ||||||
|         Text( |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           "$_tmpTargetHumidity%", |         children: <Widget>[ | ||||||
|           style: TextStyle(fontSize: Sizes.largeFontSize), |           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, | ||||||
|             ), |             ), | ||||||
|         Expanded( |  | ||||||
|           child: Slider( |  | ||||||
|             value: _tmpTargetHumidity, |             value: _tmpTargetHumidity, | ||||||
|             max: entity.maxHumidity, |             max: entity.maxHumidity, | ||||||
|             min: entity.minHumidity, |             min: entity.minHumidity, | ||||||
| @@ -395,24 +376,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|             }), |             }), | ||||||
|             onChangeEnd: (double v) => _setTargetHumidity(entity, v), |             onChangeEnd: (double v) => _setTargetHumidity(entity, v), | ||||||
|           ), |           ), | ||||||
|         ) |  | ||||||
|       ]); |  | ||||||
|     } |  | ||||||
|     if (result.isNotEmpty) { |  | ||||||
|       return Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: <Widget>[ |  | ||||||
|           Padding( |  | ||||||
|             padding: EdgeInsets.fromLTRB( |  | ||||||
|                 0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), |  | ||||||
|             child: Text("Target humidity", style: TextStyle( |  | ||||||
|                 fontSize: Sizes.stateFontSize |  | ||||||
|             )), |  | ||||||
|           ), |  | ||||||
|           Row( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.center, |  | ||||||
|             children: result, |  | ||||||
|           ), |  | ||||||
|           Container( |           Container( | ||||||
|             height: Sizes.rowPadding, |             height: Sizes.rowPadding, | ||||||
|           ) |           ) | ||||||
| @@ -429,7 +392,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|     _resetTimer?.cancel(); |  | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,23 +33,16 @@ class ClimateStateWidget extends StatelessWidget { | |||||||
|               children: <Widget>[ |               children: <Widget>[ | ||||||
|                 Text("$displayState", |                 Text("$displayState", | ||||||
|                     textAlign: TextAlign.right, |                     textAlign: TextAlign.right, | ||||||
|                     style: new TextStyle( |                     style: Theme.of(context).textTheme.body2), | ||||||
|                       fontWeight: FontWeight.bold, |  | ||||||
|                       fontSize: Sizes.stateFontSize, |  | ||||||
|                     )), |  | ||||||
|                 Text(" $targetTemp", |                 Text(" $targetTemp", | ||||||
|                     textAlign: TextAlign.right, |                     textAlign: TextAlign.right, | ||||||
|                     style: new TextStyle( |                     style: Theme.of(context).textTheme.body1) | ||||||
|                       fontSize: Sizes.stateFontSize, |  | ||||||
|                     )) |  | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|             entity.currentTemperature != null ? |             entity.currentTemperature != null ? | ||||||
|             Text("Currently: ${entity.currentTemperature}", |             Text("Currently: ${entity.currentTemperature}", | ||||||
|                 textAlign: TextAlign.right, |                 textAlign: TextAlign.right, | ||||||
|                 style: new TextStyle( |                 style: Theme.of(context).textTheme.subtitle | ||||||
|                     fontSize: Sizes.stateFontSize, |  | ||||||
|                     color: Colors.black45) |  | ||||||
|             ) : |             ) : | ||||||
|             Container(height: 0.0,) |             Container(height: 0.0,) | ||||||
|           ], |           ], | ||||||
|   | |||||||
| @@ -3,21 +3,17 @@ part of '../../../main.dart'; | |||||||
| class ModeSelectorWidget extends StatelessWidget { | class ModeSelectorWidget extends StatelessWidget { | ||||||
|  |  | ||||||
|   final String caption; |   final String caption; | ||||||
|   final List<String> options; |   final List options; | ||||||
|   final String value; |   final String value; | ||||||
|   final double captionFontSize; |  | ||||||
|   final double valueFontSize; |  | ||||||
|   final onChange; |   final onChange; | ||||||
|   final EdgeInsets padding; |   final EdgeInsets padding; | ||||||
|  |  | ||||||
|   ModeSelectorWidget({ |   ModeSelectorWidget({ | ||||||
|     Key key, |     Key key, | ||||||
|     @required this.caption, |     @required this.caption, | ||||||
|     @required this.options, |     this.options: const [], | ||||||
|     this.value, |     this.value, | ||||||
|     @required this.onChange, |     @required this.onChange, | ||||||
|     this.captionFontSize, |  | ||||||
|     this.valueFontSize, |  | ||||||
|     this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), |     this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
| @@ -28,9 +24,7 @@ class ModeSelectorWidget extends StatelessWidget { | |||||||
|       child: Column( |       child: Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           Text("$caption", style: TextStyle( |           Text("$caption", style: Theme.of(context).textTheme.body1), | ||||||
|               fontSize: captionFontSize ?? Sizes.stateFontSize |  | ||||||
|           )), |  | ||||||
|           Row( |           Row( | ||||||
|             children: <Widget>[ |             children: <Widget>[ | ||||||
|               Expanded( |               Expanded( | ||||||
| @@ -40,15 +34,12 @@ class ModeSelectorWidget extends StatelessWidget { | |||||||
|                     value: value, |                     value: value, | ||||||
|                     iconSize: 30.0, |                     iconSize: 30.0, | ||||||
|                     isExpanded: true, |                     isExpanded: true, | ||||||
|                     style: TextStyle( |                     style: Theme.of(context).textTheme.title, | ||||||
|                       fontSize: valueFontSize ?? Sizes.largeFontSize, |  | ||||||
|                       color: Colors.black, |  | ||||||
|                     ), |  | ||||||
|                     hint: Text("Select ${caption.toLowerCase()}"), |                     hint: Text("Select ${caption.toLowerCase()}"), | ||||||
|                     items: options.map((String value) { |                     items: options.map((value) { | ||||||
|                       return new DropdownMenuItem<String>( |                       return new DropdownMenuItem<String>( | ||||||
|                         value: value, |                         value: '$value', | ||||||
|                         child: Text(value), |                         child: Text('$value'), | ||||||
|                       ); |                       ); | ||||||
|                     }).toList(), |                     }).toList(), | ||||||
|                     onChanged: (mode) => onChange(mode), |                     onChanged: (mode) => onChange(mode), | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ class ModeSwitchWidget extends StatelessWidget { | |||||||
|  |  | ||||||
|   final String caption; |   final String caption; | ||||||
|   final onChange; |   final onChange; | ||||||
|   final double captionFontSize; |  | ||||||
|   final bool value; |   final bool value; | ||||||
|   final bool expanded; |   final bool expanded; | ||||||
|   final EdgeInsets padding; |   final EdgeInsets padding; | ||||||
| @@ -13,7 +12,6 @@ class ModeSwitchWidget extends StatelessWidget { | |||||||
|     Key key, |     Key key, | ||||||
|     @required this.caption, |     @required this.caption, | ||||||
|     @required this.onChange, |     @required this.onChange, | ||||||
|     this.captionFontSize, |  | ||||||
|     this.value, |     this.value, | ||||||
|     this.expanded: true, |     this.expanded: true, | ||||||
|     this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding) |     this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding) | ||||||
| @@ -25,7 +23,7 @@ class ModeSwitchWidget extends StatelessWidget { | |||||||
|       padding: this.padding, |       padding: this.padding, | ||||||
|       child: Row( |       child: Row( | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           _buildCaption(), |           _buildCaption(context), | ||||||
|           Switch( |           Switch( | ||||||
|             onChanged: (value) => onChange(value), |             onChanged: (value) => onChange(value), | ||||||
|             value: value ?? false, |             value: value ?? false, | ||||||
| @@ -35,12 +33,10 @@ class ModeSwitchWidget extends StatelessWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildCaption() { |   Widget _buildCaption(BuildContext context) { | ||||||
|     Widget captionWidget = Text( |     Widget captionWidget = Text( | ||||||
|       "$caption", |       "$caption", | ||||||
|       style: TextStyle( |       style: Theme.of(context).textTheme.body1, | ||||||
|           fontSize: captionFontSize ?? Sizes.stateFontSize |  | ||||||
|       ), |  | ||||||
|     ); |     ); | ||||||
|     if (expanded) { |     if (expanded) { | ||||||
|       return Expanded( |       return Expanded( | ||||||
|   | |||||||
| @@ -2,8 +2,7 @@ part of '../../../main.dart'; | |||||||
|  |  | ||||||
| class TemperatureControlWidget extends StatelessWidget { | class TemperatureControlWidget extends StatelessWidget { | ||||||
|   final double value; |   final double value; | ||||||
|   final double fontSize; |   final bool active; | ||||||
|   final Color fontColor; |  | ||||||
|   final onInc; |   final onInc; | ||||||
|   final onDec; |   final onDec; | ||||||
|  |  | ||||||
| @@ -12,8 +11,9 @@ class TemperatureControlWidget extends StatelessWidget { | |||||||
|         @required this.value, |         @required this.value, | ||||||
|         @required this.onInc, |         @required this.onInc, | ||||||
|         @required this.onDec, |         @required this.onDec, | ||||||
|         this.fontSize, |         //this.fontSize, | ||||||
|         this.fontColor}) |         this.active: false | ||||||
|  |       }) | ||||||
|       : super(key: key); |       : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -23,10 +23,7 @@ class TemperatureControlWidget extends StatelessWidget { | |||||||
|       children: <Widget>[ |       children: <Widget>[ | ||||||
|         Text( |         Text( | ||||||
|           "$value", |           "$value", | ||||||
|           style: TextStyle( |           style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1, | ||||||
|               fontSize: fontSize ?? 24.0, |  | ||||||
|               color: fontColor ?? Colors.black |  | ||||||
|           ), |  | ||||||
|         ), |         ), | ||||||
|         Column( |         Column( | ||||||
|           children: <Widget>[ |           children: <Widget>[ | ||||||
|   | |||||||
| @@ -40,8 +40,8 @@ class CoverEntity extends Entity { | |||||||
|       CoverEntity.SUPPORT_SET_TILT_POSITION); |       CoverEntity.SUPPORT_SET_TILT_POSITION); | ||||||
|  |  | ||||||
|  |  | ||||||
|   double get currentPosition => _getDoubleAttributeValue('current_position'); |   double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0; | ||||||
|   double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position'); |   double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0; | ||||||
|   bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0); |   bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0); | ||||||
|   bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed)); |   bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed)); | ||||||
|   bool get canTiltBeOpened => currentTiltPosition < 100; |   bool get canTiltBeOpened => currentTiltPosition < 100; | ||||||
|   | |||||||
| @@ -62,13 +62,10 @@ class _CoverControlWidgetState extends State<CoverControlWidget> { | |||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           Padding( |           Padding( | ||||||
|             padding: EdgeInsets.fromLTRB( |             padding: EdgeInsets.only(top: Sizes.rowPadding), | ||||||
|                 0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), |             child: Text("Position"), | ||||||
|             child: Text("Position", style: TextStyle( |  | ||||||
|                 fontSize: Sizes.stateFontSize |  | ||||||
|             )), |  | ||||||
|           ), |           ), | ||||||
|           Slider( |           UniversalSlider( | ||||||
|             value: _tmpPosition, |             value: _tmpPosition, | ||||||
|             min: 0.0, |             min: 0.0, | ||||||
|             max: 100.0, |             max: 100.0, | ||||||
| @@ -80,8 +77,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> { | |||||||
|               }); |               }); | ||||||
|             }, |             }, | ||||||
|             onChangeEnd: (double value) => _setNewPosition(entity, value), |             onChangeEnd: (double value) => _setNewPosition(entity, value), | ||||||
|           ), |           ) | ||||||
|           Container(height: Sizes.rowPadding,) |  | ||||||
|         ], |         ], | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
| @@ -98,7 +94,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> { | |||||||
|     } |     } | ||||||
|     if (entity.supportSetTiltPosition) { |     if (entity.supportSetTiltPosition) { | ||||||
|       controls.addAll(<Widget>[ |       controls.addAll(<Widget>[ | ||||||
|         Slider( |         UniversalSlider( | ||||||
|           value: _tmpTiltPosition, |           value: _tmpTiltPosition, | ||||||
|           min: 0.0, |           min: 0.0, | ||||||
|           max: 100.0, |           max: 100.0, | ||||||
| @@ -117,10 +113,8 @@ class _CoverControlWidgetState extends State<CoverControlWidget> { | |||||||
|     if (controls.isNotEmpty) { |     if (controls.isNotEmpty) { | ||||||
|       controls.insert(0, Padding( |       controls.insert(0, Padding( | ||||||
|         padding: EdgeInsets.fromLTRB( |         padding: EdgeInsets.fromLTRB( | ||||||
|             0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding), |             0.0, Sizes.rowPadding, 0.0, 0), | ||||||
|         child: Text("Tilt position", style: TextStyle( |         child: Text("Tilt position"), | ||||||
|             fontSize: Sizes.stateFontSize |  | ||||||
|         )), |  | ||||||
|       )); |       )); | ||||||
|       return Column( |       return Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|   | |||||||
| @@ -9,10 +9,8 @@ class DateTimeStateWidget extends StatelessWidget { | |||||||
|         padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), |         padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), | ||||||
|         child: GestureDetector( |         child: GestureDetector( | ||||||
|           child: Text("${entity.formattedState}", |           child: Text("${entity.formattedState}", | ||||||
|               textAlign: TextAlign.right, |               textAlign: TextAlign.right | ||||||
|               style: new TextStyle( |             ), | ||||||
|                 fontSize: Sizes.stateFontSize, |  | ||||||
|               )), |  | ||||||
|           onTap: () => _handleStateTap(context, entity), |           onTap: () => _handleStateTap(context, entity), | ||||||
|         )); |         )); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -11,25 +11,23 @@ class DefaultEntityContainer extends StatelessWidget { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final EntityModel entityModel = EntityModel.of(context); |     final EntityModel entityModel = EntityModel.of(context); | ||||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { |     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) { | ||||||
|       return MissedEntityWidget(); |       return MissedEntityWidget(); | ||||||
|     } |     } | ||||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) { |     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) { | ||||||
|       return Divider( |       return Divider(); | ||||||
|         color: Colors.black45, |  | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) { |     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) { | ||||||
|       return Column( |       return Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         mainAxisSize: MainAxisSize.min, |         mainAxisSize: MainAxisSize.min, | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           Divider( |           Divider(), | ||||||
|             color: Colors.black45, |  | ||||||
|           ), |  | ||||||
|           Text( |           Text( | ||||||
|               "${entityModel.entityWrapper.entity.displayName}", |               "${entityModel.entityWrapper.entity.displayName}", | ||||||
|             style: TextStyle(color: Colors.blue), |             style: HAClientTheme().getLinkTextStyle(context).copyWith( | ||||||
|  |               decoration: TextDecoration.none | ||||||
|  |             ) | ||||||
|           ) |           ) | ||||||
|         ], |         ], | ||||||
|       ); |       ); | ||||||
| @@ -38,7 +36,6 @@ class DefaultEntityContainer extends StatelessWidget { | |||||||
|       mainAxisSize: MainAxisSize.max, |       mainAxisSize: MainAxisSize.max, | ||||||
|       children: <Widget>[ |       children: <Widget>[ | ||||||
|         EntityIcon(), |         EntityIcon(), | ||||||
|  |  | ||||||
|         Flexible( |         Flexible( | ||||||
|           fit: FlexFit.tight, |           fit: FlexFit.tight, | ||||||
|           flex: 3, |           flex: 3, | ||||||
| @@ -61,6 +58,11 @@ class DefaultEntityContainer extends StatelessWidget { | |||||||
|             entityModel.entityWrapper.handleTap(); |             entityModel.entityWrapper.handleTap(); | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|  |         onDoubleTap: () { | ||||||
|  |           if (entityModel.handleTap) { | ||||||
|  |             entityModel.entityWrapper.handleDoubleTap(); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|         child: result, |         child: result, | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -1,13 +1,6 @@ | |||||||
| part of '../main.dart'; | part of '../main.dart'; | ||||||
|  |  | ||||||
| class StatelessEntityType { | enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink} | ||||||
|   static const NONE = 0; |  | ||||||
|   static const MISSED = 1; |  | ||||||
|   static const DIVIDER = 2; |  | ||||||
|   static const SECTION = 3; |  | ||||||
|   static const CALL_SERVICE = 4; |  | ||||||
|   static const WEBLINK = 5; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class Entity { | class Entity { | ||||||
|  |  | ||||||
| @@ -76,8 +69,8 @@ class Entity { | |||||||
|   String entityPicture; |   String entityPicture; | ||||||
|   String state; |   String state; | ||||||
|   String displayState; |   String displayState; | ||||||
|   DateTime _lastUpdated; |   DateTime lastUpdatedTimestamp; | ||||||
|   int statelessType = 0; |   StatelessEntityType statelessType = StatelessEntityType.none; | ||||||
|  |  | ||||||
|   List<Entity> childEntities = []; |   List<Entity> childEntities = []; | ||||||
|   String deviceClass; |   String deviceClass; | ||||||
| @@ -85,8 +78,21 @@ class Entity { | |||||||
|     chartType: EntityHistoryWidgetType.simple |     chartType: EntityHistoryWidgetType.simple | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   String get displayName => |   String get displayName { | ||||||
|       attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " ")); |     if (attributes.containsKey('friendly_name')) { | ||||||
|  |       return attributes['friendly_name']; | ||||||
|  |     } | ||||||
|  |     if (attributes.containsKey('name')) {  | ||||||
|  |       return attributes['name']; | ||||||
|  |     } | ||||||
|  |     if (entityId == null) { | ||||||
|  |       return ""; | ||||||
|  |     } | ||||||
|  |     if (entityId.contains(".")) { | ||||||
|  |       return entityId.split(".")[1].replaceAll("_", " "); | ||||||
|  |     } | ||||||
|  |     return entityId; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   bool get isView => |   bool get isView => | ||||||
|       (domain == "group") && |       (domain == "group") && | ||||||
| @@ -120,42 +126,47 @@ class Entity { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Entity.missed(String entityId) { |   Entity.missed(String entityId) { | ||||||
|     statelessType = StatelessEntityType.MISSED; |     statelessType = StatelessEntityType.missed; | ||||||
|     attributes = {"hidden": false}; |     attributes = {"hidden": false}; | ||||||
|     this.entityId = entityId; |     this.entityId = entityId; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Entity.divider() { |   Entity.divider() { | ||||||
|     statelessType = StatelessEntityType.DIVIDER; |     statelessType = StatelessEntityType.divider; | ||||||
|     attributes = {"hidden": false}; |     attributes = {"hidden": false}; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Entity.section(String label) { |   Entity.section(String label) { | ||||||
|     statelessType = StatelessEntityType.SECTION; |     statelessType = StatelessEntityType.section; | ||||||
|     attributes = {"hidden": false, "friendly_name": "$label"}; |     attributes = {"hidden": false, "friendly_name": "$label"}; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Entity.ghost(String name, String icon) { | ||||||
|  |     statelessType = StatelessEntityType.ghost; | ||||||
|  |     attributes = {"icon": icon, "hidden": false, "friendly_name": name}; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Entity.callService({String icon, String name, String service, String actionName}) { |   Entity.callService({String icon, String name, String service, String actionName}) { | ||||||
|     statelessType = StatelessEntityType.CALL_SERVICE; |     statelessType = StatelessEntityType.callService; | ||||||
|     entityId = service; |     entityId = service; | ||||||
|     displayState = actionName?.toUpperCase() ?? "RUN"; |     displayState = actionName?.toUpperCase() ?? "RUN"; | ||||||
|     attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"}; |     attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"}; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Entity.weblink({String url, String name, String icon}) { |   Entity.weblink({String url, String name, String icon}) { | ||||||
|     statelessType = StatelessEntityType.WEBLINK; |     statelessType = StatelessEntityType.webLink; | ||||||
|     entityId = "custom.custom"; //TODO wtf?? |     entityId = "custom.custom"; | ||||||
|     attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; |     attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void update(Map rawData, String webHost) { |   void update(Map rawData, String webHost) { | ||||||
|     attributes = rawData["attributes"] ?? {}; |     attributes = rawData["attributes"] ?? {}; | ||||||
|     domain = rawData["entity_id"].split(".")[0]; |     domain = rawData["entity_id"] != null ? rawData["entity_id"].split(".")[0] : null; | ||||||
|     entityId = rawData["entity_id"]; |     entityId = rawData["entity_id"]; | ||||||
|     deviceClass = attributes["device_class"]; |     deviceClass = attributes["device_class"]; | ||||||
|     state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"]; |     state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"]; | ||||||
|     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state); |     displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state); | ||||||
|     _lastUpdated = DateTime.tryParse(rawData["last_updated"]); |     lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]); | ||||||
|     entityPicture = _getEntityPictureUrl(webHost); |     entityPicture = _getEntityPictureUrl(webHost); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -211,27 +222,19 @@ class Entity { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget buildBadgeWidget(BuildContext context) { |  | ||||||
|     return EntityModel( |  | ||||||
|       entityWrapper: EntityWrapper(entity: this), |  | ||||||
|       child: BadgeWidget(), |  | ||||||
|       handleTap: true, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   String getAttribute(String attributeName) { |   String getAttribute(String attributeName) { | ||||||
|     if (attributes != null) { |     if (attributes != null) { | ||||||
|       return attributes["$attributeName"]; |       return attributes["$attributeName"].toString(); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   String _getLastUpdatedFormatted() { |   String _getLastUpdatedFormatted() { | ||||||
|     if (_lastUpdated == null) { |     if (lastUpdatedTimestamp == null) { | ||||||
|       return "-"; |       return "-"; | ||||||
|     } else { |     } else { | ||||||
|       DateTime now = DateTime.now(); |       DateTime now = DateTime.now(); | ||||||
|       Duration d = now.difference(_lastUpdated); |       Duration d = now.difference(lastUpdatedTimestamp); | ||||||
|       String text; |       String text; | ||||||
|       int v; |       int v; | ||||||
|       if (d.inDays == 0) { |       if (d.inDays == 0) { | ||||||
|   | |||||||
| @@ -1,77 +0,0 @@ | |||||||
| part of '../main.dart'; |  | ||||||
|  |  | ||||||
| class EntityColor { |  | ||||||
|  |  | ||||||
|   static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0); |  | ||||||
|  |  | ||||||
|   static const badgeColors = { |  | ||||||
|     "default": Color.fromRGBO(223, 76, 30, 1.0), |  | ||||||
|     "binary_sensor": Color.fromRGBO(3, 155, 229, 1.0) |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   static const _stateColors = { |  | ||||||
|     EntityState.on: Colors.amber, |  | ||||||
|     "auto": Colors.amber, |  | ||||||
|     EntityState.active: Colors.amber, |  | ||||||
|     EntityState.playing: Colors.amber, |  | ||||||
|     EntityState.paused: Colors.amber, |  | ||||||
|     "above_horizon": Colors.amber, |  | ||||||
|     EntityState.home:  Colors.amber, |  | ||||||
|     EntityState.open:  Colors.amber, |  | ||||||
|     EntityState.cleaning:  Colors.amber, |  | ||||||
|     EntityState.returning:  Colors.amber, |  | ||||||
|     EntityState.off: defaultStateColor, |  | ||||||
|     EntityState.closed: defaultStateColor, |  | ||||||
|     "below_horizon": defaultStateColor, |  | ||||||
|     "default": defaultStateColor, |  | ||||||
|     EntityState.idle: defaultStateColor, |  | ||||||
|     "heat": Colors.redAccent, |  | ||||||
|     "cool": Colors.lightBlue, |  | ||||||
|     EntityState.unavailable: Colors.black26, |  | ||||||
|     EntityState.unknown: Colors.black26, |  | ||||||
|     EntityState.alarm_disarmed: Colors.green, |  | ||||||
|     EntityState.alarm_armed_away: Colors.redAccent, |  | ||||||
|     EntityState.alarm_armed_custom_bypass: Colors.redAccent, |  | ||||||
|     EntityState.alarm_armed_home: Colors.redAccent, |  | ||||||
|     EntityState.alarm_armed_night: Colors.redAccent, |  | ||||||
|     EntityState.alarm_triggered: Colors.redAccent, |  | ||||||
|     EntityState.alarm_arming: Colors.amber, |  | ||||||
|     EntityState.alarm_disarming: Colors.amber, |  | ||||||
|     EntityState.alarm_pending: Colors.amber, |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   static Color stateColor(String state) { |  | ||||||
|     return _stateColors[state] ?? _stateColors["default"]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static charts.Color chartHistoryStateColor(String state, int id) { |  | ||||||
|     Color c = _stateColors[state]; |  | ||||||
|     if (c != null) { |  | ||||||
|       return charts.Color( |  | ||||||
|           r: c.red, |  | ||||||
|           g: c.green, |  | ||||||
|           b: c.blue, |  | ||||||
|           a: c.alpha |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       double r = id.toDouble() % 10; |  | ||||||
|       return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static Color historyStateColor(String state, int id) { |  | ||||||
|     Color c = _stateColors[state]; |  | ||||||
|     if (c != null) { |  | ||||||
|       return c; |  | ||||||
|     } else { |  | ||||||
|       if (id > -1) { |  | ||||||
|         double r = id.toDouble() % 10; |  | ||||||
|         charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault; |  | ||||||
|         return Color.fromARGB(c1.a, c1.r, c1.g, c1.b); |  | ||||||
|       } else { |  | ||||||
|         return _stateColors[EntityState.on]; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -3,12 +3,18 @@ part of '../main.dart'; | |||||||
| class EntityIcon extends StatelessWidget { | class EntityIcon extends StatelessWidget { | ||||||
|  |  | ||||||
|   final EdgeInsetsGeometry padding; |   final EdgeInsetsGeometry padding; | ||||||
|  |   final EdgeInsetsGeometry iconPadding; | ||||||
|  |   final EdgeInsetsGeometry imagePadding; | ||||||
|   final double size; |   final double size; | ||||||
|   final Color color; |   final Color color; | ||||||
|  |   final bool showBadge; | ||||||
|  |  | ||||||
|   const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key); |   const EntityIcon({Key key, this.color, this.showBadge: true, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key); | ||||||
|  |  | ||||||
|   int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { |   int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { | ||||||
|  |     if (entityId == null) { | ||||||
|  |       return 0; | ||||||
|  |     } | ||||||
|     String domain = entityId.split(".")[0]; |     String domain = entityId.split(".")[0]; | ||||||
|     String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"]; |     String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"]; | ||||||
|     String iconNameByDeviceClass; |     String iconNameByDeviceClass; | ||||||
| @@ -23,52 +29,110 @@ class EntityIcon extends StatelessWidget { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget buildIcon(EntityWrapper data, Color color) { |   @override | ||||||
|     if (data == null) { |   Widget build(BuildContext context) { | ||||||
|       return null; |     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||||
|  |     Color iconColor; | ||||||
|  |     if (color != null) { | ||||||
|  |       iconColor = color; | ||||||
|  |     } else if (entityWrapper.stateColor) { | ||||||
|  |       iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context); | ||||||
|  |     } else { | ||||||
|  |       iconColor = HAClientTheme().getOffStateColor(context); | ||||||
|     } |     } | ||||||
|     if (data.entityPicture != null) { |     Widget iconWidget; | ||||||
|       return Container( |     bool isPicture = false; | ||||||
|         height: size+12, |     if (entityWrapper == null) { | ||||||
|  |       iconWidget = Container( | ||||||
|  |         width: size, | ||||||
|  |         height: size, | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       if (entityWrapper.entityPicture != null) { | ||||||
|  |         iconWidget = ClipOval( | ||||||
|  |           child: CachedNetworkImage( | ||||||
|  |             imageUrl: '${entityWrapper.entityPicture}', | ||||||
|             width: size+12, |             width: size+12, | ||||||
|         decoration: BoxDecoration( |  | ||||||
|             shape: BoxShape.circle, |  | ||||||
|             image: DecorationImage( |  | ||||||
|             fit: BoxFit.cover, |             fit: BoxFit.cover, | ||||||
|               image: CachedNetworkImageProvider( |             height: size+12, | ||||||
|                 "${data.entityPicture}" |             errorWidget: (context, str, dyn) { | ||||||
|               ), |               return Padding( | ||||||
|             ) |                 padding: iconPadding ?? padding, | ||||||
|  |                 child: _buildIcon(entityWrapper, iconColor) | ||||||
|  |               ); | ||||||
|  |             }, | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|  |         isPicture = true; | ||||||
|  |       } else { | ||||||
|  |         iconWidget = _buildIcon(entityWrapper, iconColor); | ||||||
|       } |       } | ||||||
|     String iconName = data.icon; |     } | ||||||
|  |     EdgeInsetsGeometry computedPadding; | ||||||
|  |     if (isPicture && imagePadding != null) { | ||||||
|  |       computedPadding = imagePadding; | ||||||
|  |     } else if (!isPicture && iconPadding != null) { | ||||||
|  |       computedPadding = iconPadding; | ||||||
|  |     } else { | ||||||
|  |       computedPadding = padding; | ||||||
|  |     } | ||||||
|  |     return Padding( | ||||||
|  |       padding: computedPadding, | ||||||
|  |       child: iconWidget, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildIcon(EntityWrapper entityWrapper, Color iconColor) { | ||||||
|  |     Widget iconWidget; | ||||||
|  |     String iconName = entityWrapper.icon; | ||||||
|     int iconCode = 0; |     int iconCode = 0; | ||||||
|     if (iconName.length > 0) { |     if (iconName.length > 0) { | ||||||
|       iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); |       iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); | ||||||
|     } else { |     } else { | ||||||
|       iconCode = getDefaultIconByEntityId(data.entity.entityId, |       iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId, | ||||||
|           data.entity.deviceClass, data.entity.state); // |           entityWrapper.entity.deviceClass, entityWrapper.entity.state); // | ||||||
|     } |     } | ||||||
|     return Padding( |     if (showBadge && entityWrapper.entity is LightEntity && | ||||||
|         padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0), |       (entityWrapper.entity as LightEntity).supportColor && | ||||||
|         child: Icon( |       (entityWrapper.entity as LightEntity).color != null && | ||||||
|  |       (entityWrapper.entity as LightEntity).color.toColor() != Colors.white | ||||||
|  |       ) { | ||||||
|  |       Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();   | ||||||
|  |       iconWidget = Stack( | ||||||
|  |         children: <Widget>[ | ||||||
|  |           Icon( | ||||||
|             IconData(iconCode, fontFamily: 'Material Design Icons'), |             IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||||
|             size: size, |             size: size, | ||||||
|           color: color, |             color: iconColor, | ||||||
|         ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; |  | ||||||
|     return Padding( |  | ||||||
|       padding: padding, |  | ||||||
|       child: buildIcon( |  | ||||||
|           entityWrapper, |  | ||||||
|           color ?? EntityColor.stateColor(entityWrapper.entity.state) |  | ||||||
|           ), |           ), | ||||||
|  |           Positioned( | ||||||
|  |             bottom: 0, | ||||||
|  |             right: 0, | ||||||
|  |             child: Container( | ||||||
|  |               width: size / 3, | ||||||
|  |               height: size / 3, | ||||||
|  |               decoration: BoxDecoration( | ||||||
|  |                 color: lightColor, | ||||||
|  |                 shape: BoxShape.circle, | ||||||
|  |                 boxShadow: <BoxShadow>[ | ||||||
|  |                   BoxShadow( | ||||||
|  |                     spreadRadius: 0, | ||||||
|  |                     blurRadius: 0, | ||||||
|  |                     offset: Offset(0.3, 0.3) | ||||||
|  |                   ) | ||||||
|  |                 ] | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ) | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       iconWidget = Icon( | ||||||
|  |         IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||||
|  |         size: size, | ||||||
|  |         color: iconColor, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |     return iconWidget; | ||||||
|  |   } | ||||||
| } | } | ||||||
| @@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget { | |||||||
|   final bool handleTap; |   final bool handleTap; | ||||||
|  |  | ||||||
|   static EntityModel of(BuildContext context) { |   static EntityModel of(BuildContext context) { | ||||||
|     return context.inheritFromWidgetOfExactType(EntityModel); |     return context.dependOnInheritedWidgetOfExactType<EntityModel>(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   bool updateShouldNotify(InheritedWidget oldWidget) { |   bool updateShouldNotify(EntityModel oldWidget) { | ||||||
|     return true; |     return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -5,18 +5,24 @@ class EntityName extends StatelessWidget { | |||||||
|   final EdgeInsetsGeometry padding; |   final EdgeInsetsGeometry padding; | ||||||
|   final TextOverflow textOverflow; |   final TextOverflow textOverflow; | ||||||
|   final bool wordsWrap; |   final bool wordsWrap; | ||||||
|   final double fontSize; |  | ||||||
|   final TextAlign textAlign; |   final TextAlign textAlign; | ||||||
|   final int maxLines; |   final int maxLines; | ||||||
|  |   final TextStyle textStyle; | ||||||
|  |  | ||||||
|   const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key); |   const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.textStyle, this.wordsWrap: true, this.textAlign: TextAlign.left}) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; |     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||||
|     TextStyle textStyle = TextStyle(fontSize: fontSize); |     TextStyle tStyle; | ||||||
|     if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) { |     if (textStyle == null) { | ||||||
|       textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline); |       if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) { | ||||||
|  |         tStyle = HAClientTheme().getLinkTextStyle(context); | ||||||
|  |       } else { | ||||||
|  |         tStyle = Theme.of(context).textTheme.body1; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       tStyle = textStyle; | ||||||
|     } |     } | ||||||
|     return Padding( |     return Padding( | ||||||
|       padding: padding, |       padding: padding, | ||||||
| @@ -25,7 +31,7 @@ class EntityName extends StatelessWidget { | |||||||
|         overflow: textOverflow, |         overflow: textOverflow, | ||||||
|         softWrap: wordsWrap, |         softWrap: wordsWrap, | ||||||
|         maxLines: maxLines, |         maxLines: maxLines, | ||||||
|         style: textStyle, |         style: tStyle, | ||||||
|         textAlign: textAlign, |         textAlign: textAlign, | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -2,10 +2,9 @@ part of '../main.dart'; | |||||||
|  |  | ||||||
| class EntityPageLayout extends StatelessWidget { | class EntityPageLayout extends StatelessWidget { | ||||||
|  |  | ||||||
|   final bool showClose; |  | ||||||
|   final Entity entity; |   final Entity entity; | ||||||
|  |  | ||||||
|   EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key); |   EntityPageLayout({Key key, this.entity}) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
| @@ -14,38 +13,6 @@ class EntityPageLayout extends StatelessWidget { | |||||||
|       child: ListView( |       child: ListView( | ||||||
|           padding: EdgeInsets.all(0), |           padding: EdgeInsets.all(0), | ||||||
|           children: <Widget>[ |           children: <Widget>[ | ||||||
|             showClose ? |  | ||||||
|             Container( |  | ||||||
|               color: Colors.blue[300], |  | ||||||
|               height: 40, |  | ||||||
|               child: Row( |  | ||||||
|                 children: <Widget>[ |  | ||||||
|                   Expanded( |  | ||||||
|                     child: Padding( |  | ||||||
|                       padding: EdgeInsets.only(left: 8), |  | ||||||
|                       child: Text( |  | ||||||
|                         entity.displayName, |  | ||||||
|                         style: TextStyle( |  | ||||||
|                             fontWeight: FontWeight.bold, |  | ||||||
|                             color: Colors.white, |  | ||||||
|                             fontSize: 22 |  | ||||||
|                         ), |  | ||||||
|                       ), |  | ||||||
|                     ), |  | ||||||
|                   ), |  | ||||||
|                   IconButton( |  | ||||||
|                     padding: EdgeInsets.all(0), |  | ||||||
|                     icon: Icon(Icons.close), |  | ||||||
|                     color: Colors.white, |  | ||||||
|                     iconSize: 36.0, |  | ||||||
|                     onPressed: () { |  | ||||||
|                       eventBus.fire(ShowEntityPageEvent()); |  | ||||||
|                     }, |  | ||||||
|                   ) |  | ||||||
|                 ], |  | ||||||
|               ), |  | ||||||
|             ) : |  | ||||||
|             Container(height: 0, width: 0,), |  | ||||||
|             Padding( |             Padding( | ||||||
|               padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding), |               padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding), | ||||||
|               child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)), |               child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)), | ||||||
|   | |||||||
							
								
								
									
										71
									
								
								lib/entities/entity_picture.widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,71 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class EntityPicture extends StatelessWidget { | ||||||
|  |  | ||||||
|  |   final EdgeInsetsGeometry padding; | ||||||
|  |   final BoxFit fit; | ||||||
|  |    | ||||||
|  |   const EntityPicture({Key key, this.padding: const EdgeInsets.all(0.0), this.fit: BoxFit.cover}) : super(key: key); | ||||||
|  |  | ||||||
|  |   int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { | ||||||
|  |     String domain = entityId.split(".")[0]; | ||||||
|  |     String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"]; | ||||||
|  |     String iconNameByDeviceClass; | ||||||
|  |     if (deviceClass != null) { | ||||||
|  |       iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"]; | ||||||
|  |     } | ||||||
|  |     String iconName = iconNameByDeviceClass ?? iconNameByDomain; | ||||||
|  |     if (iconName != null) { | ||||||
|  |       return MaterialDesignIcons.iconsDataMap[iconName] ?? 0; | ||||||
|  |     } else { | ||||||
|  |       return 0; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget buildIcon(EntityWrapper data, BuildContext context) { | ||||||
|  |     if (data == null) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     String iconName = data.icon; | ||||||
|  |     int iconCode = 0; | ||||||
|  |     if (iconName.length > 0) { | ||||||
|  |       iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); | ||||||
|  |     } else { | ||||||
|  |       iconCode = getDefaultIconByEntityId(data.entity.entityId, | ||||||
|  |           data.entity.deviceClass, data.entity.state); // | ||||||
|  |     } | ||||||
|  |     Widget iconPicture = Container( | ||||||
|  |       child: Center( | ||||||
|  |         child: Icon( | ||||||
|  |           IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||||
|  |           size: Sizes.largeIconSize, | ||||||
|  |           color: HAClientTheme().getOffStateColor(context), | ||||||
|  |         ) | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |      | ||||||
|  |      | ||||||
|  |     if (data.entityPicture != null) { | ||||||
|  |       return CachedNetworkImage( | ||||||
|  |         imageUrl: data.entityPicture, | ||||||
|  |         fit: this.fit, | ||||||
|  |         errorWidget: (context, _, __) => iconPicture, | ||||||
|  |         placeholder: (context, _) => iconPicture, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return iconPicture; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||||
|  |     return Padding( | ||||||
|  |       padding: padding, | ||||||
|  |       child: buildIcon( | ||||||
|  |           entityWrapper, | ||||||
|  |           context | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -2,30 +2,31 @@ part of '../main.dart'; | |||||||
|  |  | ||||||
| class EntityWrapper { | class EntityWrapper { | ||||||
|  |  | ||||||
|   String displayName; |   String overrideName; | ||||||
|   String icon; |   String overrideIcon; | ||||||
|   String unitOfMeasurement; |   final bool stateColor; | ||||||
|   String entityPicture; |  | ||||||
|   EntityUIAction uiAction; |   EntityUIAction uiAction; | ||||||
|   Entity entity; |   Entity entity; | ||||||
|  |   String unitOfMeasurementOverride; | ||||||
|  |   final List stateFilter; | ||||||
|  |  | ||||||
|  |   String get icon => overrideIcon ?? entity.icon; | ||||||
|  |   String get entityPicture => entity.entityPicture; | ||||||
|  |   String get displayName => overrideName ?? entity.displayName; | ||||||
|  |   String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement; | ||||||
|  |  | ||||||
|   EntityWrapper({ |   EntityWrapper({ | ||||||
|     this.entity, |     this.entity, | ||||||
|     String icon, |     this.overrideIcon, | ||||||
|     String displayName, |     this.overrideName, | ||||||
|     this.uiAction |     this.stateColor: true, | ||||||
|  |     this.uiAction, | ||||||
|  |     this.stateFilter | ||||||
|   }) { |   }) { | ||||||
|     if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) { |     if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || entity.statelessType == StatelessEntityType.webLink) { | ||||||
|       this.icon = icon ?? entity.icon; |  | ||||||
|       if (icon == null) { |  | ||||||
|         entityPicture = entity.entityPicture; |  | ||||||
|       } |  | ||||||
|       this.displayName = displayName ?? entity.displayName; |  | ||||||
|       if (uiAction == null) { |       if (uiAction == null) { | ||||||
|         uiAction = EntityUIAction(); |         uiAction = EntityUIAction(); | ||||||
|       } |       } | ||||||
|       unitOfMeasurement = entity.unitOfMeasurement; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -53,16 +54,16 @@ class EntityWrapper { | |||||||
|  |  | ||||||
|       case EntityUIAction.moreInfo: { |       case EntityUIAction.moreInfo: { | ||||||
|         eventBus.fire( |         eventBus.fire( | ||||||
|             new ShowEntityPageEvent(entity: entity)); |             new ShowEntityPageEvent(entityId: entity.entityId)); | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       case EntityUIAction.navigate: { |       case EntityUIAction.navigate: { | ||||||
|         if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) { |         if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) { | ||||||
|           //TODO handle local urls |           //TODO handle local urls | ||||||
|           Logger.w("Local urls is not supported yet"); |           Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.tapService}'); | ||||||
|         } else { |         } else { | ||||||
|           Launcher.launchURL(uiAction.tapService); |           Launcher.launchURLInBrowser(uiAction.tapService); | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
| @@ -93,16 +94,56 @@ class EntityWrapper { | |||||||
|  |  | ||||||
|         case EntityUIAction.moreInfo: { |         case EntityUIAction.moreInfo: { | ||||||
|           eventBus.fire( |           eventBus.fire( | ||||||
|               new ShowEntityPageEvent(entity: entity)); |               new ShowEntityPageEvent(entityId: entity.entityId)); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         case EntityUIAction.navigate: { |         case EntityUIAction.navigate: { | ||||||
|           if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) { |           if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) { | ||||||
|             //TODO handle local urls |             //TODO handle local urls | ||||||
|             Logger.w("Local urls is not supported yet"); |             Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.holdService}'); | ||||||
|           } else { |           } else { | ||||||
|             Launcher.launchURL(uiAction.holdService); |             Launcher.launchURLInBrowser(uiAction.holdService); | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         default: { | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void handleDoubleTap() { | ||||||
|  |       switch (uiAction.doubleTapAction) { | ||||||
|  |         case EntityUIAction.toggle: { | ||||||
|  |           ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         case EntityUIAction.callService: { | ||||||
|  |           if (uiAction.doubleTapService != null) { | ||||||
|  |             ConnectionManager().callService( | ||||||
|  |               domain: uiAction.doubleTapService.split(".")[0], | ||||||
|  |               service: uiAction.doubleTapService.split(".")[1], | ||||||
|  |               data: uiAction.doubleTapServiceData | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         case EntityUIAction.moreInfo: { | ||||||
|  |           eventBus.fire( | ||||||
|  |               new ShowEntityPageEvent(entityId: entity.entityId)); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         case EntityUIAction.navigate: { | ||||||
|  |           if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) { | ||||||
|  |             //TODO handle local urls | ||||||
|  |             Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.doubleTapService}'); | ||||||
|  |           } else { | ||||||
|  |             Launcher.launchURLInBrowser(uiAction.doubleTapService); | ||||||
|           } |           } | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
| @@ -114,3 +155,63 @@ class EntityWrapper { | |||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class EntityUIAction { | ||||||
|  |   static const moreInfo = 'more-info'; | ||||||
|  |   static const toggle = 'toggle'; | ||||||
|  |   static const callService = 'call-service'; | ||||||
|  |   static const navigate = 'navigate'; | ||||||
|  |   static const none = 'none'; | ||||||
|  |  | ||||||
|  |   String tapAction = EntityUIAction.moreInfo; | ||||||
|  |   String tapNavigationPath; | ||||||
|  |   String tapService; | ||||||
|  |   Map<String, dynamic> tapServiceData; | ||||||
|  |   String holdAction = EntityUIAction.moreInfo; | ||||||
|  |   String holdNavigationPath; | ||||||
|  |   String holdService; | ||||||
|  |   Map<String, dynamic> holdServiceData; | ||||||
|  |   String doubleTapAction = EntityUIAction.none; | ||||||
|  |   String doubleTapNavigationPath; | ||||||
|  |   String doubleTapService; | ||||||
|  |   Map<String, dynamic> doubleTapServiceData; | ||||||
|  |  | ||||||
|  |   EntityUIAction({rawEntityData}) { | ||||||
|  |     if (rawEntityData != null) { | ||||||
|  |       if (rawEntityData["tap_action"] != null) { | ||||||
|  |         if (rawEntityData["tap_action"] is String) { | ||||||
|  |           tapAction = rawEntityData["tap_action"]; | ||||||
|  |         } else { | ||||||
|  |           tapAction = | ||||||
|  |               rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo; | ||||||
|  |           tapNavigationPath = rawEntityData["tap_action"]["navigation_path"]; | ||||||
|  |           tapService = rawEntityData["tap_action"]["service"]; | ||||||
|  |           tapServiceData = rawEntityData["tap_action"]["service_data"]; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (rawEntityData["hold_action"] != null) { | ||||||
|  |         if (rawEntityData["hold_action"] is String) { | ||||||
|  |           holdAction = rawEntityData["hold_action"]; | ||||||
|  |         } else { | ||||||
|  |           holdAction = | ||||||
|  |               rawEntityData["hold_action"]["action"] ?? EntityUIAction.none; | ||||||
|  |           holdNavigationPath = rawEntityData["hold_action"]["navigation_path"]; | ||||||
|  |           holdService = rawEntityData["hold_action"]["service"]; | ||||||
|  |           holdServiceData = rawEntityData["hold_action"]["service_data"]; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (rawEntityData["double_tap_action"] != null) { | ||||||
|  |         if (rawEntityData["double_tap_action"] is String) { | ||||||
|  |           doubleTapAction = rawEntityData["double_tap_action"]; | ||||||
|  |         } else { | ||||||
|  |           doubleTapAction = | ||||||
|  |               rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none; | ||||||
|  |           doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"]; | ||||||
|  |           doubleTapService = rawEntityData["double_tap_action"]["service"]; | ||||||
|  |           doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"]; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								lib/entities/error_entity_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | part of '../main.dart'; | ||||||
|  |  | ||||||
|  | class ErrorEntityWidget extends StatelessWidget { | ||||||
|  |    | ||||||
|  |   final String text; | ||||||
|  |  | ||||||
|  |   ErrorEntityWidget({ | ||||||
|  |     Key key, this.text | ||||||
|  |   }) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final EntityModel entityModel = EntityModel.of(context); | ||||||
|  |     String errorText = text ?? "Entity error: ${entityModel.entityWrapper.entity?.entityId}"; | ||||||
|  |     return Container( | ||||||
|  |         child: Padding( | ||||||
|  |           padding: EdgeInsets.all(5.0), | ||||||
|  |           child: Text(errorText), | ||||||
|  |         ), | ||||||
|  |         color: Theme.of(context).errorColor, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -6,7 +6,6 @@ class FlatServiceButton extends StatelessWidget { | |||||||
|   final String serviceName; |   final String serviceName; | ||||||
|   final String entityId; |   final String entityId; | ||||||
|   final String text; |   final String text; | ||||||
|   final double fontSize; |  | ||||||
|  |  | ||||||
|   FlatServiceButton({ |   FlatServiceButton({ | ||||||
|     Key key, |     Key key, | ||||||
| @@ -14,7 +13,6 @@ class FlatServiceButton extends StatelessWidget { | |||||||
|     @required this.serviceName, |     @required this.serviceName, | ||||||
|     @required this.entityId, |     @required this.entityId, | ||||||
|     @required this.text, |     @required this.text, | ||||||
|     this.fontSize: Sizes.stateFontSize |  | ||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
|  |  | ||||||
|   void _setNewState() { |   void _setNewState() { | ||||||
| @@ -24,7 +22,7 @@ class FlatServiceButton extends StatelessWidget { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return SizedBox( |     return SizedBox( | ||||||
|         height: fontSize*2.5, |         height: Theme.of(context).textTheme.subhead.fontSize*2.5, | ||||||
|         child: FlatButton( |         child: FlatButton( | ||||||
|           onPressed: (() { |           onPressed: (() { | ||||||
|             _setNewState(); |             _setNewState(); | ||||||
| @@ -32,8 +30,7 @@ class FlatServiceButton extends StatelessWidget { | |||||||
|           child: Text( |           child: Text( | ||||||
|             text, |             text, | ||||||
|             textAlign: TextAlign.right, |             textAlign: TextAlign.right, | ||||||
|             style: |             style: HAClientTheme().getActionTextStyle(context), | ||||||
|             new TextStyle(fontSize: fontSize, color: Colors.blue), |  | ||||||
|           ), |           ), | ||||||
|         ) |         ) | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -134,8 +134,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | |||||||
|             _tmpBrightness = value.round(); |             _tmpBrightness = value.round(); | ||||||
|           }); |           }); | ||||||
|         }, |         }, | ||||||
|         min: 1.0, |         min: 1, | ||||||
|         max: 255.0, |         max: 255, | ||||||
|  |         divisions: 254, | ||||||
|  |         label: '${val?.toInt() ?? ''}', | ||||||
|         onChangeEnd: (value) => _setBrightness(entity, value), |         onChangeEnd: (value) => _setBrightness(entity, value), | ||||||
|         value: val, |         value: val, | ||||||
|         leading: Icon(Icons.brightness_5), |         leading: Icon(Icons.brightness_5), | ||||||
| @@ -155,10 +157,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | |||||||
|             _tmpWhiteValue = value.round(); |             _tmpWhiteValue = value.round(); | ||||||
|           }); |           }); | ||||||
|         }, |         }, | ||||||
|         min: 0.0, |         min: 0, | ||||||
|         max: 255.0, |         max: 255, | ||||||
|  |         divisions: 255, | ||||||
|  |         label: '$_tmpWhiteValue', | ||||||
|         onChangeEnd: (value) => _setWhiteValue(entity, value), |         onChangeEnd: (value) => _setWhiteValue(entity, value), | ||||||
|         value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(), |         value: _tmpWhiteValue?.toDouble() ?? 0.0, | ||||||
|         leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")), |         leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")), | ||||||
|         title: "White", |         title: "White", | ||||||
|       ); |       ); | ||||||
| @@ -183,18 +187,20 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | |||||||
|       } |       } | ||||||
|       return UniversalSlider( |       return UniversalSlider( | ||||||
|         title: "Color temperature", |         title: "Color temperature", | ||||||
|         leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),), |         leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)), | ||||||
|         value:  val, |         value:  val, | ||||||
|         onChangeEnd: (value) => _setColorTemp(entity, value), |         onChangeEnd: (value) => _setColorTemp(entity, value), | ||||||
|         max: entity.maxMireds, |         max: entity.maxMireds, | ||||||
|         min: entity.minMireds, |         min: entity.minMireds, | ||||||
|  |         divisions: (entity.maxMireds - entity.minMireds).toInt(), | ||||||
|  |         label: '$_tmpColorTemp', | ||||||
|         onChanged: (value) { |         onChanged: (value) { | ||||||
|           setState(() { |           setState(() { | ||||||
|             _changedHere = true; |             _changedHere = true; | ||||||
|             _tmpColorTemp = value.round(); |             _tmpColorTemp = value.round(); | ||||||
|           }); |           }); | ||||||
|         }, |         }, | ||||||
|         closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),), |         closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),), | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       return Container(width: 0.0, height: 0.0); |       return Container(width: 0.0, height: 0.0); | ||||||
| @@ -224,7 +230,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> { | |||||||
|                 }, |                 }, | ||||||
|               ), |               ), | ||||||
|               FlatButton( |               FlatButton( | ||||||
|                 color: savedColor?.toColor() ?? Colors.transparent, |                 color: savedColor?.toColor() ?? Theme.of(context).backgroundColor, | ||||||
|                 child: Text('Paste color'), |                 child: Text('Paste color'), | ||||||
|                 onPressed: savedColor == null ? null : () { |                 onPressed: savedColor == null ? null : () { | ||||||
|                   _setColor(entity, savedColor); |                   _setColor(entity, savedColor); | ||||||
|   | |||||||
| @@ -28,8 +28,7 @@ class LockStateWidget extends StatelessWidget { | |||||||
|           onPressed: () => _unlock(entity), |           onPressed: () => _unlock(entity), | ||||||
|           child: Text("UNLOCK", |           child: Text("UNLOCK", | ||||||
|               textAlign: TextAlign.right, |               textAlign: TextAlign.right, | ||||||
|               style: |               style: HAClientTheme().getActionTextStyle(context) | ||||||
|               new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), |  | ||||||
|             ), |             ), | ||||||
|           ) |           ) | ||||||
|         ), |         ), | ||||||
| @@ -39,8 +38,7 @@ class LockStateWidget extends StatelessWidget { | |||||||
|               onPressed: () => _lock(entity), |               onPressed: () => _lock(entity), | ||||||
|               child: Text("LOCK", |               child: Text("LOCK", | ||||||
|                 textAlign: TextAlign.right, |                 textAlign: TextAlign.right, | ||||||
|                 style: |                 style: HAClientTheme().getActionTextStyle(context), | ||||||
|                 new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), |  | ||||||
|               ), |               ), | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
| @@ -56,8 +54,7 @@ class LockStateWidget extends StatelessWidget { | |||||||
|             child: Text( |             child: Text( | ||||||
|               entity.isLocked ? "UNLOCK" : "LOCK", |               entity.isLocked ? "UNLOCK" : "LOCK", | ||||||
|               textAlign: TextAlign.right, |               textAlign: TextAlign.right, | ||||||
|               style: |               style: HAClientTheme().getActionTextStyle(context), | ||||||
|               new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue), |  | ||||||
|             ), |             ), | ||||||
|           ) |           ) | ||||||
|       ); |       ); | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> { | |||||||
|     return LinearProgressIndicator( |     return LinearProgressIndicator( | ||||||
|       value: progress, |       value: progress, | ||||||
|       backgroundColor: Colors.black45, |       backgroundColor: Colors.black45, | ||||||
|       valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)), |       valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,12 +13,6 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> { | |||||||
|   double _currentPosition = 0; |   double _currentPosition = 0; | ||||||
|   int _savedPosition = 0; |   int _savedPosition = 0; | ||||||
|  |  | ||||||
|   final TextStyle _seekTextStyle = TextStyle( |  | ||||||
|       fontSize: 20, |  | ||||||
|       color: Colors.blue, |  | ||||||
|       fontWeight: FontWeight.bold |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   initState() { |   initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
| @@ -53,8 +47,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> { | |||||||
|         buttons.add( |         buttons.add( | ||||||
|             RaisedButton( |             RaisedButton( | ||||||
|               child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"), |               child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"), | ||||||
|               color: Colors.orange, |               color: Theme.of(context).accentColor, | ||||||
|               focusColor: Colors.white, |  | ||||||
|               onPressed: () { |               onPressed: () { | ||||||
|                 ConnectionManager().callService( |                 ConnectionManager().callService( | ||||||
|                     domain: "media_player", |                     domain: "media_player", | ||||||
| @@ -79,16 +72,20 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> { | |||||||
|               children: <Widget>[ |               children: <Widget>[ | ||||||
|                 Text("00:00"), |                 Text("00:00"), | ||||||
|                 Expanded( |                 Expanded( | ||||||
|                   child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle), |                   child: Text( | ||||||
|  |                     "${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}", | ||||||
|  |                     textAlign: TextAlign.center, | ||||||
|  |                     style: Theme.of(context).textTheme.title.copyWith( | ||||||
|  |                       color: Colors.blue | ||||||
|  |                     ) | ||||||
|  |                   ), | ||||||
|                 ), |                 ), | ||||||
|                 Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}") |                 Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}") | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|             Container(height: 10,), |             UniversalSlider( | ||||||
|             Slider( |  | ||||||
|               min: 0, |               min: 0, | ||||||
|               activeColor: Colors.amber, |               activeColor: Theme.of(context).accentColor, | ||||||
|               inactiveColor: Colors.black26, |  | ||||||
|               max: entity.durationSeconds.toDouble(), |               max: entity.durationSeconds.toDouble(), | ||||||
|               value: _currentPosition, |               value: _currentPosition, | ||||||
|               onChangeStart: (val) { |               onChangeStart: (val) { | ||||||
|   | |||||||
| @@ -12,14 +12,14 @@ class MediaPlayerWidget extends StatelessWidget { | |||||||
|         Stack( |         Stack( | ||||||
|           alignment: AlignmentDirectional.topEnd, |           alignment: AlignmentDirectional.topEnd, | ||||||
|           children: <Widget>[ |           children: <Widget>[ | ||||||
|             _buildImage(entity), |             _buildImage(entity, context), | ||||||
|             Positioned( |             Positioned( | ||||||
|               bottom: 0.0, |               bottom: 0.0, | ||||||
|               left: 0.0, |               left: 0.0, | ||||||
|               right: 0.0, |               right: 0.0, | ||||||
|               child: Container( |               child: Container( | ||||||
|                 color: Colors.black45, |                 color: Colors.black45, | ||||||
|                 child: _buildState(entity), |                 child: _buildState(entity, context), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             Positioned( |             Positioned( | ||||||
| @@ -35,12 +35,9 @@ class MediaPlayerWidget extends StatelessWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildState(MediaPlayerEntity entity) { |   Widget _buildState(MediaPlayerEntity entity, BuildContext context) { | ||||||
|     TextStyle style = TextStyle( |     TextStyle style = Theme.of(context).textTheme.body1.copyWith( | ||||||
|         fontSize: 14.0, |       color: Colors.white | ||||||
|         color: Colors.white, |  | ||||||
|         fontWeight: FontWeight.normal, |  | ||||||
|         height: 1.2 |  | ||||||
|     ); |     ); | ||||||
|     List<Widget> states = []; |     List<Widget> states = []; | ||||||
|     states.add(Text("${entity.displayName}", style: style)); |     states.add(Text("${entity.displayName}", style: style)); | ||||||
| @@ -71,7 +68,7 @@ class MediaPlayerWidget extends StatelessWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildImage(MediaPlayerEntity entity) { |   Widget _buildImage(MediaPlayerEntity entity, BuildContext context) { | ||||||
|     String state = entity.state; |     String state = entity.state; | ||||||
|     if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) { |     if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) { | ||||||
|       return Container( |       return Container( | ||||||
| @@ -97,7 +94,7 @@ class MediaPlayerWidget extends StatelessWidget { | |||||||
|           Icon( |           Icon( | ||||||
|             MaterialDesignIcons.getIconDataFromIconName("mdi:movie"), |             MaterialDesignIcons.getIconDataFromIconName("mdi:movie"), | ||||||
|             size: 150.0, |             size: 150.0, | ||||||
|             color: EntityColor.stateColor("$state"), |             color: HAClientTheme().getColorByEntityState("$state", context), | ||||||
|           ) |           ) | ||||||
|         ], |         ], | ||||||
|       ); |       ); | ||||||
| @@ -231,7 +228,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget { | |||||||
|           IconButton( |           IconButton( | ||||||
|               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( |               icon: Icon(MaterialDesignIcons.getIconDataFromIconName( | ||||||
|                   "mdi:dots-vertical")), |                   "mdi:dots-vertical")), | ||||||
|               onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: entity)) |               onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: entity.entityId)) | ||||||
|           ) |           ) | ||||||
|       ); |       ); | ||||||
|     } else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) { |     } else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) { | ||||||
| @@ -356,13 +353,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> { | |||||||
|         volumeStepWidget = Row( |         volumeStepWidget = Row( | ||||||
|           mainAxisSize: MainAxisSize.min, |           mainAxisSize: MainAxisSize.min, | ||||||
|           children: <Widget>[ |           children: <Widget>[ | ||||||
|             IconButton( |  | ||||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")), |  | ||||||
|                 onPressed: () => _setVolumeUp(entity.entityId) |  | ||||||
|             ), |  | ||||||
|             IconButton( |             IconButton( | ||||||
|                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")), |                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")), | ||||||
|                 onPressed: () => _setVolumeDown(entity.entityId) |                 onPressed: () => _setVolumeDown(entity.entityId) | ||||||
|  |             ), | ||||||
|  |             IconButton( | ||||||
|  |                 icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")), | ||||||
|  |                 onPressed: () => _setVolumeUp(entity.entityId) | ||||||
|             ) |             ) | ||||||
|           ], |           ], | ||||||
|         ); |         ); | ||||||
|   | |||||||
| @@ -7,10 +7,10 @@ class SimpleEntityState extends StatelessWidget { | |||||||
|   final EdgeInsetsGeometry padding; |   final EdgeInsetsGeometry padding; | ||||||
|   final int maxLines; |   final int maxLines; | ||||||
|   final String customValue; |   final String customValue; | ||||||
|   final double fontSize; |   final TextStyle textStyle; | ||||||
|   final bool bold; |   //final bool bold; | ||||||
|  |  | ||||||
|   const SimpleEntityState({Key key,this.bold: false, this.maxLines: 10, this.fontSize: Sizes.stateFontSize, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key); |   const SimpleEntityState({Key key,/*this.bold: false,*/ this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.textStyle, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
| @@ -22,16 +22,19 @@ class SimpleEntityState extends StatelessWidget { | |||||||
|     } else { |     } else { | ||||||
|       state = customValue; |       state = customValue; | ||||||
|     } |     } | ||||||
|     TextStyle textStyle =  TextStyle( |     TextStyle tStyle; | ||||||
|       fontSize: this.fontSize, |     if (textStyle != null) { | ||||||
|       fontWeight: FontWeight.normal |       tStyle = textStyle; | ||||||
|  |     } else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) { | ||||||
|  |       tStyle = Theme.of(context).textTheme.subhead.copyWith( | ||||||
|  |         color: HAClientTheme().getLinkTextStyle(context).color | ||||||
|       ); |       ); | ||||||
|     if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) { |     } else { | ||||||
|       textStyle = textStyle.apply(color: Colors.blue); |       tStyle = Theme.of(context).textTheme.body1; | ||||||
|     } |     } | ||||||
|     if (this.bold) { |     /*if (this.bold) { | ||||||
|       textStyle = textStyle.apply(fontWeightDelta: 100); |       textStyle = textStyle.apply(fontWeightDelta: 100); | ||||||
|     } |     }*/ | ||||||
|     while (state.contains("  ")){ |     while (state.contains("  ")){ | ||||||
|       state = state.replaceAll("  ", " "); |       state = state.replaceAll("  ", " "); | ||||||
|     } |     } | ||||||
| @@ -43,7 +46,7 @@ class SimpleEntityState extends StatelessWidget { | |||||||
|         maxLines: maxLines, |         maxLines: maxLines, | ||||||
|         overflow: TextOverflow.ellipsis, |         overflow: TextOverflow.ellipsis, | ||||||
|         softWrap: true, |         softWrap: true, | ||||||
|         style: textStyle |         style: tStyle | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|     if (expanded) { |     if (expanded) { | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> { | |||||||
|     } else { |     } else { | ||||||
|       _changedHere = false; |       _changedHere = false; | ||||||
|     } |     } | ||||||
|     Widget slider = Slider( |     Widget slider = UniversalSlider( | ||||||
|       min: entity.minValue * _multiplier, |       min: entity.minValue * _multiplier, | ||||||
|       max: entity.maxValue * _multiplier, |       max: entity.maxValue * _multiplier, | ||||||
|       value: (_newValue <= entity.maxValue) && |       value: (_newValue <= entity.maxValue) && | ||||||
| @@ -62,8 +62,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> { | |||||||
|       children: <Widget>[ |       children: <Widget>[ | ||||||
|         Text( |         Text( | ||||||
|           "$_newValue", |           "$_newValue", | ||||||
|           style: TextStyle( |           style: Theme.of(context).textTheme.display1.copyWith( | ||||||
|             fontSize: Sizes.largeFontSize, |  | ||||||
|             color: Colors.blue |             color: Colors.blue | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|   | |||||||
| @@ -8,8 +8,8 @@ class TimerEntity extends Entity { | |||||||
|   @override |   @override | ||||||
|   void update(Map rawData, String webHost) { |   void update(Map rawData, String webHost) { | ||||||
|     super.update(rawData, webHost); |     super.update(rawData, webHost); | ||||||
|  |     if (attributes.containsKey('duration')) { | ||||||
|       String durationSource = "${attributes["duration"]}"; |       String durationSource = "${attributes["duration"]}"; | ||||||
|     if (durationSource != null && durationSource.isNotEmpty) { |  | ||||||
|       try { |       try { | ||||||
|         List<String> durationList = durationSource.split(":"); |         List<String> durationList = durationSource.split(":"); | ||||||
|         if (durationList.length == 1) { |         if (durationList.length == 1) { | ||||||
| @@ -26,11 +26,11 @@ class TimerEntity extends Entity { | |||||||
|               seconds: int.tryParse(durationList[2]) ?? 0 |               seconds: int.tryParse(durationList[2]) ?? 0 | ||||||
|           ); |           ); | ||||||
|         } else { |         } else { | ||||||
|           Logger.e("Strange $entityId duration format: $durationSource"); |           Logger.e("Strange timer duration format: $durationSource"); | ||||||
|           duration = Duration(seconds: 0); |           duration = Duration(seconds: 0); | ||||||
|         } |         } | ||||||
|       } catch (e) { |       } catch (e, stacktrace) { | ||||||
|         Logger.e("Error parsing duration for $entityId: ${e.toString()}"); |         Logger.e("Error parsing timer duration \'$durationSource\': $e", stacktrace: stacktrace); | ||||||
|         duration = Duration(seconds: 0); |         duration = Duration(seconds: 0); | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -27,11 +27,11 @@ class _TimerStateState extends State<TimerState> { | |||||||
|             try { |             try { | ||||||
|               int passed = DateTime |               int passed = DateTime | ||||||
|                   .now() |                   .now() | ||||||
|                   .difference(entity._lastUpdated) |                   .difference(entity.lastUpdatedTimestamp) | ||||||
|                   .inSeconds; |                   .inSeconds; | ||||||
|               remaining = Duration(seconds: entity.duration.inSeconds - passed); |               remaining = Duration(seconds: entity.duration.inSeconds - passed); | ||||||
|             } catch (e) { |             } catch (e, stacktrace) { | ||||||
|               Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}"); |               Logger.e("Error calculating ${entity.entityId} remaining time: $e", stacktrace: stacktrace); | ||||||
|               remaining = Duration(seconds: 0); |               remaining = Duration(seconds: 0); | ||||||
|             } |             } | ||||||
|           }); |           }); | ||||||
|   | |||||||
| @@ -1,58 +1,111 @@ | |||||||
| part of '../main.dart'; | part of '../main.dart'; | ||||||
|  |  | ||||||
| class UniversalSlider extends StatelessWidget { | class UniversalSlider extends StatefulWidget { | ||||||
|  |  | ||||||
|   final onChanged; |   final Function onChanged; | ||||||
|   final onChangeEnd; |   final Function onChangeEnd; | ||||||
|  |   final Function onChangeStart; | ||||||
|   final Widget leading; |   final Widget leading; | ||||||
|   final Widget closing; |   final Widget closing; | ||||||
|   final String title; |   final String title; | ||||||
|   final double min; |   final double min; | ||||||
|  |   final Color activeColor; | ||||||
|   final double max; |   final double max; | ||||||
|   final double value; |   final double value; | ||||||
|  |   final int divisions; | ||||||
|  |   final String label; | ||||||
|   final EdgeInsets padding; |   final EdgeInsets padding; | ||||||
|  |  | ||||||
|   const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key); |   const UniversalSlider({Key key, this.onChanged, this.label, this.onChangeStart, this.activeColor, this.divisions, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<StatefulWidget> createState() { | ||||||
|  |     return UniversalSliderState(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class UniversalSliderState extends State<UniversalSlider> { | ||||||
|  |  | ||||||
|  |   double _value; | ||||||
|  |   bool _changeStarted = false; | ||||||
|  |   bool _changedHere = false; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     List <Widget> row = []; |     List <Widget> row = []; | ||||||
|     if (leading != null) { |     List <Widget> col = []; | ||||||
|       row.add(leading); |     if (!_changedHere) { | ||||||
|  |       _value = widget.value; | ||||||
|  |     } else { | ||||||
|  |       _changedHere = false; | ||||||
|  |     } | ||||||
|  |     if (widget.leading != null) { | ||||||
|  |       row.add(widget.leading); | ||||||
|     } |     } | ||||||
|     row.add( |     row.add( | ||||||
|       Flexible( |       Flexible( | ||||||
|         child: Slider( |         child: Slider( | ||||||
|             value: value, |           value: _value ?? math.max(widget.max ?? 100, _value ?? 0), | ||||||
|             min: min, |           min: widget.min ?? 0, | ||||||
|             max: max, |           max: widget.max ?? 100, | ||||||
|             onChanged: (value) => onChanged(value), |           activeColor: widget.activeColor, | ||||||
|             onChangeEnd: (value) => onChangeEnd(value), |           label: widget.label, | ||||||
|  |           onChangeStart: (value) { | ||||||
|  |             _changeStarted = true; | ||||||
|  |             widget.onChangeStart?.call(value);  | ||||||
|  |           }, | ||||||
|  |           divisions: widget.divisions, | ||||||
|  |           onChanged: (value) { | ||||||
|  |             setState(() { | ||||||
|  |               _value = value; | ||||||
|  |               _changedHere = true; | ||||||
|  |             }); | ||||||
|  |             widget.onChanged?.call(value); | ||||||
|  |           }, | ||||||
|  |           onChangeEnd: (value) { | ||||||
|  |             _changeStarted = false; | ||||||
|  |             setState(() { | ||||||
|  |               _value = value; | ||||||
|  |               _changedHere = true; | ||||||
|  |             }); | ||||||
|  |             Timer(Duration(milliseconds: 500), () { | ||||||
|  |               if (!_changeStarted) { | ||||||
|  |                 widget.onChangeEnd?.call(value); | ||||||
|  |               } | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|         ), |         ), | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
|     if (closing != null) { |     if (widget.closing != null) { | ||||||
|       row.add(closing); |       row.add(widget.closing); | ||||||
|     } |     } | ||||||
|     return Padding( |     if (widget.title != null) { | ||||||
|       padding: padding, |       col.addAll(<Widget>[ | ||||||
|       child: Column( |  | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|         children: <Widget>[ |  | ||||||
|         Container(height: Sizes.rowPadding,), |         Container(height: Sizes.rowPadding,), | ||||||
|           Text( |         Text('${widget.title}'), | ||||||
|             "$title", |       ]); | ||||||
|             style: TextStyle(fontSize: Sizes.stateFontSize), |     } | ||||||
|           ), |     col.addAll(<Widget>[ | ||||||
|       Container(height: Sizes.rowPadding,), |       Container(height: Sizes.rowPadding,), | ||||||
|       Row( |       Row( | ||||||
|         mainAxisSize: MainAxisSize.min, |         mainAxisSize: MainAxisSize.min, | ||||||
|         children: row, |         children: row, | ||||||
|       ), |       ), | ||||||
|       Container(height: Sizes.rowPadding,) |       Container(height: Sizes.rowPadding,) | ||||||
|         ], |     ]); | ||||||
|  |     return Padding( | ||||||
|  |       padding: widget.padding, | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: col, | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -10,7 +10,7 @@ class VacuumControls extends StatelessWidget { | |||||||
|       child: Column( |       child: Column( | ||||||
|         mainAxisSize: MainAxisSize.min, |         mainAxisSize: MainAxisSize.min, | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           _buildStatusAndBattery(entity), |           _buildStatusAndBattery(entity, context), | ||||||
|           _buildCommands(entity), |           _buildCommands(entity), | ||||||
|           _buildFanSpeed(entity), |           _buildFanSpeed(entity), | ||||||
|           _buildAdditionalInfo(entity) |           _buildAdditionalInfo(entity) | ||||||
| @@ -19,12 +19,12 @@ class VacuumControls extends StatelessWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildStatusAndBattery(VacuumEntity entity) { |   Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) { | ||||||
|     List<Widget> result = []; |     List<Widget> result = []; | ||||||
|     if (entity.supportStatus) { |     if (entity.supportStatus) { | ||||||
|       result.addAll( |       result.addAll( | ||||||
|           <Widget>[ |           <Widget>[ | ||||||
|             Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),), |             Text("Status:"), | ||||||
|             Container(width: 6,), |             Container(width: 6,), | ||||||
|             Expanded( |             Expanded( | ||||||
|               //flex: 1, |               //flex: 1, | ||||||
| @@ -33,10 +33,7 @@ class VacuumControls extends StatelessWidget { | |||||||
|                 maxLines: 1, |                 maxLines: 1, | ||||||
|                 softWrap: true, |                 softWrap: true, | ||||||
|                 overflow: TextOverflow.ellipsis, |                 overflow: TextOverflow.ellipsis, | ||||||
|                 style: TextStyle( |                 style: Theme.of(context).textTheme.body2, | ||||||
|                     fontSize: Sizes.stateFontSize, |  | ||||||
|                     fontWeight: FontWeight.bold |  | ||||||
|                 ), |  | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ] |           ] | ||||||
| @@ -48,7 +45,7 @@ class VacuumControls extends StatelessWidget { | |||||||
|       result.addAll(<Widget>[ |       result.addAll(<Widget>[ | ||||||
|         Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)), |         Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)), | ||||||
|         Container(width: 6,), |         Container(width: 6,), | ||||||
|         Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize)) |         Text("$batteryLevel %") | ||||||
|       ] |       ] | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @@ -172,7 +169,7 @@ class VacuumControls extends StatelessWidget { | |||||||
|         mainAxisSize: MainAxisSize.min, |         mainAxisSize: MainAxisSize.min, | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         children: <Widget>[ |         children: <Widget>[ | ||||||
|           Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)), |           Text("Vacuum cleaner commands:"), | ||||||
|           Container(height: Sizes.rowPadding,), |           Container(height: Sizes.rowPadding,), | ||||||
|           Row( |           Row( | ||||||
|             mainAxisSize: MainAxisSize.max, |             mainAxisSize: MainAxisSize.max, | ||||||
|   | |||||||
| @@ -27,10 +27,7 @@ class VacuumStateButton extends StatelessWidget { | |||||||
|           text: "RETURN TO DOCK" |           text: "RETURN TO DOCK" | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       result = Text(entity.state.toUpperCase(), style: TextStyle( |       result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead); | ||||||
|           fontSize: 16, |  | ||||||
|           color: Colors.grey |  | ||||||
|       )); |  | ||||||
|     } |     } | ||||||
|     return Padding( |     return Padding( | ||||||
|       padding: EdgeInsets.only(right: 15), |       padding: EdgeInsets.only(right: 15), | ||||||
|   | |||||||