Compare commits
86 Commits
0.6.0-alph
...
0.6.6
Author | SHA1 | Date | |
---|---|---|---|
56a333a852 | |||
c5922368de | |||
8c2316a51a | |||
e2e6c015de | |||
0a6ff4586d | |||
fc228d85ae | |||
61823cb43b | |||
127e0b8182 | |||
38c37fa212 | |||
dfaf2a2924 | |||
c90c40c046 | |||
d2049b726a | |||
6508f109f7 | |||
37e63637a7 | |||
6650c5c145 | |||
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 | |||
7beab9ae93 | |||
8c0d1f90a3 | |||
05c05ba768 | |||
67e885e76a | |||
594bce0b8d | |||
7f6569e0db | |||
1c829c8364 | |||
7ca4b02e6d | |||
fadfefd836 | |||
37155901ef | |||
fbbb96409d | |||
5126c54914 | |||
916d0b7e3c | |||
0815840a9c | |||
bc237796b2 | |||
7f44800f64 | |||
85ac746e9d | |||
8215175098 | |||
39ee8b1799 | |||
c76d3d68c8 | |||
cde257922b | |||
be0c9d3372 | |||
13102a6b04 | |||
57c3083f9f |
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,4 +11,5 @@ build/
|
||||
.idea/
|
||||
|
||||
key.properties
|
||||
premium_features_manager.class.dart
|
||||
pubspec.lock
|
@ -1,12 +1,9 @@
|
||||
[](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart)
|
||||
# HA Client
|
||||
## Native Android client for Home Assistant
|
||||
### With Lovelace UI support
|
||||
|
||||
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)
|
||||
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)
|
||||
|
@ -7,6 +7,10 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.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
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
@ -14,7 +18,7 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:name=".Application"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
@ -46,5 +50,20 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</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>
|
||||
</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);
|
||||
}
|
||||
}
|
@ -3,8 +3,9 @@ package com.keyboardcrumbs.hassclient;
|
||||
import android.os.Bundle;
|
||||
import io.flutter.app.FlutterActivity;
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
public class MainActivity extends FlutterShareReceiverActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
1232
android/hs_err_pid766.log
Normal file
1232
android/hs_err_pid766.log
Normal file
File diff suppressed because it is too large
Load Diff
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!
|
||||
```
|
@ -13,7 +13,12 @@ class HACard {
|
||||
int columnsCount;
|
||||
List stateFilter;
|
||||
List states;
|
||||
List conditions;
|
||||
String content;
|
||||
String unit;
|
||||
int min;
|
||||
int max;
|
||||
Map severity;
|
||||
|
||||
HACard({
|
||||
this.name,
|
||||
@ -26,8 +31,17 @@ class HACard {
|
||||
this.showEmpty: true,
|
||||
this.content,
|
||||
this.states,
|
||||
this.conditions: const [],
|
||||
this.unit,
|
||||
this.min,
|
||||
this.max,
|
||||
this.severity,
|
||||
@required this.type
|
||||
});
|
||||
}) {
|
||||
if (this.columnsCount <= 0) {
|
||||
this.columnsCount = 4;
|
||||
}
|
||||
}
|
||||
|
||||
List<EntityWrapper> getEntitiesToShow() {
|
||||
return entities.where((entityWrapper) {
|
@ -24,33 +24,58 @@ class CardWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
if (card.conditions.isNotEmpty) {
|
||||
bool showCardByConditions = true;
|
||||
for (var condition in card.conditions) {
|
||||
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||
if (conditionEntity != null &&
|
||||
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||
) {
|
||||
showCardByConditions = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!showCardByConditions) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
}
|
||||
|
||||
switch (card.type) {
|
||||
|
||||
case CardType.entities: {
|
||||
case CardType.ENTITIES: {
|
||||
return _buildEntitiesCard(context);
|
||||
}
|
||||
|
||||
case CardType.glance: {
|
||||
case CardType.GLANCE: {
|
||||
return _buildGlanceCard(context);
|
||||
}
|
||||
|
||||
case CardType.mediaControl: {
|
||||
case CardType.MEDIA_CONTROL: {
|
||||
return _buildMediaControlsCard(context);
|
||||
}
|
||||
|
||||
case CardType.entityButton: {
|
||||
case CardType.ENTITY_BUTTON: {
|
||||
return _buildEntityButtonCard(context);
|
||||
}
|
||||
|
||||
case CardType.markdown: {
|
||||
case CardType.GAUGE: {
|
||||
return _buildGaugeCard(context);
|
||||
}
|
||||
|
||||
/* case CardType.LIGHT: {
|
||||
return _buildLightCard(context);
|
||||
}*/
|
||||
|
||||
case CardType.MARKDOWN: {
|
||||
return _buildMarkdownCard(context);
|
||||
}
|
||||
|
||||
case CardType.alarmPanel: {
|
||||
case CardType.ALARM_PANEL: {
|
||||
return _buildAlarmPanelCard(context);
|
||||
}
|
||||
|
||||
case CardType.horizontalStack: {
|
||||
case CardType.HORIZONTAL_STACK: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
@ -73,7 +98,7 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
|
||||
case CardType.verticalStack: {
|
||||
case CardType.VERTICAL_STACK: {
|
||||
if (card.childCards.isNotEmpty) {
|
||||
List<Widget> children = [];
|
||||
card.childCards.forEach((card) {
|
||||
@ -107,7 +132,7 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
body.add(CardHeader(name: card.name));
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
if (!entity.entity.isHidden) {
|
||||
body.add(
|
||||
@ -134,7 +159,7 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name));
|
||||
body.add(CardHeader(name: card.name));
|
||||
body.add(MarkdownBody(data: card.content));
|
||||
return Card(
|
||||
child: Padding(
|
||||
@ -146,7 +171,7 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(
|
||||
body.add(CardHeader(
|
||||
name: card.name ?? "",
|
||||
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||
style: TextStyle(
|
||||
@ -198,39 +223,51 @@ class CardWidget extends StatelessWidget {
|
||||
return Container(height: 0.0, width: 0.0,);
|
||||
}
|
||||
List<Widget> rows = [];
|
||||
rows.add(CardHeaderWidget(name: card.name));
|
||||
rows.add(CardHeader(name: card.name));
|
||||
|
||||
List<Widget> result = [];
|
||||
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
result.add(
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1/columnsCount,
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: GlanceEntityContainer(
|
||||
showName: card.showName,
|
||||
showState: card.showState,
|
||||
),
|
||||
handleTap: true
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
rows.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
|
||||
child: Wrap(
|
||||
//alignment: WrapAlignment.spaceAround,
|
||||
runSpacing: Sizes.rowPadding*2,
|
||||
children: result,
|
||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
List<Widget> buttons = [];
|
||||
double buttonWidth = constraints.maxWidth / columnsCount;
|
||||
entitiesToShow.forEach((EntityWrapper entity) {
|
||||
buttons.add(
|
||||
SizedBox(
|
||||
width: buttonWidth,
|
||||
child: EntityModel(
|
||||
entityWrapper: entity,
|
||||
child: GlanceCardEntityContainer(
|
||||
showName: card.showName,
|
||||
showState: card.showState,
|
||||
),
|
||||
handleTap: true
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
return Wrap(
|
||||
//spacing: 5.0,
|
||||
//alignment: WrapAlignment.spaceEvenly,
|
||||
runSpacing: Sizes.doubleRowPadding,
|
||||
children: buttons,
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return Card(
|
||||
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: rows
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -250,7 +287,41 @@ class CardWidget extends StatelessWidget {
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: ButtonEntityContainer(),
|
||||
child: EntityButtonCardBody(),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGaugeCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
card.linkedEntityWrapper.unitOfMeasurement = card.unit ??
|
||||
card.linkedEntityWrapper.unitOfMeasurement;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: GaugeCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLightCard(BuildContext context) {
|
||||
card.linkedEntityWrapper.displayName = card.name ??
|
||||
card.linkedEntityWrapper.displayName;
|
||||
return Card(
|
||||
child: EntityModel(
|
||||
entityWrapper: card.linkedEntityWrapper,
|
||||
child: LightCardBody(
|
||||
min: card.min,
|
||||
max: card.max,
|
||||
severity: card.severity,
|
||||
),
|
||||
handleTap: true
|
||||
)
|
||||
);
|
||||
@ -258,7 +329,7 @@ class CardWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildUnsupportedCard(BuildContext context) {
|
||||
List<Widget> body = [];
|
||||
body.add(CardHeaderWidget(name: card.name ?? ""));
|
||||
body.add(CardHeader(name: card.name ?? ""));
|
||||
List<Widget> result = [];
|
||||
if (card.linkedEntityWrapper != null) {
|
||||
result.addAll(<Widget>[
|
||||
@ -289,4 +360,4 @@ class CardWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class CardHeaderWidget extends StatelessWidget {
|
||||
class CardHeader extends StatelessWidget {
|
||||
|
||||
final String name;
|
||||
final Widget trailing;
|
||||
final Widget subtitle;
|
||||
|
||||
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
@ -1,8 +1,8 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class ButtonEntityContainer extends StatelessWidget {
|
||||
class EntityButtonCardBody extends StatelessWidget {
|
||||
|
||||
ButtonEntityContainer({
|
||||
EntityButtonCardBody({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -15,24 +15,25 @@ class ButtonEntityContainer extends StatelessWidget {
|
||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||
return Container(width: 0.0, height: 0.0,);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitHeight,
|
||||
child: EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
size: Sizes.iconSize,
|
||||
)
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return EntityIcon(
|
||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||
size: constraints.maxWidth / 2.5,
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
_buildName()
|
||||
],
|
||||
_buildName()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
153
lib/cards/widgets/gauge_card_body.dart
Normal file
153
lib/cards/widgets/gauge_card_body.dart
Normal file
@ -0,0 +1,153 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class GaugeCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_GaugeCardBodyState createState() => _GaugeCardBodyState();
|
||||
}
|
||||
|
||||
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
||||
|
||||
List<charts.Series> seriesList;
|
||||
|
||||
List<charts.Series<GaugeSegment, String>> _createData(double value) {
|
||||
double fixedValue;
|
||||
if (value > widget.max) {
|
||||
fixedValue = widget.max.toDouble();
|
||||
} else if (value < widget.min) {
|
||||
fixedValue = widget.min.toDouble();
|
||||
} else {
|
||||
fixedValue = value;
|
||||
}
|
||||
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
|
||||
Color mainColor;
|
||||
if (widget.severity != null) {
|
||||
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
|
||||
mainColor = Colors.red;
|
||||
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
|
||||
mainColor = Colors.amber;
|
||||
} else {
|
||||
mainColor = Colors.green;
|
||||
}
|
||||
} else {
|
||||
mainColor = Colors.green;
|
||||
}
|
||||
final data = [
|
||||
GaugeSegment('Main', toShow, mainColor),
|
||||
GaugeSegment('Rest', 100 - toShow, Colors.black45),
|
||||
];
|
||||
|
||||
return [
|
||||
charts.Series<GaugeSegment, String>(
|
||||
id: 'Segments',
|
||||
domainFn: (GaugeSegment segment, _) => segment.segment,
|
||||
measureFn: (GaugeSegment segment, _) => segment.value,
|
||||
colorFn: (GaugeSegment segment, _) => segment.color,
|
||||
// Set a label accessor to control the text of the arc label.
|
||||
labelAccessorFn: (GaugeSegment segment, _) =>
|
||||
segment.segment == 'Main' ? '${segment.value}' : null,
|
||||
data: data,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double verticalOffset;
|
||||
if(constraints.maxWidth > 150.0) {
|
||||
verticalOffset = 0.2;
|
||||
} else if (constraints.maxWidth > 100.0) {
|
||||
verticalOffset = 0.3;
|
||||
} else {
|
||||
verticalOffset = 0.3;
|
||||
}
|
||||
return FractionallySizedBox(
|
||||
heightFactor: 2,
|
||||
widthFactor: 1,
|
||||
alignment: FractionalOffset(0,verticalOffset),
|
||||
child: charts.PieChart(
|
||||
_createData(entityWrapper.entity.doubleState),
|
||||
animate: false,
|
||||
defaultRenderer: charts.ArcRendererConfig(
|
||||
arcRatio: 0.4,
|
||||
startAngle: pi,
|
||||
arcLength: pi,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GaugeSegment {
|
||||
final String segment;
|
||||
final double value;
|
||||
final charts.Color color;
|
||||
|
||||
GaugeSegment(this.segment, this.value, Color color)
|
||||
: this.color = charts.Color(
|
||||
r: color.red, g: color.green, b: color.blue, a: color.alpha);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class GlanceEntityContainer extends StatelessWidget {
|
||||
class GlanceCardEntityContainer extends StatelessWidget {
|
||||
|
||||
final bool showName;
|
||||
final bool showState;
|
||||
@ -9,7 +9,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
final double nameFontSize;
|
||||
final bool wordsWrapInName;
|
||||
|
||||
GlanceEntityContainer({
|
||||
GlanceCardEntityContainer({
|
||||
Key key,
|
||||
@required this.showName,
|
||||
@required this.showState,
|
||||
@ -39,10 +39,10 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
result.add(
|
||||
EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
)
|
||||
EntityIcon(
|
||||
padding: EdgeInsets.all(0.0),
|
||||
size: iconSize,
|
||||
)
|
||||
);
|
||||
if (!nameInTheBottom) {
|
||||
if (showState) {
|
||||
@ -54,14 +54,9 @@ class GlanceEntityContainer extends StatelessWidget {
|
||||
|
||||
return Center(
|
||||
child: InkResponse(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
//mainAxisAlignment: MainAxisAlignment.start,
|
||||
//crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: result,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: result,
|
||||
),
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
90
lib/cards/widgets/light_card_body.dart
Normal file
90
lib/cards/widgets/light_card_body.dart
Normal file
@ -0,0 +1,90 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightCardBody extends StatefulWidget {
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
final Map severity;
|
||||
|
||||
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LightCardBodyState createState() => _LightCardBodyState();
|
||||
}
|
||||
|
||||
class _LightCardBodyState extends State<LightCardBody> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
LightEntity entity = entityWrapper.entity;
|
||||
Logger.d("Light brightness: ${entity.brightness}");
|
||||
|
||||
return FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: Container(
|
||||
//color: Colors.redAccent,
|
||||
child: SingleCircularSlider(
|
||||
255,
|
||||
entity.brightness ?? 0,
|
||||
baseColor: Colors.white,
|
||||
handlerColor: Colors.blue[200],
|
||||
selectionColor: Colors.blue[100],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => entityWrapper.handleTap(),
|
||||
onLongPress: () => entityWrapper.handleHold(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
overflow: Overflow.clip,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
||||
child: SimpleEntityState(
|
||||
//textAlign: TextAlign.center,
|
||||
expanded: false,
|
||||
maxLines: 1,
|
||||
bold: true,
|
||||
textAlign: TextAlign.center,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
fontSize: fontSize,
|
||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double fontSize = constraints.maxHeight / 7;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: fontSize),
|
||||
child: EntityName(
|
||||
fontSize: fontSize,
|
||||
maxLines: 1,
|
||||
padding: EdgeInsets.all(0.0),
|
||||
textAlign: TextAlign.center,
|
||||
textOverflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of 'main.dart';
|
||||
|
||||
class EntityState {
|
||||
static const on = 'on';
|
||||
@ -77,23 +77,40 @@ class EntityUIAction {
|
||||
}
|
||||
|
||||
class CardType {
|
||||
static const horizontalStack = "horizontal-stack";
|
||||
static const verticalStack = "vertical-stack";
|
||||
static const entities = "entities";
|
||||
static const glance = "glance";
|
||||
static const mediaControl = "media-control";
|
||||
static const weatherForecast = "weather-forecast";
|
||||
static const thermostat = "thermostat";
|
||||
static const sensor = "sensor";
|
||||
static const plantStatus = "plant-status";
|
||||
static const pictureEntity = "picture-entity";
|
||||
static const pictureElements = "picture-elements";
|
||||
static const picture = "picture";
|
||||
static const map = "map";
|
||||
static const iframe = "iframe";
|
||||
static const gauge = "gauge";
|
||||
static const entityButton = "entity-button";
|
||||
static const conditional = "conditional";
|
||||
static const alarmPanel = "alarm-panel";
|
||||
static const markdown = "markdown";
|
||||
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||
static const VERTICAL_STACK = "vertical-stack";
|
||||
static const ENTITIES = "entities";
|
||||
static const GLANCE = "glance";
|
||||
static const MEDIA_CONTROL = "media-control";
|
||||
static const WEATHER_FORECAST = "weather-forecast";
|
||||
static const THERMOSTAT = "thermostat";
|
||||
static const SENSOR = "sensor";
|
||||
static const PLANT_STATUS = "plant-status";
|
||||
static const PICTURE_ENTITY = "picture-entity";
|
||||
static const PICTURE_ELEMENTS = "picture-elements";
|
||||
static const PICTURE = "picture";
|
||||
static const MAP = "map";
|
||||
static const IFRAME = "iframe";
|
||||
static const GAUGE = "gauge";
|
||||
static const ENTITY_BUTTON = "entity-button";
|
||||
static const CONDITIONAL = "conditional";
|
||||
static const ALARM_PANEL = "alarm-panel";
|
||||
static const MARKDOWN = "markdown";
|
||||
static const LIGHT = "light";
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
static const rightWidgetPadding = 10.0;
|
||||
static const leftWidgetPadding = 10.0;
|
||||
static const buttonPadding = 4.0;
|
||||
static const extendedWidgetHeight = 50.0;
|
||||
static const iconSize = 28.0;
|
||||
static const largeIconSize = 46.0;
|
||||
static const stateFontSize = 15.0;
|
||||
static const nameFontSize = 15.0;
|
||||
static const smallFontSize = 14.0;
|
||||
static const largeFontSize = 24.0;
|
||||
static const inputWidth = 160.0;
|
||||
static const rowPadding = 10.0;
|
||||
static const doubleRowPadding = rowPadding*2;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class AlarmControlPanelEntity extends Entity {
|
||||
AlarmControlPanelEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class AlarmControlPanelControlsWidget extends StatefulWidget {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class AutomationEntity extends Entity {
|
||||
AutomationEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class ButtonEntity extends Entity {
|
||||
ButtonEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class CameraEntity extends Entity {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class ClimateEntity extends Entity {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class ClimateControlWidget extends StatefulWidget {
|
||||
|
||||
@ -408,53 +408,4 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
||||
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(),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class ClimateStateWidget extends StatelessWidget {
|
||||
@override
|
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(),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class CoverEntity extends Entity {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class CoverControlWidget extends StatefulWidget {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class CoverStateWidget extends StatelessWidget {
|
||||
void _open(CoverEntity entity) {
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class DateTimeEntity extends Entity {
|
||||
DateTimeEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class DateTimeStateWidget extends StatelessWidget {
|
||||
@override
|
@ -4,6 +4,7 @@ class EntityWrapper {
|
||||
|
||||
String displayName;
|
||||
String icon;
|
||||
String unitOfMeasurement;
|
||||
String entityPicture;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
@ -24,6 +25,7 @@ class EntityWrapper {
|
||||
if (uiAction == null) {
|
||||
uiAction = EntityUIAction();
|
||||
}
|
||||
unitOfMeasurement = entity.unitOfMeasurement;
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +62,7 @@ class EntityWrapper {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
HAUtils.launchURL(uiAction.tapService);
|
||||
Launcher.launchURL(uiAction.tapService);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -100,7 +102,7 @@ class EntityWrapper {
|
||||
//TODO handle local urls
|
||||
Logger.w("Local urls is not supported yet");
|
||||
} else {
|
||||
HAUtils.launchURL(uiAction.holdService);
|
||||
Launcher.launchURL(uiAction.holdService);
|
||||
}
|
||||
break;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class FanEntity extends Entity {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class FanControlsWidget extends StatefulWidget {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class GroupEntity extends Entity {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class LightEntity extends Entity {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class LightControlsWidget extends StatefulWidget {
|
||||
|
||||
@ -17,7 +17,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
String _tmpEffect;
|
||||
|
||||
void _resetState(LightEntity entity) {
|
||||
_tmpBrightness = entity.brightness ?? 0;
|
||||
_tmpBrightness = entity.brightness ?? 1;
|
||||
_tmpWhiteValue = entity.whiteValue ?? 0;
|
||||
_tmpColorTemp = entity.colorTemp ?? entity.minMireds?.toInt();
|
||||
_tmpColor = entity.color ?? _tmpColor;
|
||||
@ -28,15 +28,9 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
setState(() {
|
||||
_tmpBrightness = value.round();
|
||||
_changedHere = true;
|
||||
if (_tmpBrightness > 0) {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"brightness": _tmpBrightness}));
|
||||
} else {
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_off", entity.entityId,
|
||||
null));
|
||||
}
|
||||
eventBus.fire(new ServiceCallEvent(
|
||||
entity.domain, "turn_on", entity.entityId,
|
||||
{"brightness": _tmpBrightness}));
|
||||
});
|
||||
}
|
||||
|
||||
@ -114,10 +108,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
||||
_tmpBrightness = value.round();
|
||||
});
|
||||
},
|
||||
min: 0.0,
|
||||
min: 1.0,
|
||||
max: 255.0,
|
||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||
value: _tmpBrightness == null ? 0.0 : _tmpBrightness.toDouble(),
|
||||
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
|
||||
leading: Icon(Icons.brightness_5),
|
||||
title: "Brightness",
|
||||
);
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class LockEntity extends Entity {
|
||||
LockEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class LockStateWidget extends StatelessWidget {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class MediaPlayerEntity extends Entity {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class MediaPlayerWidget extends StatelessWidget {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class SelectEntity extends Entity {
|
||||
List<String> get listOptions => attributes["options"] != null
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SelectStateWidget extends StatefulWidget {
|
||||
|
@ -1,8 +1,4 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
}
|
||||
part of '../../main.dart';
|
||||
|
||||
class SensorEntity extends Entity {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class SliderEntity extends Entity {
|
||||
SliderEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SliderControlsWidget extends StatefulWidget {
|
||||
|
5
lib/entities/sun/sun_entity.class.dart
Normal file
5
lib/entities/sun/sun_entity.class.dart
Normal file
@ -0,0 +1,5 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class SunEntity extends Entity {
|
||||
SunEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class SwitchEntity extends Entity {
|
||||
SwitchEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class SwitchStateWidget extends StatefulWidget {
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class TextEntity extends Entity {
|
||||
TextEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class TextInputStateWidget extends StatefulWidget {
|
||||
|
||||
@ -73,13 +73,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
obscureText: entity.isPasswordField,
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _tmpValue,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _tmpValue.length)
|
||||
)
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
|
||||
onChanged: (value) {
|
||||
_tmpValue = value;
|
||||
}),
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of '../../main.dart';
|
||||
|
||||
class TimerEntity extends Entity {
|
||||
TimerEntity(Map rawData, String webHost) : super(rawData, webHost);
|
@ -1,4 +1,4 @@
|
||||
part of '../../main.dart';
|
||||
part of '../../../main.dart';
|
||||
|
||||
class TimerState extends StatefulWidget {
|
||||
//final bool expanded;
|
@ -149,6 +149,17 @@ class EntityCollection {
|
||||
return _allEntities[entityId] != null;
|
||||
}
|
||||
|
||||
List<Entity> getByDomains(List<String> domains) {
|
||||
List<Entity> result = [];
|
||||
_allEntities.forEach((id, entity) {
|
||||
if (domains.contains(entity.domain)) {
|
||||
Logger.d("getByDomain: ${entity.isHidden}");
|
||||
result.add(entity);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Entity> filterEntitiesForDefaultView() {
|
||||
List<Entity> result = [];
|
||||
List<Entity> groups = [];
|
||||
|
@ -57,7 +57,7 @@ class BadgeWidget extends StatelessWidget {
|
||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||
stateFontSize = 8.0;
|
||||
}
|
||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||
badgeIcon = Center(
|
||||
child: Text(
|
||||
"${entityModel.entityWrapper.entity.displayState}",
|
||||
|
@ -20,21 +20,9 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
String streamUrl = "";
|
||||
|
||||
launchStream() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebviewScaffold(
|
||||
url: "$streamUrl",
|
||||
withZoom: true,
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context)
|
||||
),
|
||||
title: new Text("${_entity.displayName}"),
|
||||
),
|
||||
),
|
||||
)
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: streamUrl
|
||||
);
|
||||
}
|
||||
|
||||
@ -47,7 +35,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
.entity;
|
||||
started = true;
|
||||
}
|
||||
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||
.entityId}?token=${_entity.attributes['access_token']}';
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
|
@ -7,8 +7,10 @@ class SimpleEntityState extends StatelessWidget {
|
||||
final EdgeInsetsGeometry padding;
|
||||
final int maxLines;
|
||||
final String customValue;
|
||||
final double fontSize;
|
||||
final bool bold;
|
||||
|
||||
const SimpleEntityState({Key key, this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||
const SimpleEntityState({Key key,this.bold: false, this.maxLines: 10, this.fontSize: Sizes.stateFontSize, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -21,18 +23,22 @@ class SimpleEntityState extends StatelessWidget {
|
||||
state = customValue;
|
||||
}
|
||||
TextStyle textStyle = TextStyle(
|
||||
fontSize: Sizes.stateFontSize,
|
||||
fontSize: this.fontSize,
|
||||
fontWeight: FontWeight.normal
|
||||
);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||
textStyle = textStyle.apply(color: Colors.blue);
|
||||
}
|
||||
if (this.bold) {
|
||||
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||
}
|
||||
while (state.contains(" ")){
|
||||
state = state.replaceAll(" ", " ");
|
||||
}
|
||||
Widget result = Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
||||
"$state ${entityModel.entityWrapper.unitOfMeasurement}",
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
@ -34,32 +34,37 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onLongPress: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleHold();
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleTap();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
Widget result = Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
EntityIcon(),
|
||||
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 3,
|
||||
child: EntityName(
|
||||
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
||||
),
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 3,
|
||||
child: EntityName(
|
||||
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
||||
),
|
||||
state
|
||||
],
|
||||
),
|
||||
),
|
||||
state
|
||||
],
|
||||
);
|
||||
if (entityModel.handleTap) {
|
||||
return InkWell(
|
||||
onLongPress: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleHold();
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (entityModel.handleTap) {
|
||||
entityModel.entityWrapper.handleTap();
|
||||
}
|
||||
},
|
||||
child: result,
|
||||
);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ class EntityColor {
|
||||
"auto": Colors.amber,
|
||||
EntityState.active: Colors.amber,
|
||||
EntityState.playing: Colors.amber,
|
||||
EntityState.paused: Colors.amber,
|
||||
"above_horizon": Colors.amber,
|
||||
EntityState.home: Colors.amber,
|
||||
EntityState.open: Colors.amber,
|
||||
|
@ -148,7 +148,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
});
|
||||
|
||||
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
||||
_selectedId = 0;
|
||||
_selectedId = numericDataLists.length -1;
|
||||
}
|
||||
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
||||
numericDataLists.forEach((attrName, dataList) {
|
||||
@ -202,6 +202,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
_selectedId -= 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = _parsedHistory.first.data.length - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectNext() {
|
||||
@ -210,6 +215,12 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
||||
_selectedId += 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = 0;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectionChanged(charts.SelectionModel model) {
|
||||
|
@ -47,7 +47,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
||||
}
|
||||
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||
_historyLastUpdated = now;
|
||||
Connection().getHistory(entityId).then((history){
|
||||
ConnectionManager().getHistory(entityId).then((history){
|
||||
if (!_disposed) {
|
||||
setState(() {
|
||||
_history = history.isNotEmpty ? history[0] : [];
|
||||
|
@ -103,7 +103,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
||||
id: widget.rawHistory.length
|
||||
));
|
||||
if (_selectedId == -1) {
|
||||
_selectedId = 0;
|
||||
_selectedId = data.length - 1;
|
||||
}
|
||||
return [
|
||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||
@ -132,6 +132,11 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
||||
_selectedId -= 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = _parsedHistory.first.data.length - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectNext() {
|
||||
@ -140,6 +145,12 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
||||
_selectedId += 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = 0;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectionChanged(charts.SelectionModel model) {
|
||||
|
@ -101,7 +101,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
||||
colorId: data.last.colorId
|
||||
));
|
||||
if (_selectedId == -1) {
|
||||
_selectedId = 0;
|
||||
_selectedId = data.length - 1;
|
||||
}
|
||||
return [
|
||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||
@ -137,14 +137,25 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
||||
_selectedId -= 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = _parsedHistory.first.data.length - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectNext() {
|
||||
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
|
||||
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
|
||||
setState(() {
|
||||
_selectedId += 1;
|
||||
});
|
||||
}
|
||||
else {
|
||||
setState(() {
|
||||
_selectedId = 0;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectionChanged(charts.SelectionModel model) {
|
||||
|
@ -11,6 +11,7 @@ class HomeAssistant {
|
||||
EntityCollection entities;
|
||||
HomeAssistantUI ui;
|
||||
Map _instanceConfig = {};
|
||||
Map services;
|
||||
String _userName;
|
||||
HSVColor savedColor;
|
||||
|
||||
@ -23,7 +24,7 @@ class HomeAssistant {
|
||||
Duration fetchTimeout = Duration(seconds: 30);
|
||||
|
||||
String get locationName {
|
||||
if (Connection().useLovelace) {
|
||||
if (ConnectionManager().useLovelace) {
|
||||
return ui?.title ?? "";
|
||||
} else {
|
||||
return _instanceConfig["location_name"] ?? "";
|
||||
@ -36,8 +37,8 @@ class HomeAssistant {
|
||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||
|
||||
HomeAssistant._internal() {
|
||||
Connection().onStateChangeCallback = _handleEntityStateChange;
|
||||
Device().loadDeviceInfo();
|
||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||
DeviceInfoManager().loadDeviceInfo();
|
||||
}
|
||||
|
||||
Completer _fetchCompleter;
|
||||
@ -47,18 +48,18 @@ class HomeAssistant {
|
||||
Logger.w("Previous data fetch is not completed yet");
|
||||
return _fetchCompleter.future;
|
||||
}
|
||||
if (entities == null) entities = EntityCollection(Connection().httpWebHost);
|
||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||
_fetchCompleter = Completer();
|
||||
List<Future> futures = [];
|
||||
futures.add(_getStates());
|
||||
if (Connection().useLovelace) {
|
||||
if (ConnectionManager().useLovelace) {
|
||||
futures.add(_getLovelace());
|
||||
}
|
||||
futures.add(_getConfig());
|
||||
futures.add(_getServices());
|
||||
futures.add(_getUserInfo());
|
||||
futures.add(_getPanels());
|
||||
futures.add(Connection().sendSocketMessage(
|
||||
futures.add(ConnectionManager().sendSocketMessage(
|
||||
type: "subscribe_events",
|
||||
additionalData: {"event_type": "state_changed"},
|
||||
));
|
||||
@ -66,9 +67,9 @@ class HomeAssistant {
|
||||
if (isMobileAppEnabled) {
|
||||
_createUI();
|
||||
_fetchCompleter.complete();
|
||||
checkAppRegistration();
|
||||
MobileAppIntegrationManager.checkAppRegistration();
|
||||
} 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) {
|
||||
_fetchCompleter.completeError(e);
|
||||
@ -78,125 +79,15 @@ class HomeAssistant {
|
||||
|
||||
Future logout() async {
|
||||
Logger.d("Logging out...");
|
||||
await Connection().logout().then((_) {
|
||||
await ConnectionManager().logout().then((_) {
|
||||
ui?.clear();
|
||||
entities?.clear();
|
||||
panels?.clear();
|
||||
});
|
||||
}
|
||||
|
||||
Map _getAppRegistrationData() {
|
||||
return {
|
||||
"app_version": "$appVersion",
|
||||
"device_name": "$userName's ${Device().model}",
|
||||
"manufacturer": Device().manufacturer,
|
||||
"model": Device().model,
|
||||
"os_name": Device().osName,
|
||||
"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",
|
||||
"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(ShowDialogEvent(
|
||||
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) {
|
||||
Logger.d("App registration works fine");
|
||||
if (showOkDialog) {
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
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");
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
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();
|
||||
});
|
||||
},
|
||||
));
|
||||
} else {
|
||||
Logger.e("Error updating app registration: ${e.toString()}");
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
title: "App integration is not working properly",
|
||||
body: "Something wrong with HA Client integration on your Home Assistant server. Try to remove current app integration from Configuration -> Integrationds using web UI, restart your Home Assistant and go back to the app. NOTE that after clicking 'Ok' current integration data will be removed from the app and new integration wll be created on Home Assistant side on next app launch.",
|
||||
positiveText: "Ok",
|
||||
negativeText: "I'll handle it",
|
||||
onPositive: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.remove("app-webhook-id");
|
||||
Connection().webhookId = null;
|
||||
HAUtils.launchURL(Connection().httpWebHost+"/config/integrations/dashboard");
|
||||
});
|
||||
},
|
||||
));
|
||||
}
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
Future _getConfig() async {
|
||||
await Connection().sendSocketMessage(type: "get_config").then((data) {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
}).catchError((e) {
|
||||
throw HAError("Error getting config: ${e}");
|
||||
@ -204,7 +95,7 @@ class HomeAssistant {
|
||||
}
|
||||
|
||||
Future _getStates() async {
|
||||
await Connection().sendSocketMessage(type: "get_states").then(
|
||||
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||
(data) => entities.parse(data)
|
||||
).catchError((e) {
|
||||
throw HAError("Error getting states: $e");
|
||||
@ -212,27 +103,31 @@ class HomeAssistant {
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getUserInfo() async {
|
||||
_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}");
|
||||
});
|
||||
}
|
||||
|
||||
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("Got ${data.length} services");
|
||||
Logger.d("Media extractor: ${data["media_extractor"]}");
|
||||
services = data;
|
||||
}).catchError((e) {
|
||||
Logger.w("Can't get services: ${e}");
|
||||
});
|
||||
}
|
||||
|
||||
Future _getPanels() async {
|
||||
panels.clear();
|
||||
await Connection().sendSocketMessage(type: "get_panels").then((data) {
|
||||
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
||||
data.forEach((k,v) {
|
||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||
panels.add(Panel(
|
||||
@ -272,7 +167,8 @@ class HomeAssistant {
|
||||
count: viewCounter,
|
||||
id: "${rawView['id']}",
|
||||
name: rawView['title'],
|
||||
iconName: rawView['icon']
|
||||
iconName: rawView['icon'],
|
||||
panel: rawView['panel'] ?? false,
|
||||
);
|
||||
|
||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||
@ -296,31 +192,29 @@ class HomeAssistant {
|
||||
List<HACard> result = [];
|
||||
rawCards.forEach((rawCard){
|
||||
try {
|
||||
bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
||||
var rawCardInfo = rawCard["card"] ?? rawCard;
|
||||
HACard card = HACard(
|
||||
id: "card",
|
||||
name: isThereCardOptionsInside ? rawCard["card"]["title"] ??
|
||||
rawCard["card"]["name"] : rawCard["title"] ?? rawCard["name"],
|
||||
type: isThereCardOptionsInside
|
||||
? rawCard["card"]['type']
|
||||
: rawCard['type'],
|
||||
columnsCount: isThereCardOptionsInside
|
||||
? rawCard["card"]['columns'] ?? 4
|
||||
: rawCard['columns'] ?? 4,
|
||||
showName: isThereCardOptionsInside ? rawCard["card"]['show_name'] ??
|
||||
true : rawCard['show_name'] ?? true,
|
||||
showState: isThereCardOptionsInside
|
||||
? rawCard["card"]['show_state'] ?? true
|
||||
: rawCard['show_state'] ?? true,
|
||||
showEmpty: rawCard['show_empty'] ?? true,
|
||||
stateFilter: rawCard['state_filter'] ?? [],
|
||||
states: rawCard['states'],
|
||||
content: rawCard['content']
|
||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||
showName: rawCardInfo['show_name'] ?? true,
|
||||
showState: rawCardInfo['show_state'] ?? true,
|
||||
showEmpty: rawCardInfo['show_empty'] ?? true,
|
||||
stateFilter: rawCardInfo['state_filter'] ?? [],
|
||||
states: rawCardInfo['states'],
|
||||
conditions: rawCard['conditions'] ?? [],
|
||||
content: rawCardInfo['content'],
|
||||
min: rawCardInfo['min'] ?? 0,
|
||||
max: rawCardInfo['max'] ?? 100,
|
||||
unit: rawCardInfo['unit'],
|
||||
severity: rawCardInfo['severity']
|
||||
);
|
||||
if (rawCard["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCard["cards"]);
|
||||
if (rawCardInfo["cards"] != null) {
|
||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||
}
|
||||
rawCard["entities"]?.forEach((rawEntity) {
|
||||
rawCardInfo["entities"]?.forEach((rawEntity) {
|
||||
if (rawEntity is String) {
|
||||
if (entities.isExist(rawEntity)) {
|
||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||
@ -383,15 +277,15 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (rawCard["entity"] != null) {
|
||||
var en = rawCard["entity"];
|
||||
if (rawCardInfo["entity"] != null) {
|
||||
var en = rawCardInfo["entity"];
|
||||
if (en is String) {
|
||||
if (entities.isExist(en)) {
|
||||
Entity e = entities.get(en);
|
||||
card.linkedEntityWrapper = EntityWrapper(
|
||||
entity: e,
|
||||
icon: rawCard["icon"],
|
||||
displayName: rawCard["name"],
|
||||
icon: rawCardInfo["icon"],
|
||||
displayName: rawCardInfo["name"],
|
||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
||||
);
|
||||
} else {
|
||||
@ -421,7 +315,7 @@ class HomeAssistant {
|
||||
|
||||
void _createUI() {
|
||||
ui = HomeAssistantUI();
|
||||
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
|
||||
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
||||
Logger.d("Creating Lovelace UI");
|
||||
_parseLovelace();
|
||||
} else {
|
||||
|
879
lib/main.dart
879
lib/main.dart
@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -15,41 +15,48 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:progress_indicators/progress_indicators.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
//import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'plugins/circular_slider/single_circular_slider.dart';
|
||||
import 'package:share/receive_share_state.dart';
|
||||
import 'package:share/share.dart';
|
||||
|
||||
part 'entity_class/const.dart';
|
||||
part 'entity_class/entity.class.dart';
|
||||
part 'entity_class/entity_wrapper.class.dart';
|
||||
part 'entity_class/timer_entity.dart';
|
||||
part 'entity_class/switch_entity.class.dart';
|
||||
part 'entity_class/button_entity.class.dart';
|
||||
part 'entity_class/text_entity.class.dart';
|
||||
part 'entity_class/climate_entity.class.dart';
|
||||
part 'entity_class/cover_entity.class.dart';
|
||||
part 'entity_class/date_time_entity.class.dart';
|
||||
part 'entity_class/light_entity.class.dart';
|
||||
part 'entity_class/select_entity.class.dart';
|
||||
part 'entity_class/other_entity.class.dart';
|
||||
part 'entity_class/slider_entity.dart';
|
||||
part 'entity_class/media_player_entity.class.dart';
|
||||
part 'entity_class/lock_entity.class.dart';
|
||||
part 'entity_class/group_entity.class.dart';
|
||||
part 'entity_class/fan_entity.class.dart';
|
||||
part 'entity_class/automation_entity.dart';
|
||||
part 'entity_class/camera_entity.class.dart';
|
||||
part 'entity_class/alarm_control_panel.class.dart';
|
||||
import 'utils/logger.dart';
|
||||
|
||||
part 'const.dart';
|
||||
part 'utils/launcher.dart';
|
||||
part 'entities/entity.class.dart';
|
||||
part 'entities/entity_wrapper.class.dart';
|
||||
part 'entities/timer/timer_entity.class.dart';
|
||||
part 'entities/switch/switch_entity.class.dart';
|
||||
part 'entities/button/button_entity.class.dart';
|
||||
part 'entities/text/text_entity.class.dart';
|
||||
part 'entities/climate/climate_entity.class.dart';
|
||||
part 'entities/cover/cover_entity.class.dart';
|
||||
part 'entities/date_time/date_time_entity.class.dart';
|
||||
part 'entities/light/light_entity.class.dart';
|
||||
part 'entities/select/select_entity.class.dart';
|
||||
part 'entities/sun/sun_entity.class.dart';
|
||||
part 'entities/sensor/sensor_entity.class.dart';
|
||||
part 'entities/slider/slider_entity.dart';
|
||||
part 'entities/media_player/media_player_entity.class.dart';
|
||||
part 'entities/lock/lock_entity.class.dart';
|
||||
part 'entities/group/group_entity.class.dart';
|
||||
part 'entities/fan/fan_entity.class.dart';
|
||||
part 'entities/automation/automation_entity.class.dart';
|
||||
part 'entities/camera/camera_entity.class.dart';
|
||||
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
||||
part 'entity_widgets/common/badge.dart';
|
||||
part 'entity_widgets/model_widgets.dart';
|
||||
part 'entity_widgets/default_entity_container.dart';
|
||||
part 'entity_widgets/missed_entity.dart';
|
||||
part 'entity_widgets/glance_entity_container.dart';
|
||||
part 'entity_widgets/button_entity_container.dart';
|
||||
part 'cards/widgets/glance_card_entity_container.dart';
|
||||
part 'cards/widgets/entity_button_card_body.widget.dart';
|
||||
part 'entity_widgets/common/entity_attributes_list.dart';
|
||||
part 'entity_widgets/entity_icon.dart';
|
||||
part 'entity_widgets/entity_name.dart';
|
||||
@ -68,51 +75,64 @@ part 'entity_widgets/history_chart/numeric_state_history_chart.dart';
|
||||
part 'entity_widgets/history_chart/combined_history_chart.dart';
|
||||
part 'entity_widgets/history_chart/history_control_widget.dart';
|
||||
part 'entity_widgets/history_chart/entity_history_moment.dart';
|
||||
part 'entity_widgets/state/switch_state.dart';
|
||||
part 'entity_widgets/controls/slider_controls.dart';
|
||||
part 'entity_widgets/state/text_input_state.dart';
|
||||
part 'entity_widgets/state/select_state.dart';
|
||||
part 'entity_widgets/state/simple_state.dart';
|
||||
part 'entity_widgets/state/timer_state.dart';
|
||||
part 'entity_widgets/state/climate_state.dart';
|
||||
part 'entity_widgets/state/cover_state.dart';
|
||||
part 'entity_widgets/state/date_time_state.dart';
|
||||
part 'entity_widgets/state/lock_state.dart';
|
||||
part 'entity_widgets/controls/climate_controls.dart';
|
||||
part 'entity_widgets/controls/cover_controls.dart';
|
||||
part 'entity_widgets/controls/light_controls.dart';
|
||||
part 'entity_widgets/controls/media_player_widgets.dart';
|
||||
part 'entity_widgets/controls/fan_controls.dart';
|
||||
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
|
||||
part 'settings.page.dart';
|
||||
part 'panel.page.dart';
|
||||
part 'entities/switch/widget/switch_state.dart';
|
||||
part 'entities/slider/widgets/slider_controls.dart';
|
||||
part 'entities/text/widgets/text_input_state.dart';
|
||||
part 'entities/select/widgets/select_state.dart';
|
||||
part 'entity_widgets/common/simple_state.dart';
|
||||
part 'entities/timer/widgets/timer_state.dart';
|
||||
part 'entities/climate/widgets/climate_state.widget.dart';
|
||||
part 'entities/cover/widgets/cover_state.dart';
|
||||
part 'entities/date_time/widgets/date_time_state.dart';
|
||||
part 'entities/lock/widgets/lock_state.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/light/widgets/light_controls.dart';
|
||||
part 'entities/media_player/widgets/media_player_widgets.dart';
|
||||
part 'entities/fan/widgets/fan_controls.dart';
|
||||
part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart';
|
||||
part 'pages/settings.page.dart';
|
||||
part 'pages/purchase.page.dart';
|
||||
part 'pages/widgets/product_purchase.widget.dart';
|
||||
part 'pages/widgets/page_loading_indicator.dart';
|
||||
part 'pages/widgets/page_loading_error.dart';
|
||||
part 'pages/panel.page.dart';
|
||||
part 'pages/main.page.dart';
|
||||
part 'home_assistant.class.dart';
|
||||
part 'log.page.dart';
|
||||
part 'entity.page.dart';
|
||||
part 'utils.class.dart';
|
||||
part 'pages/log.page.dart';
|
||||
part 'pages/entity.page.dart';
|
||||
part 'mdi.class.dart';
|
||||
part 'entity_collection.class.dart';
|
||||
part 'auth_manager.class.dart';
|
||||
part 'connection.class.dart';
|
||||
part 'device.class.dart';
|
||||
part 'ui_class/ui.dart';
|
||||
part 'ui_class/view.class.dart';
|
||||
part 'ui_class/card.class.dart';
|
||||
part 'ui_class/sizes_class.dart';
|
||||
part 'ui_class/panel_class.dart';
|
||||
part 'ui_widgets/view.dart';
|
||||
part 'ui_widgets/card_widget.dart';
|
||||
part 'ui_widgets/card_header_widget.dart';
|
||||
part 'ui_widgets/config_panel_widget.dart';
|
||||
part 'managers/auth_manager.class.dart';
|
||||
part 'managers/location_manager.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.dart';
|
||||
part 'view.class.dart';
|
||||
part 'cards/card.class.dart';
|
||||
part 'panels/panel_class.dart';
|
||||
part 'view.dart';
|
||||
part 'cards/card_widget.dart';
|
||||
part 'cards/widgets/card_header.widget.dart';
|
||||
part 'panels/config_panel_widget.dart';
|
||||
part 'panels/widgets/link_to_web_config.dart';
|
||||
part 'types/ha_error.dart';
|
||||
part 'types/event_bus_events.dart';
|
||||
part 'cards/widgets/gauge_card_body.dart';
|
||||
part 'cards/widgets/light_card_body.dart';
|
||||
part 'pages/play_media.page.dart';
|
||||
|
||||
|
||||
EventBus eventBus = new EventBus();
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = "HA Client";
|
||||
const appVersion = "0.6.0-alpha3";
|
||||
const appVersion = "0.6.6";
|
||||
|
||||
void main() {
|
||||
void main() async {
|
||||
FlutterError.onError = (errorDetails) {
|
||||
Logger.e( "${errorDetails.exception}");
|
||||
if (Logger.isInDebugMode) {
|
||||
@ -121,7 +141,11 @@ void main() {
|
||||
};
|
||||
|
||||
runZoned(() {
|
||||
runApp(new HAClientApp());
|
||||
//AndroidAlarmManager.initialize().then((_) {
|
||||
runApp(new HAClientApp());
|
||||
// print("Running MAIN isolate ${Isolate.current.hashCode}");
|
||||
//});
|
||||
|
||||
}, onError: (error, stack) {
|
||||
Logger.e("$error");
|
||||
Logger.e("$stack");
|
||||
@ -133,7 +157,6 @@ void main() {
|
||||
|
||||
class HAClientApp extends StatelessWidget {
|
||||
|
||||
final HomeAssistant homeAssistant = HomeAssistant();
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -144,726 +167,40 @@ class HAClientApp extends StatelessWidget {
|
||||
),
|
||||
initialRoute: "/",
|
||||
routes: {
|
||||
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
|
||||
"/": (context) => MainPage(title: 'HA Client'),
|
||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||
"/configuration": (context) => PanelPage(title: "Configuration"),
|
||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||
"/play-media": (context) => PlayMediaPage(mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",),
|
||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||
"/login": (_) => WebviewScaffold(
|
||||
url: "${Connection().oauthUrl}",
|
||||
"/login": (context) => WebviewScaffold(
|
||||
url: "${ConnectionManager().oauthUrl}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
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 to your Home Assistant"),
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 _stateSubscription;
|
||||
StreamSubscription _settingsSubscription;
|
||||
StreamSubscription _serviceCallSubscription;
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
StreamSubscription _showErrorSubscription;
|
||||
StreamSubscription _startAuthSubscription;
|
||||
StreamSubscription _showDialogSubscription;
|
||||
StreamSubscription _reloadUISubscription;
|
||||
int _previousViewCount;
|
||||
bool _showLoginButton = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 (_showDialogSubscription == null) {
|
||||
_showDialogSubscription = eventBus.on<ShowDialogEvent>().listen((event){
|
||||
_showDialog(
|
||||
title: event.title,
|
||||
body: event.body,
|
||||
onPositive: event.onPositive,
|
||||
onNegative: event.onNegative,
|
||||
positiveText: event.positiveText,
|
||||
negativeText: event.negativeText
|
||||
);
|
||||
});
|
||||
}
|
||||
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() {
|
||||
Logger.d("_showOAuth: ${Connection().oauthUrl}");
|
||||
Navigator.of(context).pushNamed('/login');
|
||||
}
|
||||
|
||||
_setErrorState(HAError e) {
|
||||
if (e == null) {
|
||||
_showErrorBottomBar(
|
||||
HAError("Unknown error")
|
||||
);
|
||||
} else {
|
||||
_showErrorBottomBar(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDialog({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(
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")),
|
||||
title: Text("Report an issue"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
||||
},
|
||||
),
|
||||
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 ?? ""),
|
||||
title: new Text("Login with HA"),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||
"mdi:dots-vertical"), color: Colors.white,),
|
||||
FlatButton(
|
||||
child: Text("Manual", style: TextStyle(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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
|
||||
},
|
||||
)
|
||||
],
|
||||
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
|
||||
),
|
||||
)
|
||||
:
|
||||
widget.homeAssistant.buildViews(context, _viewsTabController),
|
||||
"/webview": (context) => WebviewScaffold(
|
||||
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop()
|
||||
),
|
||||
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
|
||||
),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
_showDialogSubscription?.cancel();
|
||||
_showEntityPageSubscription?.cancel();
|
||||
_showErrorSubscription?.cancel();
|
||||
_startAuthSubscription?.cancel();
|
||||
_reloadUISubscription?.cancel();
|
||||
//TODO disconnect
|
||||
//widget.homeAssistant?.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class AuthManager {
|
||||
|
||||
@ -14,17 +14,16 @@ class AuthManager {
|
||||
Completer completer = Completer();
|
||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
||||
Logger.d("Webview url changed to $url");
|
||||
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
||||
String authCode = url.split("=")[1];
|
||||
Logger.d("We have auth code. Getting temporary access token...");
|
||||
Connection().sendHTTPPost(
|
||||
ConnectionManager().sendHTTPPost(
|
||||
endPoint: "/auth/token",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
includeAuthHeader: false,
|
||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}"
|
||||
).then((response) {
|
||||
Logger.d("Gottemp token");
|
||||
Logger.d("Got temp token");
|
||||
String tempToken = json.decode(response)['access_token'];
|
||||
Logger.d("Closing webview...");
|
||||
//flutterWebviewPlugin.close();
|
||||
@ -38,7 +37,7 @@ class AuthManager {
|
||||
});
|
||||
}
|
||||
});
|
||||
Logger.d("Launching OAuth: $oauthUrl");
|
||||
Logger.d("Launching OAuth");
|
||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||
return completer.future;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
Connection._internal();
|
||||
ConnectionManager._internal();
|
||||
|
||||
String _domain;
|
||||
String _port;
|
||||
@ -54,21 +54,20 @@ class Connection {
|
||||
completer.completeError(HAError.checkConnectionSettings());
|
||||
stopInit = true;
|
||||
} else {
|
||||
//_token = prefs.getString('hassio-token');
|
||||
final storage = new FlutterSecureStorage();
|
||||
try {
|
||||
_token = await storage.read(key: "hacl_llt");
|
||||
Logger.e("Long-lived token read successful");
|
||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
||||
.encodeComponent(
|
||||
'http://ha-client.homemade.systems/service/auth_callback.html')}";
|
||||
settingsLoaded = true;
|
||||
} catch (e) {
|
||||
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||
_token = null;
|
||||
await storage.delete(key: "hacl_llt");
|
||||
stopInit = true;
|
||||
}
|
||||
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 {
|
||||
if ((_domain == null) || (_port == null) ||
|
||||
@ -104,13 +103,11 @@ class Connection {
|
||||
completer?.completeError(HAError("Connection timeout"));
|
||||
});
|
||||
}).then((_) {
|
||||
Logger.d("doConnect is finished 1");
|
||||
completer?.complete();
|
||||
}).catchError((e) {
|
||||
completer?.completeError(e);
|
||||
});
|
||||
} else {
|
||||
Logger.d("doConnect is finished 2");
|
||||
completer?.complete();
|
||||
}
|
||||
}
|
||||
@ -124,7 +121,7 @@ class Connection {
|
||||
} else {
|
||||
connecting = Completer();
|
||||
_disconnect().then((_) {
|
||||
Logger.d("Socket connecting: $_webSocketAPIEndpoint...");
|
||||
Logger.d("Socket connecting...");
|
||||
_socket = IOWebSocketChannel.connect(
|
||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||
_socketSubscription = _socket.stream.listen(
|
||||
@ -150,9 +147,7 @@ class Connection {
|
||||
Logger.d("[Received] <== ${data.toString()}");
|
||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||
_messageResolver.remove("auth");
|
||||
logout().then((_) {
|
||||
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||
});
|
||||
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
} else {
|
||||
_handleMessage(data);
|
||||
}
|
||||
@ -283,6 +278,7 @@ class Connection {
|
||||
}
|
||||
|
||||
Future logout() {
|
||||
Logger.d("Logging out");
|
||||
Completer completer = Completer();
|
||||
_disconnect().whenComplete(() {
|
||||
_token = null;
|
||||
@ -303,13 +299,15 @@ class Connection {
|
||||
_tempToken = null;
|
||||
final storage = new FlutterSecureStorage();
|
||||
storage.write(key: "hacl_llt", value: "$_token").then((_) {
|
||||
completer.complete();
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool("oauth-used", true);
|
||||
completer.complete();
|
||||
});
|
||||
}).catchError((e) {
|
||||
throw e;
|
||||
});
|
||||
}).catchError((e) {
|
||||
logout();
|
||||
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
|
||||
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
@ -334,13 +332,13 @@ class Connection {
|
||||
_connect().timeout(connectTimeout, onTimeout: (){
|
||||
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||
}).then((_) {
|
||||
Logger.d("[Sending] ==> $rawMessage");
|
||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||
_socket.sink.add(rawMessage);
|
||||
}).catchError((e) {
|
||||
_completer.completeError(e);
|
||||
});
|
||||
} else {
|
||||
Logger.d("[Sending] ==> $rawMessage");
|
||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||
_socket.sink.add(rawMessage);
|
||||
}
|
||||
return _completer.future;
|
||||
@ -369,7 +367,7 @@ class Connection {
|
||||
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||
Logger.d("[Sending] ==> $url");
|
||||
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
|
||||
http.Response historyResponse;
|
||||
historyResponse = await http.get(url, headers: {
|
||||
"authorization": "Bearer $_token",
|
||||
@ -377,7 +375,7 @@ class Connection {
|
||||
});
|
||||
var history = json.decode(historyResponse.body);
|
||||
if (history is List) {
|
||||
Logger.d( "[Received] <== ${history.first.length} history recors");
|
||||
Logger.d( "[Received] <== HTTP ${history.first.length} history recors");
|
||||
return history;
|
||||
} else {
|
||||
return [];
|
||||
@ -387,7 +385,7 @@ class Connection {
|
||||
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
|
||||
Completer completer = Completer();
|
||||
String url = "$httpWebHost$endPoint";
|
||||
Logger.d("[Sending] ==> $url");
|
||||
Logger.d("[Sending] ==> HTTP $endPoint");
|
||||
Map<String, String> headers = {};
|
||||
if (contentType != null) {
|
||||
headers["Content-Type"] = contentType;
|
||||
@ -400,7 +398,7 @@ class Connection {
|
||||
headers: headers,
|
||||
body: data
|
||||
).then((response) {
|
||||
Logger.d("[Received] <== ${response.statusCode}, ${response.body}");
|
||||
Logger.d("[Received] <== HTTP ${response.statusCode}");
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 ) {
|
||||
completer.complete(response.body);
|
||||
} else {
|
@ -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;
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ class Device {
|
||||
String osName;
|
||||
String osVersion;
|
||||
|
||||
Device._internal();
|
||||
DeviceInfoManager._internal();
|
||||
|
||||
loadDeviceInfo() {
|
||||
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();
|
||||
});
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
}
|
71
lib/managers/startup_user_messages_manager.class.dart
Normal file
71
lib/managers/startup_user_messages_manager.class.dart
Normal file
@ -0,0 +1,71 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class StartupUserMessagesManager {
|
||||
|
||||
static final StartupUserMessagesManager _instance = StartupUserMessagesManager
|
||||
._internal();
|
||||
|
||||
factory StartupUserMessagesManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
StartupUserMessagesManager._internal() {}
|
||||
|
||||
bool _supportAppDevelopmentMessageShown;
|
||||
bool _whatsNewMessageShown;
|
||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-660";
|
||||
|
||||
void checkMessagesToShow() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload();
|
||||
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
||||
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
|
||||
if (!_whatsNewMessageShown) {
|
||||
_showWhatsNewMessage();
|
||||
} else 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);
|
||||
});
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
void _showWhatsNewMessage() {
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "What's new",
|
||||
body: "You can now share any media url to HA Client via Android share menu. It will try to play that media on one of your media player. There is also 'tv' button available in app header if you want to send some url manually",
|
||||
positiveText: "Full release notes",
|
||||
negativeText: "Ok",
|
||||
onPositive: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_whatsNewMessageKey, true);
|
||||
Launcher.launchURL("https://github.com/estevez-dev/ha_client/releases");
|
||||
});
|
||||
},
|
||||
onNegative: () {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
prefs.setBool(_whatsNewMessageKey, true);
|
||||
});
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
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 HomeAssistant homeAssistant;
|
||||
|
||||
@override
|
||||
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||
@ -31,7 +30,7 @@ class _EntityViewPageState extends State<EntityViewPage> {
|
||||
}
|
||||
|
||||
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.
|
||||
title: new Text(_title),
|
||||
),
|
||||
body: widget.homeAssistant.entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||
body: HomeAssistant().entities.get(widget.entityId).buildEntityPageWidget(context),
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class LogViewPage extends StatefulWidget {
|
||||
LogViewPage({Key key, this.title}) : super(key: key);
|
820
lib/pages/main.page.dart
Normal file
820
lib/pages/main.page.dart
Normal file
@ -0,0 +1,820 @@
|
||||
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 ReceiveShareState<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;
|
||||
String _savedSharedText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final Stream purchaseUpdates =
|
||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_subscription = purchaseUpdates.listen((purchases) {
|
||||
_handlePurchaseUpdates(purchases);
|
||||
});
|
||||
super.initState();
|
||||
enableShareReceiving();
|
||||
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();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@override void receiveShare(Share shared) {
|
||||
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
|
||||
_savedSharedText = shared.text;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if (_savedSharedText != null && !HomeAssistant().isNoEntities) {
|
||||
Logger.d("Got shared text: $_savedSharedText");
|
||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText});
|
||||
_savedSharedText = null;
|
||||
}
|
||||
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:television"), color: Colors.white,),
|
||||
onPressed: () => Navigator.pushNamed(context, "/play-media", arguments: {"url": ""})
|
||||
),
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class PanelPage extends StatefulWidget {
|
||||
PanelPage({Key key, this.title, this.panel}) : super(key: key);
|
||||
@ -12,8 +12,6 @@ class PanelPage extends StatefulWidget {
|
||||
|
||||
class _PanelPageState extends State<PanelPage> {
|
||||
|
||||
List<ConfigurationItem> _items;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
225
lib/pages/play_media.page.dart
Normal file
225
lib/pages/play_media.page.dart
Normal file
@ -0,0 +1,225 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class PlayMediaPage extends StatefulWidget {
|
||||
|
||||
final String mediaUrl;
|
||||
|
||||
PlayMediaPage({Key key, this.mediaUrl}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PlayMediaPageState createState() => new _PlayMediaPageState();
|
||||
}
|
||||
|
||||
class _PlayMediaPageState extends State<PlayMediaPage> {
|
||||
|
||||
bool _loaded = false;
|
||||
String _error = "";
|
||||
String _validationMessage = "";
|
||||
List<Entity> _players;
|
||||
String _mediaUrl;
|
||||
String _contentType;
|
||||
bool _useMediaExtractor = false;
|
||||
bool _isMediaExtractorExist = false;
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription _refreshDataSubscription;
|
||||
final List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mediaUrl = widget.mediaUrl;
|
||||
_contentType = _contentTypes[0];
|
||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||
if (event.entityId.contains("media_player")) {
|
||||
Logger.d("State change event handled by play media page: ${event.entityId}");
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||
_loadMediaEntities();
|
||||
});
|
||||
_loadMediaEntities();
|
||||
}
|
||||
|
||||
_loadMediaEntities() async {
|
||||
if (HomeAssistant().isNoEntities) {
|
||||
setState(() {
|
||||
_loaded = false;
|
||||
});
|
||||
} else {
|
||||
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
|
||||
//_useMediaExtractor = _isMediaExtractorExist;
|
||||
_players = HomeAssistant().entities.getByDomains(["media_player"]);
|
||||
setState(() {
|
||||
if (_players.isNotEmpty) {
|
||||
_loaded = true;
|
||||
} else {
|
||||
_loaded = false;
|
||||
_error = "Looks like you don't have any media player";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _playMedia(Entity entity) {
|
||||
if (_mediaUrl == null || _mediaUrl.isEmpty) {
|
||||
setState(() {
|
||||
_validationMessage = "Media url must be specified";
|
||||
});
|
||||
} else {
|
||||
String serviceDomain;
|
||||
if (_useMediaExtractor) {
|
||||
serviceDomain = "media_extractor";
|
||||
} else {
|
||||
serviceDomain = "media_player";
|
||||
}
|
||||
Navigator.pop(context);
|
||||
ConnectionManager().callService(
|
||||
domain: serviceDomain,
|
||||
entityId: entity.entityId,
|
||||
service: "play_media",
|
||||
additionalServiceData: {
|
||||
"media_content_id": _mediaUrl,
|
||||
"media_content_type": _contentType
|
||||
}
|
||||
);
|
||||
eventBus.fire(ShowEntityPageEvent(entity));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (!_loaded) {
|
||||
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||
} else {
|
||||
List<Widget> children = [];
|
||||
children.add(CardHeader(name: "Media:"));
|
||||
children.add(
|
||||
TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Media url"
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _mediaUrl)),
|
||||
onChanged: (value) {
|
||||
_mediaUrl = value;
|
||||
}
|
||||
),
|
||||
);
|
||||
if (_validationMessage.isNotEmpty) {
|
||||
children.add(Text(
|
||||
"$_validationMessage",
|
||||
style: TextStyle(color: Colors.red)
|
||||
));
|
||||
}
|
||||
children.addAll(<Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
DropdownButton<String>(
|
||||
value: _contentType,
|
||||
isExpanded: true,
|
||||
items: _contentTypes.map((String value) {
|
||||
return new DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: new Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_contentType = value;
|
||||
});
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
if (_isMediaExtractorExist) {
|
||||
children.addAll(<Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text("Use media extractor"),
|
||||
),
|
||||
Switch(
|
||||
value: _useMediaExtractor,
|
||||
onChanged: (value) => setState((){_useMediaExtractor = value;}),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: Sizes.rowPadding,
|
||||
)
|
||||
]
|
||||
);
|
||||
} else {
|
||||
children.addAll(<Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text("You can use media extractor here"),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Launcher.launchURLInCustomTab(
|
||||
context: context,
|
||||
url: "https://www.home-assistant.io/components/media_extractor/"
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"How?",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: Sizes.doubleRowPadding,
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
children.add(CardHeader(name: "Play on:"));
|
||||
children.addAll(
|
||||
_players.map((player) => InkWell(
|
||||
child: EntityModel(
|
||||
entityWrapper: EntityWrapper(entity: player),
|
||||
handleTap: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||
child: DefaultEntityContainer(state: player._buildStatePart(context)),
|
||||
)
|
||||
),
|
||||
onTap: () => _playMedia(player),
|
||||
))
|
||||
);
|
||||
body = ListView(
|
||||
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||
scrollDirection: Axis.vertical,
|
||||
children: children
|
||||
);
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text("Play media"),
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose(){
|
||||
_stateSubscription?.cancel();
|
||||
_refreshDataSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
107
lib/pages/purchase.page.dart
Normal file
107
lib/pages/purchase.page.dart
Normal file
@ -0,0 +1,107 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class PurchasePage extends StatefulWidget {
|
||||
PurchasePage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_PurchasePageState createState() => new _PurchasePageState();
|
||||
}
|
||||
|
||||
class _PurchasePageState extends State<PurchasePage> {
|
||||
|
||||
bool _loaded = false;
|
||||
String _error = "";
|
||||
List<ProductDetails> _products;
|
||||
List<PurchaseDetails> _purchases;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
}
|
||||
|
||||
_loadProducts() async {
|
||||
final bool available = await InAppPurchaseConnection.instance.isAvailable();
|
||||
if (!available) {
|
||||
setState(() {
|
||||
_error = "Error connecting to store";
|
||||
});
|
||||
} else {
|
||||
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||
if (!response.notFoundIDs.isEmpty) {
|
||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||
}
|
||||
_products = response.productDetails;
|
||||
_loadPreviousPurchases();
|
||||
}
|
||||
}
|
||||
|
||||
_loadPreviousPurchases() async {
|
||||
final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.instance.queryPastPurchases();
|
||||
if (response.error != null) {
|
||||
setState(() {
|
||||
_error = "Error loading previous purchases";
|
||||
});
|
||||
} else {
|
||||
_purchases = response.pastPurchases;
|
||||
for (PurchaseDetails purchase in _purchases) {
|
||||
Logger.d("Previous purchase: ${purchase.status}");
|
||||
}
|
||||
if (_products.isEmpty) {
|
||||
setState(() {
|
||||
_error = "No data found in store";
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildProducts() {
|
||||
List<Widget> productWidgets = [];
|
||||
for (ProductDetails product in _products) {
|
||||
productWidgets.add(
|
||||
ProductPurchase(
|
||||
product: product,
|
||||
onBuy: (product) => _buyProduct(product),
|
||||
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
children: productWidgets
|
||||
);
|
||||
}
|
||||
|
||||
void _buyProduct(ProductDetails product) {
|
||||
Logger.d("Starting purchase of ${product.id}");
|
||||
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
|
||||
InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (!_loaded) {
|
||||
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||
} else {
|
||||
body = _buildProducts();
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text(widget.title),
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of 'main.dart';
|
||||
part of '../main.dart';
|
||||
|
||||
class ConnectionSettingsPage extends StatefulWidget {
|
||||
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
||||
@ -16,10 +16,13 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
String _newHassioPort = "";
|
||||
String _socketProtocol = "wss";
|
||||
String _newSocketProtocol = "wss";
|
||||
String _longLivedToken = "";
|
||||
String _newLongLivedToken = "";
|
||||
bool _useLovelace = true;
|
||||
bool _newUseLovelace = true;
|
||||
|
||||
String oauthUrl;
|
||||
bool useOAuth = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -30,6 +33,23 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
|
||||
_loadSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final storage = new FlutterSecureStorage();
|
||||
|
||||
try {
|
||||
useOAuth = prefs.getBool("oauth-used") ?? true;
|
||||
} catch (e) {
|
||||
useOAuth = true;
|
||||
}
|
||||
|
||||
if (!useOAuth) {
|
||||
try {
|
||||
_longLivedToken = _newLongLivedToken =
|
||||
await storage.read(key: "hacl_llt");
|
||||
} catch (e) {
|
||||
_longLivedToken = _newLongLivedToken = "";
|
||||
await storage.delete(key: "hacl_llt");
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||
@ -48,16 +68,32 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
(_newHassioPort != _hassioPort) ||
|
||||
(_newHassioDomain != _hassioDomain) ||
|
||||
(_newSocketProtocol != _socketProtocol) ||
|
||||
(_newUseLovelace != _useLovelace));
|
||||
(_newUseLovelace != _useLovelace) ||
|
||||
(_newLongLivedToken != _longLivedToken));
|
||||
|
||||
}
|
||||
|
||||
_saveSettings() async {
|
||||
_newHassioDomain = _newHassioDomain.trim();
|
||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||
}
|
||||
_newHassioDomain = _newHassioDomain.split("/")[0];
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final storage = new FlutterSecureStorage();
|
||||
if (_newLongLivedToken.isNotEmpty) {
|
||||
_newLongLivedToken = _newLongLivedToken.trim();
|
||||
prefs.setBool("oauth-used", false);
|
||||
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
||||
} else if (!useOAuth) {
|
||||
await storage.delete(key: "hacl_llt");
|
||||
}
|
||||
prefs.setString("hassio-domain", _newHassioDomain);
|
||||
if (_newHassioPort == null || _newHassioPort.isEmpty) {
|
||||
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
|
||||
} else {
|
||||
_newHassioPort = _newHassioPort.trim();
|
||||
}
|
||||
prefs.setString("hassio-port", _newHassioPort);
|
||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||
@ -118,13 +154,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: "Home Assistant domain or ip address"
|
||||
),
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioDomain,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioDomain.length)
|
||||
)
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
|
||||
onChanged: (value) {
|
||||
_newHassioDomain = value;
|
||||
}
|
||||
@ -133,13 +163,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: "Home Assistant port (default is 8123)"
|
||||
),
|
||||
controller: new TextEditingController.fromValue(
|
||||
new TextEditingValue(
|
||||
text: _newHassioPort,
|
||||
selection:
|
||||
new TextSelection.collapsed(offset: _newHassioPort.length)
|
||||
)
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
|
||||
onChanged: (value) {
|
||||
_newHassioPort = value;
|
||||
}
|
||||
@ -171,6 +195,27 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Authentication settings",
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontSize: 20.0
|
||||
),
|
||||
),
|
||||
Container(height: 10.0,),
|
||||
Text(
|
||||
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
new TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Long-lived token"
|
||||
),
|
||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
|
||||
onChanged: (value) {
|
||||
_newLongLivedToken = value;
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
32
lib/pages/widgets/page_loading_error.dart
Normal file
32
lib/pages/widgets/page_loading_error.dart
Normal file
@ -0,0 +1,32 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class PageLoadingError extends StatelessWidget {
|
||||
|
||||
final String errorText;
|
||||
|
||||
const PageLoadingError({Key key, this.errorText: "Error"}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.redAccent,
|
||||
size: 48.0
|
||||
)
|
||||
),
|
||||
Text(this.errorText, style: TextStyle(color: Colors.black45))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
23
lib/pages/widgets/page_loading_indicator.dart
Normal file
23
lib/pages/widgets/page_loading_indicator.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class PageLoadingIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||
child: CircularProgressIndicator()
|
||||
),
|
||||
Text("Loading...", style: TextStyle(color: Colors.black45))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
74
lib/pages/widgets/product_purchase.widget.dart
Normal file
74
lib/pages/widgets/product_purchase.widget.dart
Normal file
@ -0,0 +1,74 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ProductPurchase extends StatelessWidget {
|
||||
|
||||
final ProductDetails product;
|
||||
final onBuy;
|
||||
final purchased;
|
||||
|
||||
const ProductPurchase({Key key, @required this.product, @required this.onBuy, this.purchased}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String period = "";
|
||||
Color priceColor;
|
||||
String buttonText = '';
|
||||
String buttonTextInactive = '';
|
||||
if (product.id.contains("year")) {
|
||||
period += "/ year";
|
||||
buttonText = "Subscribe";
|
||||
buttonTextInactive = "Already";
|
||||
priceColor = Colors.amber;
|
||||
} else {
|
||||
period += "";
|
||||
buttonText = "Pay";
|
||||
buttonTextInactive = "Paid";
|
||||
priceColor = Colors.deepOrangeAccent;
|
||||
}
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${product.title}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text(
|
||||
"${product.description}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 4,
|
||||
softWrap: true,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("${product.price} $period", style: TextStyle(color: priceColor)),
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: RaisedButton(
|
||||
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
|
||||
color: Colors.blue,
|
||||
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();
|
||||
}
|
||||
}
|
@ -17,28 +17,25 @@ class Panel {
|
||||
final Map config;
|
||||
String icon;
|
||||
bool isHidden = true;
|
||||
bool isWebView = false;
|
||||
|
||||
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
||||
if (icon == null || !icon.startsWith("mdi:")) {
|
||||
icon = Panel.iconsByComponent[type];
|
||||
}
|
||||
isHidden = (type != "iframe" && type != "config");
|
||||
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
|
||||
isWebView = (type != 'config');
|
||||
}
|
||||
|
||||
void handleOpen(BuildContext context) {
|
||||
if (type == "iframe") {
|
||||
Logger.d("Launching custom tab with ${config["url"]}");
|
||||
HAUtils.launchURLInCustomTab(context: context, url: config["url"]);
|
||||
} else if (type == "config") {
|
||||
if (type == "config") {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
String url = "${Connection().httpWebHost}/$urlPath";
|
||||
Logger.d("Launching custom tab with $url");
|
||||
HAUtils.launchURLInCustomTab(context: context, url: url);
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
|
||||
}
|
||||
}
|
||||
|
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 open web version"),
|
||||
onTap: () {
|
||||
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
77
lib/plugins/circular_slider/base_painter.dart
Normal file
77
lib/plugins/circular_slider/base_painter.dart
Normal file
@ -0,0 +1,77 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
|
||||
class BasePainter extends CustomPainter {
|
||||
Color baseColor;
|
||||
Color selectionColor;
|
||||
int primarySectors;
|
||||
int secondarySectors;
|
||||
double sliderStrokeWidth;
|
||||
|
||||
Offset center;
|
||||
double radius;
|
||||
|
||||
BasePainter({
|
||||
@required this.baseColor,
|
||||
@required this.selectionColor,
|
||||
@required this.primarySectors,
|
||||
@required this.secondarySectors,
|
||||
@required this.sliderStrokeWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint base = _getPaint(color: baseColor);
|
||||
|
||||
center = Offset(size.width / 2, size.height / 2);
|
||||
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
|
||||
// we need this in the parent to calculate if the user clicks on the circumference
|
||||
|
||||
assert(radius > 0);
|
||||
|
||||
canvas.drawCircle(center, radius, base);
|
||||
|
||||
if (primarySectors > 0) {
|
||||
_paintSectors(primarySectors, 8.0, selectionColor, canvas);
|
||||
}
|
||||
|
||||
if (secondarySectors > 0) {
|
||||
_paintSectors(secondarySectors, 6.0, baseColor, canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void _paintSectors(
|
||||
int sectors, double radiusPadding, Color color, Canvas canvas) {
|
||||
Paint section = _getPaint(color: color, width: 2.0);
|
||||
|
||||
var endSectors =
|
||||
getSectionsCoordinatesInCircle(center, radius + radiusPadding, sectors);
|
||||
var initSectors =
|
||||
getSectionsCoordinatesInCircle(center, radius - radiusPadding, sectors);
|
||||
_paintLines(canvas, initSectors, endSectors, section);
|
||||
}
|
||||
|
||||
void _paintLines(
|
||||
Canvas canvas, List<Offset> inits, List<Offset> ends, Paint section) {
|
||||
assert(inits.length == ends.length && inits.length > 0);
|
||||
|
||||
for (var i = 0; i < inits.length; i++) {
|
||||
canvas.drawLine(inits[i], ends[i], section);
|
||||
}
|
||||
}
|
||||
|
||||
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
|
||||
Paint()
|
||||
..color = color
|
||||
..strokeCap = StrokeCap.round
|
||||
..style = style ?? PaintingStyle.stroke
|
||||
..strokeWidth = width ?? sliderStrokeWidth;
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
366
lib/plugins/circular_slider/circular_slider_paint.dart
Normal file
366
lib/plugins/circular_slider/circular_slider_paint.dart
Normal file
@ -0,0 +1,366 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'base_painter.dart';
|
||||
import 'slider_painter.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
enum CircularSliderMode { singleHandler, doubleHandler }
|
||||
|
||||
enum SlidingState { none, endIsBiggerThanStart, endIsSmallerThanStart }
|
||||
|
||||
typedef SelectionChanged<T> = void Function(T a, T b, T c);
|
||||
|
||||
class CircularSliderPaint extends StatefulWidget {
|
||||
final CircularSliderMode mode;
|
||||
final int init;
|
||||
final int end;
|
||||
final int divisions;
|
||||
final int primarySectors;
|
||||
final int secondarySectors;
|
||||
final SelectionChanged<int> onSelectionChange;
|
||||
final SelectionChanged<int> onSelectionEnd;
|
||||
final Color baseColor;
|
||||
final Color selectionColor;
|
||||
final Color handlerColor;
|
||||
final double handlerOutterRadius;
|
||||
final Widget child;
|
||||
final bool showRoundedCapInSelection;
|
||||
final bool showHandlerOutter;
|
||||
final double sliderStrokeWidth;
|
||||
final bool shouldCountLaps;
|
||||
|
||||
CircularSliderPaint({
|
||||
@required this.mode,
|
||||
@required this.divisions,
|
||||
@required this.init,
|
||||
@required this.end,
|
||||
this.child,
|
||||
@required this.primarySectors,
|
||||
@required this.secondarySectors,
|
||||
@required this.onSelectionChange,
|
||||
@required this.onSelectionEnd,
|
||||
@required this.baseColor,
|
||||
@required this.selectionColor,
|
||||
@required this.handlerColor,
|
||||
@required this.handlerOutterRadius,
|
||||
@required this.showRoundedCapInSelection,
|
||||
@required this.showHandlerOutter,
|
||||
@required this.sliderStrokeWidth,
|
||||
@required this.shouldCountLaps,
|
||||
});
|
||||
|
||||
@override
|
||||
_CircularSliderState createState() => _CircularSliderState();
|
||||
}
|
||||
|
||||
class _CircularSliderState extends State<CircularSliderPaint> {
|
||||
bool _isInitHandlerSelected = false;
|
||||
bool _isEndHandlerSelected = false;
|
||||
|
||||
SliderPainter _painter;
|
||||
|
||||
/// start angle in radians where we need to locate the init handler
|
||||
double _startAngle;
|
||||
|
||||
/// end angle in radians where we need to locate the end handler
|
||||
double _endAngle;
|
||||
|
||||
/// the absolute angle in radians representing the selection
|
||||
double _sweepAngle;
|
||||
|
||||
/// in case we have a double slider and we want to move the whole selection by clicking in the slider
|
||||
/// this will capture the position in the selection relative to the initial handler
|
||||
/// that way we will be able to keep the selection constant when moving
|
||||
int _differenceFromInitPoint;
|
||||
|
||||
/// will store the number of full laps (2pi radians) as part of the selection
|
||||
int _laps = 0;
|
||||
|
||||
/// will be used to calculate in the next movement if we need to increase or decrease _laps
|
||||
SlidingState _slidingState = SlidingState.none;
|
||||
|
||||
bool get isDoubleHandler => widget.mode == CircularSliderMode.doubleHandler;
|
||||
|
||||
bool get isSingleHandler => widget.mode == CircularSliderMode.singleHandler;
|
||||
|
||||
bool get isBothHandlersSelected =>
|
||||
_isEndHandlerSelected && _isInitHandlerSelected;
|
||||
|
||||
bool get isNoHandlersSelected =>
|
||||
!_isEndHandlerSelected && !_isInitHandlerSelected;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_calculatePaintData();
|
||||
}
|
||||
|
||||
// we need to update this widget both with gesture detector but
|
||||
// also when the parent widget rebuilds itself
|
||||
@override
|
||||
void didUpdateWidget(CircularSliderPaint oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.init != widget.init || oldWidget.end != widget.end) {
|
||||
_calculatePaintData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawGestureDetector(
|
||||
gestures: <Type, GestureRecognizerFactory>{
|
||||
CustomPanGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
|
||||
() => CustomPanGestureRecognizer(
|
||||
onPanDown: _onPanDown,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
onPanEnd: _onPanEnd,
|
||||
),
|
||||
(CustomPanGestureRecognizer instance) {},
|
||||
),
|
||||
},
|
||||
child: CustomPaint(
|
||||
painter: BasePainter(
|
||||
baseColor: widget.baseColor,
|
||||
selectionColor: widget.selectionColor,
|
||||
primarySectors: widget.primarySectors,
|
||||
secondarySectors: widget.secondarySectors,
|
||||
sliderStrokeWidth: widget.sliderStrokeWidth,
|
||||
),
|
||||
foregroundPainter: _painter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _calculatePaintData() {
|
||||
var initPercent = isDoubleHandler
|
||||
? valueToPercentage(widget.init, widget.divisions)
|
||||
: 0.0;
|
||||
var endPercent = valueToPercentage(widget.end, widget.divisions);
|
||||
var sweep = getSweepAngle(initPercent, endPercent);
|
||||
|
||||
var previousStartAngle = _startAngle;
|
||||
var previousEndAngle = _endAngle;
|
||||
|
||||
_startAngle = isDoubleHandler ? percentageToRadians(initPercent) : 0.0;
|
||||
_endAngle = percentageToRadians(endPercent);
|
||||
_sweepAngle = percentageToRadians(sweep.abs());
|
||||
|
||||
// update full laps if need be
|
||||
if (widget.shouldCountLaps) {
|
||||
var newSlidingState = _calculateSlidingState(_startAngle, _endAngle);
|
||||
if (isSingleHandler) {
|
||||
_laps = _calculateLapsForsSingleHandler(
|
||||
_endAngle, previousEndAngle, _slidingState, _laps);
|
||||
_slidingState = newSlidingState;
|
||||
} else {
|
||||
// is double handler
|
||||
if (newSlidingState != _slidingState) {
|
||||
_laps = _calculateLapsForDoubleHandler(
|
||||
_startAngle,
|
||||
_endAngle,
|
||||
previousStartAngle,
|
||||
previousEndAngle,
|
||||
_slidingState,
|
||||
newSlidingState,
|
||||
_laps);
|
||||
_slidingState = newSlidingState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_painter = SliderPainter(
|
||||
mode: widget.mode,
|
||||
startAngle: _startAngle,
|
||||
endAngle: _endAngle,
|
||||
sweepAngle: _sweepAngle,
|
||||
selectionColor: widget.selectionColor,
|
||||
handlerColor: widget.handlerColor,
|
||||
handlerOutterRadius: widget.handlerOutterRadius,
|
||||
showRoundedCapInSelection: widget.showRoundedCapInSelection,
|
||||
showHandlerOutter: widget.showHandlerOutter,
|
||||
sliderStrokeWidth: widget.sliderStrokeWidth,
|
||||
);
|
||||
}
|
||||
|
||||
int _calculateLapsForsSingleHandler(
|
||||
double end, double prevEnd, SlidingState slidingState, int laps) {
|
||||
if (slidingState != SlidingState.none) {
|
||||
if (radiansWasModuloed(end, prevEnd)) {
|
||||
var lapIncrement = end < prevEnd ? 1 : -1;
|
||||
var newLaps = laps + lapIncrement;
|
||||
return newLaps < 0 ? 0 : newLaps;
|
||||
}
|
||||
}
|
||||
return laps;
|
||||
}
|
||||
|
||||
int _calculateLapsForDoubleHandler(
|
||||
double start,
|
||||
double end,
|
||||
double prevStart,
|
||||
double prevEnd,
|
||||
SlidingState slidingState,
|
||||
SlidingState newSlidingState,
|
||||
int laps) {
|
||||
if (slidingState != SlidingState.none) {
|
||||
if (!radiansWasModuloed(start, prevStart) &&
|
||||
!radiansWasModuloed(end, prevEnd)) {
|
||||
var lapIncrement =
|
||||
newSlidingState == SlidingState.endIsBiggerThanStart ? 1 : -1;
|
||||
var newLaps = laps + lapIncrement;
|
||||
return newLaps < 0 ? 0 : newLaps;
|
||||
}
|
||||
}
|
||||
return laps;
|
||||
}
|
||||
|
||||
SlidingState _calculateSlidingState(double start, double end) {
|
||||
return end > start
|
||||
? SlidingState.endIsBiggerThanStart
|
||||
: SlidingState.endIsSmallerThanStart;
|
||||
}
|
||||
|
||||
void _onPanUpdate(Offset details) {
|
||||
if (!_isInitHandlerSelected && !_isEndHandlerSelected) {
|
||||
return;
|
||||
}
|
||||
if (_painter.center == null) {
|
||||
return;
|
||||
}
|
||||
_handlePan(details, false);
|
||||
}
|
||||
|
||||
void _onPanEnd(Offset details) {
|
||||
_handlePan(details, true);
|
||||
|
||||
_isInitHandlerSelected = false;
|
||||
_isEndHandlerSelected = false;
|
||||
}
|
||||
|
||||
void _handlePan(Offset details, bool isPanEnd) {
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
var position = renderBox.globalToLocal(details);
|
||||
|
||||
var angle = coordinatesToRadians(_painter.center, position);
|
||||
var percentage = radiansToPercentage(angle);
|
||||
var newValue = percentageToValue(percentage, widget.divisions);
|
||||
|
||||
if (isBothHandlersSelected) {
|
||||
var newValueInit =
|
||||
(newValue - _differenceFromInitPoint) % widget.divisions;
|
||||
if (newValueInit != widget.init) {
|
||||
var newValueEnd =
|
||||
(widget.end + (newValueInit - widget.init)) % widget.divisions;
|
||||
widget.onSelectionChange(newValueInit, newValueEnd, _laps);
|
||||
if (isPanEnd) {
|
||||
widget.onSelectionEnd(newValueInit, newValueEnd, _laps);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// isDoubleHandler but one handler was selected
|
||||
if (_isInitHandlerSelected) {
|
||||
widget.onSelectionChange(newValue, widget.end, _laps);
|
||||
if (isPanEnd) {
|
||||
widget.onSelectionEnd(newValue, widget.end, _laps);
|
||||
}
|
||||
} else {
|
||||
widget.onSelectionChange(widget.init, newValue, _laps);
|
||||
if (isPanEnd) {
|
||||
widget.onSelectionEnd(widget.init, newValue, _laps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _onPanDown(Offset details) {
|
||||
if (_painter == null) {
|
||||
return false;
|
||||
}
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
var position = renderBox.globalToLocal(details);
|
||||
|
||||
if (position == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSingleHandler) {
|
||||
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
|
||||
_isEndHandlerSelected = true;
|
||||
_onPanUpdate(details);
|
||||
}
|
||||
} else {
|
||||
_isInitHandlerSelected = isPointInsideCircle(
|
||||
position, _painter.initHandler, widget.handlerOutterRadius);
|
||||
|
||||
if (!_isInitHandlerSelected) {
|
||||
_isEndHandlerSelected = isPointInsideCircle(
|
||||
position, _painter.endHandler, widget.handlerOutterRadius);
|
||||
}
|
||||
|
||||
if (isNoHandlersSelected) {
|
||||
// we check if the user pressed in the selection in a double handler slider
|
||||
// that means the user wants to move the selection as a whole
|
||||
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
|
||||
var angle = coordinatesToRadians(_painter.center, position);
|
||||
if (isAngleInsideRadiansSelection(angle, _startAngle, _sweepAngle)) {
|
||||
_isEndHandlerSelected = true;
|
||||
_isInitHandlerSelected = true;
|
||||
var positionPercentage = radiansToPercentage(angle);
|
||||
|
||||
// no need to account for negative values, that will be sorted out in the onPanUpdate
|
||||
_differenceFromInitPoint =
|
||||
percentageToValue(positionPercentage, widget.divisions) -
|
||||
widget.init;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _isInitHandlerSelected || _isEndHandlerSelected;
|
||||
}
|
||||
}
|
||||
|
||||
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
final Function onPanDown;
|
||||
final Function onPanUpdate;
|
||||
final Function onPanEnd;
|
||||
|
||||
CustomPanGestureRecognizer({
|
||||
@required this.onPanDown,
|
||||
@required this.onPanUpdate,
|
||||
@required this.onPanEnd,
|
||||
});
|
||||
|
||||
@override
|
||||
void addPointer(PointerEvent event) {
|
||||
if (onPanDown(event.position)) {
|
||||
startTrackingPointer(event.pointer);
|
||||
resolve(GestureDisposition.accepted);
|
||||
} else {
|
||||
stopTrackingPointer(event.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event) {
|
||||
if (event is PointerMoveEvent) {
|
||||
onPanUpdate(event.position);
|
||||
}
|
||||
if (event is PointerUpEvent) {
|
||||
onPanEnd(event.position);
|
||||
stopTrackingPointer(event.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'customPan';
|
||||
|
||||
@override
|
||||
void didStopTrackingLastPointer(int pointer) {}
|
||||
}
|
148
lib/plugins/circular_slider/double_circular_slider.dart
Normal file
148
lib/plugins/circular_slider/double_circular_slider.dart
Normal file
@ -0,0 +1,148 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'circular_slider_paint.dart';
|
||||
|
||||
/// Returns a widget which displays a circle to be used as a slider.
|
||||
///
|
||||
/// Required arguments are init and end to set the initial selection.
|
||||
/// onSelectionChange is a callback function which returns new values as the user
|
||||
/// changes the interval.
|
||||
/// The rest of the params are used to change the look and feel.
|
||||
///
|
||||
/// DoubleCircularSlider(5, 10, onSelectionChange: () => {});
|
||||
class DoubleCircularSlider extends StatefulWidget {
|
||||
/// the selection will be values between 0..divisions; max value is 300
|
||||
final int divisions;
|
||||
|
||||
/// the initial value in the selection
|
||||
final int init;
|
||||
|
||||
/// the end value in the selection
|
||||
final int end;
|
||||
|
||||
/// the number of primary sectors to be painted
|
||||
/// will be painted using selectionColor
|
||||
final int primarySectors;
|
||||
|
||||
/// the number of secondary sectors to be painted
|
||||
/// will be painted using baseColor
|
||||
final int secondarySectors;
|
||||
|
||||
/// an optional widget that would be mounted inside the circle
|
||||
final Widget child;
|
||||
|
||||
/// height of the canvas, default at 220
|
||||
final double height;
|
||||
|
||||
/// width of the canvas, default at 220
|
||||
final double width;
|
||||
|
||||
/// color of the base circle and sections
|
||||
final Color baseColor;
|
||||
|
||||
/// color of the selection
|
||||
final Color selectionColor;
|
||||
|
||||
/// color of the handlers
|
||||
final Color handlerColor;
|
||||
|
||||
/// callback function when init and end change
|
||||
/// (int init, int end) => void
|
||||
final SelectionChanged<int> onSelectionChange;
|
||||
|
||||
/// callback function when init and end finish
|
||||
/// (int init, int end) => void
|
||||
final SelectionChanged<int> onSelectionEnd;
|
||||
|
||||
/// outter radius for the handlers
|
||||
final double handlerOutterRadius;
|
||||
|
||||
/// if true an extra handler ring will be displayed in the handler
|
||||
final bool showHandlerOutter;
|
||||
|
||||
/// stroke width for the slider, defaults at 12.0
|
||||
final double sliderStrokeWidth;
|
||||
|
||||
/// if true, the onSelectionChange will also return the number of laps in the slider
|
||||
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
|
||||
final bool shouldCountLaps;
|
||||
|
||||
DoubleCircularSlider(
|
||||
this.divisions,
|
||||
this.init,
|
||||
this.end, {
|
||||
this.height,
|
||||
this.width,
|
||||
this.child,
|
||||
this.primarySectors,
|
||||
this.secondarySectors,
|
||||
this.baseColor,
|
||||
this.selectionColor,
|
||||
this.handlerColor,
|
||||
this.onSelectionChange,
|
||||
this.onSelectionEnd,
|
||||
this.handlerOutterRadius,
|
||||
this.showHandlerOutter,
|
||||
this.sliderStrokeWidth,
|
||||
this.shouldCountLaps,
|
||||
}) : assert(init >= 0 && init <= divisions,
|
||||
'init has to be > 0 and < divisions value'),
|
||||
assert(end >= 0 && end <= divisions,
|
||||
'end has to be > 0 and < divisions value'),
|
||||
assert(divisions >= 0 && divisions <= 300,
|
||||
'divisions has to be > 0 and <= 300');
|
||||
|
||||
@override
|
||||
_DoubleCircularSliderState createState() => _DoubleCircularSliderState();
|
||||
}
|
||||
|
||||
class _DoubleCircularSliderState extends State<DoubleCircularSlider> {
|
||||
int _init;
|
||||
int _end;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init = widget.init;
|
||||
_end = widget.end;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.height ?? 220,
|
||||
width: widget.width ?? 220,
|
||||
child: CircularSliderPaint(
|
||||
mode: CircularSliderMode.doubleHandler,
|
||||
init: _init,
|
||||
end: _end,
|
||||
divisions: widget.divisions,
|
||||
primarySectors: widget.primarySectors ?? 0,
|
||||
secondarySectors: widget.secondarySectors ?? 0,
|
||||
child: widget.child,
|
||||
onSelectionChange: (newInit, newEnd, laps) {
|
||||
if (widget.onSelectionChange != null) {
|
||||
widget.onSelectionChange(newInit, newEnd, laps);
|
||||
}
|
||||
setState(() {
|
||||
_init = newInit;
|
||||
_end = newEnd;
|
||||
});
|
||||
},
|
||||
onSelectionEnd: (newInit, newEnd, laps) {
|
||||
if (widget.onSelectionEnd != null) {
|
||||
widget.onSelectionEnd(newInit, newEnd, laps);
|
||||
}
|
||||
},
|
||||
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
|
||||
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
|
||||
selectionColor:
|
||||
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
|
||||
handlerColor: widget.handlerColor ?? Colors.white,
|
||||
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
|
||||
showRoundedCapInSelection: false,
|
||||
showHandlerOutter: widget.showHandlerOutter ?? true,
|
||||
shouldCountLaps: widget.shouldCountLaps ?? false,
|
||||
));
|
||||
}
|
||||
}
|
147
lib/plugins/circular_slider/single_circular_slider.dart
Normal file
147
lib/plugins/circular_slider/single_circular_slider.dart
Normal file
@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'circular_slider_paint.dart';
|
||||
|
||||
import '../../utils/logger.dart';
|
||||
|
||||
/// Returns a widget which displays a circle to be used as a slider.
|
||||
///
|
||||
/// Required arguments are position and divisions to set the initial selection.
|
||||
/// onSelectionChange is a callback function which returns new values as the user
|
||||
/// changes the interval.
|
||||
/// The rest of the params are used to change the look and feel.
|
||||
///
|
||||
/// SingleCircularSlider(5, 10, onSelectionChange: () => {});
|
||||
class SingleCircularSlider extends StatefulWidget {
|
||||
/// the selection will be values between 0..divisions; max value is 300
|
||||
final int divisions;
|
||||
|
||||
/// the initial value in the selection
|
||||
int position;
|
||||
|
||||
/// the number of primary sectors to be painted
|
||||
/// will be painted using selectionColor
|
||||
final int primarySectors;
|
||||
|
||||
/// the number of secondary sectors to be painted
|
||||
/// will be painted using baseColor
|
||||
final int secondarySectors;
|
||||
|
||||
/// an optional widget that would be mounted inside the circle
|
||||
final Widget child;
|
||||
|
||||
/// height of the canvas, default at 220
|
||||
final double height;
|
||||
|
||||
/// width of the canvas, default at 220
|
||||
final double width;
|
||||
|
||||
/// color of the base circle and sections
|
||||
final Color baseColor;
|
||||
|
||||
/// color of the selection
|
||||
final Color selectionColor;
|
||||
|
||||
/// color of the handlers
|
||||
final Color handlerColor;
|
||||
|
||||
/// callback function when init and end change
|
||||
/// (int init, int end) => void
|
||||
final SelectionChanged<int> onSelectionChange;
|
||||
|
||||
/// callback function when init and end finish
|
||||
/// (int init, int end) => void
|
||||
final SelectionChanged<int> onSelectionEnd;
|
||||
|
||||
/// outter radius for the handlers
|
||||
final double handlerOutterRadius;
|
||||
|
||||
/// if true will paint a rounded cap in the selection slider start
|
||||
final bool showRoundedCapInSelection;
|
||||
|
||||
/// if true an extra handler ring will be displayed in the handler
|
||||
final bool showHandlerOutter;
|
||||
|
||||
/// stroke width for the slider, defaults at 12.0
|
||||
final double sliderStrokeWidth;
|
||||
|
||||
/// if true, the onSelectionChange will also return the number of laps in the slider
|
||||
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
|
||||
final bool shouldCountLaps;
|
||||
|
||||
SingleCircularSlider(
|
||||
this.divisions,
|
||||
this.position, {
|
||||
this.height,
|
||||
this.width,
|
||||
this.child,
|
||||
this.primarySectors,
|
||||
this.secondarySectors,
|
||||
this.baseColor,
|
||||
this.selectionColor,
|
||||
this.handlerColor,
|
||||
this.onSelectionChange,
|
||||
this.onSelectionEnd,
|
||||
this.handlerOutterRadius,
|
||||
this.showRoundedCapInSelection,
|
||||
this.showHandlerOutter,
|
||||
this.sliderStrokeWidth,
|
||||
this.shouldCountLaps,
|
||||
}) : assert(position >= 0 && position <= divisions,
|
||||
'init has to be > 0 and < divisions value'),
|
||||
assert(divisions >= 0 && divisions <= 300,
|
||||
'divisions has to be > 0 and <= 300');
|
||||
|
||||
@override
|
||||
_SingleCircularSliderState createState() => _SingleCircularSliderState();
|
||||
}
|
||||
|
||||
class _SingleCircularSliderState extends State<SingleCircularSlider> {
|
||||
int _end;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_end = widget.position;
|
||||
Logger.d('Init: _end=$_end');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Logger.d('Build: _end=$_end');
|
||||
return Container(
|
||||
height: widget.height ?? 220,
|
||||
width: widget.width ?? 220,
|
||||
child: CircularSliderPaint(
|
||||
mode: CircularSliderMode.singleHandler,
|
||||
init: 0,
|
||||
end: _end,
|
||||
divisions: widget.divisions,
|
||||
primarySectors: widget.primarySectors ?? 0,
|
||||
secondarySectors: widget.secondarySectors ?? 0,
|
||||
child: widget.child,
|
||||
onSelectionChange: (newInit, newEnd, laps) {
|
||||
if (widget.onSelectionChange != null) {
|
||||
widget.onSelectionChange(newInit, newEnd, laps);
|
||||
}
|
||||
setState(() {
|
||||
_end = newEnd;
|
||||
});
|
||||
},
|
||||
onSelectionEnd: (newInit, newEnd, laps) {
|
||||
if (widget.onSelectionEnd != null) {
|
||||
widget.onSelectionEnd(newInit, newEnd, laps);
|
||||
}
|
||||
},
|
||||
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
|
||||
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
|
||||
selectionColor:
|
||||
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
|
||||
handlerColor: widget.handlerColor ?? Colors.white,
|
||||
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
|
||||
showRoundedCapInSelection: widget.showRoundedCapInSelection ?? false,
|
||||
showHandlerOutter: widget.showHandlerOutter ?? true,
|
||||
shouldCountLaps: widget.shouldCountLaps ?? false,
|
||||
));
|
||||
}
|
||||
}
|
77
lib/plugins/circular_slider/slider_painter.dart
Normal file
77
lib/plugins/circular_slider/slider_painter.dart
Normal file
@ -0,0 +1,77 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'circular_slider_paint.dart' show CircularSliderMode;
|
||||
import 'utils.dart';
|
||||
|
||||
class SliderPainter extends CustomPainter {
|
||||
CircularSliderMode mode;
|
||||
double startAngle;
|
||||
double endAngle;
|
||||
double sweepAngle;
|
||||
Color selectionColor;
|
||||
Color handlerColor;
|
||||
double handlerOutterRadius;
|
||||
bool showRoundedCapInSelection;
|
||||
bool showHandlerOutter;
|
||||
double sliderStrokeWidth;
|
||||
|
||||
Offset initHandler;
|
||||
Offset endHandler;
|
||||
Offset center;
|
||||
double radius;
|
||||
|
||||
SliderPainter({
|
||||
@required this.mode,
|
||||
@required this.startAngle,
|
||||
@required this.endAngle,
|
||||
@required this.sweepAngle,
|
||||
@required this.selectionColor,
|
||||
@required this.handlerColor,
|
||||
@required this.handlerOutterRadius,
|
||||
@required this.showRoundedCapInSelection,
|
||||
@required this.showHandlerOutter,
|
||||
@required this.sliderStrokeWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint progress = _getPaint(color: selectionColor);
|
||||
|
||||
center = Offset(size.width / 2, size.height / 2);
|
||||
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
|
||||
|
||||
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
|
||||
-pi / 2 + startAngle, sweepAngle, false, progress);
|
||||
|
||||
Paint handler = _getPaint(color: handlerColor, style: PaintingStyle.fill);
|
||||
Paint handlerOutter = _getPaint(color: handlerColor, width: 2.0);
|
||||
|
||||
// draw handlers
|
||||
if (mode == CircularSliderMode.doubleHandler) {
|
||||
initHandler = radiansToCoordinates(center, -pi / 2 + startAngle, radius);
|
||||
canvas.drawCircle(initHandler, 8.0, handler);
|
||||
canvas.drawCircle(initHandler, handlerOutterRadius, handlerOutter);
|
||||
}
|
||||
|
||||
endHandler = radiansToCoordinates(center, -pi / 2 + endAngle, radius);
|
||||
canvas.drawCircle(endHandler, 8.0, handler);
|
||||
if (showHandlerOutter) {
|
||||
canvas.drawCircle(endHandler, handlerOutterRadius, handlerOutter);
|
||||
}
|
||||
}
|
||||
|
||||
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
|
||||
Paint()
|
||||
..color = color
|
||||
..strokeCap =
|
||||
showRoundedCapInSelection ? StrokeCap.round : StrokeCap.butt
|
||||
..style = style ?? PaintingStyle.stroke
|
||||
..strokeWidth = width ?? sliderStrokeWidth;
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
75
lib/plugins/circular_slider/utils.dart
Normal file
75
lib/plugins/circular_slider/utils.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
double percentageToRadians(double percentage) => ((2 * pi * percentage) / 100);
|
||||
|
||||
double radiansToPercentage(double radians) {
|
||||
var normalized = radians < 0 ? -radians : 2 * pi - radians;
|
||||
var percentage = ((100 * normalized) / (2 * pi));
|
||||
// TODO we have an inconsistency of pi/2 in terms of percentage and radians
|
||||
return (percentage + 25) % 100;
|
||||
}
|
||||
|
||||
double coordinatesToRadians(Offset center, Offset coords) {
|
||||
var a = coords.dx - center.dx;
|
||||
var b = center.dy - coords.dy;
|
||||
return atan2(b, a);
|
||||
}
|
||||
|
||||
Offset radiansToCoordinates(Offset center, double radians, double radius) {
|
||||
var dx = center.dx + radius * cos(radians);
|
||||
var dy = center.dy + radius * sin(radians);
|
||||
return Offset(dx, dy);
|
||||
}
|
||||
|
||||
double valueToPercentage(int time, int intervals) => (time / intervals) * 100;
|
||||
|
||||
int percentageToValue(double percentage, int intervals) =>
|
||||
((percentage * intervals) / 100).round();
|
||||
|
||||
bool isPointInsideCircle(Offset point, Offset center, double rradius) {
|
||||
var radius = rradius * 1.2;
|
||||
return point.dx < (center.dx + radius) &&
|
||||
point.dx > (center.dx - radius) &&
|
||||
point.dy < (center.dy + radius) &&
|
||||
point.dy > (center.dy - radius);
|
||||
}
|
||||
|
||||
bool isPointAlongCircle(Offset point, Offset center, double radius) {
|
||||
// distance is root(sqr(x2 - x1) + sqr(y2 - y1))
|
||||
// i.e., (7,8) and (3,2) -> 7.21
|
||||
var d1 = pow(point.dx - center.dx, 2);
|
||||
var d2 = pow(point.dy - center.dy, 2);
|
||||
var distance = sqrt(d1 + d2);
|
||||
return (distance - radius).abs() < 10;
|
||||
}
|
||||
|
||||
double getSweepAngle(double init, double end) {
|
||||
if (end > init) {
|
||||
return end - init;
|
||||
}
|
||||
return (100 - init + end).abs();
|
||||
}
|
||||
|
||||
List<Offset> getSectionsCoordinatesInCircle(
|
||||
Offset center, double radius, int sections) {
|
||||
var intervalAngle = (pi * 2) / sections;
|
||||
return List<int>.generate(sections, (int index) => index).map((i) {
|
||||
var radians = (pi / 2) + (intervalAngle * i);
|
||||
return radiansToCoordinates(center, radians, radius);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
bool isAngleInsideRadiansSelection(double angle, double start, double sweep) {
|
||||
var normalized = angle > pi / 2 ? 5 * pi / 2 - angle : pi / 2 - angle;
|
||||
var end = (start + sweep) % (2 * pi);
|
||||
return end > start
|
||||
? normalized > start && normalized < end
|
||||
: normalized > start || normalized < end;
|
||||
}
|
||||
|
||||
// this is not 100% accurate but it works
|
||||
// we just want to see if a value changed drastically its value
|
||||
bool radiansWasModuloed(double current, double previous) {
|
||||
return (previous - current).abs() > (3 * pi / 2);
|
||||
}
|
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;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
part of '../main.dart';
|
||||
part of 'main.dart';
|
||||
|
||||
class HomeAssistantUI {
|
||||
List<HAView> views;
|
@ -1,16 +0,0 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class Sizes {
|
||||
static const rightWidgetPadding = 10.0;
|
||||
static const leftWidgetPadding = 10.0;
|
||||
static const buttonPadding = 4.0;
|
||||
static const extendedWidgetHeight = 50.0;
|
||||
static const iconSize = 28.0;
|
||||
static const largeIconSize = 46.0;
|
||||
static const stateFontSize = 15.0;
|
||||
static const nameFontSize = 15.0;
|
||||
static const smallFontSize = 14.0;
|
||||
static const largeFontSize = 24.0;
|
||||
static const inputWidth = 160.0;
|
||||
static const rowPadding = 10.0;
|
||||
}
|
@ -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(ShowDialogEvent(
|
||||
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(ShowDialogEvent(
|
||||
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(ShowDialogEvent(
|
||||
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,197 +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 ShowDialogEvent {
|
||||
final String title;
|
||||
final String body;
|
||||
final String positiveText;
|
||||
final String negativeText;
|
||||
final onPositive;
|
||||
final onNegative;
|
||||
|
||||
ShowDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
49
lib/utils/logger.dart
Normal file
49
lib/utils/logger.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user