Compare commits

...

213 Commits

Author SHA1 Message Date
fec3c525e1 Bump version code 2020-05-30 10:23:21 +00:00
b1bbed6d80 Bump version code 2020-05-30 10:09:48 +00:00
13878cfc51 Fix FCM token crash 2020-05-30 10:02:50 +00:00
be49180205 Update github urls 2020-05-29 18:38:57 +00:00
c4a0b16553 Resolves #567 Login connection timeout issue 2020-05-29 18:26:20 +00:00
caacd5e9f4 Fix display mode name 2020-05-29 18:20:25 +00:00
5fa28abb6c Bump version code 2020-05-29 18:18:38 +00:00
e0a28c0b59 Resolves #563 Fullscreen mode 2020-05-29 18:16:59 +00:00
096e714a04 Real fullscreen for camera view 2020-05-29 16:52:57 +00:00
78893ea01f Fix for cropped screans 2020-05-29 16:35:34 +00:00
90efb29be5 Resolves #555 PayPal donate button removed 2020-05-29 16:25:07 +00:00
fca323c56b Resolves #559 2020-05-29 16:14:39 +00:00
e5fe6af5f3 Resolves #513 Allow to install on SD card 2020-05-29 09:24:23 +00:00
f0090d522d Merge pull request #566 from estevez-dev/rc/1.1.0-b3
Bump version code
2020-05-29 00:16:12 +03:00
edbfd8359b Bump version code 2020-05-28 21:15:26 +00:00
2702bb254a Bump version code 2020-05-28 20:55:08 +00:00
ca7b6ed550 Resolves #564 - Show picture-elements as button if camera_image provided 2020-05-28 20:52:23 +00:00
fb00b5d9ff Replace secure storage with encripted db 2020-05-28 20:23:13 +00:00
7ffba397ce Fix crash whne no google play services available 2020-05-28 19:17:32 +00:00
1080076e3b Merge pull request #562 from estevez-dev/rc/1.1.0-b2
Rc/1.1.0 b2
2020-05-25 18:59:27 +03:00
e295a36465 1151 2020-05-25 15:57:57 +00:00
9a09a83dc6 Request FCM token from native 2020-05-25 15:41:29 +00:00
95ca80949f bump version code 2020-05-25 14:10:29 +00:00
80b5763530 FCM token update and waiting 2020-05-25 14:09:45 +00:00
9a5e35b024 Fix notification event data 2020-05-25 12:34:53 +00:00
4493975676 defer fcm token load 2020-05-25 11:58:14 +00:00
141a68faf7 Update readme 2020-05-25 11:34:55 +00:00
a8efe7dbb6 Remove mobile app integration version check 2020-05-25 11:16:22 +00:00
9608983994 Bump version code 2020-05-25 11:06:08 +00:00
8eb15ab9a4 Notification channel description 2020-05-25 11:05:16 +00:00
aac0cfbb56 Dismiss and auto dismiss for notifications 2020-05-25 10:20:48 +00:00
343494ece0 Fix notification action receiver 2020-05-25 09:54:00 +00:00
b1e5e73278 Fix display name getting issue 2020-05-25 08:52:16 +00:00
9b5a0068fd Fix integer entity names handling in cards 2020-05-25 08:39:30 +00:00
aa26212ddd bump version code 2020-05-25 08:22:52 +00:00
1c45f96706 Remove Firebase hotfix
Firebase messages now handled by native code
2020-05-25 10:39:08 +03:00
c2d5192c51 Filter strange card data 2020-05-25 10:37:02 +03:00
88ae80507c Merge pull request #561 from estevez-dev/actionable_notifications
Actionable notifications
2020-05-25 02:34:18 +03:00
55868d1dfe Resolves #471 Actionable notification 2020-05-24 23:33:20 +00:00
92a1230267 WIP #471 Handling basic notifications in native code 2020-05-24 15:04:55 +00:00
d3f99fb262 WIP #471 Native FCM init 2020-05-24 12:42:31 +00:00
3fdf016c39 Merge pull request #560 from estevez-dev/rc/1.1.0
Rc/1.1.0
2020-05-24 11:26:53 +03:00
8ce0e8aafa remove ndk filters 2020-05-24 08:26:09 +00:00
54f6fb28ef add ndk platform filters 2020-05-24 07:57:00 +00:00
d53825f140 Merge pull request #556 from estevez-dev/rc/1.1.0
Rc/1.1.0
2020-05-23 22:50:18 +03:00
ea7e0f04ce bump build number 2020-05-23 19:27:57 +00:00
cf75989447 trigger 2020-05-23 19:07:53 +00:00
f27d55869b Prevent updates for dev builds 2020-05-23 16:26:42 +00:00
a287f597ad Merge branch 'master' of https://github.com/estevez-dev/ha_client 2020-05-23 15:53:12 +00:00
0698950f3d Changes for CD builds 2020-05-23 15:52:43 +00:00
aa58559ba6 Update README.md 2020-05-23 18:47:59 +03:00
94acc67383 1102 2020-05-23 14:28:43 +00:00
701e6a46df 1.1.0 2020-05-22 16:01:16 +00:00
1ed56ce8f1 Show entity icon on error loading entity picture 2020-05-22 13:31:07 +00:00
a6d1baca77 Fix panel view scroll issue 2020-05-22 12:54:39 +00:00
dbeda6ea68 Default states-like UI if no Lovelace config 2020-05-19 22:01:07 +00:00
3dca28a7da Fix login button display 2020-05-19 21:05:45 +00:00
da4264a409 Quick access buttons 2020-05-18 22:06:45 +00:00
302451e118 Slider labels for light controls 2020-05-18 22:06:45 +00:00
d19dbd389b Update README.md 2020-05-17 17:10:44 +03:00
05ae954b30 Quick actions bar test 2020-05-17 11:16:25 +00:00
840e266381 Update mobile app registration only on full load 2020-05-16 21:25:58 +00:00
01525a2929 Update fcm lib 2020-05-16 21:23:15 +00:00
2eb2596f37 Update push url 2020-05-16 20:04:04 +00:00
699cab3498 Fix reconnect after app resume 2020-05-14 20:53:01 +00:00
353c80b6bc Add path to every view 2020-05-14 16:44:50 +00:00
e4d1a4f823 Resolves #212 2020-05-14 15:38:22 +00:00
78d6b38b92 WIP #212 Light card 2020-05-14 12:13:23 +00:00
1499a91ef7 SHow donate message only after 14 days of use 2020-05-14 11:20:06 +00:00
9b7f7aa380 Fix slider possition change from outside 2020-05-14 10:56:52 +00:00
5683ab5158 WIP: App settings refactoring 2020-05-13 12:46:25 +00:00
a20dfaf05e WIP: AppSettings 2020-05-13 10:57:26 +00:00
24d42c9597 Fix badge entity_picture size 2020-05-09 20:47:28 +00:00
9078ad81e8 1100 2020-05-09 18:47:30 +00:00
7cba6c8a10 Revert state filter as map fix 2020-05-09 18:46:53 +00:00
c1f9c8c16d Fix for states in state filter not strings 2020-05-09 18:28:06 +00:00
1d1d132b33 Resolves #530 Badges refactoring 2020-05-09 18:08:42 +00:00
e258b3bc2c Avoid null for options in ModeSelectorWidget 2020-05-09 15:20:01 +00:00
13508ee92f WIP #530 Badges refactoring 2020-05-09 13:38:05 +00:00
4fbf58e707 Change default hold action to more-info 2020-05-09 11:39:53 +00:00
a3442f84ca Glance card padding fixes 2020-05-08 14:23:35 +00:00
6a6ab3b2cb Fix some padding inside cards 2020-05-08 13:59:04 +00:00
d9fa553e2f Fix cions 2020-05-08 13:44:08 +00:00
cde5d9b912 Fix cions 2020-05-08 13:42:25 +00:00
3468446b5b Increase auth callback timeout for authenticated webview 2020-05-08 13:36:57 +00:00
326434273a Disable external bus for authenticated webview 2020-05-08 13:29:54 +00:00
470d3be946 Fix for empty stack cards 2020-05-08 13:20:01 +00:00
d1032be6a6 Fix getting lovelace config for HA < 0.107 2020-05-08 12:59:06 +00:00
cffac8e1f8 Safe display name parse 2020-05-08 12:55:44 +00:00
870bc25dd9 Timer duration parsing error report improvement 2020-05-08 12:47:43 +00:00
de713024f6 Safe icon parse 2020-05-08 12:37:53 +00:00
4d4add4581 Dont parse castom cards. Hide unsupported 2020-05-08 12:36:13 +00:00
1670c8e505 Hide unsupported cards 2020-05-07 16:14:23 +00:00
55eb1b5125 Fix mistakes handling in gauge card 2020-05-07 15:48:20 +00:00
dbeaaaf91e Fix null data referense for panels 2020-05-07 15:38:38 +00:00
8166d8ce6d Fix null data referense 2020-05-07 15:33:45 +00:00
35bcf0c1fa Fix nu data referense 2020-05-07 15:31:18 +00:00
9c1d240962 1013 2020-05-06 19:05:11 +00:00
a76652b552 1012 2020-05-06 18:46:05 +00:00
a140f993d0 1011 2020-05-06 18:23:01 +00:00
ded60a2867 not null 2020-05-06 18:22:17 +00:00
b86602bcdb app version and whats new 2020-05-06 18:19:41 +00:00
02ea45469f 1.0.1 2020-05-06 17:52:31 +00:00
90105c3b09 Cache reading issues fix 2020-05-06 17:51:06 +00:00
3d828914cc Fix error handling 2020-05-06 17:24:13 +00:00
8cd5776bc6 Make integration work with HA < 0.104 2020-05-06 16:40:19 +00:00
17ec73b176 Report foreground location errors 2020-05-06 16:19:42 +00:00
e7cce01ca9 Remove beta 2020-05-05 12:34:20 +00:00
6c73f5d979 1008 2020-05-05 12:30:41 +00:00
f59cb5afbf Show error on registration fail 2020-05-05 12:25:34 +00:00
5629215229 Fix app registration error reporting 2020-05-05 12:20:51 +00:00
45fb637d48 Slider fixes. Also resolves #463 2020-05-05 12:12:49 +00:00
7c473eb1ca Avoid null values for slider 2020-05-05 11:35:19 +00:00
b40880c85a Remove some app integration errors 2020-05-05 11:33:04 +00:00
30329ea3ba Update main.dart 2020-05-05 11:41:14 +03:00
ca10401bee 1007 2020-05-04 14:50:23 +00:00
814e0a8b00 Habdle mistakes in card config 2020-05-04 14:47:47 +00:00
b5fbe7b86f Update main.dart 2020-05-04 14:51:55 +03:00
fc9b6f05c0 Update main.dart 2020-05-04 14:51:41 +03:00
eadae4374b Update whats_new.page.dart 2020-05-04 14:49:54 +03:00
711cb04dcf 1.0.0-beta 2020-05-04 12:37:43 +03:00
1d39b7fc7d Update pubspec.yaml 2020-05-04 12:37:01 +03:00
2fa640433a Update whats_new.page.dart 2020-05-04 12:36:32 +03:00
2445dc7869 Update startup_user_messages_manager.class.dart 2020-05-04 12:35:48 +03:00
e3e114fe94 Resolves #551 2020-05-04 12:12:18 +03:00
7a1603b423 alpha2 2020-05-04 12:11:26 +03:00
4b831821da 1005 2020-05-04 12:10:40 +03:00
1ec54953d7 Update .gitignore 2020-05-04 11:32:35 +03:00
61571600d1 Delete google-services.json 2020-05-04 11:31:47 +03:00
07a097aa50 Fix Discord link 2020-05-03 18:56:28 +00:00
ce1cebaf64 Update README.md 2020-05-03 19:48:02 +03:00
faf6f73b2a Update README.md 2020-05-03 19:47:49 +03:00
db3b5d941e 1004 2020-05-03 15:27:22 +00:00
cc60dc2b21 Fix token login popup show 2020-05-03 15:26:44 +00:00
8aa0e03187 1.0.0 alpha1 2020-05-03 14:26:46 +00:00
4492a08d6b battery updated to 1.0.0 2020-05-03 14:12:08 +00:00
792c0d0c84 Resolves #545 handle hidden entities for Lovelace 2020-05-03 14:06:43 +00:00
8221eceb78 Resolves #549 trim spaces for device name, url and long-lived token 2020-05-03 13:47:11 +00:00
12ba5598e4 Resolves #547 Remove close button from quick start 2020-05-03 13:43:04 +00:00
536cbf9445 Resolves #523 Change device name for integration 2020-05-03 13:36:40 +00:00
a87943da27 Resolves #550 socket protocol set 2020-05-03 11:48:09 +00:00
3fddc3b5a7 Resolves #546 Device name is missed 2020-05-03 11:25:09 +00:00
5bc0b0868a 1002 2020-05-03 10:30:51 +00:00
e9ad612fec Fix config panel opening 2020-05-03 10:00:32 +00:00
c62e045dae flutter_secure_storage updated to 3.3.3 2020-05-03 09:58:49 +00:00
725ec9291d WIP #523 and connection settings refactoring 2020-05-02 23:02:18 +00:00
96c8338890 Show error when media_player is not media_player 2020-05-02 08:50:21 +00:00
0996fb94da Fix getting entity on entity page when states is not loaded 2020-05-02 08:31:10 +00:00
5de2431a0f Fix getting default icon of entity without id 2020-05-02 08:27:20 +00:00
163338ea75 WIP #523 Change device name for integration 2020-05-01 22:39:32 +00:00
f28e5493dc Popup menu position fix 2020-05-01 19:15:50 +00:00
01c0a08fa8 Fix media menu closing behaviour 2020-05-01 19:02:42 +00:00
1c461d2449 Fix camera stream view navigation issue 2020-05-01 18:50:50 +00:00
915e8045a3 1001 2020-05-01 17:18:55 +00:00
f10fc7eec1 Resolves #543: Double values in gauge config 2020-05-01 17:09:09 +00:00
320bc677e0 Remove log viewer 2020-05-01 16:33:43 +00:00
46ca1948e2 Error handling improvements 2020-05-01 16:24:13 +00:00
7a0ce93cfd Fix wrong log level 2020-05-01 16:07:22 +00:00
3c0bd68b0a Fix duplicating panels 2020-05-01 16:05:07 +00:00
b4ad3061e4 Fix launchURLInBrowser 2020-05-01 15:49:50 +00:00
d6b1fbec24 Open local navigate actions in browser 2020-05-01 15:48:54 +00:00
cacdd0d304 Fix custom tab opening 2020-05-01 15:40:22 +00:00
e3e1fa3499 Don't throw exception on cached data 2020-05-01 14:42:45 +00:00
58842d1ebb Report all errors to Crashlytivs if in production 2020-05-01 14:37:05 +00:00
101569d6ee Error reporting: HomeAssistant class 2020-05-01 14:37:05 +00:00
8a180c4c0e Panels error handling 2020-05-01 14:37:05 +00:00
ba343fbd98 Final icons update for ald Android versions 2020-04-30 17:14:38 +03:00
1d528df341 Adoptive icon fixes 2020-04-30 14:04:37 +00:00
51ea0b0afa Icons update 2020-04-30 15:52:00 +03:00
9dbb697e58 Adoptive app icon 2020-04-30 12:43:56 +00:00
947558bb3d Icons update 2020-04-30 15:08:26 +03:00
8ba4cc85d8 Bump version to 1.0.0 for fist stable release 2020-04-30 11:23:20 +00:00
0f604a6ce6 New app icon 2020-04-30 13:03:16 +03:00
7e48c6535f 898 2020-04-29 22:39:47 +00:00
1d2a8b613b Disable Crashlytics in debug mode 2020-04-29 22:37:12 +00:00
89e833eb33 Fix theme settings loading 2020-04-29 22:30:53 +00:00
b65a68e0c4 Help pages links 2020-04-29 22:28:52 +00:00
bfb24b9d11 Call service state color from theme 2020-04-29 20:11:37 +00:00
0792cae2b1 Fix missed entity picture 2020-04-29 20:04:59 +00:00
a85fb3d03b Display light color as a badge on entity icon 2020-04-29 20:01:37 +00:00
ddb9a9d4e9 Fix gauge card tap events 2020-04-29 17:48:15 +00:00
29ee360ec4 Remove test exception 2020-04-29 07:49:46 +00:00
c0faaafd04 0.8.6 2020-04-29 07:48:05 +00:00
bc045344a5 Handle card rendering errors to show in ui 2020-04-29 07:45:15 +00:00
7d746fd546 896 2020-04-29 07:11:09 +00:00
3ff55f181e Fix icon_height parsing issue 2020-04-29 10:00:28 +03:00
187e12dd79 895 2020-04-29 01:19:43 +03:00
10daf2d952 0.8.5 2020-04-28 21:47:41 +00:00
31c6509d13 0.8.5 2020-04-28 21:43:48 +00:00
cb74108814 893 2020-04-28 21:12:18 +00:00
9efded2139 Possible workaround for firebase messaging crash 2020-04-28 21:09:45 +00:00
96b3e7c739 Add state_color support 2020-04-28 21:03:00 +00:00
b029146bf3 Gauge scale fixes 2020-04-28 20:36:11 +00:00
d715aaf5e5 Gauge card elements scale fix 2020-04-27 20:56:33 +00:00
0dc12963f0 Cards parsing improvements 2020-04-26 22:46:37 +00:00
4da3b40d55 WIP: Cards parsing improvements 2020-04-26 18:44:21 +00:00
f7d05a57ad Use '***' in markdawn card to add empty space 2020-04-25 21:45:58 +00:00
df01599fe0 Resloves #541 Prevent double service call when slider is moved 2020-04-25 21:24:15 +00:00
2c3335ebf3 Resolves #539 Fix button card without entity 2020-04-25 20:53:08 +00:00
05c1427aa8 Add icon support for entities card title 2020-04-25 18:23:26 +00:00
02bfaf7db6 WIP: Cards build optimization 2020-04-25 17:38:21 +00:00
f488c0810b WIP: Cards build optimization 2020-04-25 15:59:07 +00:00
8dbfb91234 Add Lovelase card widget 2020-04-25 14:53:33 +00:00
aee99e3925 Entities card build optimization 2020-04-25 14:39:15 +00:00
50d3280803 Gauge font size fix 2020-04-25 14:33:54 +00:00
a90eb5c4db Fix glance card title 2020-04-25 14:20:46 +00:00
16c06a2d48 Widget rendering improvements 2020-04-25 14:15:19 +00:00
513bf85cae Fix dashboard switching issues 2020-04-21 09:01:21 +00:00
82d7aeba02 Some cleanup 2020-04-15 18:23:29 +00:00
12f7cb86de Linear progress indicator 2020-04-15 18:21:16 +00:00
b65c885467 Bottom info bar as standalone component 2020-04-15 18:13:03 +00:00
2a828a1289 WIP: bottom info bar as separate component 2020-04-15 15:40:21 +00:00
291f12ba97 Component detection 2020-04-15 15:40:21 +00:00
114 changed files with 4115 additions and 4787 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ flutter_export_environment.sh
key.properties
.secrets.dart
pubspec.lock
google-services.json

View File

@ -1,16 +1,18 @@
# HA Client
## Native Android client for Home Assistant
### With notifications and Lovelace UI support
### With actionable notifications, location tracking and Lovelace UI support
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 [Discord](https://discord.gg/nd6FZQ) 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)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
#### Pre-release CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
#### Beta CI build
#### Last release build status
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
#### Special thanks to
- [Crewski](https://github.com/Crewski) for his [HANotify](https://github.com/Crewski/HANotify)
- [Home Assistant](https://github.com/home-assistant) for some support and [Home Assistant](https://www.home-assistant.io/)

View File

@ -79,6 +79,8 @@ flutter {
dependencies {
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'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

@ -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"
}

View File

@ -1,5 +1,6 @@
<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"
android:required="false" />
@ -8,7 +9,6 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
@ -19,6 +19,7 @@
<application
android:label="HA Client"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true">
<meta-data
@ -42,16 +43,26 @@
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".MessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<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>
<service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"

View File

@ -5,11 +5,67 @@ import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

View File

@ -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 (IOException e) {
return null;
}
}
}

View File

@ -0,0 +1,69 @@
package com.keyboardcrumbs.hassclient;
import android.content.Context;
import androidx.annotation.NonNull;
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 = "NotificationActionReceiver";
@Override
public void onReceive(Context context, Intent intent) {
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");
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

View 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>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<item android:drawable="@color/main_color" />
<!-- You can insert your own image assets here -->
<!-- <item>

View File

@ -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>

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="main_color">#709AC1</color>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,10 @@ window.externalApp.getExternalAuth = function(options) {
setTimeout(function(){
console.log("Calling a callback");
window[options.callback](true, responseData);
}, 500);
}, 900);
}
};
/*
window.externalApp.externalBus = function(message) {
console.log("External bus message: " + message);
var messageObj = JSON.parse(message);
@ -32,4 +33,5 @@ window.externalApp.externalBus = function(message) {
} else if (messageObj.type == "config_screen/show") {
HAClient.postMessage('show-settings');
}
};
};
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View 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
View 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(),
);
}
}

View File

@ -1,53 +1,124 @@
part of '../main.dart';
class HACard {
List<EntityWrapper> entities = [];
List<HACard> childCards = [];
EntityWrapper linkedEntityWrapper;
String name;
String id;
String type;
bool showName;
bool showState;
bool showEmpty;
bool showHeaderToggle;
int columnsCount;
List stateFilter;
List states;
List conditions;
String content;
String unit;
int min;
int max;
Map severity;
class CardData {
HACard({
this.name,
this.id,
this.linkedEntityWrapper,
this.columnsCount: 4,
this.showName: true,
this.showHeaderToggle: true,
this.showState: true,
this.stateFilter: const [],
this.showEmpty: true,
this.content,
this.states,
this.conditions: const [],
this.unit,
this.min,
this.max,
this.severity,
@required this.type
}) {
if (this.columnsCount <= 0) {
this.columnsCount = 4;
String type;
List<EntityWrapper> entities = [];
List conditions;
bool showEmpty;
List stateFilter;
bool stateColor = true;
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
factory CardData.parse(rawData) {
try {
if (rawData['type'] == null) {
rawData['type'] = CardType.ENTITIES;
} else if (!(rawData['type'] is String)) {
return CardData(null);
}
switch (rawData['type']) {
case CardType.ENTITIES:
case CardType.HISTORY_GRAPH:
case CardType.MAP:
case CardType.PICTURE_GLANCE:
case CardType.SENSOR:
case CardType.ENTITY:
case CardType.WEATHER_FORECAST:
case CardType.PLANT_STATUS:
if (rawData['entity'] != null) {
rawData['entities'] = [rawData['entity']];
}
return EntitiesCardData(rawData);
break;
case CardType.ALARM_PANEL:
return AlarmPanelCardData(rawData);
break;
case CardType.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:
return CardData(null);
}
} catch (error, stacktrace) {
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
return ErrorCardData(rawData);
}
}
CardData(rawData) {
if (rawData != null && rawData is Map) {
type = rawData['type'];
conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true;
stateFilter = rawData['state_filter'] ?? [];
} else {
type = CardType.UNKNOWN;
conditions = [];
showEmpty = true;
stateFilter = [];
}
}
Widget buildCardWidget() {
return UnsupportedCard(card: this);
}
List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) {
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
if (entityWrapper.entity.isHidden) {
return false;
}
List currentStateFilter;
@ -104,9 +175,8 @@ class HACard {
break;
}
}
} catch (e) {
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState');
Logger.e('$e');
} catch (e, stacktrace) {
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState: $e', stacktrace: stacktrace);
}
}
}
@ -114,10 +184,483 @@ class HACard {
}).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() {
return LightCard(card: this);
}
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';
}
}

View File

@ -1,384 +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.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 = [];
children = card.childCards.map((childCard) => Flexible(
fit: FlexFit.tight,
child: childCard.build(context),
)
).toList();
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = card.childCards.map((childCard) => childCard.build(context)).toList();
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 = [];
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.name,
trailing: headerSwitch
)
);
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}",
),
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.overrideName = 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.overrideName = card.name ??
card.linkedEntityWrapper.displayName;
card.linkedEntityWrapper.unitOfMeasurementOverride = 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.overrideName = 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
)
);
}
}

View 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
)
),
)
);
}
}

View File

@ -0,0 +1,96 @@
part of '../main.dart';
class EntityButtonCard extends StatelessWidget {
final ButtonCardData card;
EntityButtonCard({
Key key, this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = card.entity;
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
return Container(width: 0.0, height: 0.0,);
}
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
Widget buttonIcon;
if (!card.showIcon) {
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
} else if (iconSize > 0) {
buttonIcon = SizedBox(
height: iconSize,
child: FractionallySizedBox(
widthFactor: 0.5,
child: FittedBox(
fit: BoxFit.contain,
child: EntityIcon(
//padding: EdgeInsets.only(top: 6),
),
)
),
);
} else {
buttonIcon = AspectRatio(
aspectRatio: 2,
child: FractionallySizedBox(
widthFactor: 0.5,
child: FittedBox(
fit: BoxFit.fitWidth,
child: EntityIcon(
//padding: EdgeInsets.only(top: 6),
),
)
),
);
}
return CardWrapper(
child: EntityModel(
entityWrapper: card.entity,
child: InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: Center(
child: Padding(
padding: EdgeInsets.only(top: 5),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buttonIcon,
_buildName(context)
],
)
)
),
),
handleTap: true
)
);
}
Widget _buildName(BuildContext context) {
if (card.showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
textStyle: Theme.of(context).textTheme.subhead,
wordsWrap: true,
textAlign: TextAlign.center
);
}
return Container(width: 0, height: 0);
}
}

38
lib/cards/error_card.dart Normal file
View File

@ -0,0 +1,38 @@
part of '../main.dart';
class ErrorCard extends StatelessWidget {
final ErrorCardData card;
const ErrorCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
return CardWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
textAlign: TextAlign.center,
),
RaisedButton(
onPressed: () {
Clipboard.setData(new ClipboardData(text: card.cardConfig));
},
child: Text('Copy card config'),
),
RaisedButton(
onPressed: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
},
child: Text('Report issue'),
)
],
),
)
);
}
}

201
lib/cards/gauge_card.dart Normal file
View 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
View 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),
);
}
}

View 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
View 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
)
);
}
}

View 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,
)
],
),
)
);
}
}

View 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()
)
);
}
}

View 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();
}
}

View 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,);
}
}

View File

@ -4,9 +4,11 @@ class CardHeader extends StatelessWidget {
final String name;
final Widget trailing;
final Widget leading;
final Widget subtitle;
final double emptyPadding;
const CardHeader({Key key, this.name, this.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
Widget build(BuildContext context) {
@ -14,6 +16,7 @@ class CardHeader extends StatelessWidget {
if ((name != null) && (name.trim().length > 0)) {
result = new ListTile(
trailing: trailing,
leading: leading,
subtitle: subtitle,
title: Text("$name",
textAlign: TextAlign.left,
@ -21,7 +24,7 @@ class CardHeader extends StatelessWidget {
style: Theme.of(context).textTheme.headline),
);
} else {
result = new Container(width: 0.0, height: 0.0);
result = new Container(width: 0.0, height: emptyPadding);
}
return result;
}

View File

@ -0,0 +1,21 @@
part of '../../main.dart';
class CardWrapper extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: padding,
child: child
),
);
}
}

View File

@ -1,57 +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(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
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
);
}
return Container(width: 0, height: 0);
}
}

View File

@ -1,177 +0,0 @@
part of '../../main.dart';
class GaugeCardBody extends StatelessWidget {
final int min;
final int max;
final Map severity;
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
double fixedValue;
double value = entityWrapper.entity.doubleState;
if (value > max) {
fixedValue = max.toDouble();
} else if (value < min) {
fixedValue = min.toDouble();
} else {
fixedValue = value;
}
List<GaugeRange> ranges;
Color currentColor;
if (severity != null && severity["green"] is int && severity["red"] is int && severity["yellow"] is int) {
List<RangeContainer> rangesList = <RangeContainer>[
RangeContainer(severity["green"], HAClientTheme().getGreenGaugeColor()),
RangeContainer(severity["red"], HAClientTheme().getRedGaugeColor()),
RangeContainer(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: 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: min.toDouble(),
endValue: max.toDouble(),
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3,
)
];
}
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: AspectRatio(
aspectRatio: 2,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSizeFactor;
if (constraints.maxWidth > 300.0) {
fontSizeFactor = 1.6;
} else if (constraints.maxWidth > 150.0) {
fontSizeFactor = 1;
} else if (constraints.maxWidth > 100.0) {
fontSizeFactor = 0.6;
} else {
fontSizeFactor = 0.4;
}
return SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
maximum: max.toDouble(),
minimum: min.toDouble(),
showLabels: false,
useRangeColorForAxis: true,
showTicks: false,
canScaleToFit: true,
ranges: ranges,
axisLineStyle: AxisLineStyle(
thickness: 0.3,
thicknessUnit: GaugeSizeUnit.factor,
color: Colors.transparent
),
annotations: <GaugeAnnotation>[
GaugeAnnotation(
angle: -90,
positionFactor: 1.3,
//verticalAlignment: GaugeAlignment.far,
widget: EntityName(
textStyle: Theme.of(context).textTheme.body1.copyWith(
fontSize: Theme.of(context).textTheme.body1.fontSize * fontSizeFactor
),
),
),
GaugeAnnotation(
angle: 180,
positionFactor: 0,
verticalAlignment: GaugeAlignment.center,
widget: SimpleEntityState(
expanded: false,
maxLines: 1,
textAlign: TextAlign.center,
textStyle: Theme.of(context).textTheme.title.copyWith(
fontSize: Theme.of(context).textTheme.title.fontSize * fontSizeFactor,
),
),
)
],
startAngle: 180,
endAngle: 0,
pointers: <GaugePointer>[
RangePointer(
value: fixedValue,
sizeUnit: GaugeSizeUnit.factor,
width: 0.3,
color: currentColor,
enableAnimation: true,
animationType: AnimationType.bounceOut,
)
]
)
],
);
},
),
),
);
}
}
class RangeContainer {
final int startFrom;
Color color;
RangeContainer(this.startFrom, this.color);
}

View File

@ -1,84 +0,0 @@
part of '../../main.dart';
class GlanceCardEntityContainer extends StatelessWidget {
final bool showName;
final bool showState;
final bool nameInTheBottom;
final double iconSize;
final bool wordsWrapInName;
GlanceCardEntityContainer({
Key key,
@required this.showName,
@required this.showState,
this.nameInTheBottom: false,
this.iconSize: Sizes.iconSize,
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(context));
}
} 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(context));
}
return Center(
child: 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: wordsWrapInName,
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),
);
}
}

View File

@ -1,38 +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],
),
),
);
}
}

View File

@ -36,66 +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;
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"];
}
}
}
}
}
class CardType {
static const HORIZONTAL_STACK = "horizontal-stack";
static const VERTICAL_STACK = "vertical-stack";
@ -113,11 +53,17 @@ class CardType {
static const IFRAME = "iframe";
static const GAUGE = "gauge";
static const ENTITY_BUTTON = "entity-button";
static const ENTITY = "entity";
static const BUTTON = "button";
static const CONDITIONAL = "conditional";
static const ALARM_PANEL = "alarm-panel";
static const MARKDOWN = "markdown";
static const LIGHT = "light";
static const ENTITY_FILTER = "entity-filter";
static const UNKNOWN = "unknown";
static const HISTORY_GRAPH = "history-graph";
static const PICTURE_GLANCE = "picture-glance";
static const BADGES = "badges";
}
class Sizes {

View File

@ -1,148 +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 = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
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: Theme.of(context).textTheme.body1.color
);
break;
}
case "device_tracker":
case "person":
{
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Theme.of(context).textTheme.body1.color
);
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: Theme.of(context).textTheme.body1.copyWith(
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: Theme.of(context).textTheme.overline.copyWith(
color: HAClientTheme().getOnBadgeTextColor()
),
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: Theme.of(context).cardColor,
// 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: Theme.of(context).textTheme.caption,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
}
}

View File

@ -36,13 +36,13 @@ class _CameraStreamViewState extends State<CameraStreamView> {
.of(context)
.entityWrapper
.entity;
if (_entity.supportStream) {
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
HomeAssistant().getCameraStream(_entity.entityId)
.then((data) {
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
_webViewHtml = Uri.dataFromString(
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
file.replaceFirst('{{stream_url}}', '${AppSettings().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8')
).toString();
@ -69,7 +69,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
}
Future _loadMJPEG() async {
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
_streamUrl = '${AppSettings().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
var file = await rootBundle.loadString('assets/html/cameraView.html');
@ -138,8 +138,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
iconSize: 40,
color: Theme.of(context).accentColor,
onPressed: _isLoaded ? () {
eventBus.fire(ShowEntityPageEvent());
Navigator.of(context).push(
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (conext) => FullScreenPage(
child: EntityModel(
@ -154,8 +153,8 @@ class _CameraStreamViewState extends State<CameraStreamView> {
),
fullscreenDialog: true
)
).then((_) {
eventBus.fire(ShowEntityPageEvent(entity: _entity));
).then((_){
eventBus.fire(ShowEntityPageEvent(entityId: _entity.entityId));
});
} : null,
)

View File

@ -191,7 +191,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity;
Logger.d("[Climate widget build] changed here = $_changedHere");
if (_changedHere) {
//_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_changedHere = false;
@ -353,15 +352,19 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
List<Widget> result = [];
if (entity.supportTargetHumidity) {
result.addAll(<Widget>[
Text(
"$_tmpTargetHumidity%",
style: Theme.of(context).textTheme.display1,
),
Expanded(
child: Slider(
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding),
child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
),
UniversalSlider(
leading: Text(
"$_tmpTargetHumidity%",
style: Theme.of(context).textTheme.display1,
),
value: _tmpTargetHumidity,
max: entity.maxHumidity,
min: entity.minHumidity,
@ -373,22 +376,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}),
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: Theme.of(context).textTheme.body1),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: result,
),
Container(
height: Sizes.rowPadding,
)

View File

@ -11,7 +11,7 @@ class ModeSelectorWidget extends StatelessWidget {
ModeSelectorWidget({
Key key,
@required this.caption,
@required this.options,
this.options: const [],
this.value,
@required this.onChange,
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),

View File

@ -62,11 +62,10 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
padding: EdgeInsets.only(top: Sizes.rowPadding),
child: Text("Position"),
),
Slider(
UniversalSlider(
value: _tmpPosition,
min: 0.0,
max: 100.0,
@ -78,8 +77,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
});
},
onChangeEnd: (double value) => _setNewPosition(entity, value),
),
Container(height: Sizes.rowPadding,)
)
],
);
} else {
@ -96,7 +94,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
}
if (entity.supportSetTiltPosition) {
controls.addAll(<Widget>[
Slider(
UniversalSlider(
value: _tmpTiltPosition,
min: 0.0,
max: 100.0,
@ -115,7 +113,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
if (controls.isNotEmpty) {
controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
0.0, Sizes.rowPadding, 0.0, 0),
child: Text("Tilt position"),
));
return Column(

View File

@ -11,13 +11,13 @@ class DefaultEntityContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return MissedEntityWidget();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) {
return Divider();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -36,7 +36,6 @@ class DefaultEntityContainer extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
children: <Widget>[
EntityIcon(),
Flexible(
fit: FlexFit.tight,
flex: 3,

View File

@ -1,13 +1,6 @@
part of '../main.dart';
class StatelessEntityType {
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;
}
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
class Entity {
@ -76,8 +69,8 @@ class Entity {
String entityPicture;
String state;
String displayState;
DateTime _lastUpdated;
int statelessType = 0;
DateTime lastUpdatedTimestamp;
StatelessEntityType statelessType = StatelessEntityType.none;
List<Entity> childEntities = [];
String deviceClass;
@ -85,8 +78,21 @@ class Entity {
chartType: EntityHistoryWidgetType.simple
);
String get displayName =>
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
String get displayName {
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 =>
(domain == "group") &&
@ -120,42 +126,47 @@ class Entity {
}
Entity.missed(String entityId) {
statelessType = StatelessEntityType.MISSED;
statelessType = StatelessEntityType.missed;
attributes = {"hidden": false};
this.entityId = entityId;
}
Entity.divider() {
statelessType = StatelessEntityType.DIVIDER;
statelessType = StatelessEntityType.divider;
attributes = {"hidden": false};
}
Entity.section(String label) {
statelessType = StatelessEntityType.SECTION;
statelessType = StatelessEntityType.section;
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}) {
statelessType = StatelessEntityType.CALL_SERVICE;
statelessType = StatelessEntityType.callService;
entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
}
Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.WEBLINK;
entityId = "custom.custom"; //TODO wtf??
statelessType = StatelessEntityType.webLink;
entityId = "custom.custom";
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
}
void update(Map rawData, String webHost) {
attributes = rawData["attributes"] ?? {};
domain = rawData["entity_id"].split(".")[0];
domain = rawData["entity_id"] != null ? rawData["entity_id"].split(".")[0] : null;
entityId = rawData["entity_id"];
deviceClass = attributes["device_class"];
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["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);
}
@ -211,14 +222,6 @@ class Entity {
);
}
Widget buildBadgeWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: BadgeWidget(),
handleTap: true,
);
}
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"].toString();
@ -227,11 +230,11 @@ class Entity {
}
String _getLastUpdatedFormatted() {
if (_lastUpdated == null) {
if (lastUpdatedTimestamp == null) {
return "-";
} else {
DateTime now = DateTime.now();
Duration d = now.difference(_lastUpdated);
Duration d = now.difference(lastUpdatedTimestamp);
String text;
int v;
if (d.inDays == 0) {

View File

@ -3,12 +3,18 @@ part of '../main.dart';
class EntityIcon extends StatelessWidget {
final EdgeInsetsGeometry padding;
final EdgeInsetsGeometry iconPadding;
final EdgeInsetsGeometry imagePadding;
final double size;
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) {
if (entityId == null) {
return 0;
}
String domain = entityId.split(".")[0];
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
String iconNameByDeviceClass;
@ -23,52 +29,110 @@ class EntityIcon extends StatelessWidget {
}
}
Widget buildIcon(EntityWrapper data, Color color) {
if (data == null) {
return null;
@override
Widget build(BuildContext context) {
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) {
return Container(
height: size+12,
width: size+12,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit:BoxFit.cover,
image: CachedNetworkImageProvider(
"${data.entityPicture}"
),
)
),
Widget iconWidget;
bool isPicture = false;
if (entityWrapper == null) {
iconWidget = Container(
width: size,
height: size,
);
} else {
if (entityWrapper.entityPicture != null) {
iconWidget = ClipOval(
child: CachedNetworkImage(
imageUrl: '${entityWrapper.entityPicture}',
width: size+12,
fit: BoxFit.cover,
height: size+12,
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;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
}
return Padding(
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
)
);
}
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: buildIcon(
entityWrapper,
color ?? HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context)
),
);
if (showBadge && entityWrapper.entity is LightEntity &&
(entityWrapper.entity as LightEntity).supportColor &&
(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'),
size: size,
color: iconColor,
),
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;
}
}

View File

@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
final bool handleTap;
static EntityModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(EntityModel);
return context.dependOnInheritedWidgetOfExactType<EntityModel>();
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
bool updateShouldNotify(EntityModel oldWidget) {
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
}
}

View File

@ -16,7 +16,7 @@ class EntityName extends StatelessWidget {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
TextStyle tStyle;
if (textStyle == null) {
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) {
tStyle = HAClientTheme().getLinkTextStyle(context);
} else {
tStyle = Theme.of(context).textTheme.body1;

View File

@ -2,10 +2,9 @@ part of '../main.dart';
class EntityPageLayout extends StatelessWidget {
final bool showClose;
final Entity entity;
EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key);
EntityPageLayout({Key key, this.entity}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -14,34 +13,6 @@ class EntityPageLayout extends StatelessWidget {
child: ListView(
padding: EdgeInsets.all(0),
children: <Widget>[
showClose ?
Container(
color: Theme.of(context).primaryColor,
height: 40,
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8),
child: Text(
entity.displayName,
style: Theme.of(context).primaryTextTheme.headline
),
),
),
IconButton(
padding: EdgeInsets.all(0),
icon: Icon(Icons.close),
color: Theme.of(context).primaryTextTheme.headline.color,
iconSize: 36.0,
onPressed: () {
eventBus.fire(ShowEntityPageEvent());
},
)
],
),
) :
Container(height: 0, width: 0,),
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),

View File

@ -3,7 +3,8 @@ part of '../main.dart';
class EntityWrapper {
String overrideName;
final String overrideIcon;
String overrideIcon;
final bool stateColor;
EntityUIAction uiAction;
Entity entity;
String unitOfMeasurementOverride;
@ -18,10 +19,11 @@ class EntityWrapper {
this.entity,
this.overrideIcon,
this.overrideName,
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) {
if (uiAction == null) {
uiAction = EntityUIAction();
}
@ -52,16 +54,16 @@ class EntityWrapper {
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
new ShowEntityPageEvent(entityId: entity.entityId));
break;
}
case EntityUIAction.navigate: {
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.tapService}');
} else {
Launcher.launchURL(uiAction.tapService);
Launcher.launchURLInBrowser(uiAction.tapService);
}
break;
}
@ -92,16 +94,16 @@ class EntityWrapper {
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
new ShowEntityPageEvent(entityId: entity.entityId));
break;
}
case EntityUIAction.navigate: {
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.holdService}');
} else {
Launcher.launchURL(uiAction.holdService);
Launcher.launchURLInBrowser(uiAction.holdService);
}
break;
}
@ -132,16 +134,16 @@ class EntityWrapper {
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
new ShowEntityPageEvent(entityId: entity.entityId));
break;
}
case EntityUIAction.navigate: {
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.doubleTapService}');
} else {
Launcher.launchURL(uiAction.doubleTapService);
Launcher.launchURLInBrowser(uiAction.doubleTapService);
}
break;
}
@ -152,4 +154,64 @@ 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"];
}
}
}
}
}

View 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,
);
}
}

View File

@ -134,8 +134,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpBrightness = value.round();
});
},
min: 1.0,
max: 255.0,
min: 1,
max: 255,
divisions: 254,
label: '${val?.toInt() ?? ''}',
onChangeEnd: (value) => _setBrightness(entity, value),
value: val,
leading: Icon(Icons.brightness_5),
@ -155,10 +157,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpWhiteValue = value.round();
});
},
min: 0.0,
max: 255.0,
min: 0,
max: 255,
divisions: 255,
label: '$_tmpWhiteValue',
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")),
title: "White",
);
@ -188,6 +192,8 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds,
min: entity.minMireds,
divisions: (entity.maxMireds - entity.minMireds).toInt(),
label: '$_tmpColorTemp',
onChanged: (value) {
setState(() {
_changedHere = true;

View File

@ -83,8 +83,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
],
),
Container(height: 10,),
Slider(
UniversalSlider(
min: 0,
activeColor: Theme.of(context).accentColor,
max: entity.durationSeconds.toDouble(),

View File

@ -228,7 +228,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"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) {

View File

@ -25,9 +25,9 @@ class SimpleEntityState extends StatelessWidget {
TextStyle tStyle;
if (textStyle != null) {
tStyle = textStyle;
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
tStyle = Theme.of(context).textTheme.subhead.copyWith(
color: Colors.blue
color: HAClientTheme().getLinkTextStyle(context).color
);
} else {
tStyle = Theme.of(context).textTheme.body1;

View File

@ -40,7 +40,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
} else {
_changedHere = false;
}
Widget slider = Slider(
Widget slider = UniversalSlider(
min: entity.minValue * _multiplier,
max: entity.maxValue * _multiplier,
value: (_newValue <= entity.maxValue) &&

View File

@ -26,11 +26,11 @@ class TimerEntity extends Entity {
seconds: int.tryParse(durationList[2]) ?? 0
);
} else {
Logger.e("Strange $entityId duration format: $durationSource");
Logger.e("Strange timer duration format: $durationSource");
duration = Duration(seconds: 0);
}
} catch (e) {
Logger.e("Error parsing duration for $entityId: ${e.toString()}");
} catch (e, stacktrace) {
Logger.e("Error parsing timer duration \'$durationSource\': $e", stacktrace: stacktrace);
duration = Duration(seconds: 0);
}
} else {

View File

@ -27,11 +27,11 @@ class _TimerStateState extends State<TimerState> {
try {
int passed = DateTime
.now()
.difference(entity._lastUpdated)
.difference(entity.lastUpdatedTimestamp)
.inSeconds;
remaining = Duration(seconds: entity.duration.inSeconds - passed);
} catch (e) {
Logger.e("Error calculating ${entity.entityId} remaining time: ${e.toString()}");
} catch (e, stacktrace) {
Logger.e("Error calculating ${entity.entityId} remaining time: $e", stacktrace: stacktrace);
remaining = Duration(seconds: 0);
}
});

View File

@ -1,55 +1,111 @@
part of '../main.dart';
class UniversalSlider extends StatelessWidget {
class UniversalSlider extends StatefulWidget {
final onChanged;
final onChangeEnd;
final Function onChanged;
final Function onChangeEnd;
final Function onChangeStart;
final Widget leading;
final Widget closing;
final String title;
final double min;
final Color activeColor;
final double max;
final double value;
final int divisions;
final String label;
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
Widget build(BuildContext context) {
List <Widget> row = [];
if (leading != null) {
row.add(leading);
List <Widget> col = [];
if (!_changedHere) {
_value = widget.value;
} else {
_changedHere = false;
}
if (widget.leading != null) {
row.add(widget.leading);
}
row.add(
Flexible(
child: Slider(
value: value,
min: min,
max: max,
onChanged: (value) => onChanged(value),
onChangeEnd: (value) => onChangeEnd(value),
),
)
Flexible(
child: Slider(
value: _value ?? math.max(widget.max ?? 100, _value ?? 0),
min: widget.min ?? 0,
max: widget.max ?? 100,
activeColor: widget.activeColor,
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) {
row.add(closing);
if (widget.closing != null) {
row.add(widget.closing);
}
if (widget.title != null) {
col.addAll(<Widget>[
Container(height: Sizes.rowPadding,),
Text('${widget.title}'),
]);
}
col.addAll(<Widget>[
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
Container(height: Sizes.rowPadding,)
]);
return Padding(
padding: padding,
padding: widget.padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Sizes.rowPadding,),
Text("$title"),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
Container(height: Sizes.rowPadding,)
],
children: col,
),
);
}
}

View File

@ -14,7 +14,7 @@ class HomeAssistant {
HomeAssistantUI ui;
Map _instanceConfig = {};
String _userName;
String _lovelaceDashbordUrl;
String currentDashboardPath;
HSVColor savedColor;
int savedPlayerPosition;
String sendToPlayerId;
@ -22,15 +22,11 @@ class HomeAssistant {
Map services;
bool autoUi = false;
String fcmToken;
Map _rawLovelaceData;
var _rawStates;
var _rawUserInfo;
var _rawPanels;
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
List<Panel> panels = [];
Duration fetchTimeout = Duration(seconds: 30);
@ -42,11 +38,10 @@ class HomeAssistant {
return _instanceConfig["location_name"] ?? "Home";
}
}
String get userName => _userName ?? locationName;
String get userName => _userName ?? '';
String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty;
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
HomeAssistant._internal() {
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
@ -64,7 +59,7 @@ class HomeAssistant {
_fetchCompleter = Completer();
List<Future> futures = [];
if (!uiOnly) {
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost);
futures.add(_getStates(null));
futures.add(_getConfig(null));
futures.add(_getUserInfo(null));
@ -75,12 +70,11 @@ class HomeAssistant {
futures.add(_getLovelace(null));
}
Future.wait(futures).then((_) {
if (isMobileAppEnabled) {
if (isComponentEnabled('mobile_app')) {
_createUI();
_fetchCompleter.complete();
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
} else {
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
_fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
}
}).catchError((e) {
_fetchCompleter.completeError(e);
@ -93,7 +87,7 @@ class HomeAssistant {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool cached = prefs.getBool('cached');
if (cached != null && cached) {
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost);
try {
_getStates(prefs);
if (!autoUi) {
@ -103,7 +97,7 @@ class HomeAssistant {
_getUserInfo(prefs);
_getPanels(prefs);
_getServices(prefs);
if (isMobileAppEnabled) {
if (isComponentEnabled('mobile_app')) {
_createUI();
}
} catch (e) {
@ -123,9 +117,9 @@ class HomeAssistant {
await prefs.setString('cached_panels', json.encode(_rawPanels));
await prefs.setString('cached_services', json.encode(services));
await prefs.setBool('cached', true);
} catch (e) {
} catch (e, stacktrace) {
await prefs.setBool('cached', false);
Logger.e('Error saving cache: $e');
Logger.e('Error saving cache: $e', stacktrace: stacktrace);
}
Logger.d('Done saving cache');
}
@ -140,37 +134,40 @@ class HomeAssistant {
}
Future _getConfig(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) {
_instanceConfig?.clear();
if (sharedPrefs != null && sharedPrefs.containsKey('cached_config')) {
try {
var data = json.decode(sharedPrefs.getString('cached_config'));
_parseConfig(data);
} catch (e) {
throw HAError("Error getting config: $e");
} catch (e, stacktrace) {
Logger.e('Error gettong config from cache: $e', stacktrace: stacktrace);
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
throw HAError("Error getting config: $e");
Logger.e('get_config error: $e');
throw HACException("Error getting config: $e");
});
}
}
void _parseConfig(data) {
_instanceConfig = Map.from(data);
_instanceConfig = data;
}
Future _getStates(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) {
try {
var data = json.decode(sharedPrefs.getString('cached_states'));
_parseStates(data);
} catch (e) {
throw HAError("Error getting states: $e");
_parseStates(data ?? []);
} catch (e, stacktrace) {
Logger.e('Error getting cached states: $e', stacktrace: stacktrace);
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => _parseStates(data)
).catchError((e) {
throw HAError("Error getting states: $e");
Logger.e('get_states error: $e');
throw HACException("Error getting states: $e");
});
}
}
@ -181,7 +178,7 @@ class HomeAssistant {
}
Future _getLovelace(SharedPreferences sharedPrefs) {
if (sharedPrefs != null) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_lovelace')) {
try {
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
_rawLovelaceData = data;
@ -192,9 +189,9 @@ class HomeAssistant {
} else {
Completer completer = Completer();
var additionalData;
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
if (AppSettings().haVersion >= 107 && currentDashboardPath != HomeAssistant.DEFAULT_DASHBOARD) {
additionalData = {
'url_path': _lovelaceDashbordUrl
'url_path': currentDashboardPath
};
}
ConnectionManager().sendSocketMessage(
@ -204,29 +201,31 @@ class HomeAssistant {
_rawLovelaceData = data;
completer.complete();
}).catchError((e) {
if ("$e" == "config_not_found") {
if ("$e".contains("config_not_found")) {
autoUi = true;
_rawLovelaceData = null;
completer.complete();
} else {
completer.completeError(HAError("Error getting lovelace config: $e"));
Logger.e('lovelace/config error: $e');
completer.completeError(HACException("Error getting lovelace config: $e"));
}
});
return completer.future;
}
}
Future _getServices(SharedPreferences prefs) async {
if (prefs != null) {
Future _getServices(SharedPreferences prefs) async {
services?.clear();
if (prefs != null && prefs.containsKey('cached_services')) {
try {
var data = json.decode(prefs.getString('cached_services'));
_parseServices(data);
} catch (e) {
Logger.w("Can't get services: $e");
_parseServices(data ?? {});
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
}
}
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
Logger.w("Can't get services: $e");
Logger.e('get_services error: $e');
});
}
@ -236,9 +235,19 @@ class HomeAssistant {
Future _getUserInfo(SharedPreferences sharedPrefs) async {
_userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
Logger.w("Can't get user info: $e");
});
if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) {
try {
var data = json.decode(sharedPrefs.getString('cached_user'));
_parseUserInfo(data ?? {});
} catch (e, stacktrace) {
Logger.e('Error getting cached user info: $e', stacktrace: stacktrace);
}
return Future.value();
} else {
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
Logger.e('auth/current_user error: $e');
});
}
}
void _parseUserInfo(data) {
@ -247,23 +256,26 @@ class HomeAssistant {
}
Future _getPanels(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) {
if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) {
try {
var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data);
} catch (e) {
throw HAError("Error getting panels list: $e");
_parsePanels(data ?? {});
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
panels.clear();
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e) {
throw HAError("Error getting panels list: $e");
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e, stacktrace) {
Logger.e('get_panels error: $e', stacktrace: stacktrace);
panels.clear();
throw HACException('Can\'t get panels: $e');
});
}
}
void _parsePanels(data) {
_rawPanels = data;
panels.clear();
_rawPanels = data;
List<Panel> dashboards = [];
data.forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
@ -305,6 +317,10 @@ class HomeAssistant {
return completer.future;
}
bool isComponentEnabled(String name) {
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name");
}
void _handleLovelaceUpdate() {
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
eventBus.fire(new LovelaceChangedEvent());
@ -312,7 +328,6 @@ class HomeAssistant {
}
void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}");
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent(
@ -331,57 +346,5 @@ class HomeAssistant {
void _createUI() {
Logger.d("Creating Lovelace UI");
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
/*if (isServiceExist('zha_map')) {
panels.add(
Panel(
id: 'haclient_zha',
componentName: 'haclient_zha',
title: 'ZHA',
urlPath: '/haclient_zha',
icon: 'mdi:zigbee'
)
);
}*/
}
}
/*
class SendMessageQueue {
int _messageTimeout;
List<HAMessage> _queue = [];
SendMessageQueue(this._messageTimeout);
void add(String message) {
_queue.add(HAMessage(_messageTimeout, message));
}
List<String> getActualMessages() {
_queue.removeWhere((item) => item.isExpired());
List<String> result = [];
_queue.forEach((haMessage){
result.add(haMessage.message);
});
this.clear();
return result;
}
void clear() {
_queue.clear();
}
}
class HAMessage {
DateTime _timeStamp;
int _messageTimeout;
String message;
HAMessage(this._messageTimeout, this.message) {
_timeStamp = DateTime.now();
}
bool isExpired() {
return DateTime.now().difference(_timeStamp).inSeconds > _messageTimeout;
}
}*/

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
@ -13,15 +14,12 @@ import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/circular_slider/single_circular_slider.dart';
import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
import 'package:workmanager/workmanager.dart' as workManager;
@ -59,12 +57,10 @@ part 'entities/fan/fan_entity.class.dart';
part 'entities/automation/automation_entity.class.dart';
part 'entities/camera/camera_entity.class.dart';
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
part 'entities/badge.widget.dart';
part 'entities/entity_model.widget.dart';
part 'entities/default_entity_container.widget.dart';
part 'entities/missed_entity.widget.dart';
part 'cards/widgets/glance_card_entity_container.dart';
part 'cards/widgets/entity_button_card_body.widget.dart';
part 'cards/entity_button_card.dart';
part 'pages/widgets/entity_attributes_list.dart';
part 'entities/entity_icon.widget.dart';
part 'entities/entity_name.widget.dart';
@ -102,10 +98,12 @@ part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.d
part 'entities/vacuum/vacuum_entity.class.dart';
part 'entities/vacuum/widgets/vacuum_controls.dart';
part 'entities/vacuum/widgets/vacuum_state_button.dart';
part 'entities/error_entity_widget.dart';
part 'pages/settings/connection_settings.part.dart';
part 'pages/purchase.page.dart';
part 'pages/widgets/product_purchase.widget.dart';
part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/bottom_info_bar.dart';
part 'pages/widgets/page_loading_error.dart';
part 'pages/panel.page.dart';
part 'pages/main/main.page.dart';
@ -113,8 +111,8 @@ part 'pages/settings/integration_settings.part.dart';
part 'pages/settings/app_settings.page.dart';
part 'pages/settings/lookandfeel_settings.part.dart';
part 'pages/zha_page.dart';
part 'pages/quick_start.page.dart';
part 'home_assistant.class.dart';
part 'pages/log.page.dart';
part 'pages/entity.page.dart';
part 'utils/mdi.class.dart';
part 'entity_collection.class.dart';
@ -130,33 +128,44 @@ part 'view.class.dart';
part 'cards/card.class.dart';
part 'panels/panel_class.dart';
part 'viewWidget.widget.dart';
part 'cards/card_widget.dart';
part 'cards/widgets/card_header.widget.dart';
part 'panels/config_panel_widget.dart';
part 'panels/widgets/link_to_web_config.dart';
part 'types/ha_error.dart';
part 'types/event_bus_events.dart';
part 'cards/widgets/gauge_card_body.dart';
part 'cards/widgets/light_card_body.dart';
part 'cards/gauge_card.dart';
part 'cards/widgets/card_wrapper.widget.dart';
part 'cards/entities_card.dart';
part 'cards/alarm_panel_card.dart';
part 'cards/horizontal_srack_card.dart';
part 'cards/markdown_card.dart';
part 'cards/media_control_card.dart';
part 'cards/unsupported_card.dart';
part 'cards/light_card.dart';
part 'cards/error_card.dart';
part 'cards/vertical_stack_card.dart';
part 'cards/glance_card.dart';
part 'pages/play_media.page.dart';
part 'entities/entity_page_layout.widget.dart';
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
part 'pages/whats_new.page.dart';
part 'pages/fullscreen.page.dart';
part 'popups.dart';
part 'cards/badges.dart';
part 'managers/app_settings.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersionNumber = "0.8.4";
const appVersionAdd = "";
const appVersion = "$appVersionNumber$appVersionAdd";
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
//FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = 'HA Client';
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0-b2.md';
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console.
if (Logger.isInDebugMode) {
Logger.e('Caught error: $error');
Logger.e('Caught error: $error', skipCrashlytics: true);
Logger.p(stackTrace);
}
Crashlytics.instance.recordError(error, stackTrace);
@ -168,7 +177,7 @@ void main() async {
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
FlutterError.onError = (FlutterErrorDetails details) {
Logger.e("Caut Flutter runtime error: ${details.exception}");
Logger.e("Caut Flutter runtime error: ${details.exception}", skipCrashlytics: true);
if (Logger.isInDebugMode) {
FlutterError.dumpErrorToConsole(details);
}
@ -176,12 +185,17 @@ void main() async {
};
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
await AppSettings().loadStartupSettings();
await Hive.initFlutter();
if (AppSettings().displayMode == DisplayMode.fullscreen) {
SystemChrome.setEnabledSystemUIOverlays([]);
} else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
runZoned(() {
runApp(new HAClientApp(
theme: theme,
theme: AppSettings().appTheme,
));
}, onError: (error, stack) {
_reportError(error, stack);
@ -228,10 +242,12 @@ class _HAClientAppState extends State<HAClientApp> {
void _handlePurchaseUpdates(purchase) {
if (purchase is List<PurchaseDetails>) {
if (purchase[0].status == PurchaseStatus.purchased) {
eventBus.fire(ShowPopupMessageEvent(
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Thanks a lot!",
body: "Thank you for supporting HA Client development!",
buttonText: "Ok"
positiveText: "Ok"
)
));
} else {
Logger.d("Purchase change handler: ${purchase[0].status}");
@ -241,7 +257,6 @@ class _HAClientAppState extends State<HAClientApp> {
}
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
@ -260,7 +275,6 @@ class _HAClientAppState extends State<HAClientApp> {
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
),
"/log-view": (context) => LogViewPage(title: "Log"),
"/webview": (context) => standaloneWebview.WebviewScaffold(
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
appBar: new AppBar(
@ -272,22 +286,26 @@ class _HAClientAppState extends State<HAClientApp> {
),
),
"/whats-new": (context) => WhatsNewPage(),
"/quick-start": (context) => QuickStartPage(),
"/haclient_zha": (context) => ZhaPage(),
"/auth": (context) => new standaloneWebview.WebviewScaffold(
url: "${ConnectionManager().oauthUrl}",
url: "${AppSettings().oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#authentication")
icon: Icon(Icons.help),
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "https://ha-client.app/help/connection")
),
title: new Text("Login with HA"),
title: new Text("Login"),
actions: <Widget>[
FlatButton(
child: Text("Manual", style: Theme.of(context).textTheme.button.copyWith(
child: Text("Long-lived token", style: Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
)),
onPressed: () {
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
eventBus.fire(ShowPopupEvent(
goBackFirst: true,
popup: TokenLoginPopup()
));
},
)
],

View File

@ -0,0 +1,125 @@
part of '../main.dart';
enum DisplayMode {normal, fullscreen}
class AppSettings {
static const DEFAULT_HIVE_BOX = 'defaultSettingsBox';
static const AUTH_TOKEN_KEY = 'llt';
static final AppSettings _instance = AppSettings._internal();
factory AppSettings() {
return _instance;
}
AppSettings._internal();
String mobileAppDeviceName;
String _domain;
String _port;
String displayHostname;
String webSocketAPIEndpoint;
String httpWebHost;
String longLivedToken;
String tempToken;
String oauthUrl;
String webhookId;
double haVersion;
bool scrollBadges;
DisplayMode displayMode;
AppTheme appTheme;
final int defaultLocationUpdateIntervalMinutes = 20;
Duration locationUpdateInterval;
bool locationTrackingEnabled = false;
bool get isAuthenticated => longLivedToken != null;
bool get isTempAuthenticated => tempToken != null;
loadStartupSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
appTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
}
Future load(bool full) async {
if (full) {
await Hive.openBox(DEFAULT_HIVE_BOX);
Logger.d('Loading settings...');
SharedPreferences prefs = await SharedPreferences.getInstance();
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
mobileAppDeviceName = prefs.getString('app-integration-device-name');
scrollBadges = prefs.getBool('scroll-badges') ?? true;
displayHostname = "$_domain:$_port";
webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
defaultLocationUpdateIntervalMinutes);
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent(
'https://ha-client.app/service/auth_callback.html')}";
}
}
Future<dynamic> loadSingle(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.get('$key');
}
Future save(Map<String, dynamic> settings) async {
if (settings != null && settings.isNotEmpty) {
SharedPreferences prefs = await SharedPreferences.getInstance();
settings.forEach((k,v) async {
if (v is String) {
await prefs.setString(k, v);
} else if (v is bool) {
await prefs.setBool(k ,v);
} else if (v is int) {
await prefs.setInt(k ,v);
} else if (v is double) {
await prefs.setDouble(k, v);
} else {
Logger.e('Unknown setting type: <$k, $v>');
}
});
}
}
Future startAuth() {
return AuthManager().start(
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
tempToken = token;
});
}
Future clearTokens() async {
longLivedToken = null;
tempToken = null;
Hive.box(DEFAULT_HIVE_BOX).delete(AUTH_TOKEN_KEY);
}
void saveLongLivedToken(token) {
longLivedToken = token;
tempToken = null;
Hive.box(DEFAULT_HIVE_BOX).put(AUTH_TOKEN_KEY, longLivedToken);
}
bool isNotConfigured() {
return _domain == null && _port == null && webhookId == null && mobileAppDeviceName == null;
}
bool isSomethingMissed() {
return (_domain == null) || (_port == null) || (_domain.isEmpty) || (_port.isEmpty);
}
}

View File

@ -31,9 +31,9 @@ class AuthManager {
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.complete(tempToken);
}).catchError((e) {
Logger.e("Error getting temp token: ${e.toString()}");
Logger.e("Error getting temp token: $e");
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.completeError(HAError("Error getting temp token"));
completer.completeError(HACException("Error getting temp token"));
}).whenComplete(() => flutterWebviewPlugin.close());
}
});

View File

@ -10,22 +10,11 @@ class ConnectionManager {
ConnectionManager._internal();
String _domain;
String _port;
String displayHostname;
String _webSocketAPIEndpoint;
String httpWebHost;
String _token;
String _tempToken;
String oauthUrl;
String webhookId;
bool settingsLoaded = false;
int appIntegrationVersion;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
bool isConnected = false;
bool settingsLoaded = false;
var onStateChangeCallback;
var onLovelaceUpdatedCallback;
@ -35,64 +24,27 @@ class ConnectionManager {
int _currentMessageId = 0;
Map<String, Completer> _messageResolver = {};
Future init({bool loadSettings, bool forceReconnect: false}) async {
Future init({bool loadSettings, bool forceReconnect: false}) {
Completer completer = Completer();
bool stopInit = false;
if (loadSettings) {
Logger.d("Loading settings...");
SharedPreferences prefs = await SharedPreferences.getInstance();
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
displayHostname = "$_domain:$_port";
_webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
httpWebHost =
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
} else {
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
Logger.e("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent(
'https://ha-client.app/service/auth_callback.html')}";
settingsLoaded = true;
} catch (e) {
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
Logger.e("Cannt read secure storage. Need to relogin.");
stopInit = true;
}
}
} else {
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
}
}
if (!stopInit) {
if (_token == null) {
AuthManager().start(
oauthUrl: oauthUrl
).then((token) {
Logger.d("Token from AuthManager recived");
_tempToken = token;
AppSettings().load(loadSettings).then((_) {
Logger.d('Checking config...');
if (AppSettings().isNotConfigured()) {
Logger.d('This is first start');
completer.completeError(HACNotSetUpException());
} else if (AppSettings().isSomethingMissed()) {
completer.completeError(HACException.checkConnectionSettings());
} else if (!AppSettings().isAuthenticated) {
settingsLoaded = true;
AppSettings().startAuth().then((_) {
_doConnect(completer: completer, forceReconnect: forceReconnect);
}).catchError((e) {
completer.completeError(e);
});
} else {
settingsLoaded = true;
_doConnect(completer: completer, forceReconnect: forceReconnect);
}
}
}
});
return completer.future;
}
@ -106,13 +58,13 @@ class ConnectionManager {
_disconnect().then((_) {
if (e is TimeoutException) {
if (connecting != null && !connecting.isCompleted) {
connecting.completeError(HAError("Connection timeout"));
connecting.completeError(HACException("Connection timeout"));
}
completer?.completeError(HAError("Connection timeout"));
} else if (e is HAError) {
completer?.completeError(HACException("Connection timeout"));
} else if (e is HACException) {
completer?.completeError(e);
} else {
completer?.completeError(HAError("${e.toString()}"));
completer?.completeError(HACException("${e.toString()}"));
}
});
});
@ -134,7 +86,7 @@ class ConnectionManager {
Logger.d("Socket connecting...");
try {
_socket = IOWebSocketChannel.connect(
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
AppSettings().webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
_socketSubscription = _socket.stream.listen(
(message) {
isConnected = true;
@ -148,7 +100,12 @@ class ConnectionManager {
if (!connecting.isCompleted) connecting.completeError(e);
});
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
String v = data["ha_version"];
if (v != null && v.isNotEmpty) {
AppSettings().haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
}
Logger.d("Home assistant version: $v (${AppSettings().haVersion})");
Crashlytics.instance.setString('ha_version', v);
Logger.d("[Connection] Subscribing to events");
sendSocketMessage(
type: "subscribe_events",
@ -160,15 +117,15 @@ class ConnectionManager {
).whenComplete((){
_messageResolver["auth"]?.complete();
_messageResolver.remove("auth");
if (_token != null) {
if (AppSettings().isAuthenticated) {
if (!connecting.isCompleted) connecting.complete();
}
});
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
_messageResolver["auth"]?.completeError(HACException("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
_messageResolver.remove("auth");
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
if (!connecting.isCompleted) connecting.completeError(HACException("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
} else {
_handleMessage(data);
}
@ -178,7 +135,7 @@ class ConnectionManager {
onError: (e) => _handleSocketError(e, connecting)
);
} catch(exeption) {
connecting.completeError(HAError("${exeption.toString()}"));
connecting.completeError(HACException("${exeption.toString()}"));
}
});
return connecting.future;
@ -211,8 +168,8 @@ class ConnectionManager {
//Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}");
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}", skipCrashlytics: true);
_messageResolver["${data["id"]}"]?.completeError("${data["error"]["code"]}: ${data["error"]["message"]}");
}
_messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") {
@ -235,40 +192,40 @@ class ConnectionManager {
_disconnect().then((_) {
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
connectionCompleter.completeError(HACException("Disconnected", actions: [HAErrorAction.reconnect()]));
}
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
eventBus.fire(ShowErrorEvent(HACException("Unable to connect to Home Assistant")));
});
}
void _handleSocketError(e, Completer connectionCompleter) {
Logger.e("Socket stream Error: $e");
Logger.e("Socket stream Error: $e", skipCrashlytics: true);
_disconnect().then((_) {
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
connectionCompleter.completeError(HACException("Disconnected", actions: [HAErrorAction.reconnect()]));
}
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
eventBus.fire(ShowErrorEvent(HACException("Unable to connect to Home Assistant")));
});
}
Future _authenticate() {
Completer completer = Completer();
if (_token != null) {
if (AppSettings().isAuthenticated) {
Logger.d( "Long-lived token exist");
Logger.d( "[Sending] ==> auth request");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_token"},
additionalData: {"access_token": "${AppSettings().longLivedToken}"},
auth: true
).then((_) {
completer.complete();
}).catchError((e) => completer.completeError(e));
} else if (_tempToken != null) {
} else if (AppSettings().isTempAuthenticated != null) {
Logger.d("We have temp token. Loging in...");
sendSocketMessage(
type: "auth",
additionalData: {"access_token": "$_tempToken"},
additionalData: {"access_token": "${AppSettings().tempToken}"},
auth: true
).then((_) {
Logger.d("Requesting long-lived token...");
@ -281,42 +238,25 @@ class ConnectionManager {
});
}).catchError((e) => completer.completeError(e));
} else {
completer.completeError(HAError("General login error"));
completer.completeError(HACException("General login error"));
}
return completer.future;
}
Future logout() {
Future logout() async {
Logger.d("Logging out");
Completer completer = Completer();
_disconnect().whenComplete(() {
_token = null;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.delete(key: "hacl_llt").whenComplete((){
completer.complete();
});
});
return completer.future;
await _disconnect();
await AppSettings().clearTokens();
}
Future _getLongLivedToken() {
Completer completer = Completer();
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
_token = data;
_tempToken = null;
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: "$_token").then((_) {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool("oauth-used", true);
completer.complete();
});
}).catchError((e) {
throw e;
});
AppSettings().saveLongLivedToken(data);
completer.complete();
}).catchError((e) {
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
});
return completer.future;
}
@ -343,7 +283,7 @@ class ConnectionManager {
_socket.sink.add(rawMessage);
}).catchError((e) {
if (!_completer.isCompleted) {
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
_completer.completeError(HACException("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}
});
} else {
@ -372,12 +312,12 @@ class ConnectionManager {
sendHTTPPost(
endPoint: "/api/services/$domain/$service",
data: json.encode(serviceData)
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HACException(e.toString())));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else
sendHTTPPost(
endPoint: "/api/services/$domain/$service"
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError(e.toString())));
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HACException(e.toString())));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
return completer.future;
}
@ -386,11 +326,11 @@ class ConnectionManager {
DateTime now = DateTime.now();
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
String url = "${AppSettings().httpWebHost}/api/history/period/$startTime?&filter_entity_id=$entityId";
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
http.Response historyResponse;
historyResponse = await http.get(url, headers: {
"authorization": "Bearer $_token",
"authorization": "Bearer ${AppSettings().longLivedToken}",
"Content-Type": "application/json"
});
var history = json.decode(historyResponse.body);
@ -404,14 +344,14 @@ class ConnectionManager {
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
Completer completer = Completer();
String url = "$httpWebHost$endPoint";
String url = "${AppSettings().httpWebHost}$endPoint";
Logger.d("[Sending] ==> HTTP $endPoint");
Map<String, String> headers = {};
if (contentType != null) {
headers["Content-Type"] = contentType;
}
if (includeAuthHeader) {
headers["authorization"] = "Bearer $_token";
headers["authorization"] = "Bearer ${AppSettings().longLivedToken}";
}
http.post(
url,

View File

@ -13,68 +13,53 @@ class LocationManager {
init();
}
final int defaultUpdateIntervalMinutes = 20;
final String backgroundTaskId = "haclocationtask0";
final String backgroundTaskTag = "haclocation";
Duration _updateInterval;
bool _isRunning;
void init() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
_updateInterval = Duration(minutes: prefs.getInt("location-interval") ??
defaultUpdateIntervalMinutes);
_isRunning = prefs.getBool("location-enabled") ?? false;
if (_isRunning) {
if (AppSettings().locationTrackingEnabled) {
await _startLocationService();
}
}
setSettings(bool enabled, int interval) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (interval != _updateInterval.inMinutes) {
prefs.setInt("location-interval", interval);
_updateInterval = Duration(minutes: interval);
if (_isRunning) {
Logger.d("Stopping location tracking...");
_isRunning = false;
await _stopLocationService();
}
if (interval != AppSettings().locationUpdateInterval.inMinutes) {
await _stopLocationService();
}
if (enabled && !_isRunning) {
AppSettings().save({
'location-interval': interval,
'location-enabled': enabled
});
AppSettings().locationUpdateInterval = Duration(minutes: interval);
AppSettings().locationTrackingEnabled = enabled;
if (enabled && !AppSettings().locationTrackingEnabled) {
Logger.d("Starting location tracking");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("location-enabled", enabled);
_isRunning = true;
await _startLocationService();
} else if (!enabled && _isRunning) {
} else if (!enabled && AppSettings().locationTrackingEnabled) {
Logger.d("Stopping location tracking...");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("location-enabled", enabled);
_isRunning = false;
await _stopLocationService();
}
}
_startLocationService() async {
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
String webhookId = AppSettings().webhookId;
String httpWebHost = AppSettings().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
Duration interval;
int delayFactor;
int taskCount;
Logger.d("Starting location update for every ${_updateInterval
Logger.d("Starting location update for every ${AppSettings().locationUpdateInterval
.inMinutes} minutes...");
if (_updateInterval.inMinutes == 10) {
if (AppSettings().locationUpdateInterval.inMinutes == 10) {
interval = Duration(minutes: 20);
taskCount = 2;
delayFactor = 10;
} else if (_updateInterval.inMinutes == 5) {
} else if (AppSettings().locationUpdateInterval.inMinutes == 5) {
interval = Duration(minutes: 15);
taskCount = 3;
delayFactor = 5;
} else {
interval = _updateInterval;
interval = AppSettings().locationUpdateInterval;
taskCount = 1;
delayFactor = 0;
}
@ -108,40 +93,47 @@ class LocationManager {
}
updateDeviceLocation() async {
Logger.d("[Foreground location] Started");
Geolocator geolocator = Geolocator();
var battery = Battery();
String webhookId = ConnectionManager().webhookId;
String httpWebHost = ConnectionManager().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
Logger.d("[Foreground location] Getting battery level...");
int batteryLevel = await battery.batteryLevel;
Logger.d("[Foreground location] Getting device location...");
Position position = await geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
locationPermissionLevel: GeolocationPermission.locationAlways
);
if (position != null) {
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
String url = "$httpWebHost/api/webhook/$webhookId";
Map data = {
"type": "update_location",
"data": {
"gps": [position.latitude, position.longitude],
"gps_accuracy": position.accuracy,
"battery": batteryLevel ?? 100
try {
Logger.d("[Foreground location] Started");
Geolocator geolocator = Geolocator();
var battery = Battery();
String webhookId = AppSettings().webhookId;
String httpWebHost = AppSettings().httpWebHost;
if (webhookId != null && webhookId.isNotEmpty) {
Logger.d("[Foreground location] Getting battery level...");
int batteryLevel = await battery.batteryLevel;
Logger.d("[Foreground location] Getting device location...");
Position position = await geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
locationPermissionLevel: GeolocationPermission.locationAlways
);
if (position != null) {
Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})");
String url = "$httpWebHost/api/webhook/$webhookId";
Map data = {
"type": "update_location",
"data": {
"gps": [position.latitude, position.longitude],
"gps_accuracy": position.accuracy,
"battery": batteryLevel ?? 100
}
};
Logger.d("[Foreground location] Sending data home...");
http.Response response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(data)
);
if (response.statusCode >= 300) {
Logger.e('Foreground location update error: ${response.body}');
}
};
Logger.d("[Foreground location] Sending data home...");
var response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(data)
);
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
} else {
Logger.d("[Foreground location] No location. Aborting.");
}
Logger.d("[Foreground location] Got HTTP ${response.statusCode}");
} else {
Logger.d("[Foreground location] No location. Aborting.");
}
}
} catch (e, stack) {
Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
}
}

View File

@ -2,8 +2,6 @@ part of '../main.dart';
class MobileAppIntegrationManager {
static const INTEGRATION_VERSION = 3;
static final _appRegistrationData = {
"device_name": "",
"app_version": "$appVersion",
@ -12,24 +10,53 @@ class MobileAppIntegrationManager {
"os_version": DeviceInfoManager().osVersion,
"app_data": {
"push_token": "",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
}
};
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
static String getDefaultDeviceName() {
if (HomeAssistant().userName.isEmpty) {
return '${DeviceInfoManager().model}';
}
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
}
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
static Future checkAppRegistration() async {
String fcmToken = await AppSettings().loadSingle('npush-token');
if (fcmToken != null) {
Logger.d("[MobileAppIntegrationManager] token exist");
await _doCheck(fcmToken);
} else {
Logger.d("[MobileAppIntegrationManager] no fcm token. Requesting...");
try {
fcmToken = await platform.invokeMethod('getFCMToken');
await _doCheck(fcmToken);
} on PlatformException catch (e) {
Logger.e('[MobileAppIntegrationManager] Failed to get FCM token from native: ${e.message}');
}
}
}
static Future _doCheck(String fcmToken) {
Completer completer = Completer();
_appRegistrationData["device_name"] = "${HomeAssistant().userName}'s ${DeviceInfoManager().model}";
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
if (ConnectionManager().webhookId == null || forceRegister) {
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "$fcmToken";
if (AppSettings().webhookId == null) {
Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}",
"app_id": "ha_client",
"app_name": "$appName",
"os_name": DeviceInfoManager().osName,
"supports_encryption": false,
});
if (AppSettings().haVersion >= 104) {
registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}"
});
}
ConnectionManager().sendHTTPPost(
endPoint: "/api/mobile_app/registrations",
includeAuthHeader: true,
@ -37,28 +64,32 @@ class MobileAppIntegrationManager {
).then((response) {
Logger.d("Processing registration responce...");
var responseObject = json.decode(response);
ConnectionManager().webhookId = responseObject["webhook_id"];
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
SharedPreferences.getInstance().then((prefs) {
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
AppSettings().webhookId = responseObject["webhook_id"];
AppSettings().save({
'app-webhook-id': responseObject["webhook_id"]
}).then((prefs) {
completer.complete();
eventBus.fire(ShowPopupDialogEvent(
title: "Mobile app Integration was created",
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
positiveText: "Restart now",
negativeText: "Later",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart");
},
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Mobile app Integration was created",
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
positiveText: "Restart now",
negativeText: "Later",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart");
},
)
));
});
}).catchError((e) {
completer.complete();
Logger.e("Error registering the app: ${e.toString()}");
if (e is http.Response) {
Logger.e("Error registering the app: ${e.statusCode}: ${e.body}");
} else {
Logger.e("Error registering the app: ${e?.toString()}");
}
_showError();
});
return completer.future;
} else {
Logger.d("App was previously registered. Checking...");
var updateData = {
@ -66,7 +97,7 @@ class MobileAppIntegrationManager {
"data": _appRegistrationData
};
ConnectionManager().sendHTTPPost(
endPoint: "/api/webhook/${ConnectionManager().webhookId}",
endPoint: "/api/webhook/${AppSettings().webhookId}",
includeAuthHeader: false,
data: json.encode(updateData)
).then((response) {
@ -77,75 +108,49 @@ class MobileAppIntegrationManager {
registrationData = null;
}
if (registrationData == null || registrationData.isEmpty) {
Logger.d("No registration data in response. MobileApp integration was removed or broken");
Logger.w("No registration data in response. MobileApp integration was removed or broken");
_askToRegisterApp();
} else {
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
Logger.d('App registration needs to be updated');
_askToRemoveAndRegisterApp();
} else {
Logger.d('App registration works fine');
if (showOkDialog) {
eventBus.fire(ShowPopupDialogEvent(
title: "All good",
body: "HA Client integration with your Home Assistant server works fine",
positiveText: "Nice!",
negativeText: "Ok"
));
}
}
Logger.d('App registration works fine');
}
completer.complete();
}).catchError((e) {
if (e is http.Response && e.statusCode == 410) {
Logger.e("MobileApp integration was removed");
Logger.w("MobileApp integration was removed");
_askToRegisterApp();
} else if (e is http.Response) {
Logger.w("Error updating app registration: ${e.statusCode}: ${e.body}");
_showError();
} else {
Logger.e("Error updating app registration: ${e.toString()}");
eventBus.fire(ShowPopupDialogEvent(
title: "App integration is not working properly",
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue.",
positiveText: "Report to GitHub",
negativeText: "Report to Discord",
onPositive: () {
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
},
onNegative: () {
Launcher.launchURL("https://discord.gg/AUzEvwn");
},
));
Logger.w("Error updating app registration: ${e?.toString()}");
_showError();
}
completer.complete();
});
return completer.future;
}
return completer.future;
}
static void _askToRemoveAndRegisterApp() {
eventBus.fire(ShowPopupDialogEvent(
title: "Mobile app integration needs to be updated",
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
positiveText: "Ok",
negativeText: "Report an issue",
onNegative: () {
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
},
static void _showError() {
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "App integration is not working properly",
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue. You can try to remove Mobile App integration from Home Assistant and restart server to fix this issue.",
positiveText: "Report issue",
negativeText: "Close",
onPositive: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
}
)
));
}
static void _askToRegisterApp() {
eventBus.fire(ShowPopupDialogEvent(
title: "App integration is broken",
body: "Looks like app integration was removed from your Home Assistant or it needs to be updated. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant before registering and restart Home Assistant. Then go back here.",
positiveText: "Register now",
negativeText: "Cancel",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.remove("app-webhook-id");
ConnectionManager().webhookId = null;
checkAppRegistration();
});
},
eventBus.fire(ShowPopupEvent(
popup: RegisterAppPopup(
title: "Mobile App integration is missing",
body: "Looks like mobile app integration was removed from your Home Assistant or it needs to be updated.",
)
));
}

View File

@ -11,48 +11,58 @@ class StartupUserMessagesManager {
StartupUserMessagesManager._internal();
bool _supportAppDevelopmentMessageShown;
bool _needToshowDonateMessage;
bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-888";
static final _donateMsgTimerKey = "user-msg-donate-timer";
static final _donateMsgShownKey = "user-msg-donate-shpown";
static final _whatsNewMessageKey = "user-msg-whats-new-url";
void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
var tInt = prefs.getInt(_donateMsgTimerKey);
if (tInt == null) {
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
_needToshowDonateMessage = false;
} else {
bool wasShown = prefs.getBool(_donateMsgShownKey) ?? false;
_needToshowDonateMessage = (Duration(milliseconds: DateTime.now().millisecondsSinceEpoch - tInt).inDays >= 14) && !wasShown;
}
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
if (!_whatsNewMessageShown) {
_showWhatsNewMessage();
} else if (!_supportAppDevelopmentMessageShown) {
} else if (_needToshowDonateMessage) {
_showSupportAppDevelopmentMessage();
}
}
void _showSupportAppDevelopmentMessage() {
eventBus.fire(ShowPopupDialogEvent(
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Hi!",
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
positiveText: "Show options",
negativeText: "Cancel",
negativeText: "Later",
onPositive: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_supportAppDevelopmentMessageKey, true);
prefs.setBool(_donateMsgShownKey, true);
eventBus.fire(ShowPageEvent(path: "/putchase"));
});
},
onNegative: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_supportAppDevelopmentMessageKey, true);
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
});
}
)
));
}
void _showWhatsNewMessage() {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_whatsNewMessageKey, true);
prefs.setString(_whatsNewMessageKey, whatsNewUrl);
eventBus.fire(ShowPageEvent(path: "/whats-new"));
});
}
}
}

View File

@ -19,7 +19,7 @@ class HAClientTheme {
fontWeight: FontWeight.normal,
letterSpacing: 1,
),
button: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
button: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
);
static const offEntityStates = [

View File

@ -35,7 +35,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
}
_getEntity() {
_entity = HomeAssistant().entities.get(widget.entityId);
_entity = HomeAssistant().entities?.get(widget.entityId);
}
@override

View File

@ -1,18 +1,37 @@
part of '../main.dart';
class FullScreenPage extends StatelessWidget {
class FullScreenPage extends StatefulWidget {
final Widget child;
const FullScreenPage({Key key, this.child}) : super(key: key);
@override
_FullScreenPageState createState() => _FullScreenPageState();
}
class _FullScreenPageState extends State<FullScreenPage> {
@override
void initState() {
SystemChrome.setEnabledSystemUIOverlays([]);
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Center(
child: this.child,
child: this.widget.child,
),
);
}
@override
void dispose() {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
super.dispose();
}
}

View File

@ -1,52 +0,0 @@
part of '../main.dart';
class LogViewPage extends StatefulWidget {
LogViewPage({Key key, this.title}) : super(key: key);
final String title;
@override
_LogViewPageState createState() => new _LogViewPageState();
}
class _LogViewPageState extends State<LogViewPage> {
String _logData;
@override
void initState() {
super.initState();
_loadLog();
}
_loadLog() async {
_logData = Logger.getLog();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text(widget.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.content_copy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: _logData));
},
)
],
),
body: TextField(
maxLines: null,
controller: TextEditingController(
text: _logData
),
)
);
}
}

View File

@ -18,10 +18,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
StreamSubscription _showEntityPageSubscription;
StreamSubscription _showErrorSubscription;
StreamSubscription _startAuthSubscription;
StreamSubscription _showPopupDialogSubscription;
StreamSubscription _showPopupMessageSubscription;
StreamSubscription _showPopupSubscription;
StreamSubscription _reloadUISubscription;
StreamSubscription _fullReloadSubscription;
StreamSubscription _showPageSubscription;
BottomInfoBarController _bottomInfoBarController;
bool _popupShown = false;
int _previousViewCount;
bool _showLoginButton = false;
bool _preventAppRefresh = false;
@ -31,32 +33,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
super.initState();
WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
return Future.value();
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
return Future.value();
}
);
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
var initializationSettingsAndroid =
new AndroidInitializationSettings('mini_icon');
var initializationSettingsIOS = new IOSInitializationSettings(
onDidReceiveLocalNotification: null);
var initializationSettings = new InitializationSettings(
initializationSettingsAndroid, initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
_bottomInfoBarController = BottomInfoBarController();
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}");
@ -69,46 +46,32 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_fullLoad();
}
Future onSelectNotification(String payload) async {
if (payload != null) {
Logger.d('Notification clicked: ' + payload);
}
}
Future _showNotification({String title, String text}) async {
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title ?? appName,
text,
platformChannelSpecifics
);
}
void _fullLoad() {
_showInfoBottomBar(progress: true,);
_bottomInfoBarController.showInfoBottomBar(progress: true,);
Logger.d('[loading] fullLoad');
_subscribe().then((_) {
Logger.d('[loading] subscribed');
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
Logger.d('[loading] COnnection manager initialized');
SharedPreferences.getInstance().then((prefs) {
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
_fetchData(useCache: true);
LocationManager();
StartupUserMessagesManager().checkMessagesToShow();
MobileAppIntegrationManager.checkAppRegistration();
});
}, onError: (e) {
_setErrorState(e);
if (e is HACNotSetUpException) {
Navigator.of(context).pushReplacementNamed('/quick-start');
} else {
_setErrorState(e);
}
});
});
}
void _quickLoad({bool uiOnly: false}) {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
_bottomInfoBarController.showInfoBottomBar(progress: true,);
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData(useCache: false, uiOnly: uiOnly);
}, onError: (e) {
@ -123,12 +86,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
});
}
await HomeAssistant().fetchData(uiOnly).then((_) {
_hideBottomBar();
setState((){
_bottomInfoBarController.hideBottomBar();
});
HomeAssistant().saveCache();
}).catchError((e) {
if (e is HAError) {
if (e is HACException) {
_setErrorState(e);
} else {
_setErrorState(HAError(e.toString()));
_setErrorState(HACException(e.toString()));
}
});
eventBus.fire(RefreshDataFinishedEvent());
@ -140,13 +106,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
_quickLoad();
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
HomeAssistant().saveCache();
//HomeAssistant().saveCache();
}
}
Future _subscribe() {
Completer completer = Completer();
Future _subscribe() async {
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
@ -167,27 +131,22 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_quickLoad(uiOnly: true);
});
}
if (_showPopupDialogSubscription == null) {
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
_showPopupDialog(
title: event.title,
body: event.body,
onPositive: event.onPositive,
onNegative: event.onNegative,
positiveText: event.positiveText,
negativeText: event.negativeText
);
if (_fullReloadSubscription == null) {
_fullReloadSubscription = eventBus.on<FullReloadEvent>().listen((event){
_fullLoad();
});
}
if (_showPopupMessageSubscription == null) {
_showPopupMessageSubscription = eventBus.on<ShowPopupMessageEvent>().listen((event){
_showPopupDialog(
title: event.title,
body: event.body,
onPositive: event.onButtonClick,
positiveText: event.buttonText,
negativeText: null
);
if (_showPopupSubscription == null) {
_showPopupSubscription = eventBus.on<ShowPopupEvent>().listen((event){
if (!_popupShown) {
_popupShown = true;
if (event.goBackFirst) {
Navigator.of(context).pop();
}
event.popup.show(context).then((_){
_popupShown = false;
});
}
});
}
if (_serviceCallSubscription == null) {
@ -200,7 +159,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (_showEntityPageSubscription == null) {
_showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity?.entityId);
Logger.d('Showing entity page event handled: ${event.entityId}');
_showEntityPage(event.entityId);
});
}
@ -213,7 +173,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(event.error);
_bottomInfoBarController.showErrorBottomBar(event.error);
});
}
@ -231,73 +191,36 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
});
}
_firebaseMessaging.getToken().then((String token) {
/*_firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token;
completer.complete();
});
return completer.future;
});*/
}
void _showOAuth() {
_preventAppRefresh = true;
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
Navigator.of(context).pushNamed("/auth", arguments: {"url": AppSettings().oauthUrl});
}
_setErrorState(HAError e) {
_setErrorState(HACException e) {
if (e == null) {
_showErrorBottomBar(
HAError("Unknown error")
_bottomInfoBarController.showErrorBottomBar(
HACException("Unknown error")
);
} else {
_showErrorBottomBar(e);
_bottomInfoBarController.showErrorBottomBar(e);
}
}
void _showPopupDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) {
List<Widget> buttons = [];
buttons.add(FlatButton(
child: new Text("$positiveText"),
onPressed: () {
Navigator.of(context).pop();
if (onPositive != null) {
onPositive();
}
},
));
if (negativeText != null) {
buttons.add(FlatButton(
child: new Text("$negativeText"),
onPressed: () {
Navigator.of(context).pop();
if (onNegative != null) {
onNegative();
}
},
));
}
// flutter defined function
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: new Text("$title"),
content: new Text("$body"),
actions: buttons,
);
},
);
}
void _notifyServiceCalled(String domain, String service, entityId) {
_showInfoBottomBar(
_bottomInfoBarController.showInfoBottomBar(
message: "Calling $domain.$service",
duration: Duration(seconds: 4)
);
}
void _showEntityPage(String entityId) {
Logger.d('Showing entity page: $entityId');
Navigator.push(
context,
MaterialPageRoute(
@ -373,20 +296,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
]);
menuItems.addAll([
Divider(),
new ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text("Log"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/log-view');
},
),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
title: Text("Report an issue"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
},
),
Divider(),
@ -404,7 +319,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
title: Text("Help"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.app/docs");
Launcher.launchURLInCustomTab(
context: context,
url: "http://ha-client.app/help"
);
},
),
new ListTile(
@ -412,7 +330,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
title: Text("Contacts/Discussion"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("https://discord.gg/nd6FZQ");
Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
},
),
new ListTile(
@ -427,7 +345,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.app/");
Launcher.launchURLInBrowser("http://ha-client.app/");
},
child: Text(
"ha-client.app",
@ -481,116 +399,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
);
}
void _hideBottomBar() {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
setState(() {
_showBottomBar = false;
});
}
Widget _bottomBarAction;
bool _showBottomBar = false;
String _bottomBarText;
bool _bottomBarProgress;
bool _bottomBarErrorColor;
Timer _bottomBarTimer;
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
_bottomBarAction = Container(height: 0.0, width: 0.0,);
_bottomBarErrorColor = false;
setState(() {
_bottomBarText = message;
_bottomBarProgress = progress;
_showBottomBar = true;
});
if (duration != null) {
_bottomBarTimer = Timer(duration, () {
_hideBottomBar();
});
}
}
void _showErrorBottomBar(HAError error) {
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
);
_bottomBarErrorColor = true;
List<Widget> actions = [];
error.actions.forEach((HAErrorAction action) {
switch (action.type) {
case HAErrorActionType.FULL_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
_fullLoad();
},
));
break;
}
case HAErrorActionType.QUICK_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
_quickLoad();
},
));
break;
}
case HAErrorActionType.RELOGIN: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
ConnectionManager().logout().then((_) => _fullLoad());
},
));
break;
}
case HAErrorActionType.URL: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
},
));
break;
}
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Navigator.pushNamed(context, '/connection-settings');
},
));
break;
}
}
});
if (actions.isNotEmpty) {
_bottomBarAction = Row(
mainAxisSize: MainAxisSize.min,
children: actions,
mainAxisAlignment: MainAxisAlignment.end,
);
} else {
_bottomBarAction = Container(height: 0.0, width: 0.0,);
}
setState(() {
_bottomBarProgress = false;
_bottomBarText = "${error.message}";
_showBottomBar = true;
});
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> serviceMenuItems = [];
List<PopupMenuItem<String>> mediaMenuItems = [];
List<Entity> activePlayers = [];
List<Entity> activeLights = [];
Color mediaMenuIconColor;
Color lightMenuIconColor;
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
if (_previousViewCount != currentViewCount) {
@ -599,72 +414,25 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_previousViewCount = currentViewCount;
}
serviceMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
if (ConnectionManager().isAuthenticated) {
if (AppSettings().isAuthenticated) {
_showLoginButton = false;
serviceMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
}
Widget mediaMenuIcon;
int playersCount = 0;
if (!empty && !HomeAssistant().entities.isEmpty) {
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
playersCount = activePlayers.length;
mediaMenuItems.addAll(
activePlayers.map((entity) => PopupMenuItem<String>(
child: Text(
"${entity.displayName}",
style: Theme.of(context).textTheme.body1.copyWith(
color: HAClientTheme().getColorByEntityState(entity.state, context)
)
),
value: "${entity.entityId}",
)).toList()
);
activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
activeLights = HomeAssistant().entities.getByDomains(includeDomains: ["light"], stateFiler: [EntityState.on]);
}
mediaMenuItems.addAll([
PopupMenuItem<String>(
child: new Text("Play media..."),
value: "play_media",
)
]);
if (playersCount > 0) {
mediaMenuIcon = Stack(
overflow: Overflow.visible,
children: <Widget>[
Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,),
Positioned(
bottom: -4,
right: -4,
child: Container(
height: 16,
width: 16,
decoration: new BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: Center(
child: Text(
"$playersCount",
style: Theme.of(context).textTheme.caption.copyWith(
color: Colors.white
)
),
),
),
)
],
);
if (activePlayers.isNotEmpty) {
mediaMenuIconColor = Theme.of(context).accentColor;
} else {
mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,);
mediaMenuIconColor = Theme.of(context).primaryIconTheme.color;
}
if (activeLights.isNotEmpty) {
lightMenuIconColor = Theme.of(context).accentColor;
} else {
lightMenuIconColor = Theme.of(context).primaryIconTheme.color;
}
Widget mainScrollBody;
if (empty) {
@ -674,9 +442,23 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Text("Login with Home Assistant", style: Theme.of(context).textTheme.button),
color: Colors.blue,
child: Text("Login", style: Theme.of(context).textTheme.button),
color: Theme.of(context).primaryColor,
onPressed: () => _fullLoad(),
),
Container(height: 20,),
FlatButton(
child: Text("Login with long-lived token", style: Theme.of(context).textTheme.button),
color: Theme.of(context).primaryColor,
onPressed: () => eventBus.fire(ShowPopupEvent(
popup: TokenLoginPopup()
))
),
Container(height: 20,),
FlatButton(
child: Text("Connection settings", style: Theme.of(context).textTheme.button),
color: Theme.of(context).primaryColor,
onPressed: () => Navigator.of(context).pushNamed('/connection-settings')
)
]
)
@ -701,46 +483,43 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
SliverAppBar(
floating: true,
pinned: true,
primary: true,
snap: false,
primary: AppSettings().displayMode == DisplayMode.normal,
title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[
IconButton(
icon: mediaMenuIcon,
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100.0, 50, 0.0),
context: context,
items: mediaMenuItems
).then((String val) {
if (val == "play_media") {
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
} else {
_showEntityPage(val);
}
PopupMenuButton(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 15),
child: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color)
),
onSelected: (String val) {
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
_quickLoad();
});
}
},
itemBuilder: (BuildContext context) {
List<PopupMenuEntry<String>> result = [
PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
)
];
if (AppSettings().isAuthenticated) {
result.addAll([
PopupMenuDivider(),
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
)]);
}
return result;
},
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,),
onPressed: () {
showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100, 0.0, 0.0),
context: context,
items: serviceMenuItems
).then((String val) {
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
_quickLoad();
});
}
});
}
)
],
leading: IconButton(
icon: Icon(Icons.menu),
@ -748,6 +527,99 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_scaffoldKey.currentState.openDrawer();
},
),
expandedHeight: 130,
flexibleSpace: FlexibleSpaceBar(
title: Padding(
padding: EdgeInsets.only(bottom: 15),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PopupMenuButton<String>(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
child: Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:television"),
color: mediaMenuIconColor,
size: 20,
)
),
onSelected: (String val) {
if (val == "play_media") {
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
} else if (val != null) {
_showEntityPage(val);
}
},
itemBuilder: (BuildContext context) {
List<PopupMenuEntry<String>> result = [
PopupMenuDivider(),
PopupMenuItem<String>(
child: new Text("Play media..."),
value: "play_media",
)
];
if (activePlayers.isNotEmpty) {
result.insertAll(0,
activePlayers.map((entity) => PopupMenuItem<String>(
child: Text(
"${entity.displayName}",
style: Theme.of(context).textTheme.body1.copyWith(
color: HAClientTheme().getColorByEntityState(entity.state, context)
)
),
value: "${entity.entityId}",
)).toList()
);
} else {
result.insert(0, PopupMenuItem<String>(
child: new Text("No active players"),
value: "_",
enabled: false,
));
}
return result;
}
),
PopupMenuButton<String>(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
child: Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:lightbulb-outline"),
color: lightMenuIconColor,
size: 20
)
),
onSelected: (String val) {
if (val == 'turn_off_all') {
ConnectionManager().callService(
service: 'turn_off',
domain: 'light',
entityId: 'all'
);
} else if (val == 'turn_on_all') {
ConnectionManager().callService(
service: 'turn_on',
domain: 'light',
entityId: 'all'
);
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
child: new Text("Turn on all lights"),
value: "turn_on_all",
),
PopupMenuItem<String>(
child: new Text("Turn off all ligts"),
value: "turn_off_all",
enabled: activeLights.isNotEmpty,
)
],
)
],
) ),
centerTitle: true,
),
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
@ -765,60 +637,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override
Widget build(BuildContext context) {
Widget bottomBar;
if (_showBottomBar) {
List<Widget> bottomBarChildren = [];
if (_bottomBarText != null) {
bottomBarChildren.add(
Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
Sizes.rowPadding),
child: Text(
"$_bottomBarText",
textAlign: TextAlign.left,
softWrap: true,
),
)
);
}
if (_bottomBarProgress) {
bottomBarChildren.add(
CollectionScaleTransition(
children: <Widget>[
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOnStateColor(context),),
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getDisabledStateColor(context),),
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOffStateColor(context),),
],
),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: _bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: bottomBarChildren,
),
),
_bottomBarAction
],
),
);
}
}
if (HomeAssistant().isNoViews) {
return Scaffold(
key: _scaffoldKey,
primary: false,
drawer: _buildAppDrawer(),
bottomNavigationBar: bottomBar,
bottomNavigationBar: BottomInfoBar(
controller: _bottomInfoBarController,
),
body: _buildScaffoldBody(true)
);
} else {
@ -826,7 +652,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
key: _scaffoldKey,
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
bottomNavigationBar: BottomInfoBar(
controller: _bottomInfoBarController,
),
body: _buildScaffoldBody(false)
);
}
@ -835,6 +663,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
Hive.close();
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
//flutterWebviewPlugin.dispose();
_viewsTabController?.dispose();
@ -842,15 +671,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_lovelaceSubscription?.cancel();
_settingsSubscription?.cancel();
_serviceCallSubscription?.cancel();
_showPopupDialogSubscription?.cancel();
_showPopupMessageSubscription?.cancel();
_showPopupSubscription?.cancel();
_showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel();
_startAuthSubscription?.cancel();
_showPageSubscription?.cancel();
_fullReloadSubscription?.cancel();
_reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose();
}
}

View File

@ -57,8 +57,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
_loaded = false;
});
} else {
_isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor");
//_useMediaExtractor = _isMediaExtractorExist;
_isMediaExtractorExist = HomeAssistant().isComponentEnabled("media_extractor");
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
setState(() {
if (_players.isNotEmpty) {
@ -106,7 +105,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
);
HomeAssistant().sendFromPlayerId = null;
}
eventBus.fire(ShowEntityPageEvent(entity: entity));
eventBus.fire(ShowEntityPageEvent(entityId: entity.entityId));
}
}

View File

@ -31,7 +31,7 @@ class _PurchasePageState extends State<PurchasePage> {
} else {
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
if (!response.notFoundIDs.isEmpty) {
if (response.notFoundIDs.isNotEmpty) {
Logger.d("Products not found: ${response.notFoundIDs}");
}
_products = response.productDetails;
@ -90,22 +90,6 @@ class _PurchasePageState extends State<PurchasePage> {
} else {
body = _buildProducts();
}
body.add(
Card(
child: Container(
height: 80,
child: InkWell(
child: Image.network('https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif'),
onTap: () {
Launcher.launchURLInCustomTab(
context: context,
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARWGETZD2D83Q&source=url'
);
},
)
),
)
);
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){

View File

@ -0,0 +1,48 @@
part of '../main.dart';
class QuickStartPage extends StatefulWidget {
QuickStartPage({Key key, this.title}) : super(key: key);
final String title;
@override
_QuickStartPageState createState() => new _QuickStartPageState();
}
class _QuickStartPageState extends State<QuickStartPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
leading: Container(),
title: Text('Quick start'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.help),
onPressed: () {
Launcher.launchURLInCustomTab(
context: context,
url: 'https://ha-client.app/help'
);
},
)
],
),
body: ConnectionSettingsPage(
quickStart: true,
)
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -23,7 +23,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
return ListTile(
title: Text(title, style: Theme.of(context).textTheme.subhead),
title: Text(title),
leading: Icon(icon),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
@ -35,12 +35,15 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
}
Widget _buildMenu(BuildContext context) {
List<Widget> items = [
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
];
if (AppSettings().webhookId != null) {
items.insert(1, _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings));
}
return ListView(
children: <Widget>[
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
],
children: items,
);
}
@ -48,25 +51,30 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
Widget build(BuildContext context) {
Widget section;
String title;
String helpUrl;
switch (_currentSection) {
case AppSettingsSection.menu: {
section = _buildMenu(context);
title = 'App settings';
helpUrl = 'https://ha-client.app/help/';
break;
}
case AppSettingsSection.connectionSettings: {
section = ConnectionSettingsPage();
title = 'App settings - Connection';
helpUrl = 'https://ha-client.app/help/connection';
break;
}
case AppSettingsSection.integrationSettings: {
section = IntegrationSettingsPage();
title = 'App settings - Integration';
helpUrl = 'https://ha-client.app/help/mobile_app_integration';
break;
}
case AppSettingsSection.lookAndFeel: {
section = LookAndFeelSettingsPage();
title = 'App settings - Look&Feel';
helpUrl = 'https://ha-client.app/help/';
break;
}
default:
@ -86,6 +94,17 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
}
}),
title: Text(title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.help),
onPressed: () {
Launcher.launchURLInCustomTab(
context: context,
url: helpUrl
);
},
)
],
),
body: section
),

View File

@ -1,187 +1,194 @@
part of '../../main.dart';
class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
ConnectionSettingsPage({Key key, this.title, this.quickStart: false}) : super(key: key);
final String title;
final bool quickStart;
@override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
}
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = "";
String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _longLivedToken = "";
String _newLongLivedToken = "";
String _homeAssistantUrl = '';
String _deviceName;
bool _loaded = false;
bool _includeDeviceName = false;
String oauthUrl;
bool useOAuth = false;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_loadSettings();
if (!widget.quickStart) {
_loadSettings();
} else {
_deviceName = MobileAppIntegrationManager.getDefaultDeviceName();
_includeDeviceName = true;
_loaded = true;
}
}
_loadSettings() async {
_includeDeviceName = widget.quickStart || AppSettings().webhookId == null;
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
try {
useOAuth = prefs.getBool("oauth-used") ?? true;
} catch (e) {
useOAuth = true;
}
if (!useOAuth) {
try {
_longLivedToken = _newLongLivedToken =
await storage.read(key: "hacl_llt");
} catch (e) {
_longLivedToken = _newLongLivedToken = "";
await storage.delete(key: "hacl_llt");
}
}
String domain = prefs.getString('hassio-domain')?? '';
String port = prefs.getString('hassio-port') ?? '';
String urlProtocol = prefs.getString('hassio-res-protocol') ?? 'https';
_homeAssistantUrl = '$urlProtocol://$domain:$port';
_deviceName = prefs.getString('app-integration-device-name') ?? MobileAppIntegrationManager.getDefaultDeviceName();
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
_loaded = true;
});
}
bool _checkConfigChanged() {
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newLongLivedToken != _longLivedToken));
}
_saveSettings() async {
_newHassioDomain = _newHassioDomain.trim();
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
_newHassioDomain = _newHassioDomain.split("//")[1];
String socketProtocol;
String domain;
String port;
if (_homeAssistantUrl.startsWith("http") && _homeAssistantUrl.indexOf("//") > 0) {
_homeAssistantUrl.startsWith("https") ? socketProtocol = "wss" : socketProtocol = "ws";
domain = _homeAssistantUrl.split("//")[1];
} else {
domain = _homeAssistantUrl;
socketProtocol = "ws";
}
_newHassioDomain = _newHassioDomain.split("/")[0];
if (_newHassioDomain.contains(":")) {
List<String> domainAndPort = _newHassioDomain.split(":");
_newHassioDomain = domainAndPort[0];
_newHassioPort = domainAndPort[1];
domain = domain.split("/")[0];
if (domain.contains(":")) {
List<String> domainAndPort = domain.split(":");
domain = domainAndPort[0];
port = domainAndPort[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
if (_newLongLivedToken.isNotEmpty) {
_newLongLivedToken = _newLongLivedToken.trim();
prefs.setBool("oauth-used", false);
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
} else if (!useOAuth) {
await storage.delete(key: "hacl_llt");
}
prefs.setString("hassio-domain", _newHassioDomain);
if (_newHassioPort == null || _newHassioPort.isEmpty) {
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
await prefs.setString("hassio-domain", domain);
if (port == null || port.isEmpty) {
port = socketProtocol == "wss" ? "443" : "80";
} else {
_newHassioPort = _newHassioPort.trim();
port = port.trim();
}
await prefs.setString("hassio-port", port);
await prefs.setString("hassio-protocol", socketProtocol);
await prefs.setString("hassio-res-protocol", socketProtocol == "wss" ? "https" : "http");
if (_includeDeviceName) {
await prefs.setString('app-integration-device-name', _deviceName);
}
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
}
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
if (!_loaded) {
return PageLoadingIndicator();
}
List<Widget> formChildren = <Widget>[
Text(
"Home Assistant url:",
style: Theme.of(context).textTheme.headline,
),
TextFormField(
initialValue: _homeAssistantUrl,
decoration: InputDecoration(
hintText: "Please enter url",
contentPadding: EdgeInsets.all(0),
hintStyle: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context).textTheme.overline.color
)
),
onSaved: (newValue) {
_homeAssistantUrl = newValue.trim();
},
validator: (value) {
if (value.trim().isEmpty) {
return 'Url is required';
}
return null;
},
),
Container(
height: 10,
),
Text(
"For example:",
style: Theme.of(context).textTheme.body1,
),
Text(
"192.186.2.14:8123",
style: Theme.of(context).textTheme.subhead,
),
Text(
"http://myhome.duckdns.org:8123",
style: Theme.of(context).textTheme.subhead,
),
Text(
"https://efkmfrwk3r4fsfwrfrg5.ui.nabu.casa/",
style: Theme.of(context).textTheme.subhead,
),
];
if (_includeDeviceName) {
formChildren.addAll(<Widget>[
Container(
height: 30,
),
Text(
"Connection settings",
"Device name:",
style: Theme.of(context).textTheme.headline,
),
new Row(
children: [
Text("Use ssl (HTTPS)"),
Switch(
value: (_newSocketProtocol == "wss"),
onChanged: (value) {
setState(() {
_newSocketProtocol = value ? "wss" : "ws";
});
},
TextFormField(
initialValue: _deviceName,
onSaved: (newValue) {
_deviceName = newValue.trim();
},
decoration: InputDecoration(
hintText: 'Please enter device name',
contentPadding: EdgeInsets.all(0),
hintStyle: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context).textTheme.overline.color
)
],
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
onChanged: (value) {
_newHassioDomain = value;
}
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port (default is 8123)"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
onChanged: (value) {
_newHassioPort = value;
}
),
new Text(
"Try ports 80 and 443 if default is not working and you don't know why.",
style: Theme.of(context).textTheme.caption,
),
Text(
"Authentication settings",
style: Theme.of(context).textTheme.headline,
),
Container(height: 10.0,),
Text(
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
style: Theme.of(context).textTheme.body1.copyWith(
color: Colors.redAccent
),
),
new TextField(
decoration: InputDecoration(
labelText: "Long-lived token"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
onChanged: (value) {
_newLongLivedToken = value;
validator: (value) {
if (value.trim().isEmpty) {
return 'Device name is required';
}
return null;
},
),
Container(
height: Sizes.rowPadding,
),
RaisedButton(
child: Text('Apply', style: Theme.of(context).textTheme.button),
color: Theme.of(context).primaryColorDark,
]);
}
formChildren.addAll(<Widget>[
Container(
height: 30,
),
ButtonTheme(
height: 60,
child: RaisedButton(
child: Text(widget.quickStart ? 'Engage' : 'Apply', style: Theme.of(context).textTheme.button.copyWith(fontSize: 20)),
color: Theme.of(context).primaryColor,
onPressed: () {
if (_checkConfigChanged()) {
Logger.d("Settings changed. Saving...");
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
_saveSettings().then((r) {
Navigator.pop(context);
if (widget.quickStart) {
Navigator.pushReplacementNamed(context, '/');
} else {
Navigator.pop(context);
}
eventBus.fire(SettingsChangedEvent(true));
});
} else {
Logger.d("Settings was not changed");
Navigator.pop(context);
}
},
)
],
)
]);
return Form(
key: _formKey,
child: ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: formChildren,
),
);
}

View File

@ -11,7 +11,7 @@ class IntegrationSettingsPage extends StatefulWidget {
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
bool _locationTrackingEnabled = false;
bool _wait = false;
@ -28,7 +28,8 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
SharedPreferences.getInstance().then((prefs) {
setState(() {
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
_locationInterval = prefs.getInt("location-interval") ??
AppSettings().defaultLocationUpdateIntervalMinutes;
if (_locationInterval % 5 != 0) {
_locationInterval = 5 * (_locationInterval ~/ 5);
}
@ -36,7 +37,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
});
}
void incLocationInterval() {
void _incLocationInterval() {
if (_locationInterval < 720) {
setState(() {
_locationInterval = _locationInterval + 5;
@ -44,7 +45,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
}
}
void decLocationInterval() {
void _decLocationInterval() {
if (_locationInterval > 5) {
setState(() {
_locationInterval = _locationInterval - 5;
@ -52,46 +53,6 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
}
}
restart() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to restart Home Assistant?",
body: "This will restart your Home Assistant server.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart");
},
));
}
stop() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to STOP Home Assistant?",
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "stop");
},
));
}
updateRegistration() {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowPopupDialogEvent(
title: "Waaaait",
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
positiveText: "Done it already",
negativeText: "Ok, I will",
onPositive: () {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
_switchLocationTrackingState(bool state) async {
if (state) {
await LocationManager().updateDeviceLocation();
@ -146,42 +107,16 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("-", style: Theme.of(context).textTheme.title),
onPressed: () => decLocationInterval(),
onPressed: () => _decLocationInterval(),
),
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("+", style: Theme.of(context).textTheme.title),
onPressed: () => incLocationInterval(),
onPressed: () => _incLocationInterval(),
),
],
),
Divider(),
Text("Integration status", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding,),
Text(
"${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}",
style: Theme.of(context).textTheme.subtitle,
),
Container(height: 6.0,),
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
//Divider(),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RaisedButton(
color: Colors.blue,
onPressed: () => updateRegistration(),
child: Text("Check integration", style: Theme.of(context).textTheme.button)
),
Container(width: 10.0,),
RaisedButton(
color: Colors.redAccent,
onPressed: () => resetRegistration(),
child: Text("Reset integration", style: Theme.of(context).textTheme.button)
)
],
),
)
]
);
}

View File

@ -12,7 +12,8 @@ class LookAndFeelSettingsPage extends StatefulWidget {
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
AppTheme _currentTheme;
bool _changed = false;
bool _scrollBadges = false;
DisplayMode _displayMode;
@override
void initState() {
@ -25,14 +26,17 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
await prefs.reload();
SharedPreferences.getInstance().then((prefs) {
setState(() {
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme];
_currentTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
_displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
_scrollBadges = prefs.getBool('scroll-badges') ?? true;
});
});
}
_saveSettings(AppTheme theme) {
_saveTheme(AppTheme theme) {
SharedPreferences.getInstance().then((prefs) {
prefs.setInt('app-theme', theme.index);
prefs.setBool('scroll-badges', _scrollBadges);
setState(() {
_currentTheme = theme;
eventBus.fire(ChangeThemeEvent(_currentTheme));
@ -40,12 +44,34 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
});
}
Future _saveBadgesSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
AppSettings().scrollBadges = _scrollBadges;
await prefs.setBool('scroll-badges', _scrollBadges);
}
Future _saveDisplayMode(DisplayMode mode) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
AppSettings().displayMode = mode;
await prefs.setInt('display-mode', mode.index);
if (mode == DisplayMode.fullscreen) {
SystemChrome.setEnabledSystemUIOverlays([]);
} else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
}
Map appThemeName = {
AppTheme.defaultTheme: 'Default',
AppTheme.haTheme: 'Home Assistant theme',
AppTheme.darkTheme: 'Dark theme'
};
Map DisplayModeName = {
DisplayMode.normal: 'Normal',
DisplayMode.fullscreen: 'Fullscreen'
};
@override
Widget build(BuildContext context) {
return ListView(
@ -59,15 +85,56 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
iconSize: 30.0,
isExpanded: true,
style: Theme.of(context).textTheme.title,
//hint: Text("Select ${caption.toLowerCase()}"),
items: AppTheme.values.map((value) {
return new DropdownMenuItem<AppTheme>(
value: value,
child: Text('${appThemeName[value]}'),
);
}).toList(),
onChanged: (theme) => _saveSettings(theme),
)
onChanged: (theme) => _saveTheme(theme),
),
Container(height: Sizes.doubleRowPadding),
Text("Badges display:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<bool>(
value: _scrollBadges,
iconSize: 30.0,
isExpanded: true,
style: Theme.of(context).textTheme.title,
items: [true, false].map((value) {
return new DropdownMenuItem<bool>(
value: value,
child: Text('${value ? 'Horizontal scroll' : 'In rows'}'),
);
}).toList(),
onChanged: (val) {
setState(() {
_scrollBadges = val;
});
_saveBadgesSettings();
},
),
Container(height: Sizes.doubleRowPadding),
Text("Display mode:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<DisplayMode>(
value: _displayMode,
iconSize: 30.0,
isExpanded: true,
style: Theme.of(context).textTheme.title,
items: DisplayMode.values.map((value) {
return new DropdownMenuItem<DisplayMode>(
value: value,
child: Text('${DisplayModeName[value]}'),
);
}).toList(),
onChanged: (DisplayMode val) {
setState(() {
_displayMode = val;
});
_saveDisplayMode(val);
},
),
]
);
}

View File

@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
error = "";
});
http.Response response;
response = await http.get("http://ha-client.app/service/whats_new_0.8.3.md");
response = await http.get(whatsNewUrl);
if (response.statusCode == 200) {
setState(() {
data = response.body;
@ -66,4 +66,4 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
body: body
);
}
}
}

View File

@ -0,0 +1,210 @@
part of '../../main.dart';
class BottomInfoBarController {
Function show;
Function hide;
String bottomBarText;
bool bottomBarProgress;
bool bottomBarErrorColor;
Timer _bottomBarTimer;
bool initialState = false;
List<HAErrorAction> actions = [];
void hideBottomBar() {
_bottomBarTimer?.cancel();
if (hide == null) {
initialState = false;
} else {
hide();
}
}
void showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
actions.clear();
bottomBarErrorColor = false;
bottomBarText = message;
bottomBarProgress = progress;
if (show == null) {
initialState = true;
} else {
show();
}
if (duration != null) {
_bottomBarTimer = Timer(duration, () {
hideBottomBar();
});
}
}
void showErrorBottomBar(HACException error) {
actions.clear();
actions.addAll(error.actions);
bottomBarErrorColor = true;
bottomBarProgress = false;
bottomBarText = "${error.cause}";
if (show == null) {
initialState = true;
} else {
show();
}
}
}
class BottomInfoBar extends StatefulWidget {
final BottomInfoBarController controller;
const BottomInfoBar({Key key, this.controller}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _BottomInfoBarState();
}
}
class _BottomInfoBarState extends State<BottomInfoBar> {
bool _show;
@override
void initState() {
_show = widget.controller.initialState;
widget.controller.show = () {
setState(() {
_show = true;
});
};
widget.controller.hide = () {
setState(() {
_show = false;
});
};
super.initState();
}
@override
Widget build(BuildContext context) {
if (!_show) {
return Container(width: 0, height: 0,);
} else {
Widget bottomBar;
List<Widget> bottomBarChildren = [];
Widget actionsWidget;
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
);
List<Widget> actions = [];
widget.controller.actions.forEach((HAErrorAction action) {
switch (action.type) {
case HAErrorActionType.FULL_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
eventBus.fire(FullReloadEvent());
},
));
break;
}
case HAErrorActionType.QUICK_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
eventBus.fire(ReloadUIEvent());
},
));
break;
}
case HAErrorActionType.RELOGIN: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
ConnectionManager().logout().then((_) => eventBus.fire(FullReloadEvent()));
},
));
break;
}
case HAErrorActionType.URL: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
},
));
break;
}
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Navigator.pushNamed(context, '/connection-settings');
},
));
break;
}
}
});
if (actions.isNotEmpty) {
actionsWidget = Row(
mainAxisSize: MainAxisSize.min,
children: actions,
mainAxisAlignment: MainAxisAlignment.end,
);
} else {
actionsWidget = Container(height: 0.0, width: 0.0,);
}
if (widget.controller.bottomBarText != null) {
bottomBarChildren.add(
Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
Sizes.rowPadding),
child: Text(
"${widget.controller.bottomBarText}",
textAlign: TextAlign.left,
softWrap: true,
),
)
);
}
if (widget.controller.bottomBarProgress) {
bottomBarChildren.add(
LinearProgressIndicator(),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: widget.controller.bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: widget.controller.bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: bottomBarChildren,
),
),
actionsWidget
],
),
);
} else {
bottomBar = Container(height: 0,);
}
return bottomBar;
}
}
}

View File

@ -20,7 +20,7 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
Widget build(BuildContext context) {
return ListView(
children: [
LinkToWebConfig(name: "Home Assistant configuration", url: ConnectionManager().httpWebHost+"/config"),
LinkToWebConfig(name: "Home Assistant configuration", url: AppSettings().httpWebHost+"/config"),
],
);
}

View File

@ -24,27 +24,22 @@ class Panel {
icon = Panel.iconsByComponent[componentName];
}
isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools');
isWebView = (componentName != 'config' && componentName != 'lovelace' && !componentName.startsWith('haclient'));
isWebView = (componentName != 'lovelace' && !componentName.startsWith('haclient'));
}
void handleOpen(BuildContext context) {
if (componentName == "config") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else if (componentName.startsWith('haclient')) {
if (componentName.startsWith('haclient')) {
Navigator.of(context).pushNamed(urlPath);
} else if (componentName == 'lovelace') {
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
HomeAssistant().currentDashboardPath = this.urlPath;
Logger.d('Switching to dashboard: ${this.urlPath}');
HomeAssistant().autoUi = false;
SharedPreferences.getInstance().then((prefs) {
prefs.setString('lovelace_dashboard_url', this.urlPath);
eventBus.fire(ReloadUIEvent());
});
} else {
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
Launcher.launchAuthenticatedWebView(context: context, url: "${AppSettings().httpWebHost}/$urlPath", title: "${this.title}");
}
}
@ -68,10 +63,6 @@ class Panel {
Widget getWidget() {
switch (componentName) {
case "config": {
return ConfigPanelWidget();
}
default: {
return Text("Unsupported panel component: $componentName");
}

View File

@ -1,77 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'utils.dart';
class BasePainter extends CustomPainter {
Color baseColor;
Color selectionColor;
int primarySectors;
int secondarySectors;
double sliderStrokeWidth;
Offset center;
double radius;
BasePainter({
@required this.baseColor,
@required this.selectionColor,
@required this.primarySectors,
@required this.secondarySectors,
@required this.sliderStrokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
Paint base = _getPaint(color: baseColor);
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
// we need this in the parent to calculate if the user clicks on the circumference
assert(radius > 0);
canvas.drawCircle(center, radius, base);
if (primarySectors > 0) {
_paintSectors(primarySectors, 8.0, selectionColor, canvas);
}
if (secondarySectors > 0) {
_paintSectors(secondarySectors, 6.0, baseColor, canvas);
}
}
void _paintSectors(
int sectors, double radiusPadding, Color color, Canvas canvas) {
Paint section = _getPaint(color: color, width: 2.0);
var endSectors =
getSectionsCoordinatesInCircle(center, radius + radiusPadding, sectors);
var initSectors =
getSectionsCoordinatesInCircle(center, radius - radiusPadding, sectors);
_paintLines(canvas, initSectors, endSectors, section);
}
void _paintLines(
Canvas canvas, List<Offset> inits, List<Offset> ends, Paint section) {
assert(inits.length == ends.length && inits.length > 0);
for (var i = 0; i < inits.length; i++) {
canvas.drawLine(inits[i], ends[i], section);
}
}
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
Paint()
..color = color
..strokeCap = StrokeCap.round
..style = style ?? PaintingStyle.stroke
..strokeWidth = width ?? sliderStrokeWidth;
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

View File

@ -1,366 +0,0 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'base_painter.dart';
import 'slider_painter.dart';
import 'utils.dart';
enum CircularSliderMode { singleHandler, doubleHandler }
enum SlidingState { none, endIsBiggerThanStart, endIsSmallerThanStart }
typedef SelectionChanged<T> = void Function(T a, T b, T c);
class CircularSliderPaint extends StatefulWidget {
final CircularSliderMode mode;
final int init;
final int end;
final int divisions;
final int primarySectors;
final int secondarySectors;
final SelectionChanged<int> onSelectionChange;
final SelectionChanged<int> onSelectionEnd;
final Color baseColor;
final Color selectionColor;
final Color handlerColor;
final double handlerOutterRadius;
final Widget child;
final bool showRoundedCapInSelection;
final bool showHandlerOutter;
final double sliderStrokeWidth;
final bool shouldCountLaps;
CircularSliderPaint({
@required this.mode,
@required this.divisions,
@required this.init,
@required this.end,
this.child,
@required this.primarySectors,
@required this.secondarySectors,
@required this.onSelectionChange,
@required this.onSelectionEnd,
@required this.baseColor,
@required this.selectionColor,
@required this.handlerColor,
@required this.handlerOutterRadius,
@required this.showRoundedCapInSelection,
@required this.showHandlerOutter,
@required this.sliderStrokeWidth,
@required this.shouldCountLaps,
});
@override
_CircularSliderState createState() => _CircularSliderState();
}
class _CircularSliderState extends State<CircularSliderPaint> {
bool _isInitHandlerSelected = false;
bool _isEndHandlerSelected = false;
SliderPainter _painter;
/// start angle in radians where we need to locate the init handler
double _startAngle;
/// end angle in radians where we need to locate the end handler
double _endAngle;
/// the absolute angle in radians representing the selection
double _sweepAngle;
/// in case we have a double slider and we want to move the whole selection by clicking in the slider
/// this will capture the position in the selection relative to the initial handler
/// that way we will be able to keep the selection constant when moving
int _differenceFromInitPoint;
/// will store the number of full laps (2pi radians) as part of the selection
int _laps = 0;
/// will be used to calculate in the next movement if we need to increase or decrease _laps
SlidingState _slidingState = SlidingState.none;
bool get isDoubleHandler => widget.mode == CircularSliderMode.doubleHandler;
bool get isSingleHandler => widget.mode == CircularSliderMode.singleHandler;
bool get isBothHandlersSelected =>
_isEndHandlerSelected && _isInitHandlerSelected;
bool get isNoHandlersSelected =>
!_isEndHandlerSelected && !_isInitHandlerSelected;
@override
void initState() {
super.initState();
_calculatePaintData();
}
// we need to update this widget both with gesture detector but
// also when the parent widget rebuilds itself
@override
void didUpdateWidget(CircularSliderPaint oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.init != widget.init || oldWidget.end != widget.end) {
_calculatePaintData();
}
}
@override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
CustomPanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
() => CustomPanGestureRecognizer(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
),
(CustomPanGestureRecognizer instance) {},
),
},
child: CustomPaint(
painter: BasePainter(
baseColor: widget.baseColor,
selectionColor: widget.selectionColor,
primarySectors: widget.primarySectors,
secondarySectors: widget.secondarySectors,
sliderStrokeWidth: widget.sliderStrokeWidth,
),
foregroundPainter: _painter,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: widget.child,
),
),
);
}
void _calculatePaintData() {
var initPercent = isDoubleHandler
? valueToPercentage(widget.init, widget.divisions)
: 0.0;
var endPercent = valueToPercentage(widget.end, widget.divisions);
var sweep = getSweepAngle(initPercent, endPercent);
var previousStartAngle = _startAngle;
var previousEndAngle = _endAngle;
_startAngle = isDoubleHandler ? percentageToRadians(initPercent) : 0.0;
_endAngle = percentageToRadians(endPercent);
_sweepAngle = percentageToRadians(sweep.abs());
// update full laps if need be
if (widget.shouldCountLaps) {
var newSlidingState = _calculateSlidingState(_startAngle, _endAngle);
if (isSingleHandler) {
_laps = _calculateLapsForsSingleHandler(
_endAngle, previousEndAngle, _slidingState, _laps);
_slidingState = newSlidingState;
} else {
// is double handler
if (newSlidingState != _slidingState) {
_laps = _calculateLapsForDoubleHandler(
_startAngle,
_endAngle,
previousStartAngle,
previousEndAngle,
_slidingState,
newSlidingState,
_laps);
_slidingState = newSlidingState;
}
}
}
_painter = SliderPainter(
mode: widget.mode,
startAngle: _startAngle,
endAngle: _endAngle,
sweepAngle: _sweepAngle,
selectionColor: widget.selectionColor,
handlerColor: widget.handlerColor,
handlerOutterRadius: widget.handlerOutterRadius,
showRoundedCapInSelection: widget.showRoundedCapInSelection,
showHandlerOutter: widget.showHandlerOutter,
sliderStrokeWidth: widget.sliderStrokeWidth,
);
}
int _calculateLapsForsSingleHandler(
double end, double prevEnd, SlidingState slidingState, int laps) {
if (slidingState != SlidingState.none) {
if (radiansWasModuloed(end, prevEnd)) {
var lapIncrement = end < prevEnd ? 1 : -1;
var newLaps = laps + lapIncrement;
return newLaps < 0 ? 0 : newLaps;
}
}
return laps;
}
int _calculateLapsForDoubleHandler(
double start,
double end,
double prevStart,
double prevEnd,
SlidingState slidingState,
SlidingState newSlidingState,
int laps) {
if (slidingState != SlidingState.none) {
if (!radiansWasModuloed(start, prevStart) &&
!radiansWasModuloed(end, prevEnd)) {
var lapIncrement =
newSlidingState == SlidingState.endIsBiggerThanStart ? 1 : -1;
var newLaps = laps + lapIncrement;
return newLaps < 0 ? 0 : newLaps;
}
}
return laps;
}
SlidingState _calculateSlidingState(double start, double end) {
return end > start
? SlidingState.endIsBiggerThanStart
: SlidingState.endIsSmallerThanStart;
}
void _onPanUpdate(Offset details) {
if (!_isInitHandlerSelected && !_isEndHandlerSelected) {
return;
}
if (_painter.center == null) {
return;
}
_handlePan(details, false);
}
void _onPanEnd(Offset details) {
_handlePan(details, true);
_isInitHandlerSelected = false;
_isEndHandlerSelected = false;
}
void _handlePan(Offset details, bool isPanEnd) {
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details);
var angle = coordinatesToRadians(_painter.center, position);
var percentage = radiansToPercentage(angle);
var newValue = percentageToValue(percentage, widget.divisions);
if (isBothHandlersSelected) {
var newValueInit =
(newValue - _differenceFromInitPoint) % widget.divisions;
if (newValueInit != widget.init) {
var newValueEnd =
(widget.end + (newValueInit - widget.init)) % widget.divisions;
widget.onSelectionChange(newValueInit, newValueEnd, _laps);
if (isPanEnd) {
widget.onSelectionEnd(newValueInit, newValueEnd, _laps);
}
}
return;
}
// isDoubleHandler but one handler was selected
if (_isInitHandlerSelected) {
widget.onSelectionChange(newValue, widget.end, _laps);
if (isPanEnd) {
widget.onSelectionEnd(newValue, widget.end, _laps);
}
} else {
widget.onSelectionChange(widget.init, newValue, _laps);
if (isPanEnd) {
widget.onSelectionEnd(widget.init, newValue, _laps);
}
}
}
bool _onPanDown(Offset details) {
if (_painter == null) {
return false;
}
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details);
if (position == null) {
return false;
}
if (isSingleHandler) {
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
_isEndHandlerSelected = true;
_onPanUpdate(details);
}
} else {
_isInitHandlerSelected = isPointInsideCircle(
position, _painter.initHandler, widget.handlerOutterRadius);
if (!_isInitHandlerSelected) {
_isEndHandlerSelected = isPointInsideCircle(
position, _painter.endHandler, widget.handlerOutterRadius);
}
if (isNoHandlersSelected) {
// we check if the user pressed in the selection in a double handler slider
// that means the user wants to move the selection as a whole
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
var angle = coordinatesToRadians(_painter.center, position);
if (isAngleInsideRadiansSelection(angle, _startAngle, _sweepAngle)) {
_isEndHandlerSelected = true;
_isInitHandlerSelected = true;
var positionPercentage = radiansToPercentage(angle);
// no need to account for negative values, that will be sorted out in the onPanUpdate
_differenceFromInitPoint =
percentageToValue(positionPercentage, widget.divisions) -
widget.init;
}
}
}
}
return _isInitHandlerSelected || _isEndHandlerSelected;
}
}
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onPanDown;
final Function onPanUpdate;
final Function onPanEnd;
CustomPanGestureRecognizer({
@required this.onPanDown,
@required this.onPanUpdate,
@required this.onPanEnd,
});
@override
void addPointer(PointerEvent event) {
if (onPanDown(event.position)) {
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
} else {
stopTrackingPointer(event.pointer);
}
}
@override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
onPanUpdate(event.position);
}
if (event is PointerUpEvent) {
onPanEnd(event.position);
stopTrackingPointer(event.pointer);
}
}
@override
String get debugDescription => 'customPan';
@override
void didStopTrackingLastPointer(int pointer) {}
}

View File

@ -1,148 +0,0 @@
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart';
/// Returns a widget which displays a circle to be used as a slider.
///
/// Required arguments are init and end to set the initial selection.
/// onSelectionChange is a callback function which returns new values as the user
/// changes the interval.
/// The rest of the params are used to change the look and feel.
///
/// DoubleCircularSlider(5, 10, onSelectionChange: () => {});
class DoubleCircularSlider extends StatefulWidget {
/// the selection will be values between 0..divisions; max value is 300
final int divisions;
/// the initial value in the selection
final int init;
/// the end value in the selection
final int end;
/// the number of primary sectors to be painted
/// will be painted using selectionColor
final int primarySectors;
/// the number of secondary sectors to be painted
/// will be painted using baseColor
final int secondarySectors;
/// an optional widget that would be mounted inside the circle
final Widget child;
/// height of the canvas, default at 220
final double height;
/// width of the canvas, default at 220
final double width;
/// color of the base circle and sections
final Color baseColor;
/// color of the selection
final Color selectionColor;
/// color of the handlers
final Color handlerColor;
/// callback function when init and end change
/// (int init, int end) => void
final SelectionChanged<int> onSelectionChange;
/// callback function when init and end finish
/// (int init, int end) => void
final SelectionChanged<int> onSelectionEnd;
/// outter radius for the handlers
final double handlerOutterRadius;
/// if true an extra handler ring will be displayed in the handler
final bool showHandlerOutter;
/// stroke width for the slider, defaults at 12.0
final double sliderStrokeWidth;
/// if true, the onSelectionChange will also return the number of laps in the slider
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
final bool shouldCountLaps;
DoubleCircularSlider(
this.divisions,
this.init,
this.end, {
this.height,
this.width,
this.child,
this.primarySectors,
this.secondarySectors,
this.baseColor,
this.selectionColor,
this.handlerColor,
this.onSelectionChange,
this.onSelectionEnd,
this.handlerOutterRadius,
this.showHandlerOutter,
this.sliderStrokeWidth,
this.shouldCountLaps,
}) : assert(init >= 0 && init <= divisions,
'init has to be > 0 and < divisions value'),
assert(end >= 0 && end <= divisions,
'end has to be > 0 and < divisions value'),
assert(divisions >= 0 && divisions <= 300,
'divisions has to be > 0 and <= 300');
@override
_DoubleCircularSliderState createState() => _DoubleCircularSliderState();
}
class _DoubleCircularSliderState extends State<DoubleCircularSlider> {
int _init;
int _end;
@override
void initState() {
super.initState();
_init = widget.init;
_end = widget.end;
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.height ?? 220,
width: widget.width ?? 220,
child: CircularSliderPaint(
mode: CircularSliderMode.doubleHandler,
init: _init,
end: _end,
divisions: widget.divisions,
primarySectors: widget.primarySectors ?? 0,
secondarySectors: widget.secondarySectors ?? 0,
child: widget.child,
onSelectionChange: (newInit, newEnd, laps) {
if (widget.onSelectionChange != null) {
widget.onSelectionChange(newInit, newEnd, laps);
}
setState(() {
_init = newInit;
_end = newEnd;
});
},
onSelectionEnd: (newInit, newEnd, laps) {
if (widget.onSelectionEnd != null) {
widget.onSelectionEnd(newInit, newEnd, laps);
}
},
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
selectionColor:
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
handlerColor: widget.handlerColor ?? Colors.white,
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
showRoundedCapInSelection: false,
showHandlerOutter: widget.showHandlerOutter ?? true,
shouldCountLaps: widget.shouldCountLaps ?? false,
));
}
}

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