Compare commits
87 Commits
beta/0.8.5
...
1.0.1
Author | SHA1 | Date | |
---|---|---|---|
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"
|
||||
}
|
@ -8,7 +8,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
@ -20,6 +19,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
|
||||
|
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 -->
|
||||
|
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))
|
||||
)
|
||||
)
|
||||
]
|
||||
|
@ -11,7 +11,8 @@ class CardData {
|
||||
|
||||
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
||||
|
||||
factory CardData.parse(Map<String, dynamic> rawData) {
|
||||
factory CardData.parse(rawData) {
|
||||
try {
|
||||
switch (rawData['type']) {
|
||||
case CardType.ENTITIES:
|
||||
return EntitiesCardData(rawData);
|
||||
@ -29,7 +30,7 @@ class CardData {
|
||||
return CardData.parse(rawData['card']);
|
||||
break;
|
||||
case CardType.ENTITY_FILTER:
|
||||
Map<String, dynamic> cardData = Map.from(rawData);
|
||||
Map cardData = Map.from(rawData);
|
||||
cardData.remove('type');
|
||||
if (rawData.containsKey('card')) {
|
||||
cardData.addAll(rawData['card']);
|
||||
@ -64,13 +65,24 @@ class CardData {
|
||||
}
|
||||
return CardData(rawData);
|
||||
}
|
||||
} catch (error, stacktrace) {
|
||||
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
|
||||
return ErrorCardData(rawData);
|
||||
}
|
||||
}
|
||||
|
||||
CardData(Map<String, dynamic> rawData) {
|
||||
CardData(rawData) {
|
||||
if (rawData != null && rawData is Map) {
|
||||
type = rawData['type'] ?? CardType.ENTITIES;
|
||||
conditions = rawData['conditions'] ?? [];
|
||||
showEmpty = rawData['show_empty'] ?? true;
|
||||
stateFilter = rawData['state_filter'] ?? [];
|
||||
} else {
|
||||
type = CardType.UNKNOWN;
|
||||
conditions = [];
|
||||
showEmpty = true;
|
||||
stateFilter = [];
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildCardWidget() {
|
||||
@ -79,7 +91,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 +148,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,7 +170,7 @@ 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'];
|
||||
@ -249,7 +260,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,7 +296,7 @@ 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'];
|
||||
@ -339,8 +350,8 @@ class GaugeCardData extends CardData {
|
||||
|
||||
String name;
|
||||
String unit;
|
||||
int min;
|
||||
int max;
|
||||
double min;
|
||||
double max;
|
||||
Map severity;
|
||||
|
||||
@override
|
||||
@ -348,12 +359,24 @@ 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"];
|
||||
@ -388,7 +411,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,7 +459,7 @@ class HorizontalStackCardData extends CardData {
|
||||
return HorizontalStackCard(card: this);
|
||||
}
|
||||
|
||||
HorizontalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
HorizontalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards')) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
@ -457,7 +480,7 @@ class VerticalStackCardData extends CardData {
|
||||
return VerticalStackCard(card: this);
|
||||
}
|
||||
|
||||
VerticalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||
VerticalStackCardData(rawData) : super(rawData) {
|
||||
if (rawData.containsKey('cards')) {
|
||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||
return CardData.parse(childCard);
|
||||
@ -479,7 +502,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 +517,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 +531,18 @@ class MediaControlCardData extends CardData {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ErrorCardData extends CardData {
|
||||
|
||||
String cardConfig;
|
||||
|
||||
@override
|
||||
Widget buildCardWidget() {
|
||||
return ErrorCard(card: this);
|
||||
}
|
||||
|
||||
ErrorCardData(rawData) : super(rawData) {
|
||||
cardConfig = '$rawData';
|
||||
}
|
||||
|
||||
}
|
||||
|
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,7 +110,9 @@ class GaugeCard extends StatelessWidget {
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: <Widget>[
|
||||
SfRadialGauge(
|
||||
IgnorePointer(
|
||||
ignoring: true,
|
||||
child: SfRadialGauge(
|
||||
axes: <RadialAxis>[
|
||||
RadialAxis(
|
||||
maximum: card.max.toDouble(),
|
||||
@ -139,6 +141,7 @@ class GaugeCard extends StatelessWidget {
|
||||
]
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
@ -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(
|
||||
|
@ -119,6 +119,7 @@ class CardType {
|
||||
static const MARKDOWN = "markdown";
|
||||
static const LIGHT = "light";
|
||||
static const ENTITY_FILTER = "entity-filter";
|
||||
static const UNKNOWN = "unknown";
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
|
@ -143,6 +143,6 @@ class BadgeWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
onTap: () =>
|
||||
eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
|
||||
eventBus.fire(new ShowEntityPageEvent(entityId: entityModel.entityWrapper.entity.entityId)));
|
||||
}
|
||||
}
|
@ -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(
|
||||
@ -155,7 +154,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
fullscreenDialog: true
|
||||
)
|
||||
).then((_){
|
||||
eventBus.fire(ShowEntityPageEvent(entity: _entity));
|
||||
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(
|
||||
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,
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
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,
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -148,7 +148,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"];
|
||||
|
@ -9,6 +9,9 @@ class EntityIcon extends StatelessWidget {
|
||||
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : 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,12 +26,13 @@ class EntityIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildIcon(EntityWrapper data, Color color) {
|
||||
Widget buildIcon(BuildContext context, EntityWrapper data, Color color) {
|
||||
Widget result;
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
if (data.entityPicture != null) {
|
||||
return Container(
|
||||
result = Container(
|
||||
height: size+12,
|
||||
width: size+12,
|
||||
decoration: BoxDecoration(
|
||||
@ -41,7 +45,7 @@ class EntityIcon extends StatelessWidget {
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
String iconName = data.icon;
|
||||
int iconCode = 0;
|
||||
if (iconName.length > 0) {
|
||||
@ -50,11 +54,46 @@ class EntityIcon extends StatelessWidget {
|
||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||
data.entity.deviceClass, data.entity.state); //
|
||||
}
|
||||
return Icon(
|
||||
result = Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: color,
|
||||
);
|
||||
if (data.entity is LightEntity &&
|
||||
(data.entity as LightEntity).supportColor &&
|
||||
(data.entity as LightEntity).color != null
|
||||
) {
|
||||
Color lightColor = (data.entity as LightEntity).color.toColor();
|
||||
if (lightColor == Colors.white) {
|
||||
return result;
|
||||
}
|
||||
result = Stack(
|
||||
children: <Widget>[
|
||||
result,
|
||||
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)
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -71,6 +110,7 @@ class EntityIcon extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: buildIcon(
|
||||
context,
|
||||
entityWrapper,
|
||||
iconColor
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
|
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) &&
|
||||
|
@ -29,8 +29,8 @@ class TimerEntity extends Entity {
|
||||
Logger.e("Strange $entityId 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 duration for $entityId: $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: (_) {
|
||||
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;
|
||||
Timer(Duration(milliseconds: 500), () {
|
||||
if (!_changeStarted) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
widget.onChangeEnd(value);
|
||||
Timer(Duration(milliseconds: 500), () {
|
||||
if (!_changeStarted) {
|
||||
widget.onChangeEnd?.call(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -70,20 +77,25 @@ class UniversalSliderState extends State<UniversalSlider> {
|
||||
if (widget.closing != null) {
|
||||
row.add(widget.closing);
|
||||
}
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
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: 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");
|
||||
} 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;
|
||||
@ -204,12 +206,13 @@ 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;
|
||||
@ -217,16 +220,17 @@ class HomeAssistant {
|
||||
}
|
||||
|
||||
Future _getServices(SharedPreferences prefs) async {
|
||||
if (prefs != null) {
|
||||
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");
|
||||
} 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,10 +240,20 @@ class HomeAssistant {
|
||||
|
||||
Future _getUserInfo(SharedPreferences sharedPrefs) async {
|
||||
_userName = null;
|
||||
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.w("Can't get user info: $e");
|
||||
Logger.e('auth/current_user error: $e');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _parseUserInfo(data) {
|
||||
_rawUserInfo = 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");
|
||||
} 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;
|
||||
}
|
||||
}*/
|
||||
|
@ -100,6 +100,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 +113,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 +143,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 +152,21 @@ 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';
|
||||
|
||||
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 +174,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);
|
||||
}
|
||||
@ -234,10 +238,12 @@ class _HAClientAppState extends State<HAClientApp> {
|
||||
void _handlePurchaseUpdates(purchase) {
|
||||
if (purchase is List<PurchaseDetails>) {
|
||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||
eventBus.fire(ShowPopupMessageEvent(
|
||||
eventBus.fire(ShowPopupEvent(
|
||||
popup: Popup(
|
||||
title: "Thanks a lot!",
|
||||
body: "Thank you for supporting HA Client development!",
|
||||
buttonText: "Ok"
|
||||
positiveText: "Ok"
|
||||
)
|
||||
));
|
||||
} else {
|
||||
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||
@ -266,7 +272,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 +283,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")
|
||||
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,8 @@ class ConnectionManager {
|
||||
String _tempToken;
|
||||
String oauthUrl;
|
||||
String webhookId;
|
||||
double haVersion;
|
||||
String mobileAppDeviceName;
|
||||
bool settingsLoaded = false;
|
||||
int appIntegrationVersion;
|
||||
bool get isAuthenticated => _token != null;
|
||||
@ -45,35 +47,40 @@ 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');
|
||||
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 +113,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 +155,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 +178,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 +190,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 +223,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 +247,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 +293,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 +328,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 +355,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 +384,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;
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ class LocationManager {
|
||||
}
|
||||
|
||||
updateDeviceLocation() async {
|
||||
try {
|
||||
Logger.d("[Foreground location] Started");
|
||||
Geolocator geolocator = Geolocator();
|
||||
var battery = Battery();
|
||||
@ -133,16 +134,22 @@ class LocationManager {
|
||||
}
|
||||
};
|
||||
Logger.d("[Foreground location] Sending data home...");
|
||||
var response = await http.post(
|
||||
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] Got HTTP ${response.statusCode}");
|
||||
} else {
|
||||
Logger.d("[Foreground location] No location. Aborting.");
|
||||
}
|
||||
}
|
||||
} catch (e, stack) {
|
||||
Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,7 +55,8 @@ class MobileAppIntegrationManager {
|
||||
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
|
||||
|
||||
completer.complete();
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
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",
|
||||
@ -52,11 +64,17 @@ class MobileAppIntegrationManager {
|
||||
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(
|
||||
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.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
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,12 +46,13 @@ 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) {
|
||||
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 (_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 (_showPopupMessageSubscription == null) {
|
||||
_showPopupMessageSubscription = eventBus.on<ShowPopupMessageEvent>().listen((event){
|
||||
_showPopupDialog(
|
||||
title: event.title,
|
||||
body: event.body,
|
||||
onPositive: event.onButtonClick,
|
||||
positiveText: event.buttonText,
|
||||
negativeText: null
|
||||
);
|
||||
});
|
||||
}
|
||||
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) {
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
List<Widget> items = [
|
||||
_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),
|
||||
],
|
||||
];
|
||||
if (ConnectionManager().webhookId != null) {
|
||||
items.insert(1, _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings));
|
||||
}
|
||||
return ListView(
|
||||
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();
|
||||
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(
|
||||
"Connection settings",
|
||||
"Home Assistant url:",
|
||||
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: _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
|
||||
)
|
||||
],
|
||||
),
|
||||
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;
|
||||
onSaved: (newValue) {
|
||||
_homeAssistantUrl = newValue.trim();
|
||||
},
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return 'Url is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
Container(
|
||||
height: Sizes.rowPadding,
|
||||
height: 10,
|
||||
),
|
||||
RaisedButton(
|
||||
child: Text('Apply', style: Theme.of(context).textTheme.button),
|
||||
color: Theme.of(context).primaryColorDark,
|
||||
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(
|
||||
"Device name:",
|
||||
style: Theme.of(context).textTheme.headline,
|
||||
),
|
||||
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
|
||||
)
|
||||
),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return 'Device name is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
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) {
|
||||
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,46 +52,6 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Are you sure you want to restart Home Assistant?",
|
||||
body: "This will restart your Home Assistant server.",
|
||||
positiveText: "Sure. Make it so",
|
||||
negativeText: "What?? No!",
|
||||
onPositive: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
stop() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Are you sure you want to STOP Home Assistant?",
|
||||
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
||||
positiveText: "Sure. Make it so",
|
||||
negativeText: "What?? No!",
|
||||
onPositive: () {
|
||||
ConnectionManager().callService(domain: "homeassistant", service: "stop");
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
updateRegistration() {
|
||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
|
||||
}
|
||||
|
||||
resetRegistration() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Waaaait",
|
||||
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
||||
positiveText: "Done it already",
|
||||
negativeText: "Ok, I will",
|
||||
onPositive: () {
|
||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
_switchLocationTrackingState(bool state) async {
|
||||
if (state) {
|
||||
await LocationManager().updateDeviceLocation();
|
||||
@ -146,42 +106,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)
|
||||
)
|
||||
],
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ 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];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
if (stacktrace != null) {
|
||||
debugPrint('$stacktrace');
|
||||
}
|
||||
DateTime t = DateTime.now();
|
||||
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
|
||||
if (_log.length > 100) {
|
||||
_log.removeAt(0);
|
||||
} else if (!skipCrashlytics) {
|
||||
Crashlytics.instance.recordError('$message', stacktrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
13
pubspec.yaml
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.8.5+896
|
||||
version: 1.0.1+1013
|
||||
|
||||
|
||||
environment:
|
||||
@ -24,12 +24,12 @@ 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
|
||||
battery: ^1.0.0
|
||||
firebase_crashlytics: ^0.1.3+3
|
||||
syncfusion_flutter_core: ^18.1.43
|
||||
syncfusion_flutter_gauges: ^18.1.43
|
||||
@ -38,19 +38,12 @@ dependencies:
|
||||
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';
|
||||
|