Compare commits
113 Commits
beta/0.8.5
...
foreground
Author | SHA1 | Date | |
---|---|---|---|
c844e21e76 | |||
24d42c9597 | |||
9078ad81e8 | |||
7cba6c8a10 | |||
c1f9c8c16d | |||
1d1d132b33 | |||
e258b3bc2c | |||
13508ee92f | |||
4fbf58e707 | |||
a3442f84ca | |||
6a6ab3b2cb | |||
d9fa553e2f | |||
cde5d9b912 | |||
3468446b5b | |||
326434273a | |||
470d3be946 | |||
d1032be6a6 | |||
cffac8e1f8 | |||
870bc25dd9 | |||
de713024f6 | |||
4d4add4581 | |||
1670c8e505 | |||
55eb1b5125 | |||
dbeaaaf91e | |||
8166d8ce6d | |||
35bcf0c1fa | |||
9c1d240962 | |||
a76652b552 | |||
a140f993d0 | |||
ded60a2867 | |||
b86602bcdb | |||
02ea45469f | |||
90105c3b09 | |||
3d828914cc | |||
8cd5776bc6 | |||
17ec73b176 | |||
e7cce01ca9 | |||
6c73f5d979 | |||
f59cb5afbf | |||
5629215229 | |||
45fb637d48 | |||
7c473eb1ca | |||
b40880c85a | |||
30329ea3ba | |||
ca10401bee | |||
814e0a8b00 | |||
b5fbe7b86f | |||
fc9b6f05c0 | |||
eadae4374b | |||
711cb04dcf | |||
1d39b7fc7d | |||
2fa640433a | |||
2445dc7869 | |||
e3e114fe94 | |||
7a1603b423 | |||
4b831821da | |||
1ec54953d7 | |||
61571600d1 | |||
07a097aa50 | |||
ce1cebaf64 | |||
faf6f73b2a | |||
db3b5d941e | |||
cc60dc2b21 | |||
8aa0e03187 | |||
4492a08d6b | |||
792c0d0c84 | |||
8221eceb78 | |||
12ba5598e4 | |||
536cbf9445 | |||
a87943da27 | |||
3fddc3b5a7 | |||
5bc0b0868a | |||
e9ad612fec | |||
c62e045dae | |||
725ec9291d | |||
96c8338890 | |||
0996fb94da | |||
5de2431a0f | |||
163338ea75 | |||
f28e5493dc | |||
01c0a08fa8 | |||
1c461d2449 | |||
915e8045a3 | |||
f10fc7eec1 | |||
320bc677e0 | |||
46ca1948e2 | |||
7a0ce93cfd | |||
3c0bd68b0a | |||
b4ad3061e4 | |||
d6b1fbec24 | |||
cacdd0d304 | |||
e3e1fa3499 | |||
58842d1ebb | |||
101569d6ee | |||
8a180c4c0e | |||
ba343fbd98 | |||
1d528df341 | |||
51ea0b0afa | |||
9dbb697e58 | |||
947558bb3d | |||
8ba4cc85d8 | |||
0f604a6ce6 | |||
7e48c6535f | |||
1d2a8b613b | |||
89e833eb33 | |||
b65a68e0c4 | |||
bfb24b9d11 | |||
0792cae2b1 | |||
a85fb3d03b | |||
ddb9a9d4e9 | |||
29ee360ec4 | |||
c0faaafd04 | |||
bc045344a5 |
1
.gitignore
vendored
@ -20,3 +20,4 @@ flutter_export_environment.sh
|
||||
key.properties
|
||||
.secrets.dart
|
||||
pubspec.lock
|
||||
google-services.json
|
||||
|
@ -6,11 +6,13 @@ Visit [ha-client.app](http://ha-client.app/) for more info.
|
||||
|
||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||
|
||||
Discuss it on [Discord](https://discord.gg/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)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||
|
||||
#### Pre-release CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||
#### Stable CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/latest_build)
|
||||
#### Beta CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||
#### Pre-release CI build
|
||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||
|
@ -1,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"
|
||||
}
|
@ -5,10 +5,14 @@
|
||||
android:required="false" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<!--
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
-->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
@ -20,6 +24,7 @@
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
@ -53,6 +58,17 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name="rekab.app.background_locator.LocatorBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"/>
|
||||
<service android:name="rekab.app.background_locator.LocatorService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="true"/>
|
||||
<service android:name="rekab.app.background_locator.IsolateHolderService"
|
||||
android:permission="android.permission.FOREGROUND_SERVICE"
|
||||
android:exported="true"/>
|
||||
|
||||
<!--
|
||||
<service
|
||||
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
@ -67,5 +83,6 @@
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
-->
|
||||
</application>
|
||||
</manifest>
|
||||
|
27
android/app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
|
||||
<path
|
||||
android:fillColor="#709ac1"
|
||||
android:fillAlpha="0"
|
||||
android:pathData="M 0 0 H 108 V 108 H 0 V 0 Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.12"
|
||||
android:pathData="M 70.506 38.389 L 108 72.466 L 108 108 L 77 108 L 35.066 72.466 L 38.373 63.769 L 36.268 50.216 L 43.335 44.523 L 51.841 34.578 L 63.096 42.478 L 68.586 42.478 L 70.506 38.389 Z" />
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.12"
|
||||
android:pathData="M 28.979 53.708 L 47.736 67.31 L 38.373 58.563 L 36.268 51.52 L 28.979 53.708 Z" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M 77.131 54.24 L 72.878 54.24 L 72.878 72.415 L 56.339 72.415 L 56.339 64.85 L 62.931 58.511 L 64.609 58.784 C 67.349 58.784 69.57 56.649 69.57 54.013 C 69.57 51.378 67.349 49.242 64.609 49.242 C 61.868 49.242 59.647 51.378 59.647 54.013 L 59.883 55.626 L 56.339 59.079 L 56.339 46.63 C 57.898 45.812 58.938 44.244 58.938 42.427 C 58.938 39.792 56.717 37.656 53.976 37.656 C 51.236 37.656 49.015 39.792 49.015 42.427 C 49.015 44.244 50.054 45.812 51.614 46.63 L 51.614 59.079 L 48.07 55.626 L 48.306 54.013 C 48.306 51.378 46.084 49.242 43.344 49.242 C 40.604 49.242 38.383 51.378 38.383 54.013 C 38.383 56.648 40.604 58.784 43.344 58.784 L 45.022 58.511 L 51.614 64.85 L 51.614 72.415 L 35.075 72.415 L 35.075 54.24 L 30.94 54.24 C 29.948 54.24 28.979 54.24 28.979 53.763 C 29.003 53.263 29.995 52.309 31.011 51.332 L 51.614 31.522 C 52.393 30.772 53.197 30 53.976 30 C 54.756 30 55.559 30.772 56.339 31.522 L 65.79 40.609 L 65.79 38.338 L 70.515 38.338 L 70.515 45.153 L 77.084 51.469 C 78.029 52.377 78.997 53.309 79.021 53.786 C 79.021 54.24 78.076 54.24 77.131 54.24 Z M 43.344 51.969 C 43.908 51.969 44.449 52.184 44.848 52.567 C 45.247 52.951 45.471 53.471 45.471 54.013 C 45.471 54.555 45.247 55.076 44.848 55.459 C 44.449 55.842 43.908 56.058 43.344 56.058 C 42.78 56.058 42.239 55.842 41.841 55.459 C 41.442 55.076 41.218 54.555 41.218 54.013 C 41.218 53.471 41.442 52.951 41.841 52.567 C 42.239 52.184 42.78 51.969 43.344 51.969 Z M 64.609 51.969 C 65.79 51.969 66.735 52.877 66.735 54.013 C 66.735 55.149 65.79 56.058 64.609 56.058 C 64.045 56.058 63.504 55.842 63.105 55.459 C 62.706 55.076 62.482 54.555 62.482 54.013 C 62.482 53.471 62.706 52.951 63.105 52.567 C 63.504 52.184 64.045 51.969 64.609 51.969 Z M 53.976 40.382 C 55.158 40.382 56.103 41.291 56.103 42.427 C 56.103 43.563 55.158 44.472 53.976 44.472 C 52.795 44.472 51.85 43.563 51.85 42.427 C 51.85 41.291 52.795 40.382 53.976 40.382 Z" />
|
||||
</group>
|
||||
</vector>
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
<item android:drawable="@color/main_color" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/main_color"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/main_color"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.8 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 9.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 20 KiB |
@ -1,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 -->
|
||||
|
@ -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');
|
||||
}
|
||||
};
|
||||
};
|
||||
*/
|
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 24 KiB |
@ -33,7 +33,7 @@ class AlarmPanelCard extends StatelessWidget {
|
||||
alignment: Alignment.centerRight,
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical")),
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.entity.entity))
|
||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entityId: card.entity.entity.entityId))
|
||||
)
|
||||
)
|
||||
]
|
||||
|
196
lib/cards/badges.dart
Normal file
@ -0,0 +1,196 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Badges extends StatelessWidget {
|
||||
final BadgesData badges;
|
||||
|
||||
const Badges({Key key, this.badges}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow();
|
||||
|
||||
if (entitiesToShow.isNotEmpty) {
|
||||
if (ConnectionManager().scrollBadges) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(height: 112),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: entitiesToShow.map((entity) =>
|
||||
EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||
child: BadgeWidget(),
|
||||
),
|
||||
handleTap: true,
|
||||
)).toList()
|
||||
),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10.0,
|
||||
runSpacing: 5,
|
||||
children: entitiesToShow.map((entity) =>
|
||||
EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: BadgeWidget(),
|
||||
handleTap: true,
|
||||
)).toList(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
class BadgeWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entityModel = EntityModel.of(context);
|
||||
Widget badgeIcon;
|
||||
String onBadgeTextValue;
|
||||
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||
switch (entityModel.entityWrapper.entity.domain) {
|
||||
case "sun":
|
||||
{
|
||||
IconData iconData;
|
||||
if (entityModel.entityWrapper.entity.state == "below_horizon") {
|
||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc);
|
||||
} else {
|
||||
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8);
|
||||
}
|
||||
badgeIcon = Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(
|
||||
iconData,
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "camera":
|
||||
case "media_player":
|
||||
case "binary_sensor":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
imagePadding: EdgeInsets.all(0.0),
|
||||
iconPadding: EdgeInsets.all(10),
|
||||
color: Theme.of(context).textTheme.body2.color
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
{
|
||||
badgeIcon = EntityIcon(
|
||||
imagePadding: EdgeInsets.all(0.0),
|
||||
iconPadding: EdgeInsets.all(10),
|
||||
color: Theme.of(context).textTheme.body2.color
|
||||
);
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.body1
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Widget onBadgeText;
|
||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||
} else {
|
||||
onBadgeText = Container(
|
||||
constraints: BoxConstraints(maxWidth: 50),
|
||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||
child: Text("$onBadgeTextValue",
|
||||
style: Theme.of(context).textTheme.overline.copyWith(
|
||||
color: HAClientTheme().getOnBadgeTextColor()
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis
|
||||
),
|
||||
decoration: new BoxDecoration(
|
||||
color: iconColor,
|
||||
borderRadius: BorderRadius.circular(9.0),
|
||||
)
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
overflow: Overflow.visible,
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: new BoxDecoration(
|
||||
// Circle shape
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).cardColor,
|
||||
// The border you want
|
||||
border: Border.all(
|
||||
width: 2.0,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 41,
|
||||
height: 41,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.center,
|
||||
child: badgeIcon,
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
bottom: -6,
|
||||
child: onBadgeText
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 45),
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.displayName}",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
fontSize: 10
|
||||
),
|
||||
softWrap: true,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
onTap: () => entityModel.entityWrapper.handleTap(),
|
||||
onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(),
|
||||
onLongPress: () => entityModel.entityWrapper.handleHold(),
|
||||
);
|
||||
}
|
||||
}
|
@ -11,66 +11,94 @@ class CardData {
|
||||
|
||||
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
||||
|
||||
factory CardData.parse(Map<String, dynamic> rawData) {
|
||||
switch (rawData['type']) {
|
||||
case CardType.ENTITIES:
|
||||
return EntitiesCardData(rawData);
|
||||
break;
|
||||
case CardType.ALARM_PANEL:
|
||||
return AlarmPanelCardData(rawData);
|
||||
break;
|
||||
case CardType.BUTTON:
|
||||
return ButtonCardData(rawData);
|
||||
break;
|
||||
case CardType.ENTITY_BUTTON:
|
||||
return ButtonCardData(rawData);
|
||||
break;
|
||||
case CardType.CONDITIONAL:
|
||||
return CardData.parse(rawData['card']);
|
||||
break;
|
||||
case CardType.ENTITY_FILTER:
|
||||
Map<String, dynamic> cardData = Map.from(rawData);
|
||||
cardData.remove('type');
|
||||
if (rawData.containsKey('card')) {
|
||||
cardData.addAll(rawData['card']);
|
||||
factory CardData.parse(rawData) {
|
||||
try {
|
||||
if (rawData['type'] == null) {
|
||||
rawData['type'] = CardType.ENTITIES;
|
||||
} else if (!(rawData['type'] is String)) {
|
||||
return CardData(null);
|
||||
}
|
||||
switch (rawData['type']) {
|
||||
case CardType.ENTITIES:
|
||||
case CardType.HISTORY_GRAPH:
|
||||
case CardType.MAP:
|
||||
case CardType.PICTURE_GLANCE:
|
||||
case CardType.SENSOR:
|
||||
case CardType.ENTITY:
|
||||
case CardType.WEATHER_FORECAST:
|
||||
case CardType.PLANT_STATUS:
|
||||
if (rawData['entity'] != null) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return EntitiesCardData(rawData);
|
||||
break;
|
||||
case CardType.ALARM_PANEL:
|
||||
return AlarmPanelCardData(rawData);
|
||||
break;
|
||||
case CardType.ENTITY_BUTTON:
|
||||
case CardType.LIGHT:
|
||||
case CardType.BUTTON:
|
||||
case CardType.PICTURE_ENTITY:
|
||||
return ButtonCardData(rawData);
|
||||
break;
|
||||
case CardType.CONDITIONAL:
|
||||
return CardData.parse(rawData['card']);
|
||||
break;
|
||||
case CardType.ENTITY_FILTER:
|
||||
Map cardData = Map.from(rawData);
|
||||
cardData.remove('type');
|
||||
if (rawData.containsKey('card')) {
|
||||
cardData.addAll(rawData['card']);
|
||||
}
|
||||
cardData['type'] ??= CardType.ENTITIES;
|
||||
return CardData.parse(cardData);
|
||||
break;
|
||||
case CardType.GAUGE:
|
||||
return GaugeCardData(rawData);
|
||||
break;
|
||||
case CardType.GLANCE:
|
||||
case CardType.THERMOSTAT:
|
||||
if (rawData['entity'] != null) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
}
|
||||
return GlanceCardData(rawData);
|
||||
break;
|
||||
case CardType.HORIZONTAL_STACK:
|
||||
return HorizontalStackCardData(rawData);
|
||||
break;
|
||||
case CardType.VERTICAL_STACK:
|
||||
return VerticalStackCardData(rawData);
|
||||
break;
|
||||
case CardType.MARKDOWN:
|
||||
return MarkdownCardData(rawData);
|
||||
break;
|
||||
case CardType.MEDIA_CONTROL:
|
||||
return MediaControlCardData(rawData);
|
||||
break;
|
||||
case CardType.BADGES:
|
||||
return BadgesData(rawData);
|
||||
break;
|
||||
default:
|
||||
return CardData(null);
|
||||
}
|
||||
cardData['type'] ??= CardType.ENTITIES;
|
||||
return CardData.parse(cardData);
|
||||
break;
|
||||
case CardType.GAUGE:
|
||||
return GaugeCardData(rawData);
|
||||
break;
|
||||
case CardType.GLANCE:
|
||||
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;
|
||||
default:
|
||||
if (rawData.containsKey('entities')) {
|
||||
return EntitiesCardData(rawData);
|
||||
} else if (rawData.containsKey('entity')) {
|
||||
rawData['entities'] = [rawData['entity']];
|
||||
return EntitiesCardData(rawData);
|
||||
}
|
||||
return CardData(rawData);
|
||||
} catch (error, stacktrace) {
|
||||
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
|
||||
return ErrorCardData(rawData);
|
||||
}
|
||||
}
|
||||
|
||||
CardData(Map<String, dynamic> rawData) {
|
||||
type = rawData['type'] ?? CardType.ENTITIES;
|
||||
conditions = rawData['conditions'] ?? [];
|
||||
showEmpty = rawData['show_empty'] ?? true;
|
||||
stateFilter = rawData['state_filter'] ?? [];
|
||||
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() {
|
||||
@ -79,7 +107,7 @@ class CardData {
|
||||
|
||||
List<EntityWrapper> getEntitiesToShow() {
|
||||
return entities.where((entityWrapper) {
|
||||
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
|
||||
if (entityWrapper.entity.isHidden) {
|
||||
return false;
|
||||
}
|
||||
List currentStateFilter;
|
||||
@ -136,9 +164,8 @@ class CardData {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -148,6 +175,64 @@ class CardData {
|
||||
|
||||
}
|
||||
|
||||
class BadgesData extends CardData {
|
||||
|
||||
String title;
|
||||
String icon;
|
||||
bool showHeaderToggle;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return Badges(badges: this);
|
||||
}
|
||||
|
||||
BadgesData(rawData) : super(rawData) {
|
||||
if (rawData['badges'] is List) {
|
||||
rawData['badges'].forEach((dynamic rawBadge) {
|
||||
if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {
|
||||
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge)));
|
||||
} else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) {
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawBadge['entity']),
|
||||
overrideName: rawBadge["name"],
|
||||
overrideIcon: rawBadge["icon"],
|
||||
)
|
||||
);
|
||||
} else if (rawBadge is Map && rawBadge.containsKey('entities')) {
|
||||
_parseEntities(rawBadge);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parseEntities(rawData) {
|
||||
var rawEntities = rawData['entities'] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
entities.add(EntityWrapper(
|
||||
entity: HomeAssistant().entities.get(rawEntity),
|
||||
stateFilter: rawData['state_filter'] ?? [],
|
||||
));
|
||||
}
|
||||
} else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) {
|
||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||
entities.add(
|
||||
EntityWrapper(
|
||||
entity: e,
|
||||
overrideName: rawEntity["name"],
|
||||
overrideIcon: rawEntity["icon"],
|
||||
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
|
||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EntitiesCardData extends CardData {
|
||||
|
||||
String title;
|
||||
@ -159,14 +244,14 @@ class EntitiesCardData extends CardData {
|
||||
return EntitiesCard(card: this);
|
||||
}
|
||||
|
||||
EntitiesCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
EntitiesCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData["title"];
|
||||
icon = rawData['icon'];
|
||||
title = rawData['title'];
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
stateColor = rawData['state_color'] ?? false;
|
||||
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
||||
//Parsing entities
|
||||
var rawEntities = rawData["entities"] ?? [];
|
||||
var rawEntities = rawData['entities'] ?? [];
|
||||
rawEntities.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||
@ -249,7 +334,7 @@ class AlarmPanelCardData extends CardData {
|
||||
return AlarmPanelCard(card: this);
|
||||
}
|
||||
|
||||
AlarmPanelCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
AlarmPanelCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
states = rawData['states'];
|
||||
@ -285,10 +370,10 @@ class ButtonCardData extends CardData {
|
||||
return EntityButtonCard(card: this);
|
||||
}
|
||||
|
||||
ButtonCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
ButtonCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
icon = rawData['icon'];
|
||||
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||
showName = rawData['show_name'] ?? true;
|
||||
showIcon = rawData['show_icon'] ?? true;
|
||||
stateColor = rawData['state_color'] ?? true;
|
||||
@ -339,8 +424,8 @@ class GaugeCardData extends CardData {
|
||||
|
||||
String name;
|
||||
String unit;
|
||||
int min;
|
||||
int max;
|
||||
double min;
|
||||
double max;
|
||||
Map severity;
|
||||
|
||||
@override
|
||||
@ -348,15 +433,27 @@ class GaugeCardData extends CardData {
|
||||
return GaugeCard(card: this);
|
||||
}
|
||||
|
||||
GaugeCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
GaugeCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
name = rawData['name'];
|
||||
unit = rawData['unit'];
|
||||
min = rawData['min'] ?? 0;
|
||||
max = rawData['max'] ?? 100;
|
||||
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"];
|
||||
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(
|
||||
@ -367,7 +464,7 @@ class GaugeCardData extends CardData {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
}
|
||||
} else {
|
||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
||||
}
|
||||
|
||||
}
|
||||
@ -388,7 +485,7 @@ class GlanceCardData extends CardData {
|
||||
return GlanceCard(card: this);
|
||||
}
|
||||
|
||||
GlanceCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
GlanceCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData["title"];
|
||||
showName = rawData['show_name'] ?? true;
|
||||
@ -436,8 +533,8 @@ class HorizontalStackCardData extends CardData {
|
||||
return HorizontalStackCard(card: this);
|
||||
}
|
||||
|
||||
HorizontalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards')) {
|
||||
HorizontalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
}).toList();
|
||||
@ -457,8 +554,8 @@ class VerticalStackCardData extends CardData {
|
||||
return VerticalStackCard(card: this);
|
||||
}
|
||||
|
||||
VerticalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards')) {
|
||||
VerticalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
}).toList();
|
||||
@ -479,7 +576,7 @@ class MarkdownCardData extends CardData {
|
||||
return MarkdownCard(card: this);
|
||||
}
|
||||
|
||||
MarkdownCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
MarkdownCardData(rawData) : super(rawData) {
|
||||
//Parsing card data
|
||||
title = rawData['title'];
|
||||
content = rawData['content'];
|
||||
@ -494,7 +591,7 @@ class MediaControlCardData extends CardData {
|
||||
return MediaControlsCard(card: this);
|
||||
}
|
||||
|
||||
MediaControlCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
MediaControlCardData(rawData) : super(rawData) {
|
||||
var entitiId = rawData["entity"];
|
||||
if (entitiId != null && entitiId is String) {
|
||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||
@ -508,3 +605,18 @@ class MediaControlCardData extends CardData {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ErrorCardData extends CardData {
|
||||
|
||||
String cardConfig;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return ErrorCard(card: this);
|
||||
}
|
||||
|
||||
ErrorCardData(rawData) : super(rawData) {
|
||||
cardConfig = '$rawData';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -62,13 +62,16 @@ class EntityButtonCard extends StatelessWidget {
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buttonIcon,
|
||||
_buildName(context)
|
||||
],
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buttonIcon,
|
||||
_buildName(context)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
|
38
lib/cards/error_card.dart
Normal file
@ -0,0 +1,38 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ErrorCard extends StatelessWidget {
|
||||
final ErrorCardData card;
|
||||
|
||||
const ErrorCard({Key key, this.card}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardWrapper(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
||||
},
|
||||
child: Text('Copy card config'),
|
||||
),
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
||||
},
|
||||
child: Text('Report issue'),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -110,35 +110,38 @@ class GaugeCard extends StatelessWidget {
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: <Widget>[
|
||||
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,
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
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,
|
||||
|
@ -30,10 +30,13 @@ class GlanceCard extends StatelessWidget {
|
||||
start, end
|
||||
).map(
|
||||
(EntityWrapper entity){
|
||||
return EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: _buildEntityContainer(context, entity),
|
||||
handleTap: true
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: _buildEntityContainer(context, entity),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
).toList()
|
||||
@ -50,17 +53,22 @@ class GlanceCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return CardWrapper(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CardHeader(name: card.title),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||
child: Table(
|
||||
children: rows
|
||||
)
|
||||
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
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -85,16 +93,14 @@ class GlanceCard extends StatelessWidget {
|
||||
result.add(_buildState());
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: InkResponse(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: result,
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
return InkResponse(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: result,
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,14 @@ class MediaControlsCard extends StatelessWidget {
|
||||
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(
|
||||
|
@ -7,11 +7,6 @@ class UnsupportedCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardWrapper(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
||||
child: Text("'${card.type}' card is not supported yet"),
|
||||
)
|
||||
);
|
||||
return Container();
|
||||
}
|
||||
}
|
@ -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,12 +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 {
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -36,7 +36,6 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 3,
|
||||
|
@ -79,7 +79,8 @@ class Entity {
|
||||
);
|
||||
|
||||
String get displayName =>
|
||||
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
|
||||
attributes["friendly_name"] ??
|
||||
(attributes["name"] ?? (entityId != null && entityId.contains('.')) ? entityId.split(".")[1].replaceAll("_", " ") : "");
|
||||
|
||||
bool get isView =>
|
||||
(domain == "group") &&
|
||||
@ -148,7 +149,7 @@ class Entity {
|
||||
|
||||
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"];
|
||||
@ -209,14 +210,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();
|
||||
|
@ -3,12 +3,17 @@ part of '../main.dart';
|
||||
class EntityIcon extends StatelessWidget {
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
final EdgeInsetsGeometry iconPadding;
|
||||
final EdgeInsetsGeometry imagePadding;
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
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.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,40 +28,6 @@ class EntityIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildIcon(EntityWrapper data, Color color) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
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}"
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
String iconName = data.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||
} else {
|
||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||
data.entity.deviceClass, data.entity.state); //
|
||||
}
|
||||
return Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
@ -68,12 +39,92 @@ class EntityIcon extends StatelessWidget {
|
||||
} else {
|
||||
iconColor = HAClientTheme().getOffStateColor(context);
|
||||
}
|
||||
Widget iconWidget;
|
||||
bool isPicture = false;
|
||||
if (entityWrapper == null) {
|
||||
iconWidget = Container(
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
} else {
|
||||
if (entityWrapper.entityPicture != null) {
|
||||
iconWidget = Container(
|
||||
height: size+12,
|
||||
width: size+12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit:BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
"${entityWrapper.entityPicture}"
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
isPicture = true;
|
||||
} else {
|
||||
String iconName = entityWrapper.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||
} else {
|
||||
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
|
||||
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
|
||||
}
|
||||
if (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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
EdgeInsetsGeometry computedPadding;
|
||||
if (isPicture && imagePadding != null) {
|
||||
computedPadding = imagePadding;
|
||||
} else if (!isPicture && iconPadding != null) {
|
||||
computedPadding = iconPadding;
|
||||
} else {
|
||||
computedPadding = padding;
|
||||
}
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: buildIcon(
|
||||
entityWrapper,
|
||||
iconColor
|
||||
),
|
||||
padding: computedPadding,
|
||||
child: iconWidget,
|
||||
);
|
||||
}
|
||||
}
|
@ -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)),
|
||||
|
@ -54,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('${ConnectionManager().httpWebHost}${uiAction.tapService}');
|
||||
} else {
|
||||
Launcher.launchURL(uiAction.tapService);
|
||||
Launcher.launchURLInBrowser(uiAction.tapService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -94,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('${ConnectionManager().httpWebHost}${uiAction.holdService}');
|
||||
} else {
|
||||
Launcher.launchURL(uiAction.holdService);
|
||||
Launcher.launchURLInBrowser(uiAction.holdService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -134,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('${ConnectionManager().httpWebHost}${uiAction.doubleTapService}');
|
||||
} else {
|
||||
Launcher.launchURL(uiAction.doubleTapService);
|
||||
Launcher.launchURLInBrowser(uiAction.doubleTapService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -154,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"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
23
lib/entities/error_entity_widget.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class ErrorEntityWidget extends StatelessWidget {
|
||||
|
||||
final String text;
|
||||
|
||||
ErrorEntityWidget({
|
||||
Key key, this.text
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
String errorText = text ?? "Entity error: ${entityModel.entityWrapper.entity?.entityId}";
|
||||
return Container(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5.0),
|
||||
child: Text(errorText),
|
||||
),
|
||||
color: Theme.of(context).errorColor,
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
|
@ -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) {
|
||||
|
@ -27,7 +27,7 @@ class SimpleEntityState extends StatelessWidget {
|
||||
tStyle = textStyle;
|
||||
} 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;
|
||||
|
@ -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) &&
|
||||
|
@ -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 {
|
||||
|
@ -30,8 +30,8 @@ class _TimerStateState extends State<TimerState> {
|
||||
.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);
|
||||
}
|
||||
});
|
||||
|
@ -2,17 +2,20 @@ part of '../main.dart';
|
||||
|
||||
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 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.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() {
|
||||
@ -35,32 +38,36 @@ class UniversalSliderState extends State<UniversalSlider> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List <Widget> row = [];
|
||||
List <Widget> col = [];
|
||||
if (widget.leading != null) {
|
||||
row.add(widget.leading);
|
||||
}
|
||||
row.add(
|
||||
Flexible(
|
||||
child: Slider(
|
||||
value: _value,
|
||||
min: widget.min,
|
||||
max: widget.max,
|
||||
onChangeStart: (_) {
|
||||
_changeStarted = true;
|
||||
value: _value ?? math.max(widget.max ?? 100, _value ?? 0),
|
||||
min: widget.min ?? 0,
|
||||
max: widget.max ?? 100,
|
||||
activeColor: widget.activeColor,
|
||||
onChangeStart: (value) {
|
||||
_changeStarted = true;
|
||||
widget.onChangeStart?.call(value);
|
||||
},
|
||||
divisions: widget.divisions,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
widget.onChanged(value);
|
||||
widget.onChanged?.call(value);
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
_changeStarted = false;
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
Timer(Duration(milliseconds: 500), () {
|
||||
if (!_changeStarted) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
widget.onChangeEnd(value);
|
||||
widget.onChangeEnd?.call(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -70,20 +77,25 @@ class UniversalSliderState extends State<UniversalSlider> {
|
||||
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: widget.padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text('${widget.title}'),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: row,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,)
|
||||
],
|
||||
children: col,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ 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;
|
||||
@ -79,7 +79,7 @@ class HomeAssistant {
|
||||
_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);
|
||||
@ -122,9 +122,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');
|
||||
}
|
||||
@ -139,38 +139,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);
|
||||
Logger.d('stream: ${_instanceConfig['components'].contains('stream')}');
|
||||
_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 +183,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,7 +194,7 @@ class HomeAssistant {
|
||||
} else {
|
||||
Completer completer = Completer();
|
||||
var additionalData;
|
||||
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||
if (ConnectionManager().haVersion >= 107 && _lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||
additionalData = {
|
||||
'url_path': _lovelaceDashbordUrl
|
||||
};
|
||||
@ -204,29 +206,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 +240,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 +261,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)}";
|
||||
@ -316,7 +333,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(
|
||||
@ -335,57 +351,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;
|
||||
}
|
||||
}*/
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@ -23,8 +25,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/dynamic_multi_column_layout.dart';
|
||||
import 'plugins/spoiler_card.dart';
|
||||
import 'package:workmanager/workmanager.dart' as workManager;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
//import 'package:workmanager/workmanager.dart' as workManager;
|
||||
//import 'package:geolocator/geolocator.dart';
|
||||
import 'package:background_locator/background_locator.dart';
|
||||
import 'package:background_locator/location_dto.dart';
|
||||
import 'package:background_locator/location_settings.dart';
|
||||
import 'package:battery/battery.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||
@ -58,7 +63,6 @@ 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';
|
||||
@ -100,6 +104,7 @@ 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';
|
||||
@ -112,8 +117,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';
|
||||
@ -142,6 +147,7 @@ part 'cards/horizontal_srack_card.dart';
|
||||
part 'cards/markdown_card.dart';
|
||||
part 'cards/media_control_card.dart';
|
||||
part 'cards/unsupported_card.dart';
|
||||
part 'cards/error_card.dart';
|
||||
part 'cards/vertical_stack_card.dart';
|
||||
part 'cards/glance_card.dart';
|
||||
part 'pages/play_media.page.dart';
|
||||
@ -150,19 +156,22 @@ 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';
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = "HA Client";
|
||||
const appVersionNumber = "0.8.5";
|
||||
const appVersionAdd = "";
|
||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||
const String appName = 'HA Client';
|
||||
const appVersionNumber = '1.0.1';
|
||||
final String appVersionAdd = secrets['version_type'] ?? '';
|
||||
final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd';
|
||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.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);
|
||||
@ -170,11 +179,11 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Crashlytics.instance.enableInDevMode = true;
|
||||
Crashlytics.instance.enableInDevMode = false;
|
||||
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);
|
||||
}
|
||||
@ -209,9 +218,12 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
|
||||
StreamSubscription _themeChangeSubscription;
|
||||
AppTheme _currentTheme = AppTheme.defaultTheme;
|
||||
|
||||
ReceivePort port = ReceivePort();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
@ -224,20 +236,29 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
_currentTheme = event.theme;
|
||||
});
|
||||
});
|
||||
/*
|
||||
workManager.Workmanager.initialize(
|
||||
updateDeviceLocationIsolate,
|
||||
isInDebugMode: false
|
||||
);
|
||||
*/
|
||||
super.initState();
|
||||
IsolateNameServer.registerPortWithName(port.sendPort, LocationManager.isolateName);
|
||||
port.listen((dynamic data) {
|
||||
// do something with data
|
||||
});
|
||||
initPlatformState();
|
||||
}
|
||||
|
||||
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}");
|
||||
@ -247,6 +268,10 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initPlatformState() async {
|
||||
await BackgroundLocator.initialize();
|
||||
}
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -266,7 +291,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(
|
||||
@ -278,22 +302,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}",
|
||||
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()
|
||||
));
|
||||
},
|
||||
)
|
||||
],
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
@ -19,6 +19,9 @@ class ConnectionManager {
|
||||
String _tempToken;
|
||||
String oauthUrl;
|
||||
String webhookId;
|
||||
double haVersion;
|
||||
bool scrollBadges;
|
||||
String mobileAppDeviceName;
|
||||
bool settingsLoaded = false;
|
||||
int appIntegrationVersion;
|
||||
bool get isAuthenticated => _token != null;
|
||||
@ -45,35 +48,41 @@ class ConnectionManager {
|
||||
_port = prefs.getString('hassio-port');
|
||||
webhookId = prefs.getString('app-webhook-id');
|
||||
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
||||
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";
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
Logger.d('$_domain$_port');
|
||||
if (_domain == null && _port == null && webhookId == null && mobileAppDeviceName == null) {
|
||||
completer.completeError(HACNotSetUpException());
|
||||
stopInit = true;
|
||||
} else if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
completer.completeError(HACException.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
} else {
|
||||
final storage = new FlutterSecureStorage();
|
||||
try {
|
||||
_token = await storage.read(key: "hacl_llt");
|
||||
Logger.e("Long-lived token read successful");
|
||||
Logger.d("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.");
|
||||
} catch (e, stacktrace) {
|
||||
completer.completeError(HACException("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||
Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
completer.completeError(HACException.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
}
|
||||
}
|
||||
@ -106,13 +115,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()}"));
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -148,7 +157,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) {
|
||||
haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
|
||||
}
|
||||
Logger.d("Home assistant version: $v ($haVersion)");
|
||||
Crashlytics.instance.setString('ha_version', v);
|
||||
Logger.d("[Connection] Subscribing to events");
|
||||
sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
@ -166,9 +180,9 @@ class ConnectionManager {
|
||||
});
|
||||
} 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 +192,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 +225,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,20 +249,20 @@ 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")));
|
||||
});
|
||||
}
|
||||
|
||||
@ -281,7 +295,7 @@ class ConnectionManager {
|
||||
});
|
||||
}).catchError((e) => completer.completeError(e));
|
||||
} else {
|
||||
completer.completeError(HAError("General login error"));
|
||||
completer.completeError(HACException("General login error"));
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
@ -316,7 +330,7 @@ class ConnectionManager {
|
||||
throw e;
|
||||
});
|
||||
}).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 +357,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 +386,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;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class LocationManager {
|
||||
defaultUpdateIntervalMinutes);
|
||||
_isRunning = prefs.getBool("location-enabled") ?? false;
|
||||
if (_isRunning) {
|
||||
await _startLocationService();
|
||||
//await _startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,20 @@ class LocationManager {
|
||||
}
|
||||
|
||||
_startLocationService() async {
|
||||
Logger.d('Starting location tracking');
|
||||
BackgroundLocator.registerLocationUpdate(
|
||||
locationCallback,
|
||||
//optional
|
||||
androidNotificationCallback: locationNotificationCallback,
|
||||
settings: LocationSettings(
|
||||
notificationTitle: "HA Client location tracking",
|
||||
notificationMsg: "HA Client is updating your device location",
|
||||
wakeLockTime: 20,
|
||||
autoStop: false,
|
||||
interval: 10
|
||||
),
|
||||
);
|
||||
/*
|
||||
String webhookId = ConnectionManager().webhookId;
|
||||
String httpWebHost = ConnectionManager().httpWebHost;
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
@ -100,53 +114,129 @@ class LocationManager {
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
_stopLocationService() async {
|
||||
Logger.d("Canceling previous schedule if any...");
|
||||
await workManager.Workmanager.cancelAll();
|
||||
Logger.d('Stopping location tracking');
|
||||
IsolateNameServer.removePortNameMapping(isolateName);
|
||||
BackgroundLocator.unRegisterLocationUpdate();
|
||||
/*Logger.d("Canceling previous schedule if any...");
|
||||
await workManager.Workmanager.cancelAll();*/
|
||||
}
|
||||
|
||||
static const String isolateName = "HAClientLocatorIsolate";
|
||||
|
||||
static void locationCallback(LocationDto locationDto) async {
|
||||
print('[Background location] Got location: $locationDto');
|
||||
sendLocationData(locationDto);
|
||||
final SendPort send = IsolateNameServer.lookupPortByName(isolateName);
|
||||
send?.send(locationDto);
|
||||
}
|
||||
|
||||
static Future<void> sendLocationData(LocationDto location) async {
|
||||
print('[Background location] Loading settings...');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String domain = prefs.getString('hassio-domain');
|
||||
String port = prefs.getString('hassio-port');
|
||||
String webhookId = prefs.getString('app-webhook-id');
|
||||
String httpWebHost =
|
||||
"${prefs.getString('hassio-res-protocol')}://$domain:$port";
|
||||
if (webhookId != null && webhookId.isNotEmpty) {
|
||||
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||
Map<String, String> headers = {};
|
||||
headers["Content-Type"] = "application/json";
|
||||
Map data = {
|
||||
"type": "update_location",
|
||||
"data": {
|
||||
"gps": [],
|
||||
"gps_accuracy": 0,
|
||||
"battery": 100
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (location.longitude != null && location.latitude != null) {
|
||||
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||
data["data"]["gps_accuracy"] = location.accuracy;
|
||||
print('[Background location] Sending...');
|
||||
try {
|
||||
http.Response response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: json.encode(data)
|
||||
);
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
print('[Background location] Success!');
|
||||
} else {
|
||||
print('[Background location] Error sending data: ${response.statusCode}');
|
||||
}
|
||||
} catch(e) {
|
||||
print('[Background location] Error sending data: $e');
|
||||
}
|
||||
} else {
|
||||
print('[Background location] Error. Location is null');
|
||||
}
|
||||
} catch (e) {
|
||||
print('[Background location] Error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void locationNotificationCallback() {
|
||||
print('[Background location] User clicked on the notification');
|
||||
}
|
||||
|
||||
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 = 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
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
void updateDeviceLocationIsolate() {
|
||||
workManager.Workmanager.executeTask((backgroundTask, data) async {
|
||||
//print("[Background $backgroundTask] Started");
|
||||
@ -234,4 +324,5 @@ void updateDeviceLocationIsolate() {
|
||||
print("[Background $backgroundTask] Finished.");*/
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
@ -16,20 +16,31 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
};
|
||||
|
||||
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 Future checkAppRegistration() {
|
||||
Completer completer = Completer();
|
||||
_appRegistrationData["device_name"] = "${HomeAssistant().userName}'s ${DeviceInfoManager().model}";
|
||||
_appRegistrationData["device_name"] = ConnectionManager().mobileAppDeviceName ?? getDefaultDeviceName();
|
||||
(_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...");
|
||||
if (ConnectionManager().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 (ConnectionManager().haVersion >= 104) {
|
||||
registrationData.addAll({
|
||||
"device_id": "${DeviceInfoManager().unicDeviceId}"
|
||||
});
|
||||
}
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/api/mobile_app/registrations",
|
||||
includeAuthHeader: true,
|
||||
@ -44,19 +55,26 @@ class MobileAppIntegrationManager {
|
||||
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
|
||||
|
||||
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 {
|
||||
@ -77,7 +95,7 @@ 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) {
|
||||
@ -85,35 +103,19 @@ class MobileAppIntegrationManager {
|
||||
_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"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
});
|
||||
@ -121,31 +123,43 @@ class MobileAppIntegrationManager {
|
||||
}
|
||||
}
|
||||
|
||||
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 to GitHub",
|
||||
negativeText: "Report to Discord",
|
||||
onPositive: () {
|
||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
onNegative: () {
|
||||
Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
|
||||
},
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
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");
|
||||
},
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
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.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
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.",
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -14,13 +14,13 @@ class StartupUserMessagesManager {
|
||||
bool _supportAppDevelopmentMessageShown;
|
||||
bool _whatsNewMessageShown;
|
||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-894";
|
||||
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;
|
||||
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
|
||||
if (!_whatsNewMessageShown) {
|
||||
_showWhatsNewMessage();
|
||||
} else if (!_supportAppDevelopmentMessageShown) {
|
||||
@ -29,7 +29,8 @@ class StartupUserMessagesManager {
|
||||
}
|
||||
|
||||
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",
|
||||
@ -45,14 +46,15 @@ class StartupUserMessagesManager {
|
||||
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
||||
});
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
void _showWhatsNewMessage() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_whatsNewMessageKey, true);
|
||||
prefs.setString(_whatsNewMessageKey, whatsNewUrl);
|
||||
eventBus.fire(ShowPageEvent(path: "/whats-new"));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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 = [
|
||||
|
@ -35,7 +35,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
}
|
||||
|
||||
_getEntity() {
|
||||
_entity = HomeAssistant().entities.get(widget.entityId);
|
||||
_entity = HomeAssistant().entities?.get(widget.entityId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -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
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -18,12 +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;
|
||||
@ -105,7 +105,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
StartupUserMessagesManager().checkMessagesToShow();
|
||||
});
|
||||
}, onError: (e) {
|
||||
_setErrorState(e);
|
||||
if (e is HACNotSetUpException) {
|
||||
Navigator.of(context).pushReplacementNamed('/quick-start');
|
||||
} else {
|
||||
_setErrorState(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -131,10 +135,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
});
|
||||
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());
|
||||
@ -178,27 +182,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_fullLoad();
|
||||
});
|
||||
}
|
||||
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 (_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) {
|
||||
@ -211,7 +205,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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -254,53 +249,16 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
|
||||
}
|
||||
|
||||
_setErrorState(HAError e) {
|
||||
_setErrorState(HACException e) {
|
||||
if (e == null) {
|
||||
_bottomInfoBarController.showErrorBottomBar(
|
||||
HAError("Unknown error")
|
||||
HACException("Unknown error")
|
||||
);
|
||||
} else {
|
||||
_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) {
|
||||
_bottomInfoBarController.showInfoBottomBar(
|
||||
message: "Calling $domain.$service",
|
||||
@ -309,6 +267,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
}
|
||||
|
||||
void _showEntityPage(String entityId) {
|
||||
Logger.d('Showing entity page: $entityId');
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@ -384,20 +343,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");
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
@ -415,7 +366,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(
|
||||
@ -423,7 +377,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(
|
||||
@ -438,7 +392,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",
|
||||
@ -580,9 +534,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')
|
||||
)
|
||||
]
|
||||
)
|
||||
@ -614,13 +582,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
icon: mediaMenuIcon,
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100.0, 50, 0.0),
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 50, 0),
|
||||
context: context,
|
||||
items: mediaMenuItems
|
||||
).then((String val) {
|
||||
if (val == "play_media") {
|
||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
|
||||
} else {
|
||||
} else if (val != null) {
|
||||
_showEntityPage(val);
|
||||
}
|
||||
});
|
||||
@ -631,7 +599,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
"mdi:dots-vertical"), color: Colors.white,),
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100, 0.0, 0.0),
|
||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 0.0, 0.0),
|
||||
context: context,
|
||||
items: serviceMenuItems
|
||||
).then((String val) {
|
||||
@ -704,8 +672,7 @@ 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();
|
||||
|
@ -105,7 +105,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
||||
);
|
||||
HomeAssistant().sendFromPlayerId = null;
|
||||
}
|
||||
eventBus.fire(ShowEntityPageEvent(entity: entity));
|
||||
eventBus.fire(ShowEntityPageEvent(entityId: entity.entityId));
|
||||
}
|
||||
}
|
||||
|
||||
|
48
lib/pages/quick_start.page.dart
Normal 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();
|
||||
}
|
||||
}
|
@ -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 (ConnectionManager().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
|
||||
),
|
||||
|
@ -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 || ConnectionManager().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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
});
|
||||
}
|
||||
|
||||
void incLocationInterval() {
|
||||
void _incLocationInterval() {
|
||||
if (_locationInterval < 720) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval + 5;
|
||||
@ -44,7 +44,7 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void decLocationInterval() {
|
||||
void _decLocationInterval() {
|
||||
if (_locationInterval > 5) {
|
||||
setState(() {
|
||||
_locationInterval = _locationInterval - 5;
|
||||
@ -52,50 +52,12 @@ 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();
|
||||
}
|
||||
*/
|
||||
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||
setState(() {
|
||||
_wait = false;
|
||||
@ -146,42 +108,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)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class LookAndFeelSettingsPage extends StatefulWidget {
|
||||
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
|
||||
AppTheme _currentTheme;
|
||||
bool _changed = false;
|
||||
bool _scrollBadges = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -25,14 +25,16 @@ 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];
|
||||
_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,6 +42,12 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future _saveOther() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
ConnectionManager().scrollBadges = _scrollBadges;
|
||||
await prefs.setBool('scroll-badges', _scrollBadges);
|
||||
}
|
||||
|
||||
Map appThemeName = {
|
||||
AppTheme.defaultTheme: 'Default',
|
||||
AppTheme.haTheme: 'Home Assistant theme',
|
||||
@ -59,15 +67,35 @@ 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;
|
||||
});
|
||||
_saveOther();
|
||||
},
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -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.5.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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,12 +40,12 @@ class BottomInfoBarController {
|
||||
}
|
||||
}
|
||||
|
||||
void showErrorBottomBar(HAError error) {
|
||||
void showErrorBottomBar(HACException error) {
|
||||
actions.clear();
|
||||
actions.addAll(error.actions);
|
||||
bottomBarErrorColor = true;
|
||||
bottomBarProgress = false;
|
||||
bottomBarText = "${error.message}";
|
||||
bottomBarText = "${error.cause}";
|
||||
if (show == null) {
|
||||
initialState = true;
|
||||
} else {
|
||||
|
@ -24,17 +24,11 @@ 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;
|
||||
@ -68,10 +62,6 @@ class Panel {
|
||||
|
||||
Widget getWidget() {
|
||||
switch (componentName) {
|
||||
case "config": {
|
||||
return ConfigPanelWidget();
|
||||
}
|
||||
|
||||
default: {
|
||||
return Text("Unsupported panel component: $componentName");
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
});
|
||||
}
|
||||
}).catchError((e) {
|
||||
Logger.e("Error loading $entityId history: $e");
|
||||
Logger.e("Error loading $entityId history: $e", skipCrashlytics: true);
|
||||
if (!_disposed) {
|
||||
setState(() {
|
||||
_history = [];
|
||||
|
213
lib/popups.dart
Normal file
@ -0,0 +1,213 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class Popup {
|
||||
final String title;
|
||||
final String body;
|
||||
final String positiveText;
|
||||
final String negativeText;
|
||||
final onPositive;
|
||||
final onNegative;
|
||||
|
||||
Popup({@required this.title, @required this.body, this.positiveText, this.negativeText, this.onPositive, this.onNegative});
|
||||
|
||||
Future show(BuildContext context) {
|
||||
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
|
||||
return 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenLoginPopup extends Popup {
|
||||
|
||||
TokenLoginPopup() : super(title: 'Login with long-lived token', body: '');
|
||||
|
||||
final _tokenLoginFormKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
Future show(BuildContext context) {
|
||||
return showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// return object of type Dialog
|
||||
return SimpleDialog(
|
||||
title: new Text('Login with long-lived token'),
|
||||
children: <Widget>[
|
||||
Form(
|
||||
key: _tokenLoginFormKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: TextFormField(
|
||||
onSaved: (newValue) {
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.write(key: "hacl_llt", value: newValue.trim()).then((_) {
|
||||
Navigator.of(context).pop();
|
||||
eventBus.fire(SettingsChangedEvent(true));
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter long-lived token',
|
||||
contentPadding: EdgeInsets.all(0),
|
||||
hintStyle: Theme.of(context).textTheme.subhead.copyWith(
|
||||
color: Theme.of(context).textTheme.overline.color
|
||||
)
|
||||
),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return 'Long-lived token can\'t be emty';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
)
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
Container(width: 10),
|
||||
FlatButton(
|
||||
child: Text('Login'),
|
||||
onPressed: () {
|
||||
if (_tokenLoginFormKey.currentState.validate()) {
|
||||
_tokenLoginFormKey.currentState.save();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RegisterAppPopup extends Popup {
|
||||
|
||||
RegisterAppPopup({String title, String body}): super(title: title, body: body);
|
||||
|
||||
final _tokenLoginFormKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
Future show(BuildContext context) {
|
||||
return showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// return object of type Dialog
|
||||
return SimpleDialog(
|
||||
title: new Text('${this.title}'),
|
||||
children: <Widget>[
|
||||
Form(
|
||||
key: _tokenLoginFormKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20),
|
||||
child: Text('${this.body}')
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: TextFormField(
|
||||
initialValue: ConnectionManager().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(),
|
||||
onSaved: (newValue) {
|
||||
String deviceName = newValue?.trim();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.remove("app-webhook-id");
|
||||
prefs.setString('app-integration-device-name', deviceName);
|
||||
ConnectionManager().webhookId = null;
|
||||
ConnectionManager().mobileAppDeviceName = deviceName;
|
||||
Navigator.of(context).pop();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Device name*',
|
||||
hintText: 'Please enter device name',
|
||||
contentPadding: EdgeInsets.all(0),
|
||||
hintStyle: Theme.of(context).textTheme.subhead.copyWith(
|
||||
color: Theme.of(context).textTheme.overline.color
|
||||
)
|
||||
),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return 'Device name can\'t be emty';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
)
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
Container(width: 10),
|
||||
FlatButton(
|
||||
child: Text('Create now'),
|
||||
onPressed: () {
|
||||
if (_tokenLoginFormKey.currentState.validate()) {
|
||||
_tokenLoginFormKey.currentState.save();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -56,30 +56,17 @@ class NotifyServiceCallEvent {
|
||||
NotifyServiceCallEvent(this.domain, this.service, this.entityId);
|
||||
}
|
||||
|
||||
class ShowPopupDialogEvent {
|
||||
final String title;
|
||||
final String body;
|
||||
final String positiveText;
|
||||
final String negativeText;
|
||||
final onPositive;
|
||||
final onNegative;
|
||||
class ShowPopupEvent {
|
||||
final Popup popup;
|
||||
final bool goBackFirst;
|
||||
|
||||
ShowPopupDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
|
||||
}
|
||||
|
||||
class ShowPopupMessageEvent {
|
||||
final String title;
|
||||
final String body;
|
||||
final String buttonText;
|
||||
final onButtonClick;
|
||||
|
||||
ShowPopupMessageEvent({this.title, this.body, this.buttonText: "Ok", this.onButtonClick});
|
||||
ShowPopupEvent({this.popup, this.goBackFirst: false});
|
||||
}
|
||||
|
||||
class ShowEntityPageEvent {
|
||||
final Entity entity;
|
||||
final String entityId;
|
||||
|
||||
ShowEntityPageEvent({this.entity});
|
||||
ShowEntityPageEvent({@required this.entityId});
|
||||
}
|
||||
|
||||
class ShowPageEvent {
|
||||
@ -90,7 +77,7 @@ class ShowPageEvent {
|
||||
}
|
||||
|
||||
class ShowErrorEvent {
|
||||
final HAError error;
|
||||
final HACException error;
|
||||
|
||||
ShowErrorEvent(this.error);
|
||||
}
|
@ -1,21 +1,33 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class HAError {
|
||||
String message;
|
||||
class HACException implements Exception {
|
||||
String cause;
|
||||
final List<HAErrorAction> actions;
|
||||
|
||||
HAError(this.message, {this.actions: const [HAErrorAction.tryAgain()]});
|
||||
HACException(this.cause, {this.actions: const [HAErrorAction.tryAgain()]});
|
||||
|
||||
HAError.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
|
||||
this.message = "Unable to connect to Home Assistant";
|
||||
HACException.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
|
||||
this.cause = "Unable to connect to Home Assistant";
|
||||
}
|
||||
|
||||
HAError.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
|
||||
this.message = "Disconnected";
|
||||
HACException.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
|
||||
this.cause = "Disconnected";
|
||||
}
|
||||
|
||||
HAError.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
|
||||
this.message = "Check connection settings";
|
||||
HACException.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
|
||||
this.cause = "Check connection settings";
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'HACException: $cause';
|
||||
}
|
||||
}
|
||||
|
||||
class HACNotSetUpException implements Exception {
|
||||
@override
|
||||
String toString() {
|
||||
return 'HA Client is not set up';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ part of '../main.dart';
|
||||
|
||||
class Launcher {
|
||||
|
||||
static void launchURL(String url) async {
|
||||
static void launchURLInBrowser(String url) async {
|
||||
if (await urlLauncher.canLaunch(url)) {
|
||||
await urlLauncher.launch(url);
|
||||
} else {
|
||||
@ -19,7 +19,6 @@ class Launcher {
|
||||
enableDefaultShare: enableDefaultShare,
|
||||
enableUrlBarHiding: true,
|
||||
showPageTitle: showPageTitle,
|
||||
animation: new CustomTabsAnimation.slideIn(),
|
||||
extraCustomTabs: <String>[
|
||||
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
|
||||
'org.mozilla.firefox',
|
||||
@ -31,7 +30,7 @@ class Launcher {
|
||||
} catch (e) {
|
||||
Logger.w("Can't open custom tab: ${e.toString()}");
|
||||
Logger.w("Launching in default browser");
|
||||
Launcher.launchURL(url);
|
||||
Launcher.launchURLInBrowser(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,11 @@
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
|
||||
enum ErrorLevel {ERROR, WARNING, DEBUG}
|
||||
|
||||
class Logger {
|
||||
|
||||
static List<String> _log = [];
|
||||
|
||||
static String getLog() {
|
||||
String res = '';
|
||||
_log.forEach((line) {
|
||||
res += "$line\n";
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
static bool get isInDebugMode {
|
||||
bool inDebugMode = false;
|
||||
|
||||
@ -27,26 +18,26 @@ class Logger {
|
||||
print(data);
|
||||
}
|
||||
|
||||
static void e(String message) {
|
||||
_writeToLog("Error", message);
|
||||
static void e(dynamic message, {dynamic stacktrace, bool skipCrashlytics: false}) {
|
||||
_writeToLog(ErrorLevel.ERROR, message.toString(), stacktrace, skipCrashlytics);
|
||||
}
|
||||
|
||||
static void w(String message) {
|
||||
_writeToLog("Warning", message);
|
||||
_writeToLog(ErrorLevel.WARNING, message, null, true);
|
||||
}
|
||||
|
||||
static void d(String message) {
|
||||
_writeToLog("Debug", message);
|
||||
_writeToLog(ErrorLevel.DEBUG, message, null, true);
|
||||
}
|
||||
|
||||
static void _writeToLog(String level, String message) {
|
||||
static void _writeToLog(ErrorLevel level, String message, dynamic stacktrace, bool skipCrashlytics) {
|
||||
if (isInDebugMode) {
|
||||
debugPrint('$message');
|
||||
}
|
||||
DateTime t = DateTime.now();
|
||||
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
|
||||
if (_log.length > 100) {
|
||||
_log.removeAt(0);
|
||||
if (stacktrace != null) {
|
||||
debugPrint('$stacktrace');
|
||||
}
|
||||
} else if (!skipCrashlytics) {
|
||||
Crashlytics.instance.recordError('$message', stacktrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ part of 'main.dart';
|
||||
|
||||
class HAView {
|
||||
List<CardData> cards = [];
|
||||
List<Entity> badges = [];
|
||||
CardData badges;
|
||||
Entity linkedEntity;
|
||||
String name;
|
||||
String id;
|
||||
@ -16,28 +16,16 @@ class HAView {
|
||||
iconName = rawData['icon'];
|
||||
isPanel = rawData['panel'] ?? false;
|
||||
|
||||
if (rawData['badges'] != null && rawData['badges'] is List) {
|
||||
rawData['badges'].forEach((entity) {
|
||||
if (entity is String) {
|
||||
if (HomeAssistant().entities.isExist(entity)) {
|
||||
Entity e = HomeAssistant().entities.get(entity);
|
||||
badges.add(e);
|
||||
}
|
||||
} else {
|
||||
String eId = '${entity['entity']}';
|
||||
if (HomeAssistant().entities.isExist(eId)) {
|
||||
Entity e = HomeAssistant().entities.get(eId);
|
||||
badges.add(e);
|
||||
}
|
||||
}
|
||||
if (rawData['badges'] != null && !isPanel) {
|
||||
badges = CardData.parse({
|
||||
'type': CardType.BADGES,
|
||||
'badges': rawData['badges']
|
||||
});
|
||||
}
|
||||
|
||||
(rawData["cards"] ?? []).forEach((rawCardData) {
|
||||
(rawData['cards'] ?? []).forEach((rawCardData) {
|
||||
cards.add(CardData.parse(rawCardData));
|
||||
});
|
||||
|
||||
//cards.addAll(_createLovelaceCards(rawData["cards"] ?? [], 1));
|
||||
}
|
||||
|
||||
Widget buildTab() {
|
||||
|
@ -18,6 +18,12 @@ class ViewWidget extends StatelessWidget {
|
||||
);
|
||||
} else {
|
||||
Widget cardsContainer;
|
||||
Widget badgesContainer;
|
||||
if (this.view.badges != null && this.view.badges is BadgesData) {
|
||||
badgesContainer = this.view.badges.buildCardWidget();
|
||||
} else {
|
||||
badgesContainer = Container(width: 0, height: 0);
|
||||
}
|
||||
if (this.view.cards.isNotEmpty) {
|
||||
cardsContainer = DynamicMultiColumnLayout(
|
||||
minColumnWidth: Sizes.minViewColumnWidth,
|
||||
@ -44,13 +50,15 @@ class ViewWidget extends StatelessWidget {
|
||||
} else {
|
||||
cardsContainer = Container();
|
||||
}
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.all(0),
|
||||
children: <Widget>[
|
||||
_buildBadges(context),
|
||||
cardsContainer
|
||||
]
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
badgesContainer,
|
||||
cardsContainer
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -63,18 +71,4 @@ class ViewWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBadges(BuildContext context) {
|
||||
if (this.view.badges.isNotEmpty) {
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10.0,
|
||||
runSpacing: 1.0,
|
||||
children: this.view.badges.map((badge) =>
|
||||
badge.buildBadgeWidget(context)).toList(),
|
||||
);
|
||||
} else {
|
||||
return Container(width: 0, height: 0,);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
22
pubspec.yaml
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.8.5+896
|
||||
version: 1.1.0+1100
|
||||
|
||||
|
||||
environment:
|
||||
@ -24,33 +24,27 @@ dependencies:
|
||||
flutter_webview_plugin: ^0.3.10+1
|
||||
webview_flutter: ^0.3.19+7
|
||||
firebase_messaging: ^6.0.13
|
||||
flutter_secure_storage: ^3.3.1+1
|
||||
flutter_secure_storage: ^3.3.3
|
||||
device_info: ^0.4.1+4
|
||||
flutter_local_notifications: ^1.1.6
|
||||
geolocator: ^5.3.1
|
||||
workmanager: ^0.2.2
|
||||
battery: ^0.3.1+7
|
||||
#geolocator: ^5.3.1
|
||||
background_locator: ^1.1.3+1
|
||||
#workmanager: ^0.2.2
|
||||
battery: ^1.0.0
|
||||
firebase_crashlytics: ^0.1.3+3
|
||||
syncfusion_flutter_core: ^18.1.43
|
||||
syncfusion_flutter_gauges: ^18.1.43
|
||||
syncfusion_flutter_core: ^18.1.48
|
||||
syncfusion_flutter_gauges: ^18.1.48
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_launcher_icons: any
|
||||
|
||||
flutter_icons:
|
||||
android: true
|
||||
ios: false
|
||||
image_path: "images/icon/icon.png"
|
||||
|
||||
flutter:
|
||||
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- images/hassio-192x192.png
|
||||
- assets/js/externalAuth.js
|
||||
- assets/html/cameraView.html
|
||||
- assets/html/cameraLiveView.html
|
||||
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||
Future<void> main() async {
|
||||
final config = {
|
||||
'syncfusion_license_key': Platform.environment['SYNCFUSION_LICENSE_KEY'],
|
||||
'version_type': Platform.environment['HA_CLIENT_VERSION_TYPE'] ?? ''
|
||||
};
|
||||
|
||||
final filename = 'lib/.secrets.dart';
|
||||
|