Compare commits
169 Commits
0.6.3
...
beta/0.7.1
Author | SHA1 | Date | |
---|---|---|---|
3234ffc20c | |||
40d72eb6e1 | |||
ced008a7c1 | |||
9c42ad687d | |||
5cda98da46 | |||
958f545f65 | |||
44165993b4 | |||
283ae6cfd4 | |||
4068b295bd | |||
e36b33dcec | |||
4b12912697 | |||
49a21967cc | |||
cf36406f2a | |||
872ad044f1 | |||
345301c03a | |||
117923413d | |||
24ccbc58c4 | |||
89c91b4b01 | |||
4494da1f4f | |||
c263542c54 | |||
c70f52a73d | |||
423813d6fb | |||
ec6a86f4b0 | |||
64cf18cb23 | |||
e0e064bc67 | |||
5cee6cbd9c | |||
43659b26f7 | |||
98e15ad429 | |||
90728cdf8b | |||
d1ec4f36cc | |||
079070071e | |||
520fd6bc38 | |||
085aead36b | |||
fcbaf298cc | |||
eedc0c9b22 | |||
f70c1e12ff | |||
ec094a4362 | |||
11646c840e | |||
86987c57c9 | |||
e4d6e842f5 | |||
cfe4dd1c59 | |||
3387ab2850 | |||
abd23e27ea | |||
2f110b20bb | |||
f88e6f9b61 | |||
2836973dca | |||
a4477e9f83 | |||
96fa7ece25 | |||
b84caa4cc3 | |||
49c212632e | |||
92165aa7ed | |||
cbbdb754aa | |||
7e3fe0608d | |||
781f39f281 | |||
bfb80f6f8c | |||
801b8f9288 | |||
b988fcfcdd | |||
dff6457cb2 | |||
f50f68f318 | |||
c869ad41d9 | |||
cd41f9a236 | |||
1dbe162bf0 | |||
1a52203bd7 | |||
753df3c724 | |||
dc62a08da3 | |||
0c26aff498 | |||
6323f8f2e6 | |||
885c0b1316 | |||
14958d9165 | |||
bf6a52e0b9 | |||
72aad5cc16 | |||
340e8569cc | |||
8fc7d0b61e | |||
5dcb27ada7 | |||
db1a076132 | |||
6707201e23 | |||
b8b92171a8 | |||
3dd7069292 | |||
7177419472 | |||
c37313cf07 | |||
a65f42d0fd | |||
78dd7df686 | |||
2ea7d9440c | |||
abdcd49368 | |||
6da7a5ab90 | |||
20ffe03139 | |||
a71213c589 | |||
d61103ac42 | |||
298a64b7ae | |||
9e2c673966 | |||
092469d668 | |||
bcf3dab0e2 | |||
7ecfc8a9ff | |||
ecf0a696f7 | |||
dc5db28e01 | |||
555f305c22 | |||
76bf07cfcd | |||
c4663576d1 | |||
a64aa73aae | |||
a3a60dd707 | |||
9c28b0085b | |||
d5baabdd53 | |||
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 | |||
13102a6b04 | |||
57c3083f9f |
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help improve HA Client
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please provide as much information as possible.
|
||||||
|
-->
|
||||||
|
**HA Client version:** <!-- Main app menu => About HA Client -->
|
||||||
|
|
||||||
|
**Home Assistant version:** <!-- 0.94.1 for example -->
|
||||||
|
|
||||||
|
**Device name:** <!-- Pixel 2 for example -->
|
||||||
|
|
||||||
|
**Android version:** <!-- 8.1 for example -->
|
||||||
|
|
||||||
|
**Connection type:** <!-- For example "Local IP" or "Remote UI" or "Own domain"-->
|
||||||
|
|
||||||
|
**Login type:** <!-- For example "HA Login" or "Manual token"-->
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
<!--
|
||||||
|
Describe your issue here
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
<!--
|
||||||
|
Please provide screenshots if it is a UI issue. Also you can attach screenshot from Home Assistant web UI as an expected result
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
<!--
|
||||||
|
Right after issue reproduced go to app menu and tap "Log". Copy log with a "Copy" button in the upper-right corner and post it below
|
||||||
|
-->
|
||||||
|
```
|
||||||
|
[Replace this text with your logs]
|
||||||
|
```
|
12
.github/ISSUE_TEMPLATE/entity-support-request.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/entity-support-request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: Entity support request
|
||||||
|
about: Suggest to add support of any entity type
|
||||||
|
title: ''
|
||||||
|
labels: ENTITY, feature/improvement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Entity type:**
|
||||||
|
|
||||||
|
**Link to documentation:**
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for HA Client if it is not a card or entity support
|
||||||
|
title: ''
|
||||||
|
labels: feature/improvement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
12
.github/ISSUE_TEMPLATE/lovelace-card-support-request.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/lovelace-card-support-request.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: Lovelace Card support request
|
||||||
|
about: Suggest to add any Lovelace card support
|
||||||
|
title: ''
|
||||||
|
labels: CARD, feature/improvement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Card name:**
|
||||||
|
|
||||||
|
**Link to card repository or web page:**
|
11
.github/no-response.yml
vendored
Normal file
11
.github/no-response.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Configuration for probot-no-response - https://github.com/probot/no-response
|
||||||
|
|
||||||
|
# Number of days of inactivity before an Issue is closed for lack of response
|
||||||
|
daysUntilClose: 14
|
||||||
|
# Label requiring a response
|
||||||
|
responseRequiredLabel: more info needed
|
||||||
|
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
|
||||||
|
closeComment: >
|
||||||
|
This issue has been automatically closed because there has been no response
|
||||||
|
to our request for more information from the original author. If the issue still relevant
|
||||||
|
feel free to reopen this issue and add more information, or report a new one.
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -9,6 +9,13 @@ build/
|
|||||||
.flutter-plugins
|
.flutter-plugins
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.theia/
|
||||||
|
.project/
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
flutter_export_environment.sh
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
|
premium_features_manager.class.dart
|
||||||
pubspec.lock
|
pubspec.lock
|
12
.gitpod.dockerfile
Normal file
12
.gitpod.dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM gitpod/workspace-full:latest
|
||||||
|
|
||||||
|
ENV ANDROID_HOME=/workspace/android-sdk \
|
||||||
|
FLUTTER_ROOT=/workspace/flutter \
|
||||||
|
FLUTTER_HOME=/workspace/flutter
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get -y install build-essential libkrb5-dev gcc make gradle openjdk-8-jdk && \
|
||||||
|
apt-get clean && \
|
||||||
|
apt-get -y autoremove
|
27
.gitpod.yml
Normal file
27
.gitpod.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
image:
|
||||||
|
file: .gitpod.dockerfile
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- before: |
|
||||||
|
export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||||
|
mkdir -p /home/gitpod/.android
|
||||||
|
touch /home/gitpod/.android/repositories.cfg
|
||||||
|
init: |
|
||||||
|
echo "Installing Flutter SDK..."
|
||||||
|
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.4-stable.tar.xz && tar -xf flutter_sdk.tar.xz && rm -f flutter_sdk.tar.xz
|
||||||
|
echo "Installing Android SDK..."
|
||||||
|
mkdir -p /workspace/android-sdk && cd /workspace/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip
|
||||||
|
/workspace/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3"
|
||||||
|
echo "Init Flutter..."
|
||||||
|
cd /workspace/ha_client
|
||||||
|
flutter upgrade
|
||||||
|
flutter doctor --android-licenses
|
||||||
|
flutter pub get
|
||||||
|
command: |
|
||||||
|
flutter pub upgrade
|
||||||
|
echo "Ready to go!"
|
||||||
|
flutter doctor
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- Dart-Code.dart-code@3.5.0-beta.1:Wg2nTABftVR/Dry4tqeY1w==
|
||||||
|
- Dart-Code.flutter@3.5.0:/kOacEWdiDRLyN/idUiM4A==
|
13
README.md
13
README.md
@ -1,12 +1,15 @@
|
|||||||
[](https://somegeeky.website/badges/flutter) [](https://somegeeky.website/badges/dart)
|
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||||
# HA Client
|
# HA Client
|
||||||
## Native Android client for Home Assistant
|
## Native Android client for Home Assistant
|
||||||
### With Lovelace UI support
|
### With notifications and Lovelace UI support
|
||||||
|
|
||||||
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
|
||||||
|
|
||||||
Join [Google Group](https://groups.google.com/d/forum/ha-client-alpha-testing) to become an alpha tester
|
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||||
|
|
||||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient) after joining the group
|
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912) or on [Discord server](https://discord.gg/AUzEvwn)
|
||||||
|
|
||||||
Discuss it in [Home Assistant community](https://community.home-assistant.io/t/alpha-testing-ha-client-native-android-client-for-home-assistant/69912)
|
#### Pre-release CI build
|
||||||
|
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
||||||
|
#### Beta CI build
|
||||||
|
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||||
|
1
android/.gitignore
vendored
1
android/.gitignore
vendored
@ -8,3 +8,4 @@
|
|||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
GeneratedPluginRegistrant.java
|
GeneratedPluginRegistrant.java
|
||||||
|
.project/
|
17
android/.project
Normal file
17
android/.project
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>android</name>
|
||||||
|
<comment>Project android created by Buildship.</comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
6
android/app/.classpath
Normal file
6
android/app/.classpath
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||||
|
<classpathentry kind="output" path="bin/default"/>
|
||||||
|
</classpath>
|
23
android/app/.project
Normal file
23
android/app/.project
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>app</name>
|
||||||
|
<comment>Project app created by Buildship.</comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
@ -50,6 +50,14 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
if (!System.getenv()["CI"]) {
|
||||||
|
debug {
|
||||||
|
keyAlias keystoreProperties['debugKeyAlias']
|
||||||
|
keyPassword keystoreProperties['debugKeyPassword']
|
||||||
|
storeFile file(keystoreProperties['debugStoreFile'])
|
||||||
|
storePassword keystoreProperties['debugStorePassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
keyAlias keystoreProperties['keyAlias']
|
keyAlias keystoreProperties['keyAlias']
|
||||||
keyPassword keystoreProperties['keyPassword']
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
@ -14,6 +14,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "441874387819-uqmkibhf361828od1982o2jhl0n3m0ov.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.keyboardcrumbs.haclient",
|
||||||
|
"certificate_hash": "bebe4d970fbebf0bff2c93244fdc7fcbcefb3470"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": "441874387819-5q7vmimci4s2jl3v0ncugv1ocp4m48nb.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.keyboardcrumbs.haclient",
|
||||||
|
"certificate_hash": "0ea12348468be44bc2aa5792ee7e8924c633da81"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": "441874387819-joi8plo5345ebt8i1dug27u2aenv5tg7.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.keyboardcrumbs.haclient",
|
||||||
|
"certificate_hash": "fcbc805d965ccf6a4d5417398d191edc9c9890b0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
@ -25,15 +49,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"services": {
|
"services": {
|
||||||
"analytics_service": {
|
|
||||||
"status": 1
|
|
||||||
},
|
|
||||||
"appinvite_service": {
|
"appinvite_service": {
|
||||||
"status": 1,
|
"other_platform_oauth_client": [
|
||||||
"other_platform_oauth_client": []
|
{
|
||||||
},
|
"client_id": "441874387819-id0hqsfprj3b5kc312faqv3lmdfpm7l8.apps.googleusercontent.com",
|
||||||
"ads_service": {
|
"client_type": 3
|
||||||
"status": 2
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.keyboardcrumbs.hassclient">
|
package="com.keyboardcrumbs.hassclient">
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
flutter needs it to communicate with the running application
|
flutter needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -14,7 +21,7 @@
|
|||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name=".Application"
|
||||||
android:label="HA Client"
|
android:label="HA Client"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
@ -45,6 +52,29 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data
|
||||||
|
android:scheme="haclient"
|
||||||
|
android:host="auth" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
android:exported="false"/>
|
||||||
|
<receiver
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
|
||||||
|
android:exported="false"/>
|
||||||
|
<receiver
|
||||||
|
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
|
||||||
|
android:enabled="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
import be.tramckrijte.workmanager.WorkmanagerPlugin;
|
||||||
|
|
||||||
|
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
WorkmanagerPlugin.setPluginRegistrantCallback(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerWith(PluginRegistry registry) {
|
||||||
|
GeneratedPluginRegistrant.registerWith(registry);
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,10 @@ package com.keyboardcrumbs.hassclient;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import io.flutter.app.FlutterActivity;
|
import io.flutter.app.FlutterActivity;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends FlutterShareReceiverActivity {
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
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!
|
||||||
|
```
|
Binary file not shown.
BIN
fonts/materialdesignicons-webfont-4.5.95.ttf
Normal file
BIN
fonts/materialdesignicons-webfont-4.5.95.ttf
Normal file
Binary file not shown.
@ -15,6 +15,10 @@ class HACard {
|
|||||||
List states;
|
List states;
|
||||||
List conditions;
|
List conditions;
|
||||||
String content;
|
String content;
|
||||||
|
String unit;
|
||||||
|
int min;
|
||||||
|
int max;
|
||||||
|
Map severity;
|
||||||
|
|
||||||
HACard({
|
HACard({
|
||||||
this.name,
|
this.name,
|
||||||
@ -27,13 +31,21 @@ class HACard {
|
|||||||
this.showEmpty: true,
|
this.showEmpty: true,
|
||||||
this.content,
|
this.content,
|
||||||
this.states,
|
this.states,
|
||||||
this.conditions,
|
this.conditions: const [],
|
||||||
|
this.unit,
|
||||||
|
this.min,
|
||||||
|
this.max,
|
||||||
|
this.severity,
|
||||||
@required this.type
|
@required this.type
|
||||||
});
|
}) {
|
||||||
|
if (this.columnsCount <= 0) {
|
||||||
|
this.columnsCount = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<EntityWrapper> getEntitiesToShow() {
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
return entities.where((entityWrapper) {
|
return entities.where((entityWrapper) {
|
||||||
if (entityWrapper.entity.isHidden) {
|
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (stateFilter.isNotEmpty) {
|
if (stateFilter.isNotEmpty) {
|
@ -25,14 +25,15 @@ class CardWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (card.conditions.isNotEmpty) {
|
if (card.conditions.isNotEmpty) {
|
||||||
bool showCardByConditions = false;
|
bool showCardByConditions = true;
|
||||||
for (var condition in card.conditions) {
|
for (var condition in card.conditions) {
|
||||||
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||||
if (conditionEntity != null &&
|
if (conditionEntity != null &&
|
||||||
(condition['state'] != null && conditionEntity.state == condition['state']) ||
|
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||||
(condition['state_not'] != null && conditionEntity.state != condition['state_not'])
|
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||||
) {
|
) {
|
||||||
showCardByConditions = true;
|
showCardByConditions = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!showCardByConditions) {
|
if (!showCardByConditions) {
|
||||||
@ -42,31 +43,39 @@ class CardWidget extends StatelessWidget {
|
|||||||
|
|
||||||
switch (card.type) {
|
switch (card.type) {
|
||||||
|
|
||||||
case CardType.entities: {
|
case CardType.ENTITIES: {
|
||||||
return _buildEntitiesCard(context);
|
return _buildEntitiesCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.glance: {
|
case CardType.GLANCE: {
|
||||||
return _buildGlanceCard(context);
|
return _buildGlanceCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.mediaControl: {
|
case CardType.MEDIA_CONTROL: {
|
||||||
return _buildMediaControlsCard(context);
|
return _buildMediaControlsCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.entityButton: {
|
case CardType.ENTITY_BUTTON: {
|
||||||
return _buildEntityButtonCard(context);
|
return _buildEntityButtonCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.markdown: {
|
case CardType.GAUGE: {
|
||||||
|
return _buildGaugeCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* case CardType.LIGHT: {
|
||||||
|
return _buildLightCard(context);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
case CardType.MARKDOWN: {
|
||||||
return _buildMarkdownCard(context);
|
return _buildMarkdownCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.alarmPanel: {
|
case CardType.ALARM_PANEL: {
|
||||||
return _buildAlarmPanelCard(context);
|
return _buildAlarmPanelCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.horizontalStack: {
|
case CardType.HORIZONTAL_STACK: {
|
||||||
if (card.childCards.isNotEmpty) {
|
if (card.childCards.isNotEmpty) {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
card.childCards.forEach((card) {
|
card.childCards.forEach((card) {
|
||||||
@ -89,7 +98,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.verticalStack: {
|
case CardType.VERTICAL_STACK: {
|
||||||
if (card.childCards.isNotEmpty) {
|
if (card.childCards.isNotEmpty) {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
card.childCards.forEach((card) {
|
card.childCards.forEach((card) {
|
||||||
@ -123,9 +132,8 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(name: card.name));
|
body.add(CardHeader(name: card.name));
|
||||||
entitiesToShow.forEach((EntityWrapper entity) {
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
if (!entity.entity.isHidden) {
|
|
||||||
body.add(
|
body.add(
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
|
||||||
@ -135,7 +143,6 @@ class CardWidget extends StatelessWidget {
|
|||||||
child: entity.entity.buildDefaultWidget(context)
|
child: entity.entity.buildDefaultWidget(context)
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -150,7 +157,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(name: card.name));
|
body.add(CardHeader(name: card.name));
|
||||||
body.add(MarkdownBody(data: card.content));
|
body.add(MarkdownBody(data: card.content));
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -162,7 +169,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildAlarmPanelCard(BuildContext context) {
|
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(
|
body.add(CardHeader(
|
||||||
name: card.name ?? "",
|
name: card.name ?? "",
|
||||||
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -183,7 +190,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:dots-vertical")),
|
"mdi:dots-vertical")),
|
||||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity))
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@ -214,18 +221,26 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
List<Widget> rows = [];
|
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;
|
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||||
|
|
||||||
|
rows.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: 1,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
double buttonWidth = constraints.maxWidth / columnsCount;
|
||||||
entitiesToShow.forEach((EntityWrapper entity) {
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
result.add(
|
buttons.add(
|
||||||
FractionallySizedBox(
|
SizedBox(
|
||||||
widthFactor: 1/columnsCount,
|
width: buttonWidth,
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
entityWrapper: entity,
|
entityWrapper: entity,
|
||||||
child: GlanceEntityContainer(
|
child: GlanceCardEntityContainer(
|
||||||
showName: card.showName,
|
showName: card.showName,
|
||||||
showState: card.showState,
|
showState: card.showState,
|
||||||
),
|
),
|
||||||
@ -234,19 +249,23 @@ class CardWidget extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
rows.add(
|
return Wrap(
|
||||||
Padding(
|
//spacing: 5.0,
|
||||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
|
//alignment: WrapAlignment.spaceEvenly,
|
||||||
child: Wrap(
|
runSpacing: Sizes.doubleRowPadding,
|
||||||
//alignment: WrapAlignment.spaceAround,
|
children: buttons,
|
||||||
runSpacing: Sizes.rowPadding*2,
|
);
|
||||||
children: result,
|
}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: rows
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +285,41 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Card(
|
return Card(
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
entityWrapper: card.linkedEntityWrapper,
|
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
|
handleTap: true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -274,7 +327,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildUnsupportedCard(BuildContext context) {
|
Widget _buildUnsupportedCard(BuildContext context) {
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(name: card.name ?? ""));
|
body.add(CardHeader(name: card.name ?? ""));
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (card.linkedEntityWrapper != null) {
|
if (card.linkedEntityWrapper != null) {
|
||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
@ -1,12 +1,12 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class CardHeaderWidget extends StatelessWidget {
|
class CardHeader extends StatelessWidget {
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final Widget trailing;
|
final Widget trailing;
|
||||||
final Widget subtitle;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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,
|
Key key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -15,25 +15,26 @@ class ButtonEntityContainer extends StatelessWidget {
|
|||||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||||
return Container(width: 0.0, height: 0.0,);
|
return Container(width: 0.0, height: 0.0,);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: 1,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FractionallySizedBox(
|
LayoutBuilder(
|
||||||
widthFactor: 0.4,
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
child: FittedBox(
|
return EntityIcon(
|
||||||
fit: BoxFit.fitHeight,
|
|
||||||
child: EntityIcon(
|
|
||||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||||
size: Sizes.iconSize,
|
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 showName;
|
||||||
final bool showState;
|
final bool showState;
|
||||||
@ -9,7 +9,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
final double nameFontSize;
|
final double nameFontSize;
|
||||||
final bool wordsWrapInName;
|
final bool wordsWrapInName;
|
||||||
|
|
||||||
GlanceEntityContainer({
|
GlanceCardEntityContainer({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.showName,
|
@required this.showName,
|
||||||
@required this.showState,
|
@required this.showState,
|
||||||
@ -54,15 +54,10 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: InkResponse(
|
child: InkResponse(
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
//mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
//crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: result,
|
children: result,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,11 @@ class EntityState {
|
|||||||
static const ok = 'ok';
|
static const ok = 'ok';
|
||||||
static const problem = 'problem';
|
static const problem = 'problem';
|
||||||
static const active = 'active';
|
static const active = 'active';
|
||||||
|
static const cleaning = 'cleaning';
|
||||||
|
static const docked = 'docked';
|
||||||
|
static const returning = 'returning';
|
||||||
|
static const error = 'error';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntityUIAction {
|
class EntityUIAction {
|
||||||
@ -77,23 +82,44 @@ class EntityUIAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CardType {
|
class CardType {
|
||||||
static const horizontalStack = "horizontal-stack";
|
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||||
static const verticalStack = "vertical-stack";
|
static const VERTICAL_STACK = "vertical-stack";
|
||||||
static const entities = "entities";
|
static const ENTITIES = "entities";
|
||||||
static const glance = "glance";
|
static const GLANCE = "glance";
|
||||||
static const mediaControl = "media-control";
|
static const MEDIA_CONTROL = "media-control";
|
||||||
static const weatherForecast = "weather-forecast";
|
static const WEATHER_FORECAST = "weather-forecast";
|
||||||
static const thermostat = "thermostat";
|
static const THERMOSTAT = "thermostat";
|
||||||
static const sensor = "sensor";
|
static const SENSOR = "sensor";
|
||||||
static const plantStatus = "plant-status";
|
static const PLANT_STATUS = "plant-status";
|
||||||
static const pictureEntity = "picture-entity";
|
static const PICTURE_ENTITY = "picture-entity";
|
||||||
static const pictureElements = "picture-elements";
|
static const PICTURE_ELEMENTS = "picture-elements";
|
||||||
static const picture = "picture";
|
static const PICTURE = "picture";
|
||||||
static const map = "map";
|
static const MAP = "map";
|
||||||
static const iframe = "iframe";
|
static const IFRAME = "iframe";
|
||||||
static const gauge = "gauge";
|
static const GAUGE = "gauge";
|
||||||
static const entityButton = "entity-button";
|
static const ENTITY_BUTTON = "entity-button";
|
||||||
static const conditional = "conditional";
|
static const CONDITIONAL = "conditional";
|
||||||
static const alarmPanel = "alarm-panel";
|
static const ALARM_PANEL = "alarm-panel";
|
||||||
static const markdown = "markdown";
|
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;
|
||||||
|
static const minViewColumnWidth = 350;
|
||||||
|
static const entityPageMaxWidth = 400.0;
|
||||||
|
static const mainPageScreenSeparatorWidth = 5.0;
|
||||||
|
static const tabletMinWidth = minViewColumnWidth + entityPageMaxWidth + 5;
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class BadgeWidget extends StatelessWidget {
|
class BadgeWidget extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
@ -57,7 +57,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||||
stateFontSize = 8.0;
|
stateFontSize = 8.0;
|
||||||
}
|
}
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||||
badgeIcon = Center(
|
badgeIcon = Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.entity.displayState}",
|
"${entityModel.entityWrapper.entity.displayState}",
|
||||||
@ -140,6 +140,6 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity)));
|
eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class CameraStreamView extends StatefulWidget {
|
class CameraStreamView extends StatefulWidget {
|
||||||
|
|
||||||
@ -20,21 +20,9 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
String streamUrl = "";
|
String streamUrl = "";
|
||||||
|
|
||||||
launchStream() {
|
launchStream() {
|
||||||
Navigator.push(
|
Launcher.launchURLInCustomTab(
|
||||||
context,
|
context: context,
|
||||||
MaterialPageRoute(
|
url: streamUrl
|
||||||
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}"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +35,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
.entity;
|
.entity;
|
||||||
started = true;
|
started = true;
|
||||||
}
|
}
|
||||||
streamUrl = '${Connection().httpWebHost}/api/camera_proxy_stream/${_entity
|
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||||
.entityId}?token=${_entity.attributes['access_token']}';
|
.entityId}?token=${_entity.attributes['access_token']}';
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
@ -409,52 +409,3 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ModeSelectorWidget extends StatelessWidget {
|
class ModeSelectorWidget extends StatelessWidget {
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class ModeSwitchWidget extends StatelessWidget {
|
class ModeSwitchWidget extends StatelessWidget {
|
||||||
|
|
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(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -34,18 +34,7 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return InkWell(
|
Widget result = Row(
|
||||||
onLongPress: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleHold();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleTap();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
EntityIcon(),
|
EntityIcon(),
|
||||||
@ -59,7 +48,23 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -211,31 +211,6 @@ class Entity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildEntityPageWidget(BuildContext context) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: EntityWrapper(entity: this),
|
|
||||||
child: EntityPageContainer(children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
|
||||||
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
|
|
||||||
),
|
|
||||||
LastUpdatedWidget(),
|
|
||||||
Divider(),
|
|
||||||
_buildAdditionalControlsForPage(context),
|
|
||||||
Divider(),
|
|
||||||
buildHistoryWidget(),
|
|
||||||
EntityAttributesList()
|
|
||||||
]),
|
|
||||||
handleTap: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildHistoryWidget() {
|
|
||||||
return EntityHistoryWidget(
|
|
||||||
config: historyConfig,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildBadgeWidget(BuildContext context) {
|
Widget buildBadgeWidget(BuildContext context) {
|
||||||
return EntityModel(
|
return EntityModel(
|
||||||
entityWrapper: EntityWrapper(entity: this),
|
entityWrapper: EntityWrapper(entity: this),
|
||||||
|
@ -14,9 +14,12 @@ class EntityColor {
|
|||||||
"auto": Colors.amber,
|
"auto": Colors.amber,
|
||||||
EntityState.active: Colors.amber,
|
EntityState.active: Colors.amber,
|
||||||
EntityState.playing: Colors.amber,
|
EntityState.playing: Colors.amber,
|
||||||
|
EntityState.paused: Colors.amber,
|
||||||
"above_horizon": Colors.amber,
|
"above_horizon": Colors.amber,
|
||||||
EntityState.home: Colors.amber,
|
EntityState.home: Colors.amber,
|
||||||
EntityState.open: Colors.amber,
|
EntityState.open: Colors.amber,
|
||||||
|
EntityState.cleaning: Colors.amber,
|
||||||
|
EntityState.returning: Colors.amber,
|
||||||
EntityState.off: defaultStateColor,
|
EntityState.off: defaultStateColor,
|
||||||
EntityState.closed: defaultStateColor,
|
EntityState.closed: defaultStateColor,
|
||||||
"below_horizon": defaultStateColor,
|
"below_horizon": defaultStateColor,
|
70
lib/entities/entity_page_layout.widget.dart
Normal file
70
lib/entities/entity_page_layout.widget.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class EntityPageLayout extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool showClose;
|
||||||
|
final Entity entity;
|
||||||
|
|
||||||
|
EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: EntityWrapper(entity: entity),
|
||||||
|
child: ListView(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
children: <Widget>[
|
||||||
|
showClose ?
|
||||||
|
Container(
|
||||||
|
color: Colors.blue[300],
|
||||||
|
height: 36,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 8),
|
||||||
|
child: Text(
|
||||||
|
entity.displayName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 22
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
color: Colors.white,
|
||||||
|
iconSize: 30.0,
|
||||||
|
onPressed: () {
|
||||||
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
) :
|
||||||
|
Container(height: 0, width: 0,),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
|
||||||
|
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),
|
||||||
|
),
|
||||||
|
LastUpdatedWidget(),
|
||||||
|
Divider(),
|
||||||
|
entity._buildAdditionalControlsForPage(context),
|
||||||
|
Divider(),
|
||||||
|
SpoilerCard(
|
||||||
|
title: "State history",
|
||||||
|
body: EntityHistoryWidget(),
|
||||||
|
),
|
||||||
|
SpoilerCard(
|
||||||
|
title: "Attributes",
|
||||||
|
body: EntityAttributesList(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ class EntityWrapper {
|
|||||||
|
|
||||||
String displayName;
|
String displayName;
|
||||||
String icon;
|
String icon;
|
||||||
|
String unitOfMeasurement;
|
||||||
String entityPicture;
|
String entityPicture;
|
||||||
EntityUIAction uiAction;
|
EntityUIAction uiAction;
|
||||||
Entity entity;
|
Entity entity;
|
||||||
@ -24,6 +25,7 @@ class EntityWrapper {
|
|||||||
if (uiAction == null) {
|
if (uiAction == null) {
|
||||||
uiAction = EntityUIAction();
|
uiAction = EntityUIAction();
|
||||||
}
|
}
|
||||||
|
unitOfMeasurement = entity.unitOfMeasurement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +53,7 @@ class EntityWrapper {
|
|||||||
|
|
||||||
case EntityUIAction.moreInfo: {
|
case EntityUIAction.moreInfo: {
|
||||||
eventBus.fire(
|
eventBus.fire(
|
||||||
new ShowEntityPageEvent(entity));
|
new ShowEntityPageEvent(entity: entity));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +62,7 @@ class EntityWrapper {
|
|||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Logger.w("Local urls is not supported yet");
|
Logger.w("Local urls is not supported yet");
|
||||||
} else {
|
} else {
|
||||||
HAUtils.launchURL(uiAction.tapService);
|
Launcher.launchURL(uiAction.tapService);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -91,7 +93,7 @@ class EntityWrapper {
|
|||||||
|
|
||||||
case EntityUIAction.moreInfo: {
|
case EntityUIAction.moreInfo: {
|
||||||
eventBus.fire(
|
eventBus.fire(
|
||||||
new ShowEntityPageEvent(entity));
|
new ShowEntityPageEvent(entity: entity));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +102,7 @@ class EntityWrapper {
|
|||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Logger.w("Local urls is not supported yet");
|
Logger.w("Local urls is not supported yet");
|
||||||
} else {
|
} else {
|
||||||
HAUtils.launchURL(uiAction.holdService);
|
Launcher.launchURL(uiAction.holdService);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class FlatServiceButton extends StatelessWidget {
|
class FlatServiceButton extends StatelessWidget {
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../../../main.dart';
|
||||||
|
|
||||||
class LightColorPicker extends StatefulWidget {
|
class LightColorPicker extends StatefulWidget {
|
||||||
|
|
||||||
@ -27,7 +27,6 @@ class LightColorPickerState extends State<LightColorPicker> {
|
|||||||
List<Widget> colorRows = [];
|
List<Widget> colorRows = [];
|
||||||
Border border;
|
Border border;
|
||||||
bool isSomethingSelected = false;
|
bool isSomethingSelected = false;
|
||||||
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
|
|
||||||
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
|
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
|
||||||
List<Widget> rowChildren = [];
|
List<Widget> rowChildren = [];
|
||||||
//Logger.d("$saturation");
|
//Logger.d("$saturation");
|
@ -100,7 +100,19 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBrightnessControl(LightEntity entity) {
|
Widget _buildBrightnessControl(LightEntity entity) {
|
||||||
if ((entity.supportBrightness) && (_tmpBrightness != null)) {
|
if (entity.supportBrightness) {
|
||||||
|
double val;
|
||||||
|
if (_tmpBrightness != null) {
|
||||||
|
if (_tmpBrightness > 255) {
|
||||||
|
val = 255;
|
||||||
|
} else if (_tmpBrightness < 1) {
|
||||||
|
val = 1;
|
||||||
|
} else {
|
||||||
|
val = _tmpBrightness.toDouble();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val = 1;
|
||||||
|
}
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -111,7 +123,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
min: 1.0,
|
min: 1.0,
|
||||||
max: 255.0,
|
max: 255.0,
|
||||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||||
value: _tmpBrightness == null ? 1.0 : _tmpBrightness.toDouble(),
|
value: val,
|
||||||
leading: Icon(Icons.brightness_5),
|
leading: Icon(Icons.brightness_5),
|
||||||
title: "Brightness",
|
title: "Brightness",
|
||||||
);
|
);
|
||||||
@ -143,10 +155,22 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildColorTempControl(LightEntity entity) {
|
Widget _buildColorTempControl(LightEntity entity) {
|
||||||
if (entity.supportColorTemp) {
|
if (entity.supportColorTemp) {
|
||||||
|
double val;
|
||||||
|
if (_tmpColorTemp != null) {
|
||||||
|
if (_tmpColorTemp > entity.maxMireds) {
|
||||||
|
val = entity.maxMireds;
|
||||||
|
} else if (_tmpColorTemp < entity.minMireds) {
|
||||||
|
val = entity.minMireds;
|
||||||
|
} else {
|
||||||
|
val = _tmpColorTemp.toDouble();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val = entity.minMireds;
|
||||||
|
}
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
title: "Color temperature",
|
title: "Color temperature",
|
||||||
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
||||||
value: _tmpColorTemp == null ? entity.maxMireds : _tmpColorTemp.toDouble(),
|
value: val,
|
||||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||||
max: entity.maxMireds,
|
max: entity.maxMireds,
|
||||||
min: entity.minMireds,
|
min: entity.minMireds,
|
||||||
@ -203,10 +227,16 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
|
|
||||||
Widget _buildEffectControl(LightEntity entity) {
|
Widget _buildEffectControl(LightEntity entity) {
|
||||||
if ((entity.supportEffect) && (entity.effectList != null)) {
|
if ((entity.supportEffect) && (entity.effectList != null)) {
|
||||||
|
Logger.d("[LIGHT] entity effects: ${entity.effectList}");
|
||||||
|
Logger.d("[LIGHT] current effect: $_tmpEffect");
|
||||||
|
List<String> list = List.from(entity.effectList);
|
||||||
|
if (_tmpEffect!= null && !list.contains(_tmpEffect)) {
|
||||||
|
list.insert(0, _tmpEffect);
|
||||||
|
}
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (effect) => _setEffect(entity, effect),
|
onChange: (effect) => _setEffect(entity, effect),
|
||||||
caption: "Effect",
|
caption: "Effect",
|
||||||
options: entity.effectList,
|
options: list,
|
||||||
value: _tmpEffect
|
value: _tmpEffect
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -74,10 +74,37 @@ class MediaPlayerEntity extends Entity {
|
|||||||
|
|
||||||
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
|
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
|
||||||
List<String> get sourceList => getStringListAttributeValue("source_list");
|
List<String> get sourceList => getStringListAttributeValue("source_list");
|
||||||
|
DateTime get positionLastUpdated => DateTime.tryParse("${attributes["media_position_updated_at"]}")?.toLocal();
|
||||||
|
int get durationSeconds => _getIntAttributeValue("media_duration");
|
||||||
|
int get positionSeconds => _getIntAttributeValue("media_position");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
return MediaPlayerControls();
|
return MediaPlayerControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool canCalculateActualPosition() {
|
||||||
|
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null && durationSeconds >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double getActualPosition() {
|
||||||
|
double result = 0;
|
||||||
|
if (canCalculateActualPosition()) {
|
||||||
|
Duration durationD;
|
||||||
|
Duration positionD;
|
||||||
|
durationD = Duration(seconds: durationSeconds);
|
||||||
|
positionD = Duration(
|
||||||
|
seconds: positionSeconds);
|
||||||
|
result = positionD.inSeconds.toDouble();
|
||||||
|
int differenceInSeconds = DateTime
|
||||||
|
.now()
|
||||||
|
.difference(positionLastUpdated)
|
||||||
|
.inSeconds;
|
||||||
|
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class MediaPlayerProgressBar extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_MediaPlayerProgressBarState createState() => _MediaPlayerProgressBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
|
||||||
|
|
||||||
|
Timer _timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||||
|
setState(() {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
double progress;
|
||||||
|
int currentPosition;
|
||||||
|
if (entity.canCalculateActualPosition()) {
|
||||||
|
currentPosition = entity.getActualPosition().toInt();
|
||||||
|
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
|
||||||
|
} else {
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class MediaPlayerSeekBar extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_MediaPlayerSeekBarState createState() => _MediaPlayerSeekBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
||||||
|
|
||||||
|
Timer _timer;
|
||||||
|
bool _seekStarted = false;
|
||||||
|
bool _changedHere = false;
|
||||||
|
double _currentPosition = 0;
|
||||||
|
int _savedPosition = 0;
|
||||||
|
|
||||||
|
final TextStyle _seekTextStyle = TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
color: Colors.blue,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||||
|
if (!_seekStarted && !_changedHere) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
|
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
||||||
|
|
||||||
|
if (entity.canCalculateActualPosition() && entity.state != EntityState.idle) {
|
||||||
|
if (HomeAssistant().sendToPlayerId == entity.entityId && HomeAssistant().savedPlayerPosition != null) {
|
||||||
|
_savedPosition = HomeAssistant().savedPlayerPosition;
|
||||||
|
HomeAssistant().savedPlayerPosition = null;
|
||||||
|
HomeAssistant().sendToPlayerId = null;
|
||||||
|
}
|
||||||
|
if (entity.state == EntityState.playing && !_seekStarted &&
|
||||||
|
!_changedHere) {
|
||||||
|
_currentPosition = entity.getActualPosition();
|
||||||
|
} else if (entity.state == EntityState.paused) {
|
||||||
|
_currentPosition = entity.positionSeconds.toDouble();
|
||||||
|
} else if (_changedHere) {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
if (_savedPosition > 0) {
|
||||||
|
buttons.add(
|
||||||
|
RaisedButton(
|
||||||
|
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
|
||||||
|
color: Colors.orange,
|
||||||
|
focusColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
eventBus.fire(ServiceCallEvent(
|
||||||
|
"media_player",
|
||||||
|
"media_seek",
|
||||||
|
"${entity.entityId}",
|
||||||
|
{"seek_position": _savedPosition}
|
||||||
|
));
|
||||||
|
setState(() {
|
||||||
|
_savedPosition = 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 20, Sizes.rightWidgetPadding, 0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("00:00"),
|
||||||
|
Expanded(
|
||||||
|
child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
|
||||||
|
),
|
||||||
|
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: 10,),
|
||||||
|
Slider(
|
||||||
|
min: 0,
|
||||||
|
activeColor: Colors.amber,
|
||||||
|
inactiveColor: Colors.black26,
|
||||||
|
max: entity.durationSeconds.toDouble(),
|
||||||
|
value: _currentPosition,
|
||||||
|
onChangeStart: (val) {
|
||||||
|
_seekStarted = true;
|
||||||
|
},
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() {
|
||||||
|
_currentPosition = val;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (val) {
|
||||||
|
_seekStarted = false;
|
||||||
|
Timer(Duration(milliseconds: 500), () {
|
||||||
|
if (!_seekStarted) {
|
||||||
|
eventBus.fire(ServiceCallEvent(
|
||||||
|
"media_player",
|
||||||
|
"media_seek",
|
||||||
|
"${entity.entityId}",
|
||||||
|
{"seek_position": val}
|
||||||
|
));
|
||||||
|
setState(() {
|
||||||
|
_changedHere = true;
|
||||||
|
_currentPosition = val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ButtonBar(
|
||||||
|
children: buttons,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -26,7 +26,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
right: 0.0,
|
right: 0.0,
|
||||||
child: MediaPlayerProgressWidget()
|
child: MediaPlayerProgressBar()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -229,7 +229,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:dots-vertical")),
|
"mdi:dots-vertical")),
|
||||||
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity))
|
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: entity))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
|
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
|
||||||
@ -305,6 +305,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
|
||||||
|
if (entity.supportSeek) {
|
||||||
|
children.add(MediaPlayerSeekBar());
|
||||||
|
} else {
|
||||||
|
children.add(MediaPlayerProgressBar());
|
||||||
|
}
|
||||||
Widget muteWidget;
|
Widget muteWidget;
|
||||||
Widget volumeStepWidget;
|
Widget volumeStepWidget;
|
||||||
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
|
||||||
@ -398,69 +403,47 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (entity.state == EntityState.playing || entity.state == EntityState.paused) {
|
||||||
|
children.add(
|
||||||
|
ButtonBar(
|
||||||
|
children: <Widget>[
|
||||||
|
RaisedButton(
|
||||||
|
child: Text("Duplicate to"),
|
||||||
|
color: Colors.blue,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () => _duplicateTo(entity),
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text("Switch to"),
|
||||||
|
color: Colors.blue,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () => _switchTo(entity),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
void _duplicateTo(entity) {
|
||||||
|
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
|
||||||
class MediaPlayerProgressWidget extends StatefulWidget {
|
if (MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
|
||||||
@override
|
Navigator.of(context).popAndPushNamed("/play-media", arguments: {"url": entity.attributes["media_content_id"], "type": entity.attributes["media_content_type"]});
|
||||||
_MediaPlayerProgressWidgetState createState() => _MediaPlayerProgressWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MediaPlayerProgressWidgetState extends State<MediaPlayerProgressWidget> {
|
|
||||||
|
|
||||||
Timer _timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
|
||||||
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
|
|
||||||
double progress;
|
|
||||||
try {
|
|
||||||
DateTime lastUpdated = DateTime.parse(
|
|
||||||
entity.attributes["media_position_updated_at"]).toLocal();
|
|
||||||
Duration duration = Duration(seconds: entity._getIntAttributeValue("media_duration") ?? 1);
|
|
||||||
Duration position = Duration(seconds: entity._getIntAttributeValue("media_position") ?? 0);
|
|
||||||
int currentPosition = position.inSeconds;
|
|
||||||
if (entity.state == EntityState.playing) {
|
|
||||||
_timer?.cancel();
|
|
||||||
_timer = Timer(Duration(seconds: 1), () {
|
|
||||||
setState(() {
|
|
||||||
});
|
|
||||||
});
|
|
||||||
int differenceInSeconds = DateTime
|
|
||||||
.now()
|
|
||||||
.difference(lastUpdated)
|
|
||||||
.inSeconds;
|
|
||||||
currentPosition = currentPosition + differenceInSeconds;
|
|
||||||
} else {
|
} else {
|
||||||
_timer?.cancel();
|
Navigator.of(context).pushNamed("/play-media", arguments: {
|
||||||
|
"url": entity.attributes["media_content_id"],
|
||||||
|
"type": entity.attributes["media_content_type"]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
progress = currentPosition / duration.inSeconds;
|
|
||||||
return LinearProgressIndicator(
|
|
||||||
value: progress,
|
|
||||||
backgroundColor: Colors.black45,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_timer?.cancel();
|
|
||||||
progress = 0.0;
|
|
||||||
}
|
|
||||||
return LinearProgressIndicator(
|
|
||||||
value: progress,
|
|
||||||
backgroundColor: Colors.black45,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _switchTo(entity) {
|
||||||
void dispose() {
|
HomeAssistant().sendFromPlayerId = entity.entityId;
|
||||||
_timer?.cancel();
|
_duplicateTo(entity);
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class SimpleEntityState extends StatelessWidget {
|
class SimpleEntityState extends StatelessWidget {
|
||||||
|
|
||||||
@ -7,8 +7,10 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
final String customValue;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -21,18 +23,22 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
state = customValue;
|
state = customValue;
|
||||||
}
|
}
|
||||||
TextStyle textStyle = TextStyle(
|
TextStyle textStyle = TextStyle(
|
||||||
fontSize: Sizes.stateFontSize,
|
fontSize: this.fontSize,
|
||||||
|
fontWeight: FontWeight.normal
|
||||||
);
|
);
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||||
textStyle = textStyle.apply(color: Colors.blue);
|
textStyle = textStyle.apply(color: Colors.blue);
|
||||||
}
|
}
|
||||||
|
if (this.bold) {
|
||||||
|
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||||
|
}
|
||||||
while (state.contains(" ")){
|
while (state.contains(" ")){
|
||||||
state = state.replaceAll(" ", " ");
|
state = state.replaceAll(" ", " ");
|
||||||
}
|
}
|
||||||
Widget result = Padding(
|
Widget result = Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Text(
|
child: Text(
|
||||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
"$state ${entityModel.entityWrapper.unitOfMeasurement}",
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
@ -73,13 +73,7 @@ class _TextInputStateWidgetState extends State<TextInputStateWidget> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
obscureText: entity.isPasswordField,
|
obscureText: entity.isPasswordField,
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _tmpValue)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _tmpValue,
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _tmpValue.length)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_tmpValue = value;
|
_tmpValue = value;
|
||||||
}),
|
}),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
part of '../../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class UniversalSlider extends StatelessWidget {
|
class UniversalSlider extends StatelessWidget {
|
||||||
|
|
98
lib/entities/vacuum/vacuum_entity.class.dart
Normal file
98
lib/entities/vacuum/vacuum_entity.class.dart
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class VacuumEntity extends Entity {
|
||||||
|
|
||||||
|
static const SUPPORT_TURN_ON = 1;
|
||||||
|
static const SUPPORT_TURN_OFF = 2;
|
||||||
|
static const SUPPORT_PAUSE = 4;
|
||||||
|
static const SUPPORT_STOP = 8;
|
||||||
|
static const SUPPORT_RETURN_HOME = 16;
|
||||||
|
static const SUPPORT_FAN_SPEED = 32;
|
||||||
|
static const SUPPORT_BATTERY = 64;
|
||||||
|
static const SUPPORT_STATUS = 128;
|
||||||
|
static const SUPPORT_SEND_COMMAND = 256;
|
||||||
|
static const SUPPORT_LOCATE = 512;
|
||||||
|
static const SUPPORT_CLEAN_SPOT = 1024;
|
||||||
|
static const SUPPORT_MAP = 2048;
|
||||||
|
static const SUPPORT_STATE = 4096;
|
||||||
|
static const SUPPORT_START = 8192;
|
||||||
|
|
||||||
|
VacuumEntity(Map rawData, String webHost) : super(rawData, webHost);
|
||||||
|
|
||||||
|
bool get supportTurnOn => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_TURN_ON) ==
|
||||||
|
VacuumEntity.SUPPORT_TURN_ON);
|
||||||
|
bool get supportTurnOff => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_TURN_OFF) ==
|
||||||
|
VacuumEntity.SUPPORT_TURN_OFF);
|
||||||
|
bool get supportPause => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_PAUSE) ==
|
||||||
|
VacuumEntity.SUPPORT_PAUSE);
|
||||||
|
bool get supportStop => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_STOP) ==
|
||||||
|
VacuumEntity.SUPPORT_STOP);
|
||||||
|
bool get supportReturnHome => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_RETURN_HOME) ==
|
||||||
|
VacuumEntity.SUPPORT_RETURN_HOME);
|
||||||
|
bool get supportFanSpeed => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_FAN_SPEED) ==
|
||||||
|
VacuumEntity.SUPPORT_FAN_SPEED);
|
||||||
|
bool get supportBattery => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_BATTERY) ==
|
||||||
|
VacuumEntity.SUPPORT_BATTERY);
|
||||||
|
bool get supportStatus => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_STATUS) ==
|
||||||
|
VacuumEntity.SUPPORT_STATUS);
|
||||||
|
bool get supportSendCommand => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_SEND_COMMAND) ==
|
||||||
|
VacuumEntity.SUPPORT_SEND_COMMAND);
|
||||||
|
bool get supportLocate => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_LOCATE) ==
|
||||||
|
VacuumEntity.SUPPORT_LOCATE);
|
||||||
|
bool get supportCleanSpot => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_CLEAN_SPOT) ==
|
||||||
|
VacuumEntity.SUPPORT_CLEAN_SPOT);
|
||||||
|
bool get supportMap => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_MAP) ==
|
||||||
|
VacuumEntity.SUPPORT_MAP);
|
||||||
|
bool get supportState => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_STATE) ==
|
||||||
|
VacuumEntity.SUPPORT_STATE);
|
||||||
|
bool get supportStart => ((supportedFeatures &
|
||||||
|
VacuumEntity.SUPPORT_START) ==
|
||||||
|
VacuumEntity.SUPPORT_START);
|
||||||
|
|
||||||
|
List<String> get fanSpeedList => getStringListAttributeValue("fan_speed_list");
|
||||||
|
String get fanSpeed => getAttribute("fan_speed");
|
||||||
|
String get status => getAttribute("status");
|
||||||
|
int get batteryLevel => _getIntAttributeValue("battery_level");
|
||||||
|
String get batteryIcon => getAttribute("battery_icon");
|
||||||
|
double get cleanedArea => _getDoubleAttributeValue("cleaned_area");
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePart(BuildContext context) {
|
||||||
|
if (supportTurnOn || supportTurnOff) {
|
||||||
|
return SwitchStateWidget(
|
||||||
|
domainForService: "vacuum",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SimpleEntityState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildStatePartForPage(BuildContext context) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: EntityWrapper(
|
||||||
|
entity: this
|
||||||
|
),
|
||||||
|
child: VacuumStateButton(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget _buildAdditionalControlsForPage(BuildContext context) {
|
||||||
|
return VacuumControls();
|
||||||
|
}
|
||||||
|
}
|
232
lib/entities/vacuum/widgets/vacuum_controls.dart
Normal file
232
lib/entities/vacuum/widgets/vacuum_controls.dart
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class VacuumControls extends StatelessWidget {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
VacuumEntity entity = EntityModel.of(context).entityWrapper.entity;
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildStatusAndBattery(entity),
|
||||||
|
_buildCommands(entity),
|
||||||
|
_buildFanSpeed(entity),
|
||||||
|
_buildAdditionalInfo(entity)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusAndBattery(VacuumEntity entity) {
|
||||||
|
List<Widget> result = [];
|
||||||
|
if (entity.supportStatus) {
|
||||||
|
result.addAll(
|
||||||
|
<Widget>[
|
||||||
|
Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
|
||||||
|
Container(width: 6,),
|
||||||
|
Expanded(
|
||||||
|
//flex: 1,
|
||||||
|
child: Text(
|
||||||
|
"${entity.status}",
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: true,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: Sizes.stateFontSize,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportBattery && entity.batteryLevel != null) {
|
||||||
|
String iconName = entity.batteryIcon ?? "mdi:battery";
|
||||||
|
int batteryLevel = entity.batteryLevel ?? 100;
|
||||||
|
result.addAll(<Widget>[
|
||||||
|
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
|
||||||
|
Container(width: 6,),
|
||||||
|
Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize))
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isEmpty) {
|
||||||
|
return Container(width: 0, height: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: result,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommands(VacuumEntity entity) {
|
||||||
|
List<Widget> commandButtons = [];
|
||||||
|
double iconSize = 32;
|
||||||
|
if (entity.supportStart) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:play")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "start"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportPause && !entity.supportStart) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:play-pause")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "start_pause"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (entity.supportPause) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:pause")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "pause"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportStop) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:stop")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "stop"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportCleanSpot) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:broom")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "clean_spot"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportLocate) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:map-marker")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "locate"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity.supportReturnHome) {
|
||||||
|
commandButtons.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-map-marker")),
|
||||||
|
iconSize: iconSize,
|
||||||
|
onPressed: () => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "return_to_base"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandButtons.isEmpty) {
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: commandButtons.map((button) => Expanded(
|
||||||
|
child: button,
|
||||||
|
)).toList(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFanSpeed(VacuumEntity entity) {
|
||||||
|
if (entity.supportFanSpeed) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: ModeSelectorWidget(
|
||||||
|
caption: "Fan speed",
|
||||||
|
options: entity.fanSpeedList,
|
||||||
|
value: entity.fanSpeed,
|
||||||
|
onChange: (val) => ConnectionManager().callService(
|
||||||
|
domain: "vacuum",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "set_fan_speed",
|
||||||
|
additionalServiceData: {"fan_speed": val}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAdditionalInfo(VacuumEntity entity) {
|
||||||
|
List<Widget> rows = [];
|
||||||
|
if (entity.cleanedArea != null) {
|
||||||
|
rows.add(
|
||||||
|
Text("Cleaned area: ${entity.cleanedArea}")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.isEmpty) {
|
||||||
|
return Container(width: 0, height: 0,);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: rows,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
40
lib/entities/vacuum/widgets/vacuum_state_button.dart
Normal file
40
lib/entities/vacuum/widgets/vacuum_state_button.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
part of '../../../main.dart';
|
||||||
|
|
||||||
|
class VacuumStateButton extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget result;
|
||||||
|
VacuumEntity entity = EntityModel.of(context).entityWrapper.entity;
|
||||||
|
if (entity.supportTurnOn && entity.supportTurnOff) {
|
||||||
|
result = FlatServiceButton(
|
||||||
|
serviceDomain: "vacuum",
|
||||||
|
serviceName: entity.state == EntityState.on ? "turn_off" : "turn_on",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
text: entity.state == EntityState.on ? "TURN OFF" : "TURN ON"
|
||||||
|
);
|
||||||
|
} else if (entity.supportStart && (entity.state == EntityState.docked || entity.state == EntityState.idle)) {
|
||||||
|
result = FlatServiceButton(
|
||||||
|
serviceDomain: "vacuum",
|
||||||
|
serviceName: "start",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
text: "START CLEANING"
|
||||||
|
);
|
||||||
|
} else if (entity.supportReturnHome && entity.state == EntityState.cleaning) {
|
||||||
|
result = FlatServiceButton(
|
||||||
|
serviceDomain: "vacuum",
|
||||||
|
serviceName: "return_to_base",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
text: "RETURN TO DOCK"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = Text(entity.state.toUpperCase(), style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(right: 15),
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -101,6 +101,9 @@ class EntityCollection {
|
|||||||
case "timer": {
|
case "timer": {
|
||||||
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
return TimerEntity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
|
case "vacuum": {
|
||||||
|
return VacuumEntity(rawEntityData, homeAssistantWebHost);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return Entity(rawEntityData, homeAssistantWebHost);
|
return Entity(rawEntityData, homeAssistantWebHost);
|
||||||
}
|
}
|
||||||
@ -149,6 +152,13 @@ class EntityCollection {
|
|||||||
return _allEntities[entityId] != null;
|
return _allEntities[entityId] != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) {
|
||||||
|
return _allEntities.values.where((entity) {
|
||||||
|
return domains.contains(entity.domain) &&
|
||||||
|
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
List<Entity> filterEntitiesForDefaultView() {
|
List<Entity> filterEntitiesForDefaultView() {
|
||||||
List<Entity> result = [];
|
List<Entity> result = [];
|
||||||
List<Entity> groups = [];
|
List<Entity> groups = [];
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityPageContainer extends StatelessWidget {
|
|
||||||
EntityPageContainer({Key key, @required this.children}) : super(key: key);
|
|
||||||
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView(
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,8 +11,13 @@ class HomeAssistant {
|
|||||||
EntityCollection entities;
|
EntityCollection entities;
|
||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
|
Map services;
|
||||||
String _userName;
|
String _userName;
|
||||||
|
bool childMode;
|
||||||
HSVColor savedColor;
|
HSVColor savedColor;
|
||||||
|
int savedPlayerPosition;
|
||||||
|
String sendToPlayerId;
|
||||||
|
String sendFromPlayerId;
|
||||||
|
|
||||||
String fcmToken;
|
String fcmToken;
|
||||||
|
|
||||||
@ -23,7 +28,7 @@ class HomeAssistant {
|
|||||||
Duration fetchTimeout = Duration(seconds: 30);
|
Duration fetchTimeout = Duration(seconds: 30);
|
||||||
|
|
||||||
String get locationName {
|
String get locationName {
|
||||||
if (Connection().useLovelace) {
|
if (ConnectionManager().useLovelace) {
|
||||||
return ui?.title ?? "";
|
return ui?.title ?? "";
|
||||||
} else {
|
} else {
|
||||||
return _instanceConfig["location_name"] ?? "";
|
return _instanceConfig["location_name"] ?? "";
|
||||||
@ -36,8 +41,8 @@ class HomeAssistant {
|
|||||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||||
|
|
||||||
HomeAssistant._internal() {
|
HomeAssistant._internal() {
|
||||||
Connection().onStateChangeCallback = _handleEntityStateChange;
|
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||||
Device().loadDeviceInfo();
|
DeviceInfoManager().loadDeviceInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer _fetchCompleter;
|
Completer _fetchCompleter;
|
||||||
@ -47,28 +52,28 @@ class HomeAssistant {
|
|||||||
Logger.w("Previous data fetch is not completed yet");
|
Logger.w("Previous data fetch is not completed yet");
|
||||||
return _fetchCompleter.future;
|
return _fetchCompleter.future;
|
||||||
}
|
}
|
||||||
if (entities == null) entities = EntityCollection(Connection().httpWebHost);
|
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
||||||
_fetchCompleter = Completer();
|
_fetchCompleter = Completer();
|
||||||
List<Future> futures = [];
|
List<Future> futures = [];
|
||||||
futures.add(_getStates());
|
futures.add(_getStates());
|
||||||
if (Connection().useLovelace) {
|
if (ConnectionManager().useLovelace) {
|
||||||
futures.add(_getLovelace());
|
futures.add(_getLovelace());
|
||||||
}
|
}
|
||||||
futures.add(_getConfig());
|
futures.add(_getConfig());
|
||||||
futures.add(_getServices());
|
futures.add(_getServices());
|
||||||
futures.add(_getUserInfo());
|
futures.add(_getUserInfo());
|
||||||
futures.add(_getPanels());
|
futures.add(_getPanels());
|
||||||
futures.add(Connection().sendSocketMessage(
|
futures.add(ConnectionManager().sendSocketMessage(
|
||||||
type: "subscribe_events",
|
type: "subscribe_events",
|
||||||
additionalData: {"event_type": "state_changed"},
|
additionalData: {"event_type": "state_changed"},
|
||||||
));
|
));
|
||||||
Future.wait(futures).then((_) {
|
Future.wait(futures).then((_) {
|
||||||
if (isMobileAppEnabled) {
|
if (isMobileAppEnabled) {
|
||||||
_createUI();
|
if (!childMode) _createUI();
|
||||||
_fetchCompleter.complete();
|
_fetchCompleter.complete();
|
||||||
checkAppRegistration();
|
MobileAppIntegrationManager.checkAppRegistration();
|
||||||
} else {
|
} else {
|
||||||
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app")]));
|
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-integration")]));
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
_fetchCompleter.completeError(e);
|
_fetchCompleter.completeError(e);
|
||||||
@ -78,133 +83,15 @@ class HomeAssistant {
|
|||||||
|
|
||||||
Future logout() async {
|
Future logout() async {
|
||||||
Logger.d("Logging out...");
|
Logger.d("Logging out...");
|
||||||
await Connection().logout().then((_) {
|
await ConnectionManager().logout().then((_) {
|
||||||
ui?.clear();
|
ui?.clear();
|
||||||
entities?.clear();
|
entities?.clear();
|
||||||
panels?.clear();
|
panels?.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Map _getAppRegistrationData() {
|
|
||||||
return {
|
|
||||||
"app_version": "$appVersion",
|
|
||||||
"device_name": "$userName's ${Device().model}",
|
|
||||||
"manufacturer": Device().manufacturer,
|
|
||||||
"model": Device().model,
|
|
||||||
"os_version": Device().osVersion,
|
|
||||||
"app_data": {
|
|
||||||
"push_token": "$fcmToken",
|
|
||||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
|
||||||
Completer completer = Completer();
|
|
||||||
if (Connection().webhookId == null || forceRegister) {
|
|
||||||
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
|
||||||
var registrationData = _getAppRegistrationData();
|
|
||||||
registrationData.addAll({
|
|
||||||
"app_id": "ha_client",
|
|
||||||
"app_name": "$appName",
|
|
||||||
"os_name": Device().osName,
|
|
||||||
"supports_encryption": false,
|
|
||||||
});
|
|
||||||
Connection().sendHTTPPost(
|
|
||||||
endPoint: "/api/mobile_app/registrations",
|
|
||||||
includeAuthHeader: true,
|
|
||||||
data: json.encode(registrationData)
|
|
||||||
).then((response) {
|
|
||||||
Logger.d("Processing registration responce...");
|
|
||||||
var responseObject = json.decode(response);
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
|
||||||
Connection().webhookId = responseObject["webhook_id"];
|
|
||||||
completer.complete();
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Mobile app Integration was created",
|
|
||||||
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
|
|
||||||
positiveText: "Restart now",
|
|
||||||
negativeText: "Later",
|
|
||||||
onPositive: () {
|
|
||||||
Connection().callService(domain: "homeassistant", service: "restart", entityId: null);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
|
||||||
completer.complete();
|
|
||||||
Logger.e("Error registering the app: ${e.toString()}");
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
} else {
|
|
||||||
Logger.d("App was previously registered. Checking...");
|
|
||||||
var updateData = {
|
|
||||||
"type": "update_registration",
|
|
||||||
"data": _getAppRegistrationData()
|
|
||||||
};
|
|
||||||
Connection().sendHTTPPost(
|
|
||||||
endPoint: "/api/webhook/${Connection().webhookId}",
|
|
||||||
includeAuthHeader: false,
|
|
||||||
data: json.encode(updateData)
|
|
||||||
).then((response) {
|
|
||||||
if (response == null || response.isEmpty) {
|
|
||||||
Logger.d("No registration data in response. MobileApp integration was removed");
|
|
||||||
_askToRegisterApp();
|
|
||||||
} else {
|
|
||||||
Logger.d("App registration works fine");
|
|
||||||
if (showOkDialog) {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "All good",
|
|
||||||
body: "HA Client integration with your Home Assistant server works fine",
|
|
||||||
positiveText: "Nice!",
|
|
||||||
negativeText: "Ok"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completer.complete();
|
|
||||||
}).catchError((e) {
|
|
||||||
if (e['code'] != null && e['code'] == 410) {
|
|
||||||
Logger.e("MobileApp integration was removed");
|
|
||||||
_askToRegisterApp();
|
|
||||||
} else {
|
|
||||||
Logger.e("Error updating app registration: ${e.toString()}");
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "App integration is not working properly",
|
|
||||||
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue.",
|
|
||||||
positiveText: "Report to GitHub",
|
|
||||||
negativeText: "Report to Discord",
|
|
||||||
onPositive: () {
|
|
||||||
HAUtils.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
|
||||||
},
|
|
||||||
onNegative: () {
|
|
||||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
completer.complete();
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _askToRegisterApp() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "App integration was removed",
|
|
||||||
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
|
||||||
positiveText: "Register now",
|
|
||||||
negativeText: "Cancel",
|
|
||||||
onPositive: () {
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
prefs.remove("app-webhook-id");
|
|
||||||
Connection().webhookId = null;
|
|
||||||
HomeAssistant().checkAppRegistration();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _getConfig() async {
|
Future _getConfig() async {
|
||||||
await Connection().sendSocketMessage(type: "get_config").then((data) {
|
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||||
_instanceConfig = Map.from(data);
|
_instanceConfig = Map.from(data);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
throw HAError("Error getting config: ${e}");
|
throw HAError("Error getting config: ${e}");
|
||||||
@ -212,7 +99,7 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future _getStates() async {
|
Future _getStates() async {
|
||||||
await Connection().sendSocketMessage(type: "get_states").then(
|
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||||
(data) => entities.parse(data)
|
(data) => entities.parse(data)
|
||||||
).catchError((e) {
|
).catchError((e) {
|
||||||
throw HAError("Error getting states: $e");
|
throw HAError("Error getting states: $e");
|
||||||
@ -220,27 +107,34 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future _getLovelace() async {
|
Future _getLovelace() async {
|
||||||
await Connection().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
||||||
throw HAError("Error getting lovelace config: $e");
|
throw HAError("Error getting lovelace config: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getUserInfo() async {
|
Future _getUserInfo() async {
|
||||||
_userName = null;
|
_userName = null;
|
||||||
await Connection().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) {
|
||||||
Logger.w("Can't get user info: ${e}");
|
_userName = data["name"];
|
||||||
|
childMode = _userName.startsWith("[child]");
|
||||||
|
}).catchError((e) {
|
||||||
|
Logger.w("Can't get user info: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getServices() async {
|
Future _getServices() async {
|
||||||
await Connection().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
|
||||||
Logger.w("Can't get services: ${e}");
|
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 {
|
Future _getPanels() async {
|
||||||
panels.clear();
|
panels.clear();
|
||||||
await Connection().sendSocketMessage(type: "get_panels").then((data) {
|
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
|
||||||
data.forEach((k,v) {
|
data.forEach((k,v) {
|
||||||
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
|
||||||
panels.add(Panel(
|
panels.add(Panel(
|
||||||
@ -280,7 +174,8 @@ class HomeAssistant {
|
|||||||
count: viewCounter,
|
count: viewCounter,
|
||||||
id: "${rawView['id']}",
|
id: "${rawView['id']}",
|
||||||
name: rawView['title'],
|
name: rawView['title'],
|
||||||
iconName: rawView['icon']
|
iconName: rawView['icon'],
|
||||||
|
panel: rawView['panel'] ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||||
@ -309,7 +204,7 @@ class HomeAssistant {
|
|||||||
HACard card = HACard(
|
HACard card = HACard(
|
||||||
id: "card",
|
id: "card",
|
||||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||||
type: rawCardInfo['type'],
|
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||||
showName: rawCardInfo['show_name'] ?? true,
|
showName: rawCardInfo['show_name'] ?? true,
|
||||||
showState: rawCardInfo['show_state'] ?? true,
|
showState: rawCardInfo['show_state'] ?? true,
|
||||||
@ -317,12 +212,17 @@ class HomeAssistant {
|
|||||||
stateFilter: rawCardInfo['state_filter'] ?? [],
|
stateFilter: rawCardInfo['state_filter'] ?? [],
|
||||||
states: rawCardInfo['states'],
|
states: rawCardInfo['states'],
|
||||||
conditions: rawCard['conditions'] ?? [],
|
conditions: rawCard['conditions'] ?? [],
|
||||||
content: rawCardInfo['content']
|
content: rawCardInfo['content'],
|
||||||
|
min: rawCardInfo['min'] ?? 0,
|
||||||
|
max: rawCardInfo['max'] ?? 100,
|
||||||
|
unit: rawCardInfo['unit'],
|
||||||
|
severity: rawCardInfo['severity']
|
||||||
);
|
);
|
||||||
if (rawCardInfo["cards"] != null) {
|
if (rawCardInfo["cards"] != null) {
|
||||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||||
}
|
}
|
||||||
rawCardInfo["entities"]?.forEach((rawEntity) {
|
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
|
||||||
|
rawEntities?.forEach((rawEntity) {
|
||||||
if (rawEntity is String) {
|
if (rawEntity is String) {
|
||||||
if (entities.isExist(rawEntity)) {
|
if (entities.isExist(rawEntity)) {
|
||||||
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
|
||||||
@ -385,8 +285,9 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (rawCardInfo["entity"] != null) {
|
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
|
||||||
var en = rawCardInfo["entity"];
|
if (rawSingleEntity != null) {
|
||||||
|
var en = rawSingleEntity;
|
||||||
if (en is String) {
|
if (en is String) {
|
||||||
if (entities.isExist(en)) {
|
if (entities.isExist(en)) {
|
||||||
Entity e = entities.get(en);
|
Entity e = entities.get(en);
|
||||||
@ -423,7 +324,7 @@ class HomeAssistant {
|
|||||||
|
|
||||||
void _createUI() {
|
void _createUI() {
|
||||||
ui = HomeAssistantUI();
|
ui = HomeAssistantUI();
|
||||||
if ((Connection().useLovelace) && (_rawLovelaceData != null)) {
|
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
|
||||||
Logger.d("Creating Lovelace UI");
|
Logger.d("Creating Lovelace UI");
|
||||||
_parseLovelace();
|
_parseLovelace();
|
||||||
} else {
|
} else {
|
||||||
|
898
lib/main.dart
898
lib/main.dart
@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:math';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -15,16 +15,26 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
import 'package:progress_indicators/progress_indicators.dart';
|
import 'package:progress_indicators/progress_indicators.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
//import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:device_info/device_info.dart';
|
import 'package:device_info/device_info.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
|
import 'plugins/circular_slider/single_circular_slider.dart';
|
||||||
|
import 'package:share/receive_share_state.dart';
|
||||||
|
import 'package:share/share.dart';
|
||||||
|
import 'plugins/dynamic_multi_column_layout.dart';
|
||||||
|
import 'plugins/spoiler_card.dart';
|
||||||
|
import 'package:uni_links/uni_links.dart';
|
||||||
|
import 'package:workmanager/workmanager.dart' as workManager;
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:battery/battery.dart';
|
||||||
|
|
||||||
|
import 'utils/logger.dart';
|
||||||
|
|
||||||
part 'const.dart';
|
part 'const.dart';
|
||||||
|
part 'utils/launcher.dart';
|
||||||
part 'entities/entity.class.dart';
|
part 'entities/entity.class.dart';
|
||||||
part 'entities/entity_wrapper.class.dart';
|
part 'entities/entity_wrapper.class.dart';
|
||||||
part 'entities/timer/timer_entity.class.dart';
|
part 'entities/timer/timer_entity.class.dart';
|
||||||
@ -46,79 +56,96 @@ part 'entities/fan/fan_entity.class.dart';
|
|||||||
part 'entities/automation/automation_entity.class.dart';
|
part 'entities/automation/automation_entity.class.dart';
|
||||||
part 'entities/camera/camera_entity.class.dart';
|
part 'entities/camera/camera_entity.class.dart';
|
||||||
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
||||||
part 'entity_widgets/common/badge.dart';
|
part 'entities/badge.widget.dart';
|
||||||
part 'entity_widgets/model_widgets.dart';
|
part 'entities/entity_model.widget.dart';
|
||||||
part 'entity_widgets/default_entity_container.dart';
|
part 'entities/default_entity_container.widget.dart';
|
||||||
part 'entity_widgets/missed_entity.dart';
|
part 'entities/missed_entity.widget.dart';
|
||||||
part 'entity_widgets/glance_entity_container.dart';
|
part 'cards/widgets/glance_card_entity_container.dart';
|
||||||
part 'entity_widgets/button_entity_container.dart';
|
part 'cards/widgets/entity_button_card_body.widget.dart';
|
||||||
part 'entity_widgets/common/entity_attributes_list.dart';
|
part 'pages/widgets/entity_attributes_list.dart';
|
||||||
part 'entity_widgets/entity_icon.dart';
|
part 'entities/entity_icon.widget.dart';
|
||||||
part 'entity_widgets/entity_name.dart';
|
part 'entities/entity_name.widget.dart';
|
||||||
part 'entity_widgets/common/last_updated.dart';
|
part 'pages/widgets/last_updated.dart';
|
||||||
part 'entity_widgets/common/mode_swicth.dart';
|
part 'entities/climate/widgets/mode_swicth.dart';
|
||||||
part 'entity_widgets/common/mode_selector.dart';
|
part 'entities/climate/widgets/mode_selector.dart';
|
||||||
part 'entity_widgets/common/universal_slider.dart';
|
part 'entities/universal_slider.widget.dart';
|
||||||
part 'entity_widgets/common/flat_service_button.dart';
|
part 'entities/flat_service_button.widget.dart';
|
||||||
part 'entity_widgets/common/light_color_picker.dart';
|
part 'entities/light/widgets/light_color_picker.dart';
|
||||||
part 'entity_widgets/common/camera_stream_view.dart';
|
part 'entities/camera/widgets/camera_stream_view.dart';
|
||||||
part 'entity_widgets/entity_colors.class.dart';
|
part 'entities/entity_colors.class.dart';
|
||||||
part 'entity_widgets/entity_page_container.dart';
|
part 'plugins/history_chart/entity_history.dart';
|
||||||
part 'entity_widgets/history_chart/entity_history.dart';
|
part 'plugins/history_chart/simple_state_history_chart.dart';
|
||||||
part 'entity_widgets/history_chart/simple_state_history_chart.dart';
|
part 'plugins/history_chart/numeric_state_history_chart.dart';
|
||||||
part 'entity_widgets/history_chart/numeric_state_history_chart.dart';
|
part 'plugins/history_chart/combined_history_chart.dart';
|
||||||
part 'entity_widgets/history_chart/combined_history_chart.dart';
|
part 'plugins/history_chart/history_control_widget.dart';
|
||||||
part 'entity_widgets/history_chart/history_control_widget.dart';
|
part 'plugins/history_chart/entity_history_moment.dart';
|
||||||
part 'entity_widgets/history_chart/entity_history_moment.dart';
|
|
||||||
part 'entities/switch/widget/switch_state.dart';
|
part 'entities/switch/widget/switch_state.dart';
|
||||||
part 'entities/slider/widgets/slider_controls.dart';
|
part 'entities/slider/widgets/slider_controls.dart';
|
||||||
part 'entities/text/widgets/text_input_state.dart';
|
part 'entities/text/widgets/text_input_state.dart';
|
||||||
part 'entities/select/widgets/select_state.dart';
|
part 'entities/select/widgets/select_state.dart';
|
||||||
part 'entity_widgets/common/simple_state.dart';
|
part 'entities/simple_state.widget.dart';
|
||||||
part 'entities/timer/widgets/timer_state.dart';
|
part 'entities/timer/widgets/timer_state.dart';
|
||||||
part 'entities/climate/widgets/climate_state.widget.dart';
|
part 'entities/climate/widgets/climate_state.widget.dart';
|
||||||
part 'entities/cover/widgets/cover_state.dart';
|
part 'entities/cover/widgets/cover_state.dart';
|
||||||
part 'entities/date_time/widgets/date_time_state.dart';
|
part 'entities/date_time/widgets/date_time_state.dart';
|
||||||
part 'entities/lock/widgets/lock_state.dart';
|
part 'entities/lock/widgets/lock_state.dart';
|
||||||
part 'entities/climate/widgets/climate_controls.dart';
|
part 'entities/climate/widgets/climate_controls.dart';
|
||||||
|
part 'entities/climate/widgets/temperature_control_widget.dart';
|
||||||
part 'entities/cover/widgets/cover_controls.widget.dart';
|
part 'entities/cover/widgets/cover_controls.widget.dart';
|
||||||
part 'entities/light/widgets/light_controls.dart';
|
part 'entities/light/widgets/light_controls.dart';
|
||||||
part 'entities/media_player/widgets/media_player_widgets.dart';
|
part 'entities/media_player/widgets/media_player_widgets.dart';
|
||||||
part 'entities/fan/widgets/fan_controls.dart';
|
part 'entities/fan/widgets/fan_controls.dart';
|
||||||
part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart';
|
part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.dart';
|
||||||
|
part 'entities/vacuum/vacuum_entity.class.dart';
|
||||||
|
part 'entities/vacuum/widgets/vacuum_controls.dart';
|
||||||
|
part 'entities/vacuum/widgets/vacuum_state_button.dart';
|
||||||
part 'pages/settings.page.dart';
|
part 'pages/settings.page.dart';
|
||||||
part 'pages/purchase.page.dart';
|
part 'pages/purchase.page.dart';
|
||||||
part 'pages/widgets/product_purchase.widget.dart';
|
part 'pages/widgets/product_purchase.widget.dart';
|
||||||
part 'pages/widgets/page_loading_indicator.dart';
|
part 'pages/widgets/page_loading_indicator.dart';
|
||||||
part 'pages/widgets/page_loading_error.dart';
|
part 'pages/widgets/page_loading_error.dart';
|
||||||
part 'pages/panel.page.dart';
|
part 'pages/panel.page.dart';
|
||||||
|
part 'pages/main.page.dart';
|
||||||
|
part 'pages/integration_settings.page.dart';
|
||||||
part 'home_assistant.class.dart';
|
part 'home_assistant.class.dart';
|
||||||
part 'pages/log.page.dart';
|
part 'pages/log.page.dart';
|
||||||
part 'pages/entity.page.dart';
|
part 'pages/entity.page.dart';
|
||||||
part 'utils.class.dart';
|
part 'utils/mdi.class.dart';
|
||||||
part 'mdi.class.dart';
|
|
||||||
part 'entity_collection.class.dart';
|
part 'entity_collection.class.dart';
|
||||||
part 'auth_manager.class.dart';
|
part 'managers/auth_manager.class.dart';
|
||||||
part 'connection.class.dart';
|
part 'managers/location_manager.class.dart';
|
||||||
part 'device.class.dart';
|
part 'managers/mobile_app_integration_manager.class.dart';
|
||||||
part 'ui_class/ui.dart';
|
part 'managers/connection_manager.class.dart';
|
||||||
part 'ui_class/view.class.dart';
|
part 'managers/device_info_manager.class.dart';
|
||||||
part 'ui_class/card.class.dart';
|
part 'managers/startup_user_messages_manager.class.dart';
|
||||||
part 'ui_class/sizes_class.dart';
|
part 'ui.dart';
|
||||||
part 'ui_class/panel_class.dart';
|
part 'view.class.dart';
|
||||||
part 'ui_widgets/view.dart';
|
part 'cards/card.class.dart';
|
||||||
part 'ui_widgets/card_widget.dart';
|
part 'panels/panel_class.dart';
|
||||||
part 'ui_widgets/card_header_widget.dart';
|
part 'viewWidget.widget.dart';
|
||||||
part 'ui_widgets/config_panel_widget.dart';
|
part 'cards/card_widget.dart';
|
||||||
|
part 'cards/widgets/card_header.widget.dart';
|
||||||
|
part 'panels/config_panel_widget.dart';
|
||||||
|
part 'panels/widgets/link_to_web_config.dart';
|
||||||
|
part 'types/ha_error.dart';
|
||||||
|
part 'types/event_bus_events.dart';
|
||||||
|
part 'cards/widgets/gauge_card_body.dart';
|
||||||
|
part 'cards/widgets/light_card_body.dart';
|
||||||
|
part 'pages/play_media.page.dart';
|
||||||
|
part 'entities/entity_page_layout.widget.dart';
|
||||||
|
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
|
||||||
|
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
|
||||||
|
part 'pages/whats_new.page.dart';
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersion = "0.6.3";
|
const appVersionNumber = "0.7.0";
|
||||||
|
const appVersionAdd = "";
|
||||||
|
const appVersion = "$appVersionNumber-$appVersionAdd";
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
FlutterError.onError = (errorDetails) {
|
FlutterError.onError = (errorDetails) {
|
||||||
Logger.e( "${errorDetails.exception}");
|
Logger.e( "${errorDetails.exception}");
|
||||||
if (Logger.isInDebugMode) {
|
if (Logger.isInDebugMode) {
|
||||||
@ -127,7 +154,12 @@ void main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
runZoned(() {
|
runZoned(() {
|
||||||
|
workManager.Workmanager.initialize(
|
||||||
|
updateDeviceLocationIsolate,
|
||||||
|
isInDebugMode: false
|
||||||
|
);
|
||||||
runApp(new HAClientApp());
|
runApp(new HAClientApp());
|
||||||
|
|
||||||
}, onError: (error, stack) {
|
}, onError: (error, stack) {
|
||||||
Logger.e("$error");
|
Logger.e("$error");
|
||||||
Logger.e("$stack");
|
Logger.e("$stack");
|
||||||
@ -139,7 +171,6 @@ void main() {
|
|||||||
|
|
||||||
class HAClientApp extends StatelessWidget {
|
class HAClientApp extends StatelessWidget {
|
||||||
|
|
||||||
final HomeAssistant homeAssistant = HomeAssistant();
|
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -150,778 +181,17 @@ class HAClientApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'HA Client', homeAssistant: homeAssistant,),
|
"/": (context) => MainPage(title: 'HA Client'),
|
||||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
||||||
"/configuration": (context) => PanelPage(title: "Configuration"),
|
"/integration-settings": (context) => IntegrationSettingsPage(title: "Integration settings"),
|
||||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
"/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'] : ''}",
|
||||||
|
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
|
||||||
|
),
|
||||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||||
"/login": (context) => WebviewScaffold(
|
"/whats-new": (context) => WhatsNewPage()
|
||||||
url: "${Connection().oauthUrl}",
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(Icons.help),
|
|
||||||
onPressed: () => HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#authentication")
|
|
||||||
),
|
|
||||||
title: new Text("Login with HA"),
|
|
||||||
actions: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
child: Text("Manual", style: TextStyle(color: Colors.white)),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed("/connection-settings");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainPage extends StatefulWidget {
|
|
||||||
MainPage({Key key, this.title, this.homeAssistant}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final HomeAssistant homeAssistant;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_MainPageState createState() => new _MainPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
|
||||||
|
|
||||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
|
||||||
StreamSubscription _stateSubscription;
|
|
||||||
StreamSubscription _settingsSubscription;
|
|
||||||
StreamSubscription _serviceCallSubscription;
|
|
||||||
StreamSubscription _showEntityPageSubscription;
|
|
||||||
StreamSubscription _showErrorSubscription;
|
|
||||||
StreamSubscription _startAuthSubscription;
|
|
||||||
StreamSubscription _showPopupDialogSubscription;
|
|
||||||
StreamSubscription _showPopupMessageSubscription;
|
|
||||||
StreamSubscription _reloadUISubscription;
|
|
||||||
int _previousViewCount;
|
|
||||||
bool _showLoginButton = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
final Stream purchaseUpdates =
|
|
||||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
|
||||||
_subscription = purchaseUpdates.listen((purchases) {
|
|
||||||
_handlePurchaseUpdates(purchases);
|
|
||||||
});
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
|
|
||||||
_firebaseMessaging.configure(
|
|
||||||
onLaunch: (data) {
|
|
||||||
Logger.d("Notification [onLaunch]: $data");
|
|
||||||
},
|
|
||||||
onMessage: (data) {
|
|
||||||
Logger.d("Notification [onMessage]: $data");
|
|
||||||
_showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
|
|
||||||
},
|
|
||||||
onResume: (data) {
|
|
||||||
Logger.d("Notification [onResume]: $data");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
|
||||||
|
|
||||||
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
|
||||||
var initializationSettingsAndroid =
|
|
||||||
new AndroidInitializationSettings('mini_icon');
|
|
||||||
var initializationSettingsIOS = new IOSInitializationSettings(
|
|
||||||
onDidReceiveLocalNotification: null);
|
|
||||||
var initializationSettings = new InitializationSettings(
|
|
||||||
initializationSettingsAndroid, initializationSettingsIOS);
|
|
||||||
flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
|
||||||
onSelectNotification: onSelectNotification);
|
|
||||||
|
|
||||||
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
|
|
||||||
Logger.d("Settings change event: reconnect=${event.reconnect}");
|
|
||||||
if (event.reconnect) {
|
|
||||||
_fullLoad();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_fullLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future onSelectNotification(String payload) async {
|
|
||||||
if (payload != null) {
|
|
||||||
Logger.d('Notification clicked: ' + payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _showNotification({String title, String text}) async {
|
|
||||||
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
|
|
||||||
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
|
|
||||||
importance: Importance.Max, priority: Priority.High);
|
|
||||||
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
|
|
||||||
var platformChannelSpecifics = new NotificationDetails(
|
|
||||||
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
|
||||||
await flutterLocalNotificationsPlugin.show(
|
|
||||||
0,
|
|
||||||
title ?? appName,
|
|
||||||
text,
|
|
||||||
platformChannelSpecifics
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fullLoad() async {
|
|
||||||
_showInfoBottomBar(progress: true,);
|
|
||||||
_subscribe().then((_) {
|
|
||||||
Connection().init(loadSettings: true, forceReconnect: true).then((__){
|
|
||||||
_fetchData();
|
|
||||||
}, onError: (e) {
|
|
||||||
_setErrorState(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _quickLoad() {
|
|
||||||
_hideBottomBar();
|
|
||||||
_showInfoBottomBar(progress: true,);
|
|
||||||
Connection().init(loadSettings: false, forceReconnect: false).then((_){
|
|
||||||
_fetchData();
|
|
||||||
}, onError: (e) {
|
|
||||||
_setErrorState(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_fetchData() async {
|
|
||||||
await widget.homeAssistant.fetchData().then((_) {
|
|
||||||
_hideBottomBar();
|
|
||||||
int currentViewCount = widget.homeAssistant.ui?.views?.length ?? 0;
|
|
||||||
if (_previousViewCount != currentViewCount) {
|
|
||||||
Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller.");
|
|
||||||
_viewsTabController = TabController(vsync: this, length: currentViewCount);
|
|
||||||
_previousViewCount = currentViewCount;
|
|
||||||
}
|
|
||||||
}).catchError((e) {
|
|
||||||
if (e is HAError) {
|
|
||||||
_setErrorState(e);
|
|
||||||
} else {
|
|
||||||
_setErrorState(HAError(e.toString()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
eventBus.fire(RefreshDataFinishedEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
||||||
Logger.d("$state");
|
|
||||||
if (state == AppLifecycleState.resumed && Connection().settingsLoaded) {
|
|
||||||
_quickLoad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handlePurchaseUpdates(purchase) {
|
|
||||||
if (purchase is List<PurchaseDetails>) {
|
|
||||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
|
||||||
eventBus.fire(ShowPopupMessageEvent(
|
|
||||||
title: "Thanks a lot!",
|
|
||||||
body: "Thank you for supporting HA Client development!",
|
|
||||||
buttonText: "Ok"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _subscribe() {
|
|
||||||
Completer completer = Completer();
|
|
||||||
if (_stateSubscription == null) {
|
|
||||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
|
||||||
if (event.needToRebuildUI) {
|
|
||||||
Logger.d("New entity. Need to rebuild UI");
|
|
||||||
_quickLoad();
|
|
||||||
} else {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_reloadUISubscription == null) {
|
|
||||||
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
|
|
||||||
_quickLoad();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_showPopupDialogSubscription == null) {
|
|
||||||
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
|
|
||||||
_showPopupDialog(
|
|
||||||
title: event.title,
|
|
||||||
body: event.body,
|
|
||||||
onPositive: event.onPositive,
|
|
||||||
onNegative: event.onNegative,
|
|
||||||
positiveText: event.positiveText,
|
|
||||||
negativeText: event.negativeText
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_showPopupMessageSubscription == null) {
|
|
||||||
_showPopupMessageSubscription = eventBus.on<ShowPopupMessageEvent>().listen((event){
|
|
||||||
_showPopupDialog(
|
|
||||||
title: event.title,
|
|
||||||
body: event.body,
|
|
||||||
onPositive: event.onButtonClick,
|
|
||||||
positiveText: event.buttonText,
|
|
||||||
negativeText: null
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_serviceCallSubscription == null) {
|
|
||||||
_serviceCallSubscription =
|
|
||||||
eventBus.on<ServiceCallEvent>().listen((event) {
|
|
||||||
_callService(event.domain, event.service, event.entityId,
|
|
||||||
event.additionalParams);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_showEntityPageSubscription == null) {
|
|
||||||
_showEntityPageSubscription =
|
|
||||||
eventBus.on<ShowEntityPageEvent>().listen((event) {
|
|
||||||
_showEntityPage(event.entity.entityId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_showErrorSubscription == null) {
|
|
||||||
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
|
||||||
_showErrorBottomBar(event.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_startAuthSubscription == null) {
|
|
||||||
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
|
|
||||||
setState(() {
|
|
||||||
_showLoginButton = event.showButton;
|
|
||||||
});
|
|
||||||
if (event.showButton) {
|
|
||||||
_showOAuth();
|
|
||||||
} else {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_firebaseMessaging.getToken().then((String token) {
|
|
||||||
HomeAssistant().fcmToken = token;
|
|
||||||
completer.complete();
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showOAuth() {
|
|
||||||
Navigator.of(context).pushNamed('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
_setErrorState(HAError e) {
|
|
||||||
if (e == null) {
|
|
||||||
_showErrorBottomBar(
|
|
||||||
HAError("Unknown error")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_showErrorBottomBar(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showPopupDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) {
|
|
||||||
List<Widget> buttons = [];
|
|
||||||
buttons.add(FlatButton(
|
|
||||||
child: new Text("$positiveText"),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
if (onPositive != null) {
|
|
||||||
onPositive();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
if (negativeText != null) {
|
|
||||||
buttons.add(FlatButton(
|
|
||||||
child: new Text("$negativeText"),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
if (onNegative != null) {
|
|
||||||
onNegative();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// flutter defined function
|
|
||||||
showDialog(
|
|
||||||
barrierDismissible: false,
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
// return object of type Dialog
|
|
||||||
return AlertDialog(
|
|
||||||
title: new Text("$title"),
|
|
||||||
content: new Text("$body"),
|
|
||||||
actions: buttons,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _callService(String domain, String service, String entityId, Map additionalParams) {
|
|
||||||
_showInfoBottomBar(
|
|
||||||
message: "Calling $domain.$service",
|
|
||||||
duration: Duration(seconds: 3)
|
|
||||||
);
|
|
||||||
Connection().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showEntityPage(String entityId) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => EntityViewPage(entityId: entityId, homeAssistant: widget.homeAssistant),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Tab> buildUIViewTabs() {
|
|
||||||
List<Tab> result = [];
|
|
||||||
|
|
||||||
if (widget.homeAssistant.ui.views.isNotEmpty) {
|
|
||||||
widget.homeAssistant.ui.views.forEach((HAView view) {
|
|
||||||
result.add(view.buildTab());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Drawer _buildAppDrawer() {
|
|
||||||
List<Widget> menuItems = [];
|
|
||||||
menuItems.add(
|
|
||||||
UserAccountsDrawerHeader(
|
|
||||||
accountName: Text(widget.homeAssistant.userName),
|
|
||||||
accountEmail: Text(Connection().displayHostname ?? "Not configured"),
|
|
||||||
/*onDetailsPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_accountMenuExpanded = !_accountMenuExpanded;
|
|
||||||
});
|
|
||||||
},*/
|
|
||||||
currentAccountPicture: CircleAvatar(
|
|
||||||
child: Text(
|
|
||||||
widget.homeAssistant.userAvatarText,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 32.0
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (widget.homeAssistant.panels.isNotEmpty) {
|
|
||||||
widget.homeAssistant.panels.forEach((Panel panel) {
|
|
||||||
if (!panel.isHidden) {
|
|
||||||
menuItems.add(
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
|
||||||
title: Text("${panel.title}"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
panel.handleOpen(context);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//TODO check for loaded
|
|
||||||
menuItems.add(
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:home-assistant")),
|
|
||||||
title: Text("Open Web UI"),
|
|
||||||
onTap: () => HAUtils.launchURL(Connection().httpWebHost),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
menuItems.addAll([
|
|
||||||
Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")),
|
|
||||||
title: Text("Connection settings"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed('/connection-settings', arguments: {"homeAssistant", widget.homeAssistant});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
menuItems.addAll([
|
|
||||||
Divider(),
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(Icons.insert_drive_file),
|
|
||||||
title: Text("Log"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed('/log-view');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new ListTile(
|
|
||||||
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(MaterialDesignIcons.getIconDataFromIconName("mdi:food")),
|
|
||||||
title: Text("Support app development"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed('/putchase');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(Icons.help),
|
|
||||||
title: Text("Help"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURL("http://ha-client.homemade.systems/docs");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
|
||||||
title: Text("Join Discord channel"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURL("https://discord.gg/AUzEvwn");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new AboutListTile(
|
|
||||||
aboutBoxChildren: <Widget>[
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURL("http://ha-client.homemade.systems/");
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"ha-client.homemade.systems",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 10.0,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"Terms and Conditions",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 10.0,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"Privacy Policy",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
applicationName: appName,
|
|
||||||
applicationVersion: appVersion
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
return new Drawer(
|
|
||||||
child: ListView(
|
|
||||||
children: menuItems,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _hideBottomBar() {
|
|
||||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
|
||||||
setState(() {
|
|
||||||
_showBottomBar = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _bottomBarAction;
|
|
||||||
bool _showBottomBar = false;
|
|
||||||
String _bottomBarText;
|
|
||||||
bool _bottomBarProgress;
|
|
||||||
Color _bottomBarColor;
|
|
||||||
Timer _bottomBarTimer;
|
|
||||||
|
|
||||||
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
|
||||||
_bottomBarTimer?.cancel();
|
|
||||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
|
||||||
_bottomBarColor = Colors.grey.shade50;
|
|
||||||
setState(() {
|
|
||||||
_bottomBarText = message;
|
|
||||||
_bottomBarProgress = progress;
|
|
||||||
_showBottomBar = true;
|
|
||||||
});
|
|
||||||
if (duration != null) {
|
|
||||||
_bottomBarTimer = Timer(duration, () {
|
|
||||||
_hideBottomBar();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showErrorBottomBar(HAError error) {
|
|
||||||
TextStyle textStyle = TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: Sizes.nameFontSize
|
|
||||||
);
|
|
||||||
_bottomBarColor = Colors.red.shade100;
|
|
||||||
List<Widget> actions = [];
|
|
||||||
error.actions.forEach((HAErrorAction action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case HAErrorActionType.FULL_RELOAD: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_fullLoad();
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.QUICK_RELOAD: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_quickLoad();
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.URL: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: "${action.url}");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pushNamed(context, '/connection-settings');
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (actions.isNotEmpty) {
|
|
||||||
_bottomBarAction = Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: actions,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_bottomBarProgress = false;
|
|
||||||
_bottomBarText = "${error.message}";
|
|
||||||
_showBottomBar = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
|
||||||
|
|
||||||
Widget _buildScaffoldBody(bool empty) {
|
|
||||||
List<PopupMenuItem<String>> popupMenuItems = [];
|
|
||||||
popupMenuItems.add(PopupMenuItem<String>(
|
|
||||||
child: new Text("Reload"),
|
|
||||||
value: "reload",
|
|
||||||
));
|
|
||||||
List<Widget> emptyBody = [
|
|
||||||
Text("."),
|
|
||||||
];
|
|
||||||
if (Connection().isAuthenticated) {
|
|
||||||
_showLoginButton = false;
|
|
||||||
popupMenuItems.add(
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
child: new Text("Logout"),
|
|
||||||
value: "logout",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (_showLoginButton) {
|
|
||||||
emptyBody = [
|
|
||||||
FlatButton(
|
|
||||||
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
|
||||||
color: Colors.blue,
|
|
||||||
onPressed: () => _fullLoad(),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return NestedScrollView(
|
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
|
||||||
return <Widget>[
|
|
||||||
SliverAppBar(
|
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
|
||||||
primary: true,
|
|
||||||
title: Text(widget.homeAssistant.locationName ?? ""),
|
|
||||||
actions: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
"mdi:dots-vertical"), color: Colors.white,),
|
|
||||||
onPressed: () {
|
|
||||||
showMenu(
|
|
||||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0),
|
|
||||||
context: context,
|
|
||||||
items: popupMenuItems
|
|
||||||
).then((String val) {
|
|
||||||
if (val == "reload") {
|
|
||||||
_quickLoad();
|
|
||||||
} else if (val == "logout") {
|
|
||||||
widget.homeAssistant.logout().then((_) {
|
|
||||||
_quickLoad();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
)
|
|
||||||
],
|
|
||||||
leading: IconButton(
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TabController _viewsTabController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget bottomBar;
|
|
||||||
if (_showBottomBar) {
|
|
||||||
List<Widget> bottomBarChildren = [];
|
|
||||||
if (_bottomBarText != null) {
|
|
||||||
bottomBarChildren.add(
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
|
||||||
Sizes.rowPadding),
|
|
||||||
child: Text(
|
|
||||||
"$_bottomBarText",
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (_bottomBarProgress) {
|
|
||||||
bottomBarChildren.add(
|
|
||||||
CollectionScaleTransition(
|
|
||||||
children: <Widget>[
|
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.on),),
|
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
|
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (bottomBarChildren.isNotEmpty) {
|
|
||||||
bottomBar = Container(
|
|
||||||
color: _bottomBarColor,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: bottomBarChildren,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_bottomBarAction
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This method is rerun every time setState is called.
|
|
||||||
if (widget.homeAssistant.isNoViews) {
|
|
||||||
return Scaffold(
|
|
||||||
key: _scaffoldKey,
|
|
||||||
primary: false,
|
|
||||||
drawer: _buildAppDrawer(),
|
|
||||||
bottomNavigationBar: bottomBar,
|
|
||||||
body: _buildScaffoldBody(true)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Scaffold(
|
|
||||||
key: _scaffoldKey,
|
|
||||||
drawer: _buildAppDrawer(),
|
|
||||||
primary: false,
|
|
||||||
bottomNavigationBar: bottomBar,
|
|
||||||
body: _buildScaffoldBody(false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
|
||||||
flutterWebviewPlugin.dispose();
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
_viewsTabController?.dispose();
|
|
||||||
_stateSubscription?.cancel();
|
|
||||||
_settingsSubscription?.cancel();
|
|
||||||
_serviceCallSubscription?.cancel();
|
|
||||||
_showPopupDialogSubscription?.cancel();
|
|
||||||
_showPopupMessageSubscription?.cancel();
|
|
||||||
_showEntityPageSubscription?.cancel();
|
|
||||||
_showErrorSubscription?.cancel();
|
|
||||||
_startAuthSubscription?.cancel();
|
|
||||||
_subscription?.cancel();
|
|
||||||
_reloadUISubscription?.cancel();
|
|
||||||
//TODO disconnect
|
|
||||||
//widget.homeAssistant?.disconnect();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
part of 'main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class AuthManager {
|
class AuthManager {
|
||||||
|
|
||||||
@ -9,24 +9,37 @@ class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AuthManager._internal();
|
AuthManager._internal();
|
||||||
|
StreamSubscription deepLinksSubscription;
|
||||||
|
|
||||||
Future getTempToken({String oauthUrl}) {
|
Future start({String oauthUrl}) {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
final flutterWebviewPlugin = new FlutterWebviewPlugin();
|
deepLinksSubscription?.cancel();
|
||||||
flutterWebviewPlugin.onUrlChanged.listen((String url) {
|
deepLinksSubscription = getUriLinksStream().listen((Uri uri) {
|
||||||
if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) {
|
Logger.d("[LINKED AUTH] We got something private");
|
||||||
String authCode = url.split("=")[1];
|
_getTempToken(oauthUrl, uri.queryParameters["code"])
|
||||||
Logger.d("We have auth code. Getting temporary access token...");
|
.then((tempToken) => completer.complete(tempToken))
|
||||||
Connection().sendHTTPPost(
|
.catchError((_){
|
||||||
|
completer.completeError(HAError("Auth error"));
|
||||||
|
});
|
||||||
|
}, onError: (err) {
|
||||||
|
Logger.e("[LINKED AUTH] Error handling linked auth: $e");
|
||||||
|
completer.completeError(HAError("Auth error"));
|
||||||
|
});
|
||||||
|
Logger.d("Launching OAuth");
|
||||||
|
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getTempToken(String oauthUrl,String authCode) {
|
||||||
|
Completer completer = Completer();
|
||||||
|
ConnectionManager().sendHTTPPost(
|
||||||
endPoint: "/auth/token",
|
endPoint: "/auth/token",
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
includeAuthHeader: false,
|
includeAuthHeader: false,
|
||||||
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}"
|
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems')}"
|
||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("Got temp token");
|
Logger.d("Got temp token");
|
||||||
String tempToken = json.decode(response)['access_token'];
|
String tempToken = json.decode(response)['access_token'];
|
||||||
Logger.d("Closing webview...");
|
|
||||||
//flutterWebviewPlugin.close();
|
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
completer.complete(tempToken);
|
completer.complete(tempToken);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
@ -35,10 +48,6 @@ class AuthManager {
|
|||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
completer.completeError(HAError("Error getting temp token"));
|
completer.completeError(HAError("Error getting temp token"));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
Logger.d("Launching OAuth");
|
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, true));
|
|
||||||
return completer.future;
|
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;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connection._internal();
|
ConnectionManager._internal();
|
||||||
|
|
||||||
String _domain;
|
String _domain;
|
||||||
String _port;
|
String _port;
|
||||||
@ -54,21 +54,20 @@ class Connection {
|
|||||||
completer.completeError(HAError.checkConnectionSettings());
|
completer.completeError(HAError.checkConnectionSettings());
|
||||||
stopInit = true;
|
stopInit = true;
|
||||||
} else {
|
} else {
|
||||||
//_token = prefs.getString('hassio-token');
|
|
||||||
final storage = new FlutterSecureStorage();
|
final storage = new FlutterSecureStorage();
|
||||||
try {
|
try {
|
||||||
_token = await storage.read(key: "hacl_llt");
|
_token = await storage.read(key: "hacl_llt");
|
||||||
Logger.e("Long-lived token read successful");
|
Logger.e("Long-lived token read successful");
|
||||||
} catch (e) {
|
|
||||||
Logger.e("Cannt read secure storage. Need to relogin.");
|
|
||||||
_token = null;
|
|
||||||
await storage.delete(key: "hacl_llt");
|
|
||||||
}
|
|
||||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||||
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
'http://ha-client.homemade.systems')}&redirect_uri=${Uri
|
||||||
.encodeComponent(
|
.encodeComponent(
|
||||||
'http://ha-client.homemade.systems/service/auth_callback.html')}";
|
'haclient://auth')}";
|
||||||
settingsLoaded = true;
|
settingsLoaded = true;
|
||||||
|
} catch (e) {
|
||||||
|
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
||||||
|
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||||
|
stopInit = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ((_domain == null) || (_port == null) ||
|
if ((_domain == null) || (_port == null) ||
|
||||||
@ -80,7 +79,7 @@ class Connection {
|
|||||||
|
|
||||||
if (!stopInit) {
|
if (!stopInit) {
|
||||||
if (_token == null) {
|
if (_token == null) {
|
||||||
AuthManager().getTempToken(
|
AuthManager().start(
|
||||||
oauthUrl: oauthUrl
|
oauthUrl: oauthUrl
|
||||||
).then((token) {
|
).then((token) {
|
||||||
Logger.d("Token from AuthManager recived");
|
Logger.d("Token from AuthManager recived");
|
||||||
@ -101,7 +100,9 @@ class Connection {
|
|||||||
if (forceReconnect || !isConnected) {
|
if (forceReconnect || !isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: () {
|
_connect().timeout(connectTimeout, onTimeout: () {
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
completer?.completeError(HAError("Connection timeout"));
|
if (completer != null && !completer.isCompleted) {
|
||||||
|
completer.completeError(HAError("Connection timeout"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).then((_) {
|
}).then((_) {
|
||||||
completer?.complete();
|
completer?.complete();
|
||||||
@ -148,9 +149,7 @@ class Connection {
|
|||||||
Logger.d("[Received] <== ${data.toString()}");
|
Logger.d("[Received] <== ${data.toString()}");
|
||||||
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
||||||
_messageResolver.remove("auth");
|
_messageResolver.remove("auth");
|
||||||
logout().then((_) {
|
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.tryAgain(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||||
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
_handleMessage(data);
|
_handleMessage(data);
|
||||||
}
|
}
|
||||||
@ -164,8 +163,6 @@ class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Future _disconnect() {
|
Future _disconnect() {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
@ -189,16 +186,16 @@ class Connection {
|
|||||||
_handleMessage(data) {
|
_handleMessage(data) {
|
||||||
if (data["type"] == "result") {
|
if (data["type"] == "result") {
|
||||||
if (data["id"] != null && data["success"]) {
|
if (data["id"] != null && data["success"]) {
|
||||||
Logger.d("[Received] <== Request id ${data['id']} was successful");
|
//Logger.d("[Received] <== Request id ${data['id']} was successful");
|
||||||
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
_messageResolver["${data["id"]}"]?.complete(data["result"]);
|
||||||
} else if (data["id"] != null) {
|
} else if (data["id"] != null) {
|
||||||
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
//Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
|
||||||
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
|
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
|
||||||
}
|
}
|
||||||
_messageResolver.remove("${data["id"]}");
|
_messageResolver.remove("${data["id"]}");
|
||||||
} else if (data["type"] == "event") {
|
} else if (data["type"] == "event") {
|
||||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
||||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
//Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
||||||
onStateChangeCallback(data["event"]["data"]);
|
onStateChangeCallback(data["event"]["data"]);
|
||||||
} else if (data["event"] != null) {
|
} else if (data["event"] != null) {
|
||||||
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
||||||
@ -281,6 +278,7 @@ class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future logout() {
|
Future logout() {
|
||||||
|
Logger.d("Logging out");
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
_disconnect().whenComplete(() {
|
_disconnect().whenComplete(() {
|
||||||
_token = null;
|
_token = null;
|
||||||
@ -309,8 +307,7 @@ class Connection {
|
|||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
logout();
|
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||||
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
|
|
||||||
});
|
});
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
@ -352,6 +349,7 @@ class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
|
Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
|
||||||
|
Completer completer = Completer();
|
||||||
Map serviceData = {};
|
Map serviceData = {};
|
||||||
if (entityId != null) {
|
if (entityId != null) {
|
||||||
serviceData["entity_id"] = entityId;
|
serviceData["entity_id"] = entityId;
|
||||||
@ -360,9 +358,17 @@ class Connection {
|
|||||||
serviceData.addAll(additionalServiceData);
|
serviceData.addAll(additionalServiceData);
|
||||||
}
|
}
|
||||||
if (serviceData.isNotEmpty)
|
if (serviceData.isNotEmpty)
|
||||||
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
sendHTTPPost(
|
||||||
|
endPoint: "/api/services/$domain/$service",
|
||||||
|
data: json.encode(serviceData)
|
||||||
|
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
|
||||||
|
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
|
||||||
else
|
else
|
||||||
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
sendHTTPPost(
|
||||||
|
endPoint: "/api/services/$domain/$service"
|
||||||
|
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));;
|
||||||
|
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List> getHistory(String entityId) async {
|
Future<List> getHistory(String entityId) async {
|
@ -1,10 +1,10 @@
|
|||||||
part of 'main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class Device {
|
class DeviceInfoManager {
|
||||||
|
|
||||||
static final Device _instance = Device._internal();
|
static final DeviceInfoManager _instance = DeviceInfoManager._internal();
|
||||||
|
|
||||||
factory Device() {
|
factory DeviceInfoManager() {
|
||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ class Device {
|
|||||||
String osName;
|
String osName;
|
||||||
String osVersion;
|
String osVersion;
|
||||||
|
|
||||||
Device._internal();
|
DeviceInfoManager._internal();
|
||||||
|
|
||||||
loadDeviceInfo() {
|
loadDeviceInfo() {
|
||||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
176
lib/managers/location_manager.class.dart
Normal file
176
lib/managers/location_manager.class.dart
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class LocationManager {
|
||||||
|
|
||||||
|
static final LocationManager _instance = LocationManager
|
||||||
|
._internal();
|
||||||
|
|
||||||
|
factory LocationManager() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationManager._internal() {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int defaultUpdateIntervalMinutes = 20;
|
||||||
|
final String backgroundTaskId = "haclocationtask4352";
|
||||||
|
final String backgroundTaskTag = "haclocation";
|
||||||
|
Duration _updateInterval;
|
||||||
|
bool _isRunning;
|
||||||
|
|
||||||
|
void init() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.reload();
|
||||||
|
_updateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
||||||
|
defaultUpdateIntervalMinutes);
|
||||||
|
_isRunning = prefs.getBool("location-enabled") ?? false;
|
||||||
|
if (_isRunning) {
|
||||||
|
await _startLocationService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSettings(bool enabled, int interval) async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
if (interval != _updateInterval.inMinutes) {
|
||||||
|
prefs.setInt("location-interval", interval);
|
||||||
|
_updateInterval = Duration(minutes: interval);
|
||||||
|
if (_isRunning) {
|
||||||
|
Logger.d("Stopping location tracking...");
|
||||||
|
_isRunning = false;
|
||||||
|
await _stopLocationService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enabled && !_isRunning) {
|
||||||
|
Logger.d("Starting location tracking");
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setBool("location-enabled", enabled);
|
||||||
|
_isRunning = true;
|
||||||
|
await _startLocationService();
|
||||||
|
} else if (!enabled && _isRunning) {
|
||||||
|
Logger.d("Stopping location tracking...");
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setBool("location-enabled", enabled);
|
||||||
|
_isRunning = false;
|
||||||
|
await _stopLocationService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_startLocationService() async {
|
||||||
|
Logger.d("Scheduling location update for every ${_updateInterval
|
||||||
|
.inMinutes} minutes...");
|
||||||
|
String webhookId = ConnectionManager().webhookId;
|
||||||
|
String httpWebHost = ConnectionManager().httpWebHost;
|
||||||
|
if (webhookId != null && webhookId.isNotEmpty) {
|
||||||
|
await workManager.Workmanager.registerPeriodicTask(
|
||||||
|
backgroundTaskId,
|
||||||
|
"haClientLocationTracking",
|
||||||
|
tag: backgroundTaskTag,
|
||||||
|
inputData: {
|
||||||
|
"webhookId": webhookId,
|
||||||
|
"httpWebHost": httpWebHost
|
||||||
|
},
|
||||||
|
frequency: _updateInterval,
|
||||||
|
existingWorkPolicy: workManager.ExistingWorkPolicy.keep,
|
||||||
|
backoffPolicy: workManager.BackoffPolicy.linear,
|
||||||
|
backoffPolicyDelay: _updateInterval,
|
||||||
|
constraints: workManager.Constraints(
|
||||||
|
networkType: workManager.NetworkType.connected
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopLocationService() async {
|
||||||
|
Logger.d("Canceling previous schedule if any...");
|
||||||
|
await workManager.Workmanager.cancelByTag(backgroundTaskTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDeviceLocation() async {
|
||||||
|
if (ConnectionManager().webhookId != null &&
|
||||||
|
ConnectionManager().webhookId.isNotEmpty) {
|
||||||
|
String url = "${ConnectionManager()
|
||||||
|
.httpWebHost}/api/webhook/${ConnectionManager().webhookId}";
|
||||||
|
Map<String, String> headers = {};
|
||||||
|
Logger.d("[Location] Getting device location...");
|
||||||
|
Position location = await Geolocator().getCurrentPosition(
|
||||||
|
desiredAccuracy: LocationAccuracy.medium);
|
||||||
|
Logger.d("[Location] Got location: ${location.latitude} ${location
|
||||||
|
.longitude}. Sending home...");
|
||||||
|
int battery = await Battery().batteryLevel;
|
||||||
|
var data = {
|
||||||
|
"type": "update_location",
|
||||||
|
"data": {
|
||||||
|
"gps": [location.latitude, location.longitude],
|
||||||
|
"gps_accuracy": location.accuracy,
|
||||||
|
"battery": battery
|
||||||
|
}
|
||||||
|
};
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
await http.post(
|
||||||
|
url,
|
||||||
|
headers: headers,
|
||||||
|
body: json.encode(data)
|
||||||
|
);
|
||||||
|
Logger.d("[Location] ...done.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDeviceLocationIsolate() {
|
||||||
|
workManager.Workmanager.executeTask((backgroundTask, data) {
|
||||||
|
//print("[Background $backgroundTask] Started");
|
||||||
|
var battery = Battery();
|
||||||
|
int batteryLevel = 100;
|
||||||
|
String webhookId = data["webhookId"];
|
||||||
|
String httpWebHost = data["httpWebHost"];
|
||||||
|
if (webhookId != null && webhookId.isNotEmpty) {
|
||||||
|
//print("[Background $backgroundTask] hour=$battery");
|
||||||
|
String url = "$httpWebHost/api/webhook/$webhookId";
|
||||||
|
Map<String, String> headers = {};
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
Map data = {
|
||||||
|
"type": "update_location",
|
||||||
|
"data": {
|
||||||
|
"gps": [],
|
||||||
|
"gps_accuracy": 0,
|
||||||
|
"battery": batteryLevel
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//print("[Background $backgroundTask] Getting battery level...");
|
||||||
|
battery.batteryLevel.then((val) => data["data"]["battery"] = val).whenComplete((){
|
||||||
|
//print("[Background $backgroundTask] Getting device location...");
|
||||||
|
Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.medium).then((location) {
|
||||||
|
//print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}");
|
||||||
|
if (location != null) {
|
||||||
|
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||||
|
data["data"]["gps_accuracy"] = location.accuracy;
|
||||||
|
//print("[Background $backgroundTask] Sending data home...");
|
||||||
|
http.post(
|
||||||
|
url,
|
||||||
|
headers: headers,
|
||||||
|
body: json.encode(data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
//print("[Background $backgroundTask] Error getting current location: ${e.toString()}. Trying last known...");
|
||||||
|
Geolocator().getLastKnownPosition(desiredAccuracy: LocationAccuracy.medium).then((location){
|
||||||
|
//print("[Background $backgroundTask] Got last known location: ${location.latitude} ${location.longitude}");
|
||||||
|
if (location != null) {
|
||||||
|
data["data"]["gps"] = [location.latitude, location.longitude];
|
||||||
|
data["data"]["gps_accuracy"] = location.accuracy;
|
||||||
|
//print("[Background $backgroundTask] Sending data home...");
|
||||||
|
http.post(
|
||||||
|
url,
|
||||||
|
headers: headers,
|
||||||
|
body: json.encode(data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Future.value(true);
|
||||||
|
});
|
||||||
|
}
|
124
lib/managers/mobile_app_integration_manager.class.dart
Normal file
124
lib/managers/mobile_app_integration_manager.class.dart
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class MobileAppIntegrationManager {
|
||||||
|
|
||||||
|
static final _appRegistrationData = {
|
||||||
|
"app_version": "$appVersion",
|
||||||
|
"device_name": "",
|
||||||
|
"manufacturer": DeviceInfoManager().manufacturer,
|
||||||
|
"model": DeviceInfoManager().model,
|
||||||
|
"os_version": DeviceInfoManager().osVersion,
|
||||||
|
"app_data": {
|
||||||
|
"push_token": "",
|
||||||
|
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static Future checkAppRegistration({bool forceRegister: false, bool showOkDialog: false}) {
|
||||||
|
Completer completer = Completer();
|
||||||
|
_appRegistrationData["device_name"] = "${HomeAssistant().userName}'s ${DeviceInfoManager().model}";
|
||||||
|
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
|
||||||
|
if (ConnectionManager().webhookId == null || forceRegister) {
|
||||||
|
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
58
lib/managers/startup_user_messages_manager.class.dart
Normal file
58
lib/managers/startup_user_messages_manager.class.dart
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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-706";
|
||||||
|
|
||||||
|
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() {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setBool(_whatsNewMessageKey, true);
|
||||||
|
eventBus.fire(ShowPageEvent(path: "/whats-new"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
3861
lib/mdi.class.dart
3861
lib/mdi.class.dart
File diff suppressed because it is too large
Load Diff
@ -1,59 +1,72 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class EntityViewPage extends StatefulWidget {
|
class EntityViewPage extends StatefulWidget {
|
||||||
EntityViewPage({Key key, @required this.entityId, @required this.homeAssistant }) : super(key: key);
|
EntityViewPage({Key key, @required this.entityId}) : super(key: key);
|
||||||
|
|
||||||
final String entityId;
|
final String entityId;
|
||||||
final HomeAssistant homeAssistant;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_EntityViewPageState createState() => new _EntityViewPageState();
|
_EntityViewPageState createState() => new _EntityViewPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EntityViewPageState extends State<EntityViewPage> {
|
class _EntityViewPageState extends State<EntityViewPage> {
|
||||||
String _title;
|
|
||||||
StreamSubscription _refreshDataSubscription;
|
StreamSubscription _refreshDataSubscription;
|
||||||
StreamSubscription _stateSubscription;
|
StreamSubscription _stateSubscription;
|
||||||
|
Entity entity;
|
||||||
|
Entity forwardToMainPage;
|
||||||
|
bool _popScheduled = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
if (event.entityId == widget.entityId) {
|
if (event.entityId == widget.entityId) {
|
||||||
Logger.d("State change event handled by entity page: ${event.entityId}");
|
entity = HomeAssistant().entities.get(widget.entityId);
|
||||||
|
Logger.d("[Entity page] State change event handled: ${event.entityId}");
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||||
|
entity = HomeAssistant().entities.get(widget.entityId);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
_prepareData();
|
entity = HomeAssistant().entities.get(widget.entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _prepareData() async {
|
|
||||||
_title = widget.homeAssistant.entities.get(widget.entityId).displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Widget body;
|
||||||
|
if (MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
|
||||||
|
if (!_popScheduled) {
|
||||||
|
_popScheduled = true;
|
||||||
|
_popAfterBuild();
|
||||||
|
}
|
||||||
|
body = PageLoadingIndicator();
|
||||||
|
} else {
|
||||||
|
body = EntityPageLayout(entity: entity);
|
||||||
|
}
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}),
|
}),
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
title: new Text("${entity.displayName}"),
|
||||||
// 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: body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_popAfterBuild() async {
|
||||||
|
forwardToMainPage = entity;
|
||||||
|
await Future.delayed(Duration(milliseconds: 300));
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose(){
|
void dispose(){
|
||||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||||
|
eventBus.fire(ShowEntityPageEvent(entity: forwardToMainPage));
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
197
lib/pages/integration_settings.page.dart
Normal file
197
lib/pages/integration_settings.page.dart
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class IntegrationSettingsPage extends StatefulWidget {
|
||||||
|
IntegrationSettingsPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_IntegrationSettingsPageState createState() => new _IntegrationSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||||
|
|
||||||
|
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
|
||||||
|
bool _locationTrackingEnabled = false;
|
||||||
|
bool _wait = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSettings() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.reload();
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
setState(() {
|
||||||
|
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||||
|
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void incLocationInterval() {
|
||||||
|
if (_locationInterval < 720) {
|
||||||
|
setState(() {
|
||||||
|
_locationInterval = _locationInterval + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void decLocationInterval() {
|
||||||
|
if (_locationInterval > 1) {
|
||||||
|
setState(() {
|
||||||
|
_locationInterval = _locationInterval - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 want to STOP Home Assistant?",
|
||||||
|
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
||||||
|
positiveText: "Sure. Make it so",
|
||||||
|
negativeText: "What?? No!",
|
||||||
|
onPositive: () {
|
||||||
|
ConnectionManager().callService(domain: "homeassistant", service: "stop", 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);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
_switchLocationTrackingState(bool state) async {
|
||||||
|
if (state) {
|
||||||
|
await LocationManager().updateDeviceLocation();
|
||||||
|
}
|
||||||
|
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||||
|
setState(() {
|
||||||
|
_wait = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
title: new Text(widget.title),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/docs#location-tracking"),
|
||||||
|
child: Text(
|
||||||
|
"Please read documentation!",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontSize: 16,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Enable device location tracking"),
|
||||||
|
Switch(
|
||||||
|
value: _locationTrackingEnabled,
|
||||||
|
onChanged: _wait ? null : (value) {
|
||||||
|
setState(() {
|
||||||
|
_locationTrackingEnabled = value;
|
||||||
|
_wait = true;
|
||||||
|
});
|
||||||
|
_switchLocationTrackingState(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text("Location update interval in minutes:"),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
//Expanded(child: Container(),),
|
||||||
|
FlatButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
child: Text("-", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||||
|
onPressed: () => decLocationInterval(),
|
||||||
|
),
|
||||||
|
Text("$_locationInterval", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||||
|
FlatButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
child: Text("+", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
||||||
|
onPressed: () => incLocationInterval(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(),
|
||||||
|
Text("Integration status", 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 integration", style: TextStyle(color: Colors.white))
|
||||||
|
),
|
||||||
|
Container(width: 10.0,),
|
||||||
|
RaisedButton(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
onPressed: () => resetRegistration(),
|
||||||
|
child: Text("Reset integration", style: TextStyle(color: Colors.white))
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
911
lib/pages/main.page.dart
Normal file
911
lib/pages/main.page.dart
Normal file
@ -0,0 +1,911 @@
|
|||||||
|
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;
|
||||||
|
String _entityToShow;
|
||||||
|
|
||||||
|
@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();
|
||||||
|
LocationManager();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_firebaseMessaging.getToken().then((String token) {
|
||||||
|
HomeAssistant().fcmToken = token;
|
||||||
|
completer.complete();
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showOAuth() {
|
||||||
|
_preventAppRefresh = true;
|
||||||
|
Launcher.launchURLInCustomTab(
|
||||||
|
url: ConnectionManager().oauthUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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.... maybe
|
||||||
|
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) {
|
||||||
|
setState(() {
|
||||||
|
_entityToShow = entityId;
|
||||||
|
});
|
||||||
|
if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
|
||||||
|
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: () {
|
||||||
|
Launcher.launchURLInCustomTab(
|
||||||
|
url: "${ConnectionManager().httpWebHost}/profile?external_auth=1"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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("WEB", 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:server-network")),
|
||||||
|
title: Text("Connection settings"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushNamed('/connection-settings');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:cellphone-settings-variant")),
|
||||||
|
title: Text("Integration settings"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushNamed('/integration-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>> serviceMenuItems = [];
|
||||||
|
List<PopupMenuItem<String>> mediaMenuItems = [];
|
||||||
|
|
||||||
|
serviceMenuItems.add(PopupMenuItem<String>(
|
||||||
|
child: new Text("Reload"),
|
||||||
|
value: "reload",
|
||||||
|
));
|
||||||
|
if (ConnectionManager().isAuthenticated) {
|
||||||
|
_showLoginButton = false;
|
||||||
|
serviceMenuItems.add(
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: new Text("Logout"),
|
||||||
|
value: "logout",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Widget mediaMenuIcon;
|
||||||
|
int playersCount = 0;
|
||||||
|
if (!empty && !HomeAssistant().entities.isEmpty) {
|
||||||
|
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(domains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
||||||
|
playersCount = activePlayers.length;
|
||||||
|
mediaMenuItems.addAll(
|
||||||
|
activePlayers.map((entity) => PopupMenuItem<String>(
|
||||||
|
child: Text(
|
||||||
|
"${entity.displayName}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: EntityColor.stateColor(entity.state)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
value: "${entity.entityId}",
|
||||||
|
)).toList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
mediaMenuItems.addAll([
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: new Text("Play media..."),
|
||||||
|
value: "play_media",
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
if (playersCount > 0) {
|
||||||
|
mediaMenuIcon = Stack(
|
||||||
|
overflow: Overflow.visible,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:television"), color: Colors.white,),
|
||||||
|
Positioned(
|
||||||
|
bottom: -4,
|
||||||
|
right: -4,
|
||||||
|
child: Container(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
decoration: new BoxDecoration(
|
||||||
|
color: Colors.orange,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:television"), color: Colors.white,);
|
||||||
|
}
|
||||||
|
Widget mainScrollBody;
|
||||||
|
if (empty) {
|
||||||
|
if (_showLoginButton) {
|
||||||
|
mainScrollBody = Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
||||||
|
color: Colors.blue,
|
||||||
|
onPressed: () => _fullLoad(),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mainScrollBody = Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text("...")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
|
||||||
|
Entity entity = HomeAssistant().entities.get(_entityToShow);
|
||||||
|
mainScrollBody = Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: HomeAssistant().buildViews(context, _viewsTabController),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: Sizes.mainPageScreenSeparatorWidth,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
|
||||||
|
child: EntityPageLayout(entity: entity, showClose: true,),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_entityToShow = null;
|
||||||
|
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NestedScrollView(
|
||||||
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
|
return <Widget>[
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
primary: true,
|
||||||
|
title: Text(HomeAssistant().locationName ?? ""),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: mediaMenuIcon,
|
||||||
|
onPressed: () {
|
||||||
|
showMenu(
|
||||||
|
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100.0, 50, 0.0),
|
||||||
|
context: context,
|
||||||
|
items: mediaMenuItems
|
||||||
|
).then((String val) {
|
||||||
|
if (val == "play_media") {
|
||||||
|
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
|
||||||
|
} else {
|
||||||
|
_showEntityPage(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:dots-vertical"), color: Colors.white,),
|
||||||
|
onPressed: () {
|
||||||
|
showMenu(
|
||||||
|
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100, 0.0, 0.0),
|
||||||
|
context: context,
|
||||||
|
items: serviceMenuItems
|
||||||
|
).then((String val) {
|
||||||
|
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: mainScrollBody
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_viewsTabController?.dispose();
|
||||||
|
_stateSubscription?.cancel();
|
||||||
|
_settingsSubscription?.cancel();
|
||||||
|
_serviceCallSubscription?.cancel();
|
||||||
|
_showPopupDialogSubscription?.cancel();
|
||||||
|
_showPopupMessageSubscription?.cancel();
|
||||||
|
_showEntityPageSubscription?.cancel();
|
||||||
|
_showErrorSubscription?.cancel();
|
||||||
|
_startAuthSubscription?.cancel();
|
||||||
|
_subscription?.cancel();
|
||||||
|
_showPageSubscription?.cancel();
|
||||||
|
_reloadUISubscription?.cancel();
|
||||||
|
//TODO disconnect
|
||||||
|
//widget.homeAssistant?.disconnect();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -12,8 +12,6 @@ class PanelPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _PanelPageState extends State<PanelPage> {
|
class _PanelPageState extends State<PanelPage> {
|
||||||
|
|
||||||
List<ConfigurationItem> _items;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
245
lib/pages/play_media.page.dart
Normal file
245
lib/pages/play_media.page.dart
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class PlayMediaPage extends StatefulWidget {
|
||||||
|
|
||||||
|
final String mediaUrl;
|
||||||
|
final String mediaType;
|
||||||
|
|
||||||
|
PlayMediaPage({Key key, this.mediaUrl, this.mediaType}) : 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;
|
||||||
|
List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_mediaUrl = widget.mediaUrl;
|
||||||
|
if (widget.mediaType.isNotEmpty) {
|
||||||
|
if (!_contentTypes.contains(widget.mediaType)) {
|
||||||
|
_contentTypes.insert(0, widget.mediaType);
|
||||||
|
_contentType = _contentTypes[0];
|
||||||
|
} else {
|
||||||
|
_contentType = widget.mediaType;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_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(domains: ["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 if (entity.state == EntityState.unavailable || entity.state == EntityState.unknown) {
|
||||||
|
setState(() {
|
||||||
|
_validationMessage = "${entity.displayName} is not available";
|
||||||
|
});
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
);
|
||||||
|
HomeAssistant().sendToPlayerId = entity.entityId;
|
||||||
|
if (HomeAssistant().sendFromPlayerId != null && HomeAssistant().sendFromPlayerId != HomeAssistant().sendToPlayerId) {
|
||||||
|
eventBus.fire(ServiceCallEvent(HomeAssistant().sendFromPlayerId.split(".")[0], "turn_off", HomeAssistant().sendFromPlayerId, null));
|
||||||
|
HomeAssistant().sendFromPlayerId = null;
|
||||||
|
}
|
||||||
|
eventBus.fire(ShowEntityPageEvent(entity: 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(){
|
||||||
|
HomeAssistant().sendFromPlayerId = null;
|
||||||
|
_stateSubscription?.cancel();
|
||||||
|
_refreshDataSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -29,7 +29,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
_error = "Error connecting to store";
|
_error = "Error connecting to store";
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const Set<String> _kIds = {'just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
|
||||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||||
if (!response.notFoundIDs.isEmpty) {
|
if (!response.notFoundIDs.isEmpty) {
|
||||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||||
|
@ -74,6 +74,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_saveSettings() async {
|
_saveSettings() async {
|
||||||
|
_newHassioDomain = _newHassioDomain.trim();
|
||||||
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
if (_newHassioDomain.indexOf("http") == 0 && _newHassioDomain.indexOf("//") > 0) {
|
||||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||||
}
|
}
|
||||||
@ -81,12 +82,18 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
final storage = new FlutterSecureStorage();
|
final storage = new FlutterSecureStorage();
|
||||||
if (_newLongLivedToken.isNotEmpty) {
|
if (_newLongLivedToken.isNotEmpty) {
|
||||||
|
_newLongLivedToken = _newLongLivedToken.trim();
|
||||||
prefs.setBool("oauth-used", false);
|
prefs.setBool("oauth-used", false);
|
||||||
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
||||||
} else if (!useOAuth) {
|
} else if (!useOAuth) {
|
||||||
await storage.delete(key: "hacl_llt");
|
await storage.delete(key: "hacl_llt");
|
||||||
}
|
}
|
||||||
prefs.setString("hassio-domain", _newHassioDomain);
|
prefs.setString("hassio-domain", _newHassioDomain);
|
||||||
|
if (_newHassioPort == null || _newHassioPort.isEmpty) {
|
||||||
|
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
|
||||||
|
} else {
|
||||||
|
_newHassioPort = _newHassioPort.trim();
|
||||||
|
}
|
||||||
prefs.setString("hassio-port", _newHassioPort);
|
prefs.setString("hassio-port", _newHassioPort);
|
||||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||||
@ -147,13 +154,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Home Assistant domain or ip address"
|
labelText: "Home Assistant domain or ip address"
|
||||||
),
|
),
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _newHassioDomain,
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _newHassioDomain.length)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_newHassioDomain = value;
|
_newHassioDomain = value;
|
||||||
}
|
}
|
||||||
@ -162,13 +163,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Home Assistant port (default is 8123)"
|
labelText: "Home Assistant port (default is 8123)"
|
||||||
),
|
),
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _newHassioPort,
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _newHassioPort.length)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_newHassioPort = value;
|
_newHassioPort = value;
|
||||||
}
|
}
|
||||||
@ -216,13 +211,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Long-lived token"
|
labelText: "Long-lived token"
|
||||||
),
|
),
|
||||||
controller: new TextEditingController.fromValue(
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
|
||||||
new TextEditingValue(
|
|
||||||
text: _newLongLivedToken ?? '',
|
|
||||||
selection:
|
|
||||||
new TextSelection.collapsed(offset: _newLongLivedToken != null ? _newLongLivedToken.length : 0)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_newLongLivedToken = value;
|
_newLongLivedToken = value;
|
||||||
}
|
}
|
||||||
|
69
lib/pages/whats_new.page.dart
Normal file
69
lib/pages/whats_new.page.dart
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class WhatsNewPage extends StatefulWidget {
|
||||||
|
WhatsNewPage({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_WhatsNewPageState createState() => new _WhatsNewPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WhatsNewPageState extends State<WhatsNewPage> {
|
||||||
|
|
||||||
|
String data = "";
|
||||||
|
String error = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadData() async {
|
||||||
|
setState(() {
|
||||||
|
data = "";
|
||||||
|
error = "";
|
||||||
|
});
|
||||||
|
http.Response response;
|
||||||
|
response = await http.get("http://ha-client.homemade.systems/service/whats_new_$appVersionNumber.md");
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
setState(() {
|
||||||
|
data = response.body;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
error = "Can't load changelog";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget body;
|
||||||
|
if (error.isNotEmpty) {
|
||||||
|
body = PageLoadingError(errorText: error,);
|
||||||
|
} else if (data.isEmpty) {
|
||||||
|
body = PageLoadingIndicator();
|
||||||
|
} else {
|
||||||
|
body = Markdown(
|
||||||
|
data: data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
onPressed: () => _loadData(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
|
// the App.build method, and use it to set our appbar title.
|
||||||
|
title: new Text("What's new"),
|
||||||
|
),
|
||||||
|
body: body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,13 +10,19 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String period = "/ ";
|
String period = "";
|
||||||
Color priceColor;
|
Color priceColor;
|
||||||
|
String buttonText = '';
|
||||||
|
String buttonTextInactive = '';
|
||||||
if (product.id.contains("year")) {
|
if (product.id.contains("year")) {
|
||||||
period += "year";
|
period += "/ year";
|
||||||
|
buttonText = "Subscribe";
|
||||||
|
buttonTextInactive = "Already";
|
||||||
priceColor = Colors.amber;
|
priceColor = Colors.amber;
|
||||||
} else {
|
} else {
|
||||||
period += "month";
|
period += "";
|
||||||
|
buttonText = "Pay";
|
||||||
|
buttonTextInactive = "Paid";
|
||||||
priceColor = Colors.deepOrangeAccent;
|
priceColor = Colors.deepOrangeAccent;
|
||||||
}
|
}
|
||||||
return Card(
|
return Card(
|
||||||
@ -55,7 +61,7 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: RaisedButton(
|
child: RaisedButton(
|
||||||
child: Text(this.purchased ? "Bought" : "Buy", style: TextStyle(color: Colors.white)),
|
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
||||||
),
|
),
|
||||||
|
32
lib/panels/config_panel_widget.dart
Normal file
32
lib/panels/config_panel_widget.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
LinkToWebConfig(name: "Home Assistant configuration", url: ConnectionManager().httpWebHost+"/config"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -17,28 +17,25 @@ class Panel {
|
|||||||
final Map config;
|
final Map config;
|
||||||
String icon;
|
String icon;
|
||||||
bool isHidden = true;
|
bool isHidden = true;
|
||||||
|
bool isWebView = false;
|
||||||
|
|
||||||
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
|
||||||
if (icon == null || !icon.startsWith("mdi:")) {
|
if (icon == null || !icon.startsWith("mdi:")) {
|
||||||
icon = Panel.iconsByComponent[type];
|
icon = Panel.iconsByComponent[type];
|
||||||
}
|
}
|
||||||
isHidden = (type != "iframe" && type != "config");
|
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
|
||||||
|
isWebView = (type != 'config');
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleOpen(BuildContext context) {
|
void handleOpen(BuildContext context) {
|
||||||
if (type == "iframe") {
|
if (type == "config") {
|
||||||
Logger.d("Launching custom tab with ${config["url"]}");
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: config["url"]);
|
|
||||||
} else if (type == "config") {
|
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PanelPage(title: "$title", panel: this),
|
builder: (context) => PanelPage(title: "$title", panel: this),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
String url = "${Connection().httpWebHost}/$urlPath";
|
Launcher.launchURLInCustomTab(url: "${ConnectionManager().httpWebHost}/$urlPath");
|
||||||
Logger.d("Launching custom tab with $url");
|
|
||||||
HAUtils.launchURLInCustomTab(context: context, url: url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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.launchURLInCustomTab(url: this.url);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
}
|
149
lib/plugins/dynamic_multi_column_layout.dart
Normal file
149
lib/plugins/dynamic_multi_column_layout.dart
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
class DynamicMultiColumnLayout extends MultiChildRenderObjectWidget {
|
||||||
|
|
||||||
|
final int minColumnWidth;
|
||||||
|
|
||||||
|
DynamicMultiColumnLayout({
|
||||||
|
Key key,
|
||||||
|
this.minColumnWidth: 350,
|
||||||
|
List<Widget> children = const <Widget>[],
|
||||||
|
}) : super(key: key, children: children);
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderCustomLayoutBox createRenderObject(BuildContext context) {
|
||||||
|
return RenderCustomLayoutBox(minColumnWidth: this.minColumnWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenderCustomLayoutBox extends RenderBox
|
||||||
|
with ContainerRenderObjectMixin<RenderBox, CustomLayoutParentData>,
|
||||||
|
RenderBoxContainerDefaultsMixin<RenderBox, CustomLayoutParentData> {
|
||||||
|
|
||||||
|
final int minColumnWidth;
|
||||||
|
|
||||||
|
RenderCustomLayoutBox({
|
||||||
|
this.minColumnWidth,
|
||||||
|
List<RenderBox> children,
|
||||||
|
}) {
|
||||||
|
addAll(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setupParentData(RenderBox child) {
|
||||||
|
if (child.parentData is! CustomLayoutParentData) {
|
||||||
|
child.parentData = CustomLayoutParentData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getIntrinsicHeight(double childSize(RenderBox child)) {
|
||||||
|
double inflexibleSpace = 0.0;
|
||||||
|
RenderBox child = firstChild;
|
||||||
|
while (child != null) {
|
||||||
|
inflexibleSpace += childSize(child);
|
||||||
|
final FlexParentData childParentData = child.parentData;
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
return inflexibleSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getIntrinsicWidth(double childSize(RenderBox child)) {
|
||||||
|
double maxSpace = 0.0;
|
||||||
|
RenderBox child = firstChild;
|
||||||
|
while (child != null) {
|
||||||
|
maxSpace = math.max(maxSpace, childSize(child));
|
||||||
|
final FlexParentData childParentData = child.parentData;
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
return maxSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicWidth(double height) {
|
||||||
|
return _getIntrinsicWidth((RenderBox child) => child.getMinIntrinsicWidth(height));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicWidth(double height) {
|
||||||
|
return _getIntrinsicWidth((RenderBox child) => child.getMaxIntrinsicWidth(height));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMinIntrinsicHeight(double width) {
|
||||||
|
return _getIntrinsicHeight((RenderBox child) => child.getMinIntrinsicHeight(width));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double computeMaxIntrinsicHeight(double width) {
|
||||||
|
return _getIntrinsicHeight((RenderBox child) => child.getMaxIntrinsicHeight(width));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
int columnsCount;
|
||||||
|
List<double> columnXPositions = [];
|
||||||
|
List<double> columnYPositions = [];
|
||||||
|
if (constraints.maxWidth < this.minColumnWidth) {
|
||||||
|
columnsCount = 1;
|
||||||
|
} else {
|
||||||
|
columnsCount = (constraints.maxWidth ~/ this.minColumnWidth);
|
||||||
|
}
|
||||||
|
if (childCount == 0 || columnsCount == 0) {
|
||||||
|
size = constraints.biggest;
|
||||||
|
assert(size.isFinite);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double columnWidth = constraints.maxWidth / columnsCount;
|
||||||
|
double startY = 0;
|
||||||
|
for (int i =0; i < columnsCount; i++) {
|
||||||
|
columnXPositions.add(i*columnWidth);
|
||||||
|
columnYPositions.add(startY);
|
||||||
|
}
|
||||||
|
RenderBox child = firstChild;
|
||||||
|
while (child != null) {
|
||||||
|
final CustomLayoutParentData childParentData = child.parentData;
|
||||||
|
|
||||||
|
int columnToAdd = 0;
|
||||||
|
double minYPosition = columnYPositions[0];
|
||||||
|
for (int i=0; i<columnsCount; i++) {
|
||||||
|
if (columnYPositions[i] < minYPosition) {
|
||||||
|
minYPosition = columnYPositions[i];
|
||||||
|
columnToAdd = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child.layout(BoxConstraints.tightFor(width: columnWidth), parentUsesSize: true);
|
||||||
|
childParentData.offset = Offset(columnXPositions[columnToAdd], columnYPositions[columnToAdd]);
|
||||||
|
final Size newSize = child.size;
|
||||||
|
columnYPositions[columnToAdd] = minYPosition + newSize.height;
|
||||||
|
|
||||||
|
child = childParentData.nextSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
double width = constraints.maxWidth;
|
||||||
|
double height = 0;
|
||||||
|
for (int i=0; i<columnsCount; i++) {
|
||||||
|
if (columnYPositions[i] > height) {
|
||||||
|
height = columnYPositions[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size = Size(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
defaultPaint(context, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool hitTestChildren(HitTestResult result, { Offset position }) {
|
||||||
|
return defaultHitTestChildren(result, position: position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomLayoutParentData extends ContainerBoxParentData<RenderBox> {
|
||||||
|
|
||||||
|
}
|
@ -148,7 +148,7 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
if ((_selectedId == -1) && (numericDataLists.isNotEmpty)) {
|
||||||
_selectedId = 0;
|
_selectedId = numericDataLists.length -1;
|
||||||
}
|
}
|
||||||
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
List<charts.Series<EntityHistoryMoment, DateTime>> result = [];
|
||||||
numericDataLists.forEach((attrName, dataList) {
|
numericDataLists.forEach((attrName, dataList) {
|
||||||
@ -202,6 +202,11 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
@ -210,6 +215,12 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
@ -17,9 +17,7 @@ class EntityHistoryConfig {
|
|||||||
|
|
||||||
class EntityHistoryWidget extends StatefulWidget {
|
class EntityHistoryWidget extends StatefulWidget {
|
||||||
|
|
||||||
final EntityHistoryConfig config;
|
const EntityHistoryWidget({Key key}) : super(key: key);
|
||||||
|
|
||||||
const EntityHistoryWidget({Key key, @required this.config}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_EntityHistoryWidgetState createState() {
|
_EntityHistoryWidgetState createState() {
|
||||||
@ -33,6 +31,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
bool _needToUpdateHistory;
|
bool _needToUpdateHistory;
|
||||||
DateTime _historyLastUpdated;
|
DateTime _historyLastUpdated;
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
Entity entity;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -47,7 +46,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
}
|
}
|
||||||
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
if (_historyLastUpdated == null || now.difference(_historyLastUpdated).inSeconds > 30) {
|
||||||
_historyLastUpdated = now;
|
_historyLastUpdated = now;
|
||||||
Connection().getHistory(entityId).then((history){
|
ConnectionManager().getHistory(entityId).then((history){
|
||||||
if (!_disposed) {
|
if (!_disposed) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_history = history.isNotEmpty ? history[0] : [];
|
_history = history.isNotEmpty ? history[0] : [];
|
||||||
@ -75,10 +74,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
} else {
|
} else {
|
||||||
_loadHistory(entity.entityId);
|
_loadHistory(entity.entityId);
|
||||||
}
|
}
|
||||||
return _buildChart();
|
return _buildChart(entity.historyConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChart() {
|
Widget _buildChart(EntityHistoryConfig config) {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
if (_history == null) {
|
if (_history == null) {
|
||||||
children.add(
|
children.add(
|
||||||
@ -90,7 +89,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
children.add(
|
children.add(
|
||||||
_selectChartWidget()
|
_selectChartWidget(config)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
children.add(Divider());
|
children.add(Divider());
|
||||||
@ -102,8 +101,8 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _selectChartWidget() {
|
Widget _selectChartWidget(EntityHistoryConfig config) {
|
||||||
switch (widget.config.chartType) {
|
switch (config.chartType) {
|
||||||
|
|
||||||
case EntityHistoryWidgetType.simple: {
|
case EntityHistoryWidgetType.simple: {
|
||||||
return SimpleStateHistoryChartWidget(
|
return SimpleStateHistoryChartWidget(
|
||||||
@ -114,14 +113,14 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
|
|||||||
case EntityHistoryWidgetType.numericState: {
|
case EntityHistoryWidgetType.numericState: {
|
||||||
return NumericStateHistoryChartWidget(
|
return NumericStateHistoryChartWidget(
|
||||||
rawHistory: _history,
|
rawHistory: _history,
|
||||||
config: widget.config,
|
config: config,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case EntityHistoryWidgetType.numericAttributes: {
|
case EntityHistoryWidgetType.numericAttributes: {
|
||||||
return CombinedHistoryChartWidget(
|
return CombinedHistoryChartWidget(
|
||||||
rawHistory: _history,
|
rawHistory: _history,
|
||||||
config: widget.config,
|
config: config,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +103,7 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
id: widget.rawHistory.length
|
id: widget.rawHistory.length
|
||||||
));
|
));
|
||||||
if (_selectedId == -1) {
|
if (_selectedId == -1) {
|
||||||
_selectedId = 0;
|
_selectedId = data.length - 1;
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
@ -132,6 +132,11 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
@ -140,6 +145,12 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
@ -101,7 +101,7 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
colorId: data.last.colorId
|
colorId: data.last.colorId
|
||||||
));
|
));
|
||||||
if (_selectedId == -1) {
|
if (_selectedId == -1) {
|
||||||
_selectedId = 0;
|
_selectedId = data.length - 1;
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
@ -137,14 +137,25 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
_selectedId -= 1;
|
_selectedId -= 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = _parsedHistory.first.data.length - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectNext() {
|
void _selectNext() {
|
||||||
if (_selectedId < (_parsedHistory.first.data.length - 2)) {
|
if (_selectedId < (_parsedHistory.first.data.length - 1)) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedId += 1;
|
_selectedId += 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
setState(() {
|
||||||
|
_selectedId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(charts.SelectionModel model) {
|
void _onSelectionChanged(charts.SelectionModel model) {
|
44
lib/plugins/spoiler_card.dart
Normal file
44
lib/plugins/spoiler_card.dart
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SpoilerCard extends StatefulWidget {
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Widget body;
|
||||||
|
final bool isExpanded;
|
||||||
|
|
||||||
|
SpoilerCard({Key key, @required this.title, @required this.body, this.isExpanded: false}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SpoilerCardState createState() => _SpoilerCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpoilerCardState extends State<SpoilerCard> {
|
||||||
|
|
||||||
|
bool _expanded;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_expanded = widget.isExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
title: Text("${widget.title}"),
|
||||||
|
trailing: Icon(
|
||||||
|
_expanded ? Icons.arrow_drop_up : Icons.arrow_drop_down,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onTap: () => setState((){_expanded = !_expanded;}),
|
||||||
|
),
|
||||||
|
_expanded ? widget.body : Container(height: 0,)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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 {
|
||||||
|
final 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);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user