Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
9160dbf7f2 | |||
243fcd7c49 | |||
c114bcfb35 | |||
83defb08f1 | |||
57ebdbbe85 | |||
c6aceed623 | |||
ba4c88ec5d | |||
ee1685e981 | |||
996fbf7bba | |||
56cd8963d7 | |||
5759aad0cb | |||
02717332f7 | |||
8d1b159f56 | |||
fb335e1100 | |||
5f0bc83d67 | |||
6a8cee2cc2 | |||
0d2f1cf9aa | |||
8efeb3da8a | |||
620aa3b8d8 | |||
ab5bf3b807 | |||
6663bcad72 | |||
113cd29f74 | |||
f2fdfb0a32 | |||
691e48a36b | |||
2036cc117f | |||
389d28a1e1 | |||
27e6198d83 | |||
de762a4878 | |||
e8efefe25d | |||
21f3e8985a | |||
622543d405 | |||
abdc0fc1c8 | |||
1ecb839042 | |||
cece4d1e16 | |||
623634cb6e | |||
f9c37f5084 | |||
3e12f4f8a4 | |||
b07ff6fe71 | |||
5a3b57c28e | |||
e858eee83b | |||
73f00d3bd7 | |||
eea59cf11b | |||
61b459ed8a | |||
dca8c309aa | |||
be53500104 | |||
bc1a791608 | |||
b112ff980a | |||
13102a6b04 | |||
57c3083f9f |
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,4 +11,5 @@ build/
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
|
premium_features_manager.class.dart
|
||||||
pubspec.lock
|
pubspec.lock
|
@ -1,12 +1,9 @@
|
|||||||
[](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart)
|
|
||||||
# HA Client
|
# HA Client
|
||||||
## Native Android client for Home Assistant
|
## Native Android client for Home Assistant
|
||||||
### With Lovelace UI support
|
### With Lovelace UI support
|
||||||
|
|
||||||
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||||
|
|
||||||
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||||
|
|
||||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
|
||||||
|
|
||||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
|
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -14,7 +18,7 @@
|
|||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name=".Application"
|
||||||
android:label="HA Client"
|
android:label="HA Client"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
@ -46,5 +50,20 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
android:exported="false"/>
|
||||||
|
<receiver
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
|
||||||
|
android:exported="false"/>
|
||||||
|
<receiver
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
||||||
|
android:enabled="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.keyboardcrumbs.hassclient;
|
||||||
|
|
||||||
|
import io.flutter.app.FlutterApplication;
|
||||||
|
import io.flutter.plugin.common.PluginRegistry;
|
||||||
|
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
|
||||||
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
|
||||||
|
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerWith(PluginRegistry registry) {
|
||||||
|
GeneratedPluginRegistrant.registerWith(registry);
|
||||||
|
}
|
||||||
|
}
|
16
assets/js/externalAuth.js
Normal file
16
assets/js/externalAuth.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
window.externalApp = {};
|
||||||
|
window.externalApp.getExternalAuth = function(options) {
|
||||||
|
console.log("Starting external auth");
|
||||||
|
var options = JSON.parse(options);
|
||||||
|
if (options && options.callback) {
|
||||||
|
var responseData = {
|
||||||
|
access_token: "[token]",
|
||||||
|
expires_in: 1800
|
||||||
|
};
|
||||||
|
console.log("Waiting for callback to be added");
|
||||||
|
setTimeout(function(){
|
||||||
|
console.log("Calling a callback");
|
||||||
|
window[options.callback](true, responseData);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
41
flutter_01.log
Normal file
41
flutter_01.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientSYJJZI/ha_client/.packages, isolateId: isolates/68989666}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
41
flutter_02.log
Normal file
41
flutter_02.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientWYMXDL/ha_client/.packages, isolateId: isolates/289688365}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
41
flutter_03.log
Normal file
41
flutter_03.log
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
Flutter crash report; please file at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=89AY052S4 lib/main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
_InternalLinkedHashMap<String, dynamic>: {code: 105, message: Isolate must be runnable, data: {request: {method: _reloadSources, params: {pause: true, rootLibUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/lib/main.dart.incremental.dill, packagesUri: file:///data/user/0/com.keyboardcrumbs.haclient/code_cache/ha_clientLNSJAH/ha_client/.packages, isolateId: isolates/866521062}}, details: Isolate must be runnable before this request is made.}}
|
||||||
|
|
||||||
|
```
|
||||||
|
null```
|
||||||
|
|
||||||
|
## flutter doctor
|
||||||
|
|
||||||
|
```
|
||||||
|
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Linux, locale en_US.UTF-8)
|
||||||
|
• Flutter version 1.7.8+hotfix.4 at /home/estevez/sdk/flutter
|
||||||
|
• Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
|
||||||
|
• Engine revision fee001c93f
|
||||||
|
• Dart version 2.4.0
|
||||||
|
|
||||||
|
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
|
||||||
|
• Android SDK at /home/estevez/Android/Sdk
|
||||||
|
• Android NDK location not configured (optional; useful for native profiling support)
|
||||||
|
• Platform android-29, build-tools 29.0.2
|
||||||
|
• Java binary at: /home/estevez/bin/android-studio/jre/bin/java
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
• All Android licenses accepted.
|
||||||
|
|
||||||
|
[✓] Android Studio (version 3.5)
|
||||||
|
• Android Studio at /home/estevez/bin/android-studio
|
||||||
|
• Flutter plugin version 38.2.3
|
||||||
|
• Dart plugin version 191.8423
|
||||||
|
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
|
||||||
|
|
||||||
|
[✓] Connected device (1 available)
|
||||||
|
• Pixel 3 XL • 89AY052S4 • android-arm64 • Android 9 (API 28)
|
||||||
|
|
||||||
|
• No issues found!
|
||||||
|
```
|
@ -408,53 +408,4 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class TemperatureControlWidget extends StatelessWidget {
|
|
||||||
final double value;
|
|
||||||
final double fontSize;
|
|
||||||
final Color fontColor;
|
|
||||||
final onInc;
|
|
||||||
final onDec;
|
|
||||||
|
|
||||||
TemperatureControlWidget(
|
|
||||||
{Key key,
|
|
||||||
@required this.value,
|
|
||||||
@required this.onInc,
|
|
||||||
@required this.onDec,
|
|
||||||
this.fontSize,
|
|
||||||
this.fontColor})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
"$value",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: fontSize ?? 24.0,
|
|
||||||
color: fontColor ?? Colors.black
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
'mdi:chevron-up')),
|
|
||||||
iconSize: 30.0,
|
|
||||||
onPressed: () => onInc(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
'mdi:chevron-down')),
|
|
||||||
iconSize: 30.0,
|
|
||||||
onPressed: () => onDec(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
50
lib/entities/climate/widgets/temperature_control_widget.dart
Normal file
50
lib/entities/climate/widgets/temperature_control_widget.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class TemperatureControlWidget extends StatelessWidget {
|
||||||
|
final double value;
|
||||||
|
final double fontSize;
|
||||||
|
final Color fontColor;
|
||||||
|
final onInc;
|
||||||
|
final onDec;
|
||||||
|
|
||||||
|
TemperatureControlWidget(
|
||||||
|
{Key key,
|
||||||
|
@required this.value,
|
||||||
|
@required this.onInc,
|
||||||
|
@required this.onDec,
|
||||||
|
this.fontSize,
|
||||||
|
this.fontColor})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
"$value",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: fontSize ?? 24.0,
|
||||||
|
color: fontColor ?? Colors.black
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
'mdi:chevron-up')),
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () => onInc(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
'mdi:chevron-down')),
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () => onDec(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -60,7 +60,7 @@ class EntityWrapper {
|
|||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Logger.w("Local urls is not supported yet");
|
Logger.w("Local urls is not supported yet");
|
||||||
} else {
|
} else {
|
||||||
HAUtils.launchURL(uiAction.tapService);
|
Launcher.launchURL(uiAction.tapService);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ class EntityWrapper {
|
|||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Logger.w("Local urls is not supported yet");
|
Logger.w("Local urls is not supported yet");
|
||||||
} else {
|
} else {
|
||||||
HAUtils.launchURL(uiAction.holdService);
|
Launcher.launchURL(uiAction.holdService);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -73,13 +73,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
obscureText: entity.isPasswordField,
|
obscureText: entity.isPasswordField,
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _tmpValue,
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _tmpValue.length)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_tmpValue = value;
|
_tmpValue = value;
|
||||||
}),
|
}),
|
||||||
|
@ -47,7 +47,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
.entity;
|
.entity;
|
||||||
started = true;
|
started = true;
|
||||||
}
|
}
|
||||||
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
|
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||||
.entityId}?token=${_entity.attributes['access_token']}';
|
.entityId}?token=${_entity.attributes['access_token']}';
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
@ -148,7 +148,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
||||||
_selectedId = 0;
|
_selectedId = numericDataLists.length -1;
|
||||||
}
|
}
|
||||||
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
||||||
numericDataLists.forEach((attrName, dataList) {
|
numericDataLists.forEach((attrName, dataList) {
|
||||||
@ -202,6 +202,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
@ -210,6 +215,12 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
@ -47,7 +47,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
}
|
}
|
||||||
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||||
_historyLastUpdated = now;
|
_historyLastUpdated = now;
|
||||||
Connection().getHistory(entityId).then((history){
|
ConnectionManager().getHistory(entityId).then((history){
|
||||||
if (!_disposed) {
|
if (!_disposed) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_history = history.isNotEmpty ? history[0] : [];
|
_history = history.isNotEmpty ? history[0] : [];
|
||||||
|
@ -103,7 +103,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
id: widget.rawHistory.length
|
id: widget.rawHistory.length
|
||||||
));
|
));
|
||||||
if (_selectedId == -1) {
|
if (_selectedId == -1) {
|
||||||
_selectedId = 0;
|
_selectedId = data.length - 1;
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
@ -132,6 +132,11 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
@ -140,6 +145,12 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
@ -101,7 +101,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
colorId: data.last.colorId
|
colorId: data.last.colorId
|
||||||
));
|
));
|
||||||
if (_selectedId == -1) {
|
if (_selectedId == -1) {
|
||||||
_selectedId = 0;
|
_selectedId = data.length - 1;
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
@ -137,14 +137,25 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
|
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
||||||
|
@ -23,7 +23,7 @@ class HomeAssistant {
|
|||||||
Duration fetchTimeout = Duration(seconds: 30);
|
Duration fetchTimeout = Duration(seconds: 30);
|
||||||
|
|
||||||
String get locationName {
|
String get locationName {
|
||||||
if (Connection().useLovelace) {
|
if (ConnectionManager().useLovelace) {
|
||||||
return ui?.title ?? "";
|
return ui?.title ?? "";
|
||||||
} else {
|
} else {
|
||||||
return _instanceConfig["location_name"] ?? "";
|
return _instanceConfig["location_name"] ?? "";
|
||||||
@ -36,8 +36,8 @@ class HomeAssistant {
|
|||||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||||
|
|
||||||
HomeAssistant._internal() {
|
HomeAssistant._internal() {
|
||||||
Connection().onStateChangeCallback = _handleEntityStateChange;
|
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||||
Device().loadDeviceInfo();
|
DeviceInfoManager().loadDeviceInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
Completer _fetchCompleter;
|
||||||
@ -47,18 +47,18 @@ class HomeAssistant {
|
|||||||
Logger.w("Previous data fetch is not completed yet");
|
Logger.w("Previous data fetch is not completed yet");
|
||||||
return _fetchCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
if (entities == null) entities = EntityCollection(Connection().httpWebHost);
|
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||||
_fetchCompleter = Completer();
|
_fetchCompleter = Completer();
|
||||||
List<Future> futures = [];
|
List<Future> futures = [];
|
||||||
futures.add(_getStates());
|
futures.add(_getStates());
|
||||||
if (Connection().useLovelace) {
|
if (ConnectionManager().useLovelace) {
|
||||||
futures.add(_getLovelace());
|
futures.add(_getLovelace());
|
||||||
}
|
}
|
||||||
futures.add(_getConfig());
|
futures.add(_getConfig());
|
||||||
futures.add(_getServices());
|
futures.add(_getServices());
|
||||||
futures.add(_getUserInfo());
|
futures.add(_getUserInfo());
|
||||||
futures.add(_getPanels());
|
futures.add(_getPanels());
|
||||||
futures.add(Connection().sendSocketMessage(
|
futures.add(ConnectionManager().sendSocketMessage(
|
||||||
type: "subscribe_events",
|
type: "subscribe_events",
|
||||||
additionalData: {"event_type": "state_changed"},
|
additionalData: {"event_type": "state_changed"},
|
||||||
));
|
));
|
||||||
@ -66,9 +66,9 @@ class HomeAssistant {
|
|||||||
if (isMobileAppEnabled) {
|
if (isMobileAppEnabled) {
|
||||||
_createUI();
|
_createUI();
|
||||||
_fetchCompleter.complete();
|
_fetchCompleter.complete();
|
||||||
checkAppRegistration();
|
MobileAppIntegrationManager.checkAppRegistration();
|
||||||
} else {
|
} else {
|
||||||
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app")]));
|
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-integration")]));
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_fetchCompleter.completeError(e);
|
_fetchCompleter.completeError(e);
|
||||||
@ -78,133 +78,15 @@ class HomeAssistant {
|
|||||||
|
|
||||||
Future logout() async {
|
Future logout() async {
|
||||||
Logger.d("Logging out...");
|
Logger.d("Logging out...");
|
||||||
await Connection().logout().then((_) {
|
await ConnectionManager().logout().then((_) {
|
||||||
ui?.clear();
|
ui?.clear();
|
||||||
entities?.clear();
|
entities?.clear();
|
||||||
panels?.clear();
|
panels?.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Map _getAppRegistrationData() {
|
|
||||||
return {
|
|
||||||
"app_version": "$appVersion",
|
|
||||||
"device_name": "$userName's ${Device().model}",
|
|
||||||
"manufacturer": Device().manufacturer,
|
|
||||||
"model": Device().model,
|
|
||||||
"os_version": Device().osVersion,
|
|
||||||
"app_data": {
|
|
||||||
"push_token": "$fcmToken",
|
|
||||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
|
||||||
Completer completer = Completer();
|
|
||||||
if (Connection().webhookId == null || forceRegister) {
|
|
||||||
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
|
||||||
var registrationData = _getAppRegistrationData();
|
|
||||||
registrationData.addAll({
|
|
||||||
"app_id": "ha_client",
|
|
||||||
"app_name": "$appName",
|
|
||||||
"os_name": Device().osName,
|
|
||||||
"supports_encryption": false,
|
|
||||||
});
|
|
||||||
Connection().sendHTTPPost(
|
|
||||||
endPoint: "/api/mobile_app/registrations",
|
|
||||||
includeAuthHeader: true,
|
|
||||||
data: json.encode(registrationData)
|
|
||||||
).then((response) {
|
|
||||||
Logger.d("Processing registration responce...");
|
|
||||||
var responseObject = json.decode(response);
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
|
||||||
Connection().webhookId = responseObject["webhook_id"];
|
|
||||||
completer.complete();
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Mobile app Integration was created",
|
|
||||||
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
|
|
||||||
positiveText: "Restart now",
|
|
||||||
negativeText: "Later",
|
|
||||||
onPositive: () {
|
|
||||||
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
|
||||||
completer.complete();
|
|
||||||
Logger.e("Error registering the app: ${e.toString()}");
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
} else {
|
|
||||||
Logger.d("App was previously registered. Checking...");
|
|
||||||
var updateData = {
|
|
||||||
"type": "update_registration",
|
|
||||||
"data": _getAppRegistrationData()
|
|
||||||
};
|
|
||||||
Connection().sendHTTPPost(
|
|
||||||
endPoint: "/api/webhook/${Connection().webhookId}",
|
|
||||||
includeAuthHeader: false,
|
|
||||||
data: json.encode(updateData)
|
|
||||||
).then((response) {
|
|
||||||
if (response == null || response.isEmpty) {
|
|
||||||
Logger.d("No registration data in response. MobileApp integration was removed");
|
|
||||||
_askToRegisterApp();
|
|
||||||
} 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['code'] != null && e['code'] == 410) {
|
|
||||||
Logger.e("MobileApp integration was removed");
|
|
||||||
_askToRegisterApp();
|
|
||||||
} 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: () {
|
|
||||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
|
||||||
},
|
|
||||||
onNegative: () {
|
|
||||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
completer.complete();
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _askToRegisterApp() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "App integration was removed",
|
|
||||||
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
|
||||||
positiveText: "Register now",
|
|
||||||
negativeText: "Cancel",
|
|
||||||
onPositive: () {
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
prefs.remove("app-webhook-id");
|
|
||||||
Connection().webhookId = null;
|
|
||||||
HomeAssistant().checkAppRegistration();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getConfig() async {
|
Future _getConfig() async {
|
||||||
await Connection().sendSocketMessage(type: "get_config").then((data) {
|
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||||
_instanceConfig = Map.from(data);
|
_instanceConfig = Map.from(data);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
throw HAError("Error getting config: ${e}");
|
throw HAError("Error getting config: ${e}");
|
||||||
@ -212,7 +94,7 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future _getStates() async {
|
Future _getStates() async {
|
||||||
await Connection().sendSocketMessage(type: "get_states").then(
|
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||||
(data) => entities.parse(data)
|
(data) => entities.parse(data)
|
||||||
).catchError((e) {
|
).catchError((e) {
|
||||||
throw HAError("Error getting states: $e");
|
throw HAError("Error getting states: $e");
|
||||||
@ -220,27 +102,27 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future _getLovelace() async {
|
Future _getLovelace() async {
|
||||||
await Connection().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
||||||
throw HAError("Error getting lovelace config: $e");
|
throw HAError("Error getting lovelace config: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getUserInfo() async {
|
Future _getUserInfo() async {
|
||||||
_userName = null;
|
_userName = null;
|
||||||
await Connection().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
||||||
Logger.w("Can't get user info: ${e}");
|
Logger.w("Can't get user info: ${e}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getServices() async {
|
Future _getServices() async {
|
||||||
await Connection().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
||||||
Logger.w("Can't get services: ${e}");
|
Logger.w("Can't get services: ${e}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getPanels() async {
|
Future _getPanels() async {
|
||||||
panels.clear();
|
panels.clear();
|
||||||
await Connection().sendSocketMessage(type: "get_panels").then((data) {
|
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
||||||
data.forEach((k,v) {
|
data.forEach((k,v) {
|
||||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||||
panels.add(Panel(
|
panels.add(Panel(
|
||||||
@ -309,7 +191,7 @@ class HomeAssistant {
|
|||||||
HACard card = HACard(
|
HACard card = HACard(
|
||||||
id: "card",
|
id: "card",
|
||||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||||
type: rawCardInfo['type'],
|
type: rawCardInfo['type'] ?? CardType.entities,
|
||||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||||
showName: rawCardInfo['show_name'] ?? true,
|
showName: rawCardInfo['show_name'] ?? true,
|
||||||
showState: rawCardInfo['show_state'] ?? true,
|
showState: rawCardInfo['show_state'] ?? true,
|
||||||
@ -423,7 +305,7 @@ class HomeAssistant {
|
|||||||
|
|
||||||
void _createUI() {
|
void _createUI() {
|
||||||
ui = HomeAssistantUI();
|
ui = HomeAssistantUI();
|
||||||
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
|
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
||||||
Logger.d("Creating Lovelace UI");
|
Logger.d("Creating Lovelace UI");
|
||||||
_parseLovelace();
|
_parseLovelace();
|
||||||
} else {
|
} else {
|
||||||
|
795
lib/main.dart
795
lib/main.dart
@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -15,7 +14,6 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
import 'package:progress_indicators/progress_indicators.dart';
|
import 'package:progress_indicators/progress_indicators.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
//import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
||||||
@ -25,6 +23,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
|
|
||||||
part 'const.dart';
|
part 'const.dart';
|
||||||
|
part 'utils/launcher.dart';
|
||||||
part 'entities/entity.class.dart';
|
part 'entities/entity.class.dart';
|
||||||
part 'entities/entity_wrapper.class.dart';
|
part 'entities/entity_wrapper.class.dart';
|
||||||
part 'entities/timer/timer_entity.class.dart';
|
part 'entities/timer/timer_entity.class.dart';
|
||||||
@ -81,6 +80,7 @@ part 'entities/cover/widgets/cover_state.dart';
|
|||||||
part 'entities/date_time/widgets/date_time_state.dart';
|
part 'entities/date_time/widgets/date_time_state.dart';
|
||||||
part 'entities/lock/widgets/lock_state.dart';
|
part 'entities/lock/widgets/lock_state.dart';
|
||||||
part 'entities/climate/widgets/climate_controls.dart';
|
part 'entities/climate/widgets/climate_controls.dart';
|
||||||
|
part 'entities/climate/widgets/temperature_control_widget.dart';
|
||||||
part 'entities/cover/widgets/cover_controls.widget.dart';
|
part 'entities/cover/widgets/cover_controls.widget.dart';
|
||||||
part 'entities/light/widgets/light_controls.dart';
|
part 'entities/light/widgets/light_controls.dart';
|
||||||
part 'entities/media_player/widgets/media_player_widgets.dart';
|
part 'entities/media_player/widgets/media_player_widgets.dart';
|
||||||
@ -92,15 +92,18 @@ part 'pages/widgets/product_purchase.widget.dart';
|
|||||||
part 'pages/widgets/page_loading_indicator.dart';
|
part 'pages/widgets/page_loading_indicator.dart';
|
||||||
part 'pages/widgets/page_loading_error.dart';
|
part 'pages/widgets/page_loading_error.dart';
|
||||||
part 'pages/panel.page.dart';
|
part 'pages/panel.page.dart';
|
||||||
|
part 'pages/main.page.dart';
|
||||||
part 'home_assistant.class.dart';
|
part 'home_assistant.class.dart';
|
||||||
part 'pages/log.page.dart';
|
part 'pages/log.page.dart';
|
||||||
part 'pages/entity.page.dart';
|
part 'pages/entity.page.dart';
|
||||||
part 'utils.class.dart';
|
|
||||||
part 'mdi.class.dart';
|
part 'mdi.class.dart';
|
||||||
part 'entity_collection.class.dart';
|
part 'entity_collection.class.dart';
|
||||||
part 'auth_manager.class.dart';
|
part 'managers/auth_manager.class.dart';
|
||||||
part 'connection.class.dart';
|
part 'managers/location_manager.class.dart';
|
||||||
part 'device.class.dart';
|
part 'managers/mobile_app_integration_manager.class.dart';
|
||||||
|
part 'managers/connection_manager.class.dart';
|
||||||
|
part 'managers/device_info_manager.class.dart';
|
||||||
|
part 'managers/startup_user_messages_manager.class.dart';
|
||||||
part 'ui_class/ui.dart';
|
part 'ui_class/ui.dart';
|
||||||
part 'ui_class/view.class.dart';
|
part 'ui_class/view.class.dart';
|
||||||
part 'ui_class/card.class.dart';
|
part 'ui_class/card.class.dart';
|
||||||
@ -109,16 +112,20 @@ part 'ui_class/panel_class.dart';
|
|||||||
part 'ui_widgets/view.dart';
|
part 'ui_widgets/view.dart';
|
||||||
part 'ui_widgets/card_widget.dart';
|
part 'ui_widgets/card_widget.dart';
|
||||||
part 'ui_widgets/card_header_widget.dart';
|
part 'ui_widgets/card_header_widget.dart';
|
||||||
part 'ui_widgets/config_panel_widget.dart';
|
part 'panels/config_panel_widget.dart';
|
||||||
|
part 'panels/widgets/link_to_web_config.dart';
|
||||||
|
part 'utils/logger.dart';
|
||||||
|
part 'types/ha_error.dart';
|
||||||
|
part 'types/event_bus_events.dart';
|
||||||
|
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersion = "0.6.4";
|
const appVersion = "0.6.5";
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
FlutterError.onError = (errorDetails) {
|
FlutterError.onError = (errorDetails) {
|
||||||
Logger.e( "${errorDetails.exception}");
|
Logger.e( "${errorDetails.exception}");
|
||||||
if (Logger.isInDebugMode) {
|
if (Logger.isInDebugMode) {
|
||||||
@ -127,7 +134,11 @@ void main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
runZoned(() {
|
runZoned(() {
|
||||||
runApp(new HAClientApp());
|
//AndroidAlarmManager.initialize().then((_) {
|
||||||
|
runApp(new HAClientApp());
|
||||||
|
// print("Running MAIN isolate ${Isolate.current.hashCode}");
|
||||||
|
//});
|
||||||
|
|
||||||
}, onError: (error, stack) {
|
}, onError: (error, stack) {
|
||||||
Logger.e("$error");
|
Logger.e("$error");
|
||||||
Logger.e("$stack");
|
Logger.e("$stack");
|
||||||
@ -139,7 +150,6 @@ void main() {
|
|||||||
|
|
||||||
class HAClientApp extends StatelessWidget {
|
class HAClientApp extends StatelessWidget {
|
||||||
|
|
||||||
final HomeAssistant homeAssistant = HomeAssistant();
|
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -150,778 +160,39 @@ class HAClientApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
|
"/": (context) => MainPage(title: 'HA Client'),
|
||||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||||
"/configuration": (context) => PanelPage(title: "Configuration"),
|
|
||||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||||
"/login": (context) => WebviewScaffold(
|
"/login": (context) => WebviewScaffold(
|
||||||
url: "${Connection().oauthUrl}",
|
url: "${ConnectionManager().oauthUrl}",
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.help),
|
icon: Icon(Icons.help),
|
||||||
onPressed: () => HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
|
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
|
||||||
),
|
),
|
||||||
title: new Text("Login with HA"),
|
title: new Text("Login with HA"),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text("Manual", style: TextStyle(color: Colors.white)),
|
child: Text("Manual", style: TextStyle(color: Colors.white)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
|
||||||
Navigator.of(context).pushNamed("/connection-settings");
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainPage extends StatefulWidget {
|
|
||||||
MainPage({Key key, this.title, this.homeAssistant}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final HomeAssistant homeAssistant;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_MainPageState createState() => new _MainPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
|
||||||
|
|
||||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
|
||||||
StreamSubscription _stateSubscription;
|
|
||||||
StreamSubscription _settingsSubscription;
|
|
||||||
StreamSubscription _serviceCallSubscription;
|
|
||||||
StreamSubscription _showEntityPageSubscription;
|
|
||||||
StreamSubscription _showErrorSubscription;
|
|
||||||
StreamSubscription _startAuthSubscription;
|
|
||||||
StreamSubscription _showPopupDialogSubscription;
|
|
||||||
StreamSubscription _showPopupMessageSubscription;
|
|
||||||
StreamSubscription _reloadUISubscription;
|
|
||||||
int _previousViewCount;
|
|
||||||
bool _showLoginButton = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
final Stream purchaseUpdates =
|
|
||||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
|
||||||
_subscription = purchaseUpdates.listen((purchases) {
|
|
||||||
_handlePurchaseUpdates(purchases);
|
|
||||||
});
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
|
|
||||||
_firebaseMessaging.configure(
|
|
||||||
onLaunch: (data) {
|
|
||||||
Logger.d("Notification [onLaunch]: $data");
|
|
||||||
},
|
|
||||||
onMessage: (data) {
|
|
||||||
Logger.d("Notification [onMessage]: $data");
|
|
||||||
_showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
|
|
||||||
},
|
|
||||||
onResume: (data) {
|
|
||||||
Logger.d("Notification [onResume]: $data");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
|
||||||
|
|
||||||
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
|
||||||
var initializationSettingsAndroid =
|
|
||||||
new AndroidInitializationSettings('mini_icon');
|
|
||||||
var initializationSettingsIOS = new IOSInitializationSettings(
|
|
||||||
onDidReceiveLocalNotification: null);
|
|
||||||
var initializationSettings = new InitializationSettings(
|
|
||||||
initializationSettingsAndroid, initializationSettingsIOS);
|
|
||||||
flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
|
||||||
onSelectNotification: onSelectNotification);
|
|
||||||
|
|
||||||
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
|
||||||
Logger.d("Settings change event: reconnect=${event.reconnect}");
|
|
||||||
if (event.reconnect) {
|
|
||||||
_fullLoad();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_fullLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future onSelectNotification(String payload) async {
|
|
||||||
if (payload != null) {
|
|
||||||
Logger.d('Notification clicked: ' + payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _showNotification({String title, String text}) async {
|
|
||||||
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
|
|
||||||
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
|
|
||||||
importance: Importance.Max, priority: Priority.High);
|
|
||||||
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
|
|
||||||
var platformChannelSpecifics = new NotificationDetails(
|
|
||||||
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
|
||||||
await flutterLocalNotificationsPlugin.show(
|
|
||||||
0,
|
|
||||||
title ?? appName,
|
|
||||||
text,
|
|
||||||
platformChannelSpecifics
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fullLoad() async {
|
|
||||||
_showInfoBottomBar(progress: true,);
|
|
||||||
_subscribe().then((_) {
|
|
||||||
Connection().init(loadSettings: true, forceReconnect: true).then((__){
|
|
||||||
_fetchData();
|
|
||||||
}, onError: (e) {
|
|
||||||
_setErrorState(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _quickLoad() {
|
|
||||||
_hideBottomBar();
|
|
||||||
_showInfoBottomBar(progress: true,);
|
|
||||||
Connection().init(loadSettings: false, forceReconnect: false).then((_){
|
|
||||||
_fetchData();
|
|
||||||
}, onError: (e) {
|
|
||||||
_setErrorState(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_fetchData() async {
|
|
||||||
await widget.homeAssistant.fetchData().then((_) {
|
|
||||||
_hideBottomBar();
|
|
||||||
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
|
|
||||||
if (_previousViewCount != currentViewCount) {
|
|
||||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
|
||||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
|
||||||
_previousViewCount = currentViewCount;
|
|
||||||
}
|
|
||||||
}).catchError((e) {
|
|
||||||
if (e is HAError) {
|
|
||||||
_setErrorState(e);
|
|
||||||
} else {
|
|
||||||
_setErrorState(HAError(e.toString()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
eventBus.fire(RefreshDataFinishedEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
||||||
Logger.d("$state");
|
|
||||||
if (state == AppLifecycleState.resumed && Connection().settingsLoaded) {
|
|
||||||
_quickLoad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handlePurchaseUpdates(purchase) {
|
|
||||||
if (purchase is List<PurchaseDetails>) {
|
|
||||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
|
||||||
eventBus.fire(ShowPopupMessageEvent(
|
|
||||||
title: "Thanks a lot!",
|
|
||||||
body: "Thank you for supporting HA Client development!",
|
|
||||||
buttonText: "Ok"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _subscribe() {
|
|
||||||
Completer completer = Completer();
|
|
||||||
if (_stateSubscription == null) {
|
|
||||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
|
||||||
if (event.needToRebuildUI) {
|
|
||||||
Logger.d("New entity. Need to rebuild UI");
|
|
||||||
_quickLoad();
|
|
||||||
} else {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_reloadUISubscription == null) {
|
|
||||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
|
||||||
_quickLoad();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_showPopupDialogSubscription == null) {
|
|
||||||
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
|
|
||||||
_showPopupDialog(
|
|
||||||
title: event.title,
|
|
||||||
body: event.body,
|
|
||||||
onPositive: event.onPositive,
|
|
||||||
onNegative: event.onNegative,
|
|
||||||
positiveText: event.positiveText,
|
|
||||||
negativeText: event.negativeText
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_showPopupMessageSubscription == null) {
|
|
||||||
_showPopupMessageSubscription = eventBus.on<ShowPopupMessageEvent>().listen((event){
|
|
||||||
_showPopupDialog(
|
|
||||||
title: event.title,
|
|
||||||
body: event.body,
|
|
||||||
onPositive: event.onButtonClick,
|
|
||||||
positiveText: event.buttonText,
|
|
||||||
negativeText: null
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_serviceCallSubscription == null) {
|
|
||||||
_serviceCallSubscription =
|
|
||||||
eventBus.on<ServiceCallEvent>().listen((event) {
|
|
||||||
_callService(event.domain, event.service, event.entityId,
|
|
||||||
event.additionalParams);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_showEntityPageSubscription == null) {
|
|
||||||
_showEntityPageSubscription =
|
|
||||||
eventBus.on<ShowEntityPageEvent>().listen((event) {
|
|
||||||
_showEntityPage(event.entity.entityId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_showErrorSubscription == null) {
|
|
||||||
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
|
||||||
_showErrorBottomBar(event.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_startAuthSubscription == null) {
|
|
||||||
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
|
||||||
setState(() {
|
|
||||||
_showLoginButton = event.showButton;
|
|
||||||
});
|
|
||||||
if (event.showButton) {
|
|
||||||
_showOAuth();
|
|
||||||
} else {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_firebaseMessaging.getToken().then((String token) {
|
|
||||||
HomeAssistant().fcmToken = token;
|
|
||||||
completer.complete();
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showOAuth() {
|
|
||||||
Navigator.of(context).pushNamed('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
_setErrorState(HAError e) {
|
|
||||||
if (e == null) {
|
|
||||||
_showErrorBottomBar(
|
|
||||||
HAError("Unknown error")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_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 _callService(String domain, String service, String entityId, Map additionalParams) {
|
|
||||||
_showInfoBottomBar(
|
|
||||||
message: "Calling $domain.$service",
|
|
||||||
duration: Duration(seconds: 3)
|
|
||||||
);
|
|
||||||
Connection().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showEntityPage(String entityId) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Tab> buildUIViewTabs() {
|
|
||||||
List<Tab> result = [];
|
|
||||||
|
|
||||||
if (widget.homeAssistant.ui.views.isNotEmpty) {
|
|
||||||
widget.homeAssistant.ui.views.forEach((HAView view) {
|
|
||||||
result.add(view.buildTab());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Drawer _buildAppDrawer() {
|
|
||||||
List<Widget> menuItems = [];
|
|
||||||
menuItems.add(
|
|
||||||
UserAccountsDrawerHeader(
|
|
||||||
accountName: Text(widget.homeAssistant.userName),
|
|
||||||
accountEmail: Text(Connection().displayHostname ?? "Not configured"),
|
|
||||||
/*onDetailsPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_accountMenuExpanded = !_accountMenuExpanded;
|
|
||||||
});
|
|
||||||
},*/
|
|
||||||
currentAccountPicture: CircleAvatar(
|
|
||||||
child: Text(
|
|
||||||
widget.homeAssistant.userAvatarText,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 32.0
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (widget.homeAssistant.panels.isNotEmpty) {
|
|
||||||
widget.homeAssistant.panels.forEach((Panel panel) {
|
|
||||||
if (!panel.isHidden) {
|
|
||||||
menuItems.add(
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
|
||||||
title: Text("${panel.title}"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
panel.handleOpen(context);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//TODO check for loaded
|
|
||||||
menuItems.add(
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
|
|
||||||
title: Text("Open Web UI"),
|
|
||||||
onTap: () => HAUtils.launchURL(Connection().httpWebHost),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
menuItems.addAll([
|
|
||||||
Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
|
|
||||||
title: Text("Connection settings"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
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(
|
"/webview": (context) => WebviewScaffold(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
|
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
|
||||||
title: Text("Report an issue"),
|
appBar: new AppBar(
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")),
|
|
||||||
title: Text("Support app development"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed('/putchase');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(Icons.help),
|
|
||||||
title: Text("Help"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURL("http://ha-client.homemade.systems/docs");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
|
||||||
title: Text("Join Discord channel"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new AboutListTile(
|
|
||||||
aboutBoxChildren: <Widget>[
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURL("http://ha-client.homemade.systems/");
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"ha-client.homemade.systems",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 10.0,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"Terms and Conditions",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 10.0,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"Privacy Policy",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
applicationName: appName,
|
|
||||||
applicationVersion: appVersion
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
return new Drawer(
|
|
||||||
child: ListView(
|
|
||||||
children: menuItems,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _hideBottomBar() {
|
|
||||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
|
||||||
setState(() {
|
|
||||||
_showBottomBar = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _bottomBarAction;
|
|
||||||
bool _showBottomBar = false;
|
|
||||||
String _bottomBarText;
|
|
||||||
bool _bottomBarProgress;
|
|
||||||
Color _bottomBarColor;
|
|
||||||
Timer _bottomBarTimer;
|
|
||||||
|
|
||||||
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
|
||||||
_bottomBarTimer?.cancel();
|
|
||||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
|
||||||
_bottomBarColor = Colors.grey.shade50;
|
|
||||||
setState(() {
|
|
||||||
_bottomBarText = message;
|
|
||||||
_bottomBarProgress = progress;
|
|
||||||
_showBottomBar = true;
|
|
||||||
});
|
|
||||||
if (duration != null) {
|
|
||||||
_bottomBarTimer = Timer(duration, () {
|
|
||||||
_hideBottomBar();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showErrorBottomBar(HAError error) {
|
|
||||||
TextStyle textStyle = TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: Sizes.nameFontSize
|
|
||||||
);
|
|
||||||
_bottomBarColor = Colors.red.shade100;
|
|
||||||
List<Widget> actions = [];
|
|
||||||
error.actions.forEach((HAErrorAction action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case HAErrorActionType.FULL_RELOAD: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_fullLoad();
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.QUICK_RELOAD: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_quickLoad();
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.URL: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: "${action.url}");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pushNamed(context, '/connection-settings');
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (actions.isNotEmpty) {
|
|
||||||
_bottomBarAction = Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: actions,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_bottomBarProgress = false;
|
|
||||||
_bottomBarText = "${error.message}";
|
|
||||||
_showBottomBar = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
|
||||||
|
|
||||||
Widget _buildScaffoldBody(bool empty) {
|
|
||||||
List<PopupMenuItem<String>> popupMenuItems = [];
|
|
||||||
popupMenuItems.add(PopupMenuItem<String>(
|
|
||||||
child: new Text("Reload"),
|
|
||||||
value: "reload",
|
|
||||||
));
|
|
||||||
List<Widget> emptyBody = [
|
|
||||||
Text("."),
|
|
||||||
];
|
|
||||||
if (Connection().isAuthenticated) {
|
|
||||||
_showLoginButton = false;
|
|
||||||
popupMenuItems.add(
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
child: new Text("Logout"),
|
|
||||||
value: "logout",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (_showLoginButton) {
|
|
||||||
emptyBody = [
|
|
||||||
FlatButton(
|
|
||||||
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
|
||||||
color: Colors.blue,
|
|
||||||
onPressed: () => _fullLoad(),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return NestedScrollView(
|
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
|
||||||
return <Widget>[
|
|
||||||
SliverAppBar(
|
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
|
||||||
primary: true,
|
|
||||||
title: Text(widget.homeAssistant.locationName ?? ""),
|
|
||||||
actions: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
"mdi:dots-vertical"), color: Colors.white,),
|
|
||||||
onPressed: () {
|
|
||||||
showMenu(
|
|
||||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
|
||||||
context: context,
|
|
||||||
items: popupMenuItems
|
|
||||||
).then((String val) {
|
|
||||||
if (val == "reload") {
|
|
||||||
_quickLoad();
|
|
||||||
} else if (val == "logout") {
|
|
||||||
widget.homeAssistant.logout().then((_) {
|
|
||||||
_quickLoad();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
)
|
|
||||||
],
|
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.menu),
|
icon: Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () => Navigator.of(context).pop()
|
||||||
_scaffoldKey.currentState.openDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
bottom: empty ? null : TabBar(
|
|
||||||
controller: _viewsTabController,
|
|
||||||
tabs: buildUIViewTabs(),
|
|
||||||
isScrollable: true,
|
|
||||||
),
|
),
|
||||||
|
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
];
|
|
||||||
},
|
},
|
||||||
body: empty ?
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: emptyBody
|
|
||||||
),
|
|
||||||
)
|
|
||||||
:
|
|
||||||
widget.homeAssistant.buildViews(context, _viewsTabController),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
TabController _viewsTabController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget bottomBar;
|
|
||||||
if (_showBottomBar) {
|
|
||||||
List<Widget> bottomBarChildren = [];
|
|
||||||
if (_bottomBarText != null) {
|
|
||||||
bottomBarChildren.add(
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
|
||||||
Sizes.rowPadding),
|
|
||||||
child: Text(
|
|
||||||
"$_bottomBarText",
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (_bottomBarProgress) {
|
|
||||||
bottomBarChildren.add(
|
|
||||||
CollectionScaleTransition(
|
|
||||||
children: <Widget>[
|
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
|
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
|
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (bottomBarChildren.isNotEmpty) {
|
|
||||||
bottomBar = Container(
|
|
||||||
color: _bottomBarColor,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: bottomBarChildren,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_bottomBarAction
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This method is rerun every time setState is called.
|
|
||||||
if (widget.homeAssistant.isNoViews) {
|
|
||||||
return Scaffold(
|
|
||||||
key: _scaffoldKey,
|
|
||||||
primary: false,
|
|
||||||
drawer: _buildAppDrawer(),
|
|
||||||
bottomNavigationBar: bottomBar,
|
|
||||||
body: _buildScaffoldBody(true)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Scaffold(
|
|
||||||
key: _scaffoldKey,
|
|
||||||
drawer: _buildAppDrawer(),
|
|
||||||
primary: false,
|
|
||||||
bottomNavigationBar: bottomBar,
|
|
||||||
body: _buildScaffoldBody(false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
|
||||||
flutterWebviewPlugin.dispose();
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
_viewsTabController?.dispose();
|
|
||||||
_stateSubscription?.cancel();
|
|
||||||
_settingsSubscription?.cancel();
|
|
||||||
_serviceCallSubscription?.cancel();
|
|
||||||
_showPopupDialogSubscription?.cancel();
|
|
||||||
_showPopupMessageSubscription?.cancel();
|
|
||||||
_showEntityPageSubscription?.cancel();
|
|
||||||
_showErrorSubscription?.cancel();
|
|
||||||
_startAuthSubscription?.cancel();
|
|
||||||
_subscription?.cancel();
|
|
||||||
_reloadUISubscription?.cancel();
|
|
||||||
//TODO disconnect
|
|
||||||
//widget.homeAssistant?.disconnect();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
part of 'main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class AuthManager {
|
class AuthManager {
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ class AuthManager {
|
|||||||
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
||||||
String authCode = url.split("=")[1];
|
String authCode = url.split("=")[1];
|
||||||
Logger.d("We have auth code. Getting temporary access token...");
|
Logger.d("We have auth code. Getting temporary access token...");
|
||||||
Connection().sendHTTPPost(
|
ConnectionManager().sendHTTPPost(
|
||||||
endPoint: "/auth/token",
|
endPoint: "/auth/token",
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
includeAuthHeader: false,
|
includeAuthHeader: false,
|
@ -1,14 +1,14 @@
|
|||||||
part of 'main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class Connection {
|
class ConnectionManager {
|
||||||
|
|
||||||
static final Connection _instance = Connection._internal();
|
static final ConnectionManager _instance = ConnectionManager._internal();
|
||||||
|
|
||||||
factory Connection() {
|
factory ConnectionManager() {
|
||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connection._internal();
|
ConnectionManager._internal();
|
||||||
|
|
||||||
String _domain;
|
String _domain;
|
||||||
String _port;
|
String _port;
|
||||||
@ -54,21 +54,20 @@ class Connection {
|
|||||||
completer.completeError(HAError.checkConnectionSettings());
|
completer.completeError(HAError.checkConnectionSettings());
|
||||||
stopInit = true;
|
stopInit = true;
|
||||||
} else {
|
} else {
|
||||||
//_token = prefs.getString('hassio-token');
|
|
||||||
final storage = new FlutterSecureStorage();
|
final storage = new FlutterSecureStorage();
|
||||||
try {
|
try {
|
||||||
_token = await storage.read(key: "hacl_llt");
|
_token = await storage.read(key: "hacl_llt");
|
||||||
Logger.e("Long-lived token read successful");
|
Logger.e("Long-lived token read successful");
|
||||||
|
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||||
|
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
||||||
|
.encodeComponent(
|
||||||
|
'http://ha-client.homemade.systems/service/auth_callback.html')}";
|
||||||
|
settingsLoaded = true;
|
||||||
} catch (e) {
|
} 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.");
|
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||||
_token = null;
|
stopInit = true;
|
||||||
await storage.delete(key: "hacl_llt");
|
|
||||||
}
|
}
|
||||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
|
||||||
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
|
||||||
.encodeComponent(
|
|
||||||
'http://ha-client.homemade.systems/service/auth_callback.html')}";
|
|
||||||
settingsLoaded = true;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ((_domain == null) || (_port == null) ||
|
if ((_domain == null) || (_port == null) ||
|
||||||
@ -148,9 +147,7 @@ class Connection {
|
|||||||
Logger.d("[Received] <== ${data.toString()}");
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||||
_messageResolver.remove("auth");
|
_messageResolver.remove("auth");
|
||||||
logout().then((_) {
|
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||||
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
_handleMessage(data);
|
_handleMessage(data);
|
||||||
}
|
}
|
||||||
@ -281,6 +278,7 @@ class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future logout() {
|
Future logout() {
|
||||||
|
Logger.d("Logging out");
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
_disconnect().whenComplete(() {
|
_disconnect().whenComplete(() {
|
||||||
_token = null;
|
_token = null;
|
||||||
@ -309,8 +307,7 @@ class Connection {
|
|||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
logout();
|
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||||
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
|
|
||||||
});
|
});
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
part of 'main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class Device {
|
class DeviceInfoManager {
|
||||||
|
|
||||||
static final Device _instance = Device._internal();
|
static final DeviceInfoManager _instance = DeviceInfoManager._internal();
|
||||||
|
|
||||||
factory Device() {
|
factory DeviceInfoManager() {
|
||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ class Device {
|
|||||||
String osName;
|
String osName;
|
||||||
String osVersion;
|
String osVersion;
|
||||||
|
|
||||||
Device._internal();
|
DeviceInfoManager._internal();
|
||||||
|
|
||||||
loadDeviceInfo() {
|
loadDeviceInfo() {
|
||||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
5
lib/managers/location_manager.class.dart
Normal file
5
lib/managers/location_manager.class.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class LocationManager {
|
||||||
|
|
||||||
|
}
|
121
lib/managers/mobile_app_integration_manager.class.dart
Normal file
121
lib/managers/mobile_app_integration_manager.class.dart
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class MobileAppIntegrationManager {
|
||||||
|
|
||||||
|
static final _appRegistrationData = {
|
||||||
|
"app_version": "$appVersion",
|
||||||
|
"device_name": "${HomeAssistant().userName}'s ${DeviceInfoManager().model}",
|
||||||
|
"manufacturer": DeviceInfoManager().manufacturer,
|
||||||
|
"model": DeviceInfoManager().model,
|
||||||
|
"os_version": DeviceInfoManager().osVersion,
|
||||||
|
"app_data": {
|
||||||
|
"push_token": "${HomeAssistant().fcmToken}",
|
||||||
|
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
||||||
|
Completer completer = Completer();
|
||||||
|
if (ConnectionManager().webhookId == null || forceRegister) {
|
||||||
|
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
||||||
|
var registrationData = Map.from(_appRegistrationData);
|
||||||
|
registrationData.addAll({
|
||||||
|
"app_id": "ha_client",
|
||||||
|
"app_name": "$appName",
|
||||||
|
"os_name": DeviceInfoManager().osName,
|
||||||
|
"supports_encryption": false,
|
||||||
|
});
|
||||||
|
ConnectionManager().sendHTTPPost(
|
||||||
|
endPoint: "/api/mobile_app/registrations",
|
||||||
|
includeAuthHeader: true,
|
||||||
|
data: json.encode(registrationData)
|
||||||
|
).then((response) {
|
||||||
|
Logger.d("Processing registration responce...");
|
||||||
|
var responseObject = json.decode(response);
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||||
|
ConnectionManager().webhookId = responseObject["webhook_id"];
|
||||||
|
completer.complete();
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Mobile app Integration was created",
|
||||||
|
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
|
||||||
|
positiveText: "Restart now",
|
||||||
|
negativeText: "Later",
|
||||||
|
onPositive: () {
|
||||||
|
ConnectionManager().callService(domain: "homeassistant", service: "restart", entityId: null);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
completer.complete();
|
||||||
|
Logger.e("Error registering the app: ${e.toString()}");
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
} else {
|
||||||
|
Logger.d("App was previously registered. Checking...");
|
||||||
|
var updateData = {
|
||||||
|
"type": "update_registration",
|
||||||
|
"data": _appRegistrationData
|
||||||
|
};
|
||||||
|
ConnectionManager().sendHTTPPost(
|
||||||
|
endPoint: "/api/webhook/${ConnectionManager().webhookId}",
|
||||||
|
includeAuthHeader: false,
|
||||||
|
data: json.encode(updateData)
|
||||||
|
).then((response) {
|
||||||
|
if (response == null || response.isEmpty) {
|
||||||
|
Logger.d("No registration data in response. MobileApp integration was removed");
|
||||||
|
_askToRegisterApp();
|
||||||
|
} 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['code'] != null && e['code'] == 410) {
|
||||||
|
Logger.e("MobileApp integration was removed");
|
||||||
|
_askToRegisterApp();
|
||||||
|
} 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");
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
completer.complete();
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _askToRegisterApp() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "App integration was removed",
|
||||||
|
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
||||||
|
positiveText: "Register now",
|
||||||
|
negativeText: "Cancel",
|
||||||
|
onPositive: () {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.remove("app-webhook-id");
|
||||||
|
ConnectionManager().webhookId = null;
|
||||||
|
checkAppRegistration();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
46
lib/managers/startup_user_messages_manager.class.dart
Normal file
46
lib/managers/startup_user_messages_manager.class.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class StartupUserMessagesManager {
|
||||||
|
|
||||||
|
static final StartupUserMessagesManager _instance = StartupUserMessagesManager
|
||||||
|
._internal();
|
||||||
|
|
||||||
|
factory StartupUserMessagesManager() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartupUserMessagesManager._internal() {}
|
||||||
|
|
||||||
|
bool _supportAppDevelopmentMessageShown;
|
||||||
|
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||||
|
|
||||||
|
void checkMessagesToShow() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.reload();
|
||||||
|
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
||||||
|
if (!_supportAppDevelopmentMessageShown) {
|
||||||
|
_showSupportAppDevelopmentMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSupportAppDevelopmentMessage() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Hi!",
|
||||||
|
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
|
||||||
|
positiveText: "Show options",
|
||||||
|
negativeText: "Cancel",
|
||||||
|
onPositive: () {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
||||||
|
eventBus.fire(ShowPageEvent(path: "/putchase"));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onNegative: () {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class EntityViewPage extends StatefulWidget {
|
class EntityViewPage extends StatefulWidget {
|
||||||
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
|
EntityViewPage({Key key, @required this.entityId}) : super(key: key);
|
||||||
|
|
||||||
final String entityId;
|
final String entityId;
|
||||||
final HomeAssistant homeAssistant;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_EntityViewPageState createState() => new _EntityViewPageState();
|
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||||
@ -31,7 +30,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _prepareData() async {
|
void _prepareData() async {
|
||||||
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
|
_title = HomeAssistant().entities.get(widget.entityId).displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -46,7 +45,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
|||||||
// the App.build method, and use it to set our appbar title.
|
// the App.build method, and use it to set our appbar title.
|
||||||
title: new Text(_title),
|
title: new Text(_title),
|
||||||
),
|
),
|
||||||
body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context),
|
body: HomeAssistant().entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
802
lib/pages/main.page.dart
Normal file
802
lib/pages/main.page.dart
Normal file
@ -0,0 +1,802 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class MainPage extends StatefulWidget {
|
||||||
|
MainPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_MainPageState createState() => new _MainPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||||
|
|
||||||
|
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||||
|
StreamSubscription _stateSubscription;
|
||||||
|
StreamSubscription _settingsSubscription;
|
||||||
|
StreamSubscription _serviceCallSubscription;
|
||||||
|
StreamSubscription _showEntityPageSubscription;
|
||||||
|
StreamSubscription _showErrorSubscription;
|
||||||
|
StreamSubscription _startAuthSubscription;
|
||||||
|
StreamSubscription _showPopupDialogSubscription;
|
||||||
|
StreamSubscription _showPopupMessageSubscription;
|
||||||
|
StreamSubscription _reloadUISubscription;
|
||||||
|
StreamSubscription _showPageSubscription;
|
||||||
|
int _previousViewCount;
|
||||||
|
bool _showLoginButton = false;
|
||||||
|
bool _preventAppRefresh = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final Stream purchaseUpdates =
|
||||||
|
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||||
|
_subscription = purchaseUpdates.listen((purchases) {
|
||||||
|
_handlePurchaseUpdates(purchases);
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
_firebaseMessaging.configure(
|
||||||
|
onLaunch: (data) {
|
||||||
|
Logger.d("Notification [onLaunch]: $data");
|
||||||
|
return Future.value();
|
||||||
|
},
|
||||||
|
onMessage: (data) {
|
||||||
|
Logger.d("Notification [onMessage]: $data");
|
||||||
|
return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
|
||||||
|
},
|
||||||
|
onResume: (data) {
|
||||||
|
Logger.d("Notification [onResume]: $data");
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
||||||
|
|
||||||
|
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
||||||
|
var initializationSettingsAndroid =
|
||||||
|
new AndroidInitializationSettings('mini_icon');
|
||||||
|
var initializationSettingsIOS = new IOSInitializationSettings(
|
||||||
|
onDidReceiveLocalNotification: null);
|
||||||
|
var initializationSettings = new InitializationSettings(
|
||||||
|
initializationSettingsAndroid, initializationSettingsIOS);
|
||||||
|
flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
||||||
|
onSelectNotification: onSelectNotification);
|
||||||
|
|
||||||
|
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
||||||
|
Logger.d("Settings change event: reconnect=${event.reconnect}");
|
||||||
|
if (event.reconnect) {
|
||||||
|
_preventAppRefresh = false;
|
||||||
|
_fullLoad();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_fullLoad();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Future onSelectNotification(String payload) async {
|
||||||
|
if (payload != null) {
|
||||||
|
Logger.d('Notification clicked: ' + payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _showNotification({String title, String text}) async {
|
||||||
|
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
|
||||||
|
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
|
||||||
|
importance: Importance.Max, priority: Priority.High);
|
||||||
|
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
|
||||||
|
var platformChannelSpecifics = new NotificationDetails(
|
||||||
|
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
title ?? appName,
|
||||||
|
text,
|
||||||
|
platformChannelSpecifics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fullLoad() async {
|
||||||
|
_showInfoBottomBar(progress: true,);
|
||||||
|
_subscribe().then((_) {
|
||||||
|
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||||
|
_fetchData();
|
||||||
|
StartupUserMessagesManager().checkMessagesToShow();
|
||||||
|
}, onError: (e) {
|
||||||
|
_setErrorState(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _quickLoad() {
|
||||||
|
_hideBottomBar();
|
||||||
|
_showInfoBottomBar(progress: true,);
|
||||||
|
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
||||||
|
_fetchData();
|
||||||
|
//StartupUserMessagesManager().checkMessagesToShow();
|
||||||
|
}, onError: (e) {
|
||||||
|
_setErrorState(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_fetchData() async {
|
||||||
|
await HomeAssistant().fetchData().then((_) {
|
||||||
|
_hideBottomBar();
|
||||||
|
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||||
|
if (_previousViewCount != currentViewCount) {
|
||||||
|
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
||||||
|
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
||||||
|
_previousViewCount = currentViewCount;
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
if (e is HAError) {
|
||||||
|
_setErrorState(e);
|
||||||
|
} else {
|
||||||
|
_setErrorState(HAError(e.toString()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
eventBus.fire(RefreshDataFinishedEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
Logger.d("$state");
|
||||||
|
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||||
|
_quickLoad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePurchaseUpdates(purchase) {
|
||||||
|
if (purchase is List<PurchaseDetails>) {
|
||||||
|
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||||
|
eventBus.fire(ShowPopupMessageEvent(
|
||||||
|
title: "Thanks a lot!",
|
||||||
|
body: "Thank you for supporting HA Client development!",
|
||||||
|
buttonText: "Ok"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
Logger.d("Purchase change handler: ${purchase[0].status}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _subscribe() {
|
||||||
|
Completer completer = Completer();
|
||||||
|
if (_stateSubscription == null) {
|
||||||
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
|
if (event.needToRebuildUI) {
|
||||||
|
Logger.d("New entity. Need to rebuild UI");
|
||||||
|
_quickLoad();
|
||||||
|
} else {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_reloadUISubscription == null) {
|
||||||
|
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
||||||
|
_quickLoad();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_showPopupDialogSubscription == null) {
|
||||||
|
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
|
||||||
|
_showPopupDialog(
|
||||||
|
title: event.title,
|
||||||
|
body: event.body,
|
||||||
|
onPositive: event.onPositive,
|
||||||
|
onNegative: event.onNegative,
|
||||||
|
positiveText: event.positiveText,
|
||||||
|
negativeText: event.negativeText
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_showPopupMessageSubscription == null) {
|
||||||
|
_showPopupMessageSubscription = eventBus.on<ShowPopupMessageEvent>().listen((event){
|
||||||
|
_showPopupDialog(
|
||||||
|
title: event.title,
|
||||||
|
body: event.body,
|
||||||
|
onPositive: event.onButtonClick,
|
||||||
|
positiveText: event.buttonText,
|
||||||
|
negativeText: null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_serviceCallSubscription == null) {
|
||||||
|
_serviceCallSubscription =
|
||||||
|
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||||
|
_callService(event.domain, event.service, event.entityId,
|
||||||
|
event.additionalParams);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_showEntityPageSubscription == null) {
|
||||||
|
_showEntityPageSubscription =
|
||||||
|
eventBus.on<ShowEntityPageEvent>().listen((event) {
|
||||||
|
_showEntityPage(event.entity.entityId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_showPageSubscription == null) {
|
||||||
|
_showPageSubscription =
|
||||||
|
eventBus.on<ShowPageEvent>().listen((event) {
|
||||||
|
_showPage(event.path, event.goBackFirst);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_showErrorSubscription == null) {
|
||||||
|
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
||||||
|
_showErrorBottomBar(event.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_startAuthSubscription == null) {
|
||||||
|
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
||||||
|
setState(() {
|
||||||
|
_showLoginButton = event.showButton;
|
||||||
|
});
|
||||||
|
if (event.showButton) {
|
||||||
|
_showOAuth();
|
||||||
|
} else {
|
||||||
|
_preventAppRefresh = false;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_firebaseMessaging.getToken().then((String token) {
|
||||||
|
HomeAssistant().fcmToken = token;
|
||||||
|
completer.complete();
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showOAuth() {
|
||||||
|
_preventAppRefresh = true;
|
||||||
|
Navigator.of(context).pushNamed('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
_setErrorState(HAError e) {
|
||||||
|
if (e == null) {
|
||||||
|
_showErrorBottomBar(
|
||||||
|
HAError("Unknown error")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO remove this shit
|
||||||
|
void _callService(String domain, String service, String entityId, Map additionalParams) {
|
||||||
|
_showInfoBottomBar(
|
||||||
|
message: "Calling $domain.$service",
|
||||||
|
duration: Duration(seconds: 3)
|
||||||
|
);
|
||||||
|
ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showEntityPage(String entityId) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EntityViewPage(entityId: entityId),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPage(String path, bool goBackFirst) {
|
||||||
|
if (goBackFirst) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Tab> buildUIViewTabs() {
|
||||||
|
List<Tab> result = [];
|
||||||
|
|
||||||
|
if (HomeAssistant().ui.views.isNotEmpty) {
|
||||||
|
HomeAssistant().ui.views.forEach((HAView view) {
|
||||||
|
result.add(view.buildTab());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Drawer _buildAppDrawer() {
|
||||||
|
List<Widget> menuItems = [];
|
||||||
|
menuItems.add(
|
||||||
|
UserAccountsDrawerHeader(
|
||||||
|
accountName: Text(HomeAssistant().userName),
|
||||||
|
accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"),
|
||||||
|
onDetailsPressed: () {
|
||||||
|
final flutterWebViewPlugin = new FlutterWebviewPlugin();
|
||||||
|
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
|
||||||
|
if (viewState.type == WebViewState.startLoad) {
|
||||||
|
Logger.d("[WebView] Injecting external auth JS");
|
||||||
|
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
||||||
|
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
"/webview",
|
||||||
|
arguments: {
|
||||||
|
"url": "${ConnectionManager().httpWebHost}/profile?external_auth=1",
|
||||||
|
"title": "Profile"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
currentAccountPicture: CircleAvatar(
|
||||||
|
child: Text(
|
||||||
|
HomeAssistant().userAvatarText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32.0
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (HomeAssistant().panels.isNotEmpty) {
|
||||||
|
HomeAssistant().panels.forEach((Panel panel) {
|
||||||
|
if (!panel.isHidden) {
|
||||||
|
menuItems.add(
|
||||||
|
new ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
||||||
|
title: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("${panel.title}"),
|
||||||
|
Container(width: 4.0,),
|
||||||
|
panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
panel.handleOpen(context);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
menuItems.addAll([
|
||||||
|
Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
|
||||||
|
title: Text("Connection settings"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushNamed('/connection-settings');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
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");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Divider(),
|
||||||
|
new ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")),
|
||||||
|
title: Text("Support app development"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushNamed('/putchase');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Divider(),
|
||||||
|
new ListTile(
|
||||||
|
leading: Icon(Icons.help),
|
||||||
|
title: Text("Help"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Launcher.launchURL("http://ha-client.homemade.systems/docs");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
new ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||||
|
title: Text("Join Discord channel"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Launcher.launchURL("https://discord.gg/AUzEvwn");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
new AboutListTile(
|
||||||
|
aboutBoxChildren: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Launcher.launchURL("http://ha-client.homemade.systems/");
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"ha-client.homemade.systems",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 10.0,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Terms and Conditions",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 10.0,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Privacy Policy",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
applicationName: appName,
|
||||||
|
applicationVersion: appVersion
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
return new Drawer(
|
||||||
|
child: ListView(
|
||||||
|
children: menuItems,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hideBottomBar() {
|
||||||
|
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
||||||
|
setState(() {
|
||||||
|
_showBottomBar = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _bottomBarAction;
|
||||||
|
bool _showBottomBar = false;
|
||||||
|
String _bottomBarText;
|
||||||
|
bool _bottomBarProgress;
|
||||||
|
Color _bottomBarColor;
|
||||||
|
Timer _bottomBarTimer;
|
||||||
|
|
||||||
|
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
||||||
|
_bottomBarTimer?.cancel();
|
||||||
|
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||||
|
_bottomBarColor = Colors.grey.shade50;
|
||||||
|
setState(() {
|
||||||
|
_bottomBarText = message;
|
||||||
|
_bottomBarProgress = progress;
|
||||||
|
_showBottomBar = true;
|
||||||
|
});
|
||||||
|
if (duration != null) {
|
||||||
|
_bottomBarTimer = Timer(duration, () {
|
||||||
|
_hideBottomBar();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorBottomBar(HAError error) {
|
||||||
|
TextStyle textStyle = TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontSize: Sizes.nameFontSize
|
||||||
|
);
|
||||||
|
_bottomBarColor = Colors.red.shade100;
|
||||||
|
List<Widget> actions = [];
|
||||||
|
error.actions.forEach((HAErrorAction action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case HAErrorActionType.FULL_RELOAD: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
_fullLoad();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.QUICK_RELOAD: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
_quickLoad();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.RELOGIN: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
ConnectionManager().logout().then((_) => _fullLoad());
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.URL: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
|
||||||
|
actions.add(FlatButton(
|
||||||
|
child: Text("${action.title}", style: textStyle),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/connection-settings');
|
||||||
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (actions.isNotEmpty) {
|
||||||
|
_bottomBarAction = Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: actions,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_bottomBarProgress = false;
|
||||||
|
_bottomBarText = "${error.message}";
|
||||||
|
_showBottomBar = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
|
Widget _buildScaffoldBody(bool empty) {
|
||||||
|
List<PopupMenuItem<String>> popupMenuItems = [];
|
||||||
|
|
||||||
|
popupMenuItems.add(PopupMenuItem<String>(
|
||||||
|
child: new Text("Reload"),
|
||||||
|
value: "reload",
|
||||||
|
));
|
||||||
|
List<Widget> emptyBody = [
|
||||||
|
Text("."),
|
||||||
|
];
|
||||||
|
if (ConnectionManager().isAuthenticated) {
|
||||||
|
_showLoginButton = false;
|
||||||
|
popupMenuItems.add(
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: new Text("Logout"),
|
||||||
|
value: "logout",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (_showLoginButton) {
|
||||||
|
emptyBody = [
|
||||||
|
FlatButton(
|
||||||
|
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
||||||
|
color: Colors.blue,
|
||||||
|
onPressed: () => _fullLoad(),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return NestedScrollView(
|
||||||
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
|
return <Widget>[
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
primary: true,
|
||||||
|
title: Text(HomeAssistant().locationName ?? ""),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:dots-vertical"), color: Colors.white,),
|
||||||
|
onPressed: () {
|
||||||
|
showMenu(
|
||||||
|
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
||||||
|
context: context,
|
||||||
|
items: popupMenuItems
|
||||||
|
).then((String val) {
|
||||||
|
if (val == "reload") {
|
||||||
|
_quickLoad();
|
||||||
|
} else if (val == "logout") {
|
||||||
|
HomeAssistant().logout().then((_) {
|
||||||
|
_quickLoad();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.menu),
|
||||||
|
onPressed: () {
|
||||||
|
_scaffoldKey.currentState.openDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
bottom: empty ? null : TabBar(
|
||||||
|
controller: _viewsTabController,
|
||||||
|
tabs: buildUIViewTabs(),
|
||||||
|
isScrollable: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
];
|
||||||
|
},
|
||||||
|
body: empty ?
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: emptyBody
|
||||||
|
),
|
||||||
|
)
|
||||||
|
:
|
||||||
|
HomeAssistant().buildViews(context, _viewsTabController),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TabController _viewsTabController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget bottomBar;
|
||||||
|
if (_showBottomBar) {
|
||||||
|
List<Widget> bottomBarChildren = [];
|
||||||
|
if (_bottomBarText != null) {
|
||||||
|
bottomBarChildren.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
||||||
|
Sizes.rowPadding),
|
||||||
|
child: Text(
|
||||||
|
"$_bottomBarText",
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_bottomBarProgress) {
|
||||||
|
bottomBarChildren.add(
|
||||||
|
CollectionScaleTransition(
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
|
||||||
|
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
|
||||||
|
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (bottomBarChildren.isNotEmpty) {
|
||||||
|
bottomBar = Container(
|
||||||
|
color: _bottomBarColor,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: bottomBarChildren,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_bottomBarAction
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This method is rerun every time setState is called.
|
||||||
|
if (HomeAssistant().isNoViews) {
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
primary: false,
|
||||||
|
drawer: _buildAppDrawer(),
|
||||||
|
bottomNavigationBar: bottomBar,
|
||||||
|
body: _buildScaffoldBody(true)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
drawer: _buildAppDrawer(),
|
||||||
|
primary: false,
|
||||||
|
bottomNavigationBar: bottomBar,
|
||||||
|
body: _buildScaffoldBody(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||||
|
flutterWebviewPlugin.dispose();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_viewsTabController?.dispose();
|
||||||
|
_stateSubscription?.cancel();
|
||||||
|
_settingsSubscription?.cancel();
|
||||||
|
_serviceCallSubscription?.cancel();
|
||||||
|
_showPopupDialogSubscription?.cancel();
|
||||||
|
_showPopupMessageSubscription?.cancel();
|
||||||
|
_showEntityPageSubscription?.cancel();
|
||||||
|
_showErrorSubscription?.cancel();
|
||||||
|
_startAuthSubscription?.cancel();
|
||||||
|
_subscription?.cancel();
|
||||||
|
_showPageSubscription?.cancel();
|
||||||
|
_reloadUISubscription?.cancel();
|
||||||
|
//TODO disconnect
|
||||||
|
//widget.homeAssistant?.disconnect();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -12,8 +12,6 @@ class PanelPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _PanelPageState extends State<PanelPage> {
|
class _PanelPageState extends State<PanelPage> {
|
||||||
|
|
||||||
List<ConfigurationItem> _items;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -10,7 +10,7 @@ class PurchasePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PurchasePageState extends State<PurchasePage> {
|
class _PurchasePageState extends State<PurchasePage> {
|
||||||
|
|
||||||
bool _loaded = false;
|
bool _loaded = false;
|
||||||
String _error = "";
|
String _error = "";
|
||||||
List<ProductDetails> _products;
|
List<ProductDetails> _products;
|
||||||
@ -29,7 +29,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
_error = "Error connecting to store";
|
_error = "Error connecting to store";
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const Set<String> _kIds = {'just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
||||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||||
if (!response.notFoundIDs.isEmpty) {
|
if (!response.notFoundIDs.isEmpty) {
|
||||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||||
@ -61,7 +61,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProducts() {
|
Widget _buildProducts() {
|
||||||
List<Widget> productWidgets = [];
|
List<Widget> productWidgets = [];
|
||||||
for (ProductDetails product in _products) {
|
for (ProductDetails product in _products) {
|
||||||
|
@ -74,6 +74,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_saveSettings() async {
|
_saveSettings() async {
|
||||||
|
_newHassioDomain = _newHassioDomain.trim();
|
||||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||||
}
|
}
|
||||||
@ -81,12 +82,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
final storage = new FlutterSecureStorage();
|
final storage = new FlutterSecureStorage();
|
||||||
if (_newLongLivedToken.isNotEmpty) {
|
if (_newLongLivedToken.isNotEmpty) {
|
||||||
|
_newLongLivedToken = _newLongLivedToken.trim();
|
||||||
prefs.setBool("oauth-used", false);
|
prefs.setBool("oauth-used", false);
|
||||||
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
||||||
} else if (!useOAuth) {
|
} else if (!useOAuth) {
|
||||||
await storage.delete(key: "hacl_llt");
|
await storage.delete(key: "hacl_llt");
|
||||||
}
|
}
|
||||||
prefs.setString("hassio-domain", _newHassioDomain);
|
prefs.setString("hassio-domain", _newHassioDomain);
|
||||||
|
if (_newHassioPort == null || _newHassioPort.isEmpty) {
|
||||||
|
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
|
||||||
|
} else {
|
||||||
|
_newHassioPort = _newHassioPort.trim();
|
||||||
|
}
|
||||||
prefs.setString("hassio-port", _newHassioPort);
|
prefs.setString("hassio-port", _newHassioPort);
|
||||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||||
@ -147,13 +154,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Home Assistant domain or ip address"
|
labelText: "Home Assistant domain or ip address"
|
||||||
),
|
),
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _newHassioDomain,
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _newHassioDomain.length)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_newHassioDomain = value;
|
_newHassioDomain = value;
|
||||||
}
|
}
|
||||||
@ -162,13 +163,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Home Assistant port (default is 8123)"
|
labelText: "Home Assistant port (default is 8123)"
|
||||||
),
|
),
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _newHassioPort,
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _newHassioPort.length)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_newHassioPort = value;
|
_newHassioPort = value;
|
||||||
}
|
}
|
||||||
@ -216,13 +211,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Long-lived token"
|
labelText: "Long-lived token"
|
||||||
),
|
),
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _newLongLivedToken ?? '',
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _newLongLivedToken != null ? _newLongLivedToken.length : 0)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_newLongLivedToken = value;
|
_newLongLivedToken = value;
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,19 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String period = "/ ";
|
String period = "";
|
||||||
Color priceColor;
|
Color priceColor;
|
||||||
|
String buttonText = '';
|
||||||
|
String buttonTextInactive = '';
|
||||||
if (product.id.contains("year")) {
|
if (product.id.contains("year")) {
|
||||||
period += "year";
|
period += "/ year";
|
||||||
|
buttonText = "Subscribe";
|
||||||
|
buttonTextInactive = "Already";
|
||||||
priceColor = Colors.amber;
|
priceColor = Colors.amber;
|
||||||
} else {
|
} else {
|
||||||
period += "month";
|
period += "";
|
||||||
|
buttonText = "Pay";
|
||||||
|
buttonTextInactive = "Paid";
|
||||||
priceColor = Colors.deepOrangeAccent;
|
priceColor = Colors.deepOrangeAccent;
|
||||||
}
|
}
|
||||||
return Card(
|
return Card(
|
||||||
@ -55,7 +61,7 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: RaisedButton(
|
child: RaisedButton(
|
||||||
child: Text(this.purchased ? "Bought" : "Buy", style: TextStyle(color: Colors.white)),
|
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
||||||
),
|
),
|
||||||
|
120
lib/panels/config_panel_widget.dart
Normal file
120
lib/panels/config_panel_widget.dart
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ConfigPanelWidget extends StatefulWidget {
|
||||||
|
ConfigPanelWidget({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
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", entityId: null);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Are you sure you wanr 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", entityId: null);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
title: Text("Mobile app integration",
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize))
|
||||||
|
),
|
||||||
|
Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
|
||||||
|
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 registration", style: TextStyle(color: Colors.white))
|
||||||
|
),
|
||||||
|
Container(width: 10.0,),
|
||||||
|
RaisedButton(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
onPressed: () => resetRegistration(),
|
||||||
|
child: Text("Reset registration", style: TextStyle(color: Colors.white))
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
LinkToWebConfig(name: "Home Assistant Cloud", url: ConnectionManager().httpWebHost+"/config/cloud/account"),
|
||||||
|
Container(height: 8.0,),
|
||||||
|
LinkToWebConfig(name: "Integrations", url: ConnectionManager().httpWebHost+"/config/integrations/dashboard"),
|
||||||
|
LinkToWebConfig(name: "Users", url: ConnectionManager().httpWebHost+"/config/users/picker"),
|
||||||
|
Container(height: 8.0,),
|
||||||
|
LinkToWebConfig(name: "General", url: ConnectionManager().httpWebHost+"/config/core"),
|
||||||
|
LinkToWebConfig(name: "Server Control", url: ConnectionManager().httpWebHost+"/config/server_control"),
|
||||||
|
LinkToWebConfig(name: "Persons", url: ConnectionManager().httpWebHost+"/config/person"),
|
||||||
|
LinkToWebConfig(name: "Entity Registry", url: ConnectionManager().httpWebHost+"/config/entity_registry"),
|
||||||
|
LinkToWebConfig(name: "Area Registry", url: ConnectionManager().httpWebHost+"/config/area_registry"),
|
||||||
|
LinkToWebConfig(name: "Automation", url: ConnectionManager().httpWebHost+"/config/automation"),
|
||||||
|
LinkToWebConfig(name: "Script", url: ConnectionManager().httpWebHost+"/config/script"),
|
||||||
|
LinkToWebConfig(name: "Customization", url: ConnectionManager().httpWebHost+"/config/customize"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
29
lib/panels/widgets/link_to_web_config.dart
Normal file
29
lib/panels/widgets/link_to_web_config.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class LinkToWebConfig extends StatelessWidget {
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
const LinkToWebConfig({Key key, @required this.name, @required this.url}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
title: Text("${this.name}",
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||||
|
subtitle: Text("Tap to opne web version"),
|
||||||
|
onTap: () {
|
||||||
|
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
82
lib/types/event_bus_events.dart
Normal file
82
lib/types/event_bus_events.dart
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class StateChangedEvent {
|
||||||
|
String entityId;
|
||||||
|
String newState;
|
||||||
|
bool needToRebuildUI;
|
||||||
|
|
||||||
|
StateChangedEvent({
|
||||||
|
this.entityId,
|
||||||
|
this.newState,
|
||||||
|
this.needToRebuildUI: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsChangedEvent {
|
||||||
|
bool reconnect;
|
||||||
|
|
||||||
|
SettingsChangedEvent(this.reconnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshDataFinishedEvent {
|
||||||
|
RefreshDataFinishedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReloadUIEvent {
|
||||||
|
ReloadUIEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartAuthEvent {
|
||||||
|
String oauthUrl;
|
||||||
|
bool showButton;
|
||||||
|
|
||||||
|
StartAuthEvent(this.oauthUrl, this.showButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceCallEvent {
|
||||||
|
String domain;
|
||||||
|
String service;
|
||||||
|
String entityId;
|
||||||
|
Map<String, dynamic> additionalParams;
|
||||||
|
|
||||||
|
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowPopupDialogEvent {
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
final String positiveText;
|
||||||
|
final String negativeText;
|
||||||
|
final onPositive;
|
||||||
|
final onNegative;
|
||||||
|
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowEntityPageEvent {
|
||||||
|
Entity entity;
|
||||||
|
|
||||||
|
ShowEntityPageEvent(this.entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowPageEvent {
|
||||||
|
final String path;
|
||||||
|
final bool goBackFirst;
|
||||||
|
|
||||||
|
ShowPageEvent({@required this.path, this.goBackFirst: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowErrorEvent {
|
||||||
|
final HAError error;
|
||||||
|
|
||||||
|
ShowErrorEvent(this.error);
|
||||||
|
}
|
45
lib/types/ha_error.dart
Normal file
45
lib/types/ha_error.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class HAError {
|
||||||
|
String message;
|
||||||
|
final List<HAErrorAction> actions;
|
||||||
|
|
||||||
|
HAError(this.message, {this.actions: const [HAErrorAction.tryAgain()]});
|
||||||
|
|
||||||
|
HAError.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
|
||||||
|
this.message = "Unable to connect to Home Assistant";
|
||||||
|
}
|
||||||
|
|
||||||
|
HAError.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
|
||||||
|
this.message = "Disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
HAError.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
|
||||||
|
this.message = "Check connection settings";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HAErrorAction {
|
||||||
|
final String title;
|
||||||
|
final int type;
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
const HAErrorAction({@required this.title, this.type: HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
const HAErrorAction.tryAgain({this.title = "Try again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
const HAErrorAction.reconnect({this.title = "Reconnect", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
const HAErrorAction.reload({this.title = "Reload", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
||||||
|
|
||||||
|
const HAErrorAction.loginAgain({this.title = "Login again", this.type = HAErrorActionType.RELOGIN, this.url});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HAErrorActionType {
|
||||||
|
static const FULL_RELOAD = 0;
|
||||||
|
static const QUICK_RELOAD = 1;
|
||||||
|
static const URL = 3;
|
||||||
|
static const OPEN_CONNECTION_SETTINGS = 4;
|
||||||
|
static const RELOGIN = 5;
|
||||||
|
}
|
@ -29,7 +29,11 @@ class HACard {
|
|||||||
this.states,
|
this.states,
|
||||||
this.conditions: const [],
|
this.conditions: const [],
|
||||||
@required this.type
|
@required this.type
|
||||||
});
|
}) {
|
||||||
|
if (this.columnsCount <= 0) {
|
||||||
|
this.columnsCount = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<EntityWrapper> getEntitiesToShow() {
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
return entities.where((entityWrapper) {
|
return entities.where((entityWrapper) {
|
||||||
|
@ -17,28 +17,26 @@ class Panel {
|
|||||||
final Map config;
|
final Map config;
|
||||||
String icon;
|
String icon;
|
||||||
bool isHidden = true;
|
bool isHidden = true;
|
||||||
|
bool isWebView = false;
|
||||||
|
|
||||||
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
||||||
if (icon == null || !icon.startsWith("mdi:")) {
|
if (icon == null || !icon.startsWith("mdi:")) {
|
||||||
icon = Panel.iconsByComponent[type];
|
icon = Panel.iconsByComponent[type];
|
||||||
}
|
}
|
||||||
isHidden = (type != "iframe" && type != "config");
|
Logger.d("New panel '$title'. type=$type, icon=$icon, urlPath=$urlPath");
|
||||||
|
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
|
||||||
|
isWebView = (type != 'config');
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleOpen(BuildContext context) {
|
void handleOpen(BuildContext context) {
|
||||||
if (type == "iframe") {
|
if (type == "config") {
|
||||||
Logger.d("Launching custom tab with ${config["url"]}");
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: config["url"]);
|
|
||||||
} else if (type == "config") {
|
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PanelPage(title: "$title", panel: this),
|
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
String url = "${Connection().httpWebHost}/$urlPath";
|
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
|
||||||
Logger.d("Launching custom tab with $url");
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,331 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class ConfigPanelWidget extends StatefulWidget {
|
|
||||||
ConfigPanelWidget({Key key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ConfigPanelWidgetState createState() => new _ConfigPanelWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConfigurationItem {
|
|
||||||
ConfigurationItem({ this.isExpanded: false, this.header, this.body });
|
|
||||||
|
|
||||||
bool isExpanded;
|
|
||||||
final String header;
|
|
||||||
final Widget body;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
|
||||||
|
|
||||||
List<ConfigurationItem> _items;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_items = <ConfigurationItem>[
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Home Assistant Cloud',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/cloud/account");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Integrations',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/integrations/dashboard");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Users',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/users/picker");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'General',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/core");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text("Server management", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text("Control your Home Assistant server from HA Client."),
|
|
||||||
Divider(),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Restart', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () => restart(),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
child: Text("Stop", style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () => stop(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Persons',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/person");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Entity Registry',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/entity_registry");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Area Registry',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/area_registry");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Automation',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/automation");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Script',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/script");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Customization',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Open web version', style: TextStyle(color: Colors.blue)),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: Connection().httpWebHost+"/config/customize");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ConfigurationItem(
|
|
||||||
header: 'Mobile app',
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 0.0, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
Text("Registration", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text("${HomeAssistant().userName}'s ${Device().model}, ${Device().osName} ${Device().osVersion}"),
|
|
||||||
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.min,
|
|
||||||
children: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () => updateRegistration(),
|
|
||||||
child: Text("Check registration", style: TextStyle(color: Colors.blue))
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () => resetRegistration(),
|
|
||||||
child: Text("Reset registration", style: TextStyle(color: Colors.red))
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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: () {
|
|
||||||
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Are you sure you wanr 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: () {
|
|
||||||
Connection().callService(domain: "homeassistant", service: "stop", entityId: null);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRegistration() {
|
|
||||||
HomeAssistant().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: () {
|
|
||||||
HomeAssistant().checkAppRegistration(showOkDialog: true, forceRegister: true);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
new ExpansionPanelList(
|
|
||||||
expansionCallback: (int index, bool isExpanded) {
|
|
||||||
setState(() {
|
|
||||||
_items[index].isExpanded = !_items[index].isExpanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
children: _items.map((ConfigurationItem item) {
|
|
||||||
return new ExpansionPanel(
|
|
||||||
headerBuilder: (BuildContext context, bool isExpanded) {
|
|
||||||
return CardHeaderWidget(
|
|
||||||
name: item.header,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isExpanded: item.isExpanded,
|
|
||||||
body: new Container(
|
|
||||||
child: item.body,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,206 +0,0 @@
|
|||||||
part of 'main.dart';
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
assert(inDebugMode = true);
|
|
||||||
|
|
||||||
return inDebugMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void e(String message) {
|
|
||||||
_writeToLog("Error", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void w(String message) {
|
|
||||||
_writeToLog("Warning", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void d(String message) {
|
|
||||||
_writeToLog("Debug", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _writeToLog(String level, String message) {
|
|
||||||
if (isInDebugMode) {
|
|
||||||
debugPrint('$message');
|
|
||||||
}
|
|
||||||
DateTime t = DateTime.now();
|
|
||||||
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
|
|
||||||
if (_log.length > 100) {
|
|
||||||
_log.removeAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class HAError {
|
|
||||||
String message;
|
|
||||||
final List<HAErrorAction> actions;
|
|
||||||
|
|
||||||
HAError(this.message, {this.actions: const [HAErrorAction.tryAgain()]});
|
|
||||||
|
|
||||||
HAError.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
|
|
||||||
this.message = "Unable to connect to Home Assistant";
|
|
||||||
}
|
|
||||||
|
|
||||||
HAError.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
|
|
||||||
this.message = "Disconnected";
|
|
||||||
}
|
|
||||||
|
|
||||||
HAError.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
|
|
||||||
this.message = "Check connection settings";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HAErrorAction {
|
|
||||||
final String title;
|
|
||||||
final int type;
|
|
||||||
final String url;
|
|
||||||
|
|
||||||
const HAErrorAction({@required this.title, this.type: HAErrorActionType.FULL_RELOAD, this.url});
|
|
||||||
|
|
||||||
const HAErrorAction.tryAgain({this.title = "Try again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
|
||||||
|
|
||||||
const HAErrorAction.reconnect({this.title = "Reconnect", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
|
||||||
|
|
||||||
const HAErrorAction.reload({this.title = "Reload", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
|
||||||
|
|
||||||
const HAErrorAction.loginAgain({this.title = "Login again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class HAErrorActionType {
|
|
||||||
static const FULL_RELOAD = 0;
|
|
||||||
static const QUICK_RELOAD = 1;
|
|
||||||
static const LOGOUT = 2;
|
|
||||||
static const URL = 3;
|
|
||||||
static const OPEN_CONNECTION_SETTINGS = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
class HAUtils {
|
|
||||||
static void launchURL(String url) async {
|
|
||||||
if (await urlLauncher.canLaunch(url)) {
|
|
||||||
await urlLauncher.launch(url);
|
|
||||||
} else {
|
|
||||||
Logger.e( "Could not launch $url");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void launchURLInCustomTab({BuildContext context, String url, bool enableDefaultShare: true, bool showPageTitle: true}) async {
|
|
||||||
try {
|
|
||||||
await launch(
|
|
||||||
"$url",
|
|
||||||
option: new CustomTabsOption(
|
|
||||||
toolbarColor: Theme.of(context).primaryColor,
|
|
||||||
enableDefaultShare: enableDefaultShare,
|
|
||||||
enableUrlBarHiding: true,
|
|
||||||
showPageTitle: showPageTitle,
|
|
||||||
animation: new CustomTabsAnimation.slideIn()
|
|
||||||
// or user defined animation.
|
|
||||||
/*animation: new CustomTabsAnimation(
|
|
||||||
startEnter: 'slide_up',
|
|
||||||
startExit: 'android:anim/fade_out',
|
|
||||||
endEnter: 'android:anim/fade_in',
|
|
||||||
endExit: 'slide_down',
|
|
||||||
)*/,
|
|
||||||
extraCustomTabs: <String>[
|
|
||||||
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
|
|
||||||
'org.mozilla.firefox',
|
|
||||||
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
|
|
||||||
'com.microsoft.emmx',
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
Logger.w("Can't open custom tab: ${e.toString()}");
|
|
||||||
Logger.w("Launching in default browser");
|
|
||||||
HAUtils.launchURL(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StateChangedEvent {
|
|
||||||
String entityId;
|
|
||||||
String newState;
|
|
||||||
bool needToRebuildUI;
|
|
||||||
|
|
||||||
StateChangedEvent({
|
|
||||||
this.entityId,
|
|
||||||
this.newState,
|
|
||||||
this.needToRebuildUI: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsChangedEvent {
|
|
||||||
bool reconnect;
|
|
||||||
|
|
||||||
SettingsChangedEvent(this.reconnect);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RefreshDataFinishedEvent {
|
|
||||||
RefreshDataFinishedEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReloadUIEvent {
|
|
||||||
ReloadUIEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
class StartAuthEvent {
|
|
||||||
String oauthUrl;
|
|
||||||
bool showButton;
|
|
||||||
|
|
||||||
StartAuthEvent(this.oauthUrl, this.showButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServiceCallEvent {
|
|
||||||
String domain;
|
|
||||||
String service;
|
|
||||||
String entityId;
|
|
||||||
Map<String, dynamic> additionalParams;
|
|
||||||
|
|
||||||
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShowPopupDialogEvent {
|
|
||||||
final String title;
|
|
||||||
final String body;
|
|
||||||
final String positiveText;
|
|
||||||
final String negativeText;
|
|
||||||
final onPositive;
|
|
||||||
final onNegative;
|
|
||||||
|
|
||||||
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});
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShowEntityPageEvent {
|
|
||||||
Entity entity;
|
|
||||||
|
|
||||||
ShowEntityPageEvent(this.entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShowErrorEvent {
|
|
||||||
final HAError error;
|
|
||||||
|
|
||||||
ShowErrorEvent(this.error);
|
|
||||||
}
|
|
69
lib/utils/launcher.dart
Normal file
69
lib/utils/launcher.dart
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class Launcher {
|
||||||
|
|
||||||
|
static void launchURL(String url) async {
|
||||||
|
if (await urlLauncher.canLaunch(url)) {
|
||||||
|
await urlLauncher.launch(url);
|
||||||
|
} else {
|
||||||
|
Logger.e( "Could not launch $url");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void launchAuthenticatedWebView({BuildContext context, String url, String title}) {
|
||||||
|
if (url.contains("?")) {
|
||||||
|
url += "&external_auth=1";
|
||||||
|
} else {
|
||||||
|
url += "?external_auth=1";
|
||||||
|
}
|
||||||
|
final flutterWebViewPlugin = new FlutterWebviewPlugin();
|
||||||
|
flutterWebViewPlugin.onStateChanged.listen((viewState) async {
|
||||||
|
if (viewState.type == WebViewState.startLoad) {
|
||||||
|
Logger.d("[WebView] Injecting external auth JS");
|
||||||
|
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
||||||
|
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
"/webview",
|
||||||
|
arguments: {
|
||||||
|
"url": "$url",
|
||||||
|
"title": "${title ?? ''}"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void launchURLInCustomTab({BuildContext context, String url, bool enableDefaultShare: true, bool showPageTitle: true}) async {
|
||||||
|
try {
|
||||||
|
await launch(
|
||||||
|
"$url",
|
||||||
|
option: new CustomTabsOption(
|
||||||
|
toolbarColor: Theme.of(context).primaryColor,
|
||||||
|
enableDefaultShare: enableDefaultShare,
|
||||||
|
enableUrlBarHiding: true,
|
||||||
|
showPageTitle: showPageTitle,
|
||||||
|
animation: new CustomTabsAnimation.slideIn()
|
||||||
|
// or user defined animation.
|
||||||
|
/*animation: new CustomTabsAnimation(
|
||||||
|
startEnter: 'slide_up',
|
||||||
|
startExit: 'android:anim/fade_out',
|
||||||
|
endEnter: 'android:anim/fade_in',
|
||||||
|
endExit: 'slide_down',
|
||||||
|
)*/,
|
||||||
|
extraCustomTabs: <String>[
|
||||||
|
// ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox
|
||||||
|
'org.mozilla.firefox',
|
||||||
|
// ref. https://play.google.com/store/apps/details?id=com.microsoft.emmx
|
||||||
|
'com.microsoft.emmx',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.w("Can't open custom tab: ${e.toString()}");
|
||||||
|
Logger.w("Launching in default browser");
|
||||||
|
Launcher.launchURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
46
lib/utils/logger.dart
Normal file
46
lib/utils/logger.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
assert(inDebugMode = true);
|
||||||
|
|
||||||
|
return inDebugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void e(String message) {
|
||||||
|
_writeToLog("Error", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void w(String message) {
|
||||||
|
_writeToLog("Warning", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void d(String message) {
|
||||||
|
_writeToLog("Debug", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _writeToLog(String level, String message) {
|
||||||
|
if (isInDebugMode) {
|
||||||
|
debugPrint('$message');
|
||||||
|
}
|
||||||
|
DateTime t = DateTime.now();
|
||||||
|
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
|
||||||
|
if (_log.length > 100) {
|
||||||
|
_log.removeAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
pubspec.lock
12
pubspec.lock
@ -49,14 +49,14 @@ packages:
|
|||||||
name: charts_common
|
name: charts_common
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.8.0"
|
||||||
charts_flutter:
|
charts_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: charts_flutter
|
name: charts_flutter
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.8.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -77,7 +77,7 @@ packages:
|
|||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1+1"
|
version: "2.1.2"
|
||||||
date_format:
|
date_format:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -131,7 +131,7 @@ packages:
|
|||||||
name: flutter_launcher_icons
|
name: flutter_launcher_icons
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.2+1"
|
version: "0.7.3"
|
||||||
flutter_local_notifications:
|
flutter_local_notifications:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -192,7 +192,7 @@ packages:
|
|||||||
name: in_app_purchase
|
name: in_app_purchase
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1"
|
version: "0.2.1+3"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -309,7 +309,7 @@ packages:
|
|||||||
name: sqflite
|
name: sqflite
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.6+3"
|
version: "1.1.6+4"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.6.4+640
|
version: 0.6.5+652
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||||
@ -18,7 +18,7 @@ dependencies:
|
|||||||
date_format: any
|
date_format: any
|
||||||
charts_flutter: any
|
charts_flutter: any
|
||||||
flutter_markdown: any
|
flutter_markdown: any
|
||||||
in_app_purchase: ^0.2.1
|
in_app_purchase: ^0.2.1+2
|
||||||
# flutter_svg: ^0.10.3
|
# flutter_svg: ^0.10.3
|
||||||
flutter_custom_tabs: ^0.6.0
|
flutter_custom_tabs: ^0.6.0
|
||||||
firebase_messaging: ^5.1.4
|
firebase_messaging: ^5.1.4
|
||||||
@ -48,9 +48,9 @@ flutter:
|
|||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
|
||||||
assets:
|
assets:
|
||||||
- images/hassio-192x192.png
|
- images/hassio-192x192.png
|
||||||
|
- assets/js/externalAuth.js
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.io/assets-and-images/#resolution-aware.
|
# https://flutter.io/assets-and-images/#resolution-aware.
|
||||||
|
Reference in New Issue
Block a user