Compare commits

...

167 Commits

Author SHA1 Message Date
7d746fd546 896 2020-04-29 07:11:09 +00:00
3ff55f181e Fix icon_height parsing issue 2020-04-29 10:00:28 +03:00
187e12dd79 895 2020-04-29 01:19:43 +03:00
10daf2d952 0.8.5 2020-04-28 21:47:41 +00:00
31c6509d13 0.8.5 2020-04-28 21:43:48 +00:00
cb74108814 893 2020-04-28 21:12:18 +00:00
9efded2139 Possible workaround for firebase messaging crash 2020-04-28 21:09:45 +00:00
96b3e7c739 Add state_color support 2020-04-28 21:03:00 +00:00
b029146bf3 Gauge scale fixes 2020-04-28 20:36:11 +00:00
d715aaf5e5 Gauge card elements scale fix 2020-04-27 20:56:33 +00:00
0dc12963f0 Cards parsing improvements 2020-04-26 22:46:37 +00:00
4da3b40d55 WIP: Cards parsing improvements 2020-04-26 18:44:21 +00:00
f7d05a57ad Use '***' in markdawn card to add empty space 2020-04-25 21:45:58 +00:00
df01599fe0 Resloves #541 Prevent double service call when slider is moved 2020-04-25 21:24:15 +00:00
2c3335ebf3 Resolves #539 Fix button card without entity 2020-04-25 20:53:08 +00:00
05c1427aa8 Add icon support for entities card title 2020-04-25 18:23:26 +00:00
02bfaf7db6 WIP: Cards build optimization 2020-04-25 17:38:21 +00:00
f488c0810b WIP: Cards build optimization 2020-04-25 15:59:07 +00:00
8dbfb91234 Add Lovelase card widget 2020-04-25 14:53:33 +00:00
aee99e3925 Entities card build optimization 2020-04-25 14:39:15 +00:00
50d3280803 Gauge font size fix 2020-04-25 14:33:54 +00:00
a90eb5c4db Fix glance card title 2020-04-25 14:20:46 +00:00
16c06a2d48 Widget rendering improvements 2020-04-25 14:15:19 +00:00
513bf85cae Fix dashboard switching issues 2020-04-21 09:01:21 +00:00
82d7aeba02 Some cleanup 2020-04-15 18:23:29 +00:00
12f7cb86de Linear progress indicator 2020-04-15 18:21:16 +00:00
b65c885467 Bottom info bar as standalone component 2020-04-15 18:13:03 +00:00
2a828a1289 WIP: bottom info bar as separate component 2020-04-15 15:40:21 +00:00
291f12ba97 Component detection 2020-04-15 15:40:21 +00:00
6afbd37d71 892 2020-04-15 15:46:06 +03:00
0e8869878f 0.8.4 2020-04-15 12:06:06 +00:00
7c2cfe3215 MobileApp integration force update 2020-04-15 11:59:40 +00:00
c376c0e952 Minor fix 2020-04-14 21:08:10 +00:00
da5f663396 Gauge card optimizations 2020-04-14 18:39:21 +00:00
0e92418a33 Handle missed entity on Entity page 2020-04-14 18:15:28 +00:00
2eef7cfe5e Improve Horizontal and Vertical stack building 2020-04-13 19:24:37 +00:00
de4e0bfb3a Add new Button card support 2020-04-13 18:17:14 +00:00
8bf2d31e72 Bring back separate entity page 2020-04-13 18:15:14 +00:00
2125c46143 Gauge style improvements 2020-04-13 14:01:17 +00:00
5402eb84df Some default icons update 2020-04-13 13:13:49 +00:00
ad5aa0898f Set header toggle default to false 2020-04-13 13:05:59 +00:00
040d40b614 PayPal donate button 2020-04-11 19:26:25 +00:00
8e58f22c56 Merge pull request #533 from estevez-dev/pre-release/889
890
2020-04-11 21:16:12 +03:00
c91695fbe5 890 2020-04-11 21:15:08 +03:00
c43741da49 889 2020-04-11 20:28:35 +03:00
f2563a0397 Fix startup crash 2020-04-11 20:25:54 +03:00
fba4017819 0.8.3 2020-04-11 16:55:50 +00:00
5f23e108a1 Settings page and theme selection 2020-04-11 16:09:35 +00:00
68d14bd13d Fallback to MJPEG camera stream if streaming component is missing 2020-04-11 13:13:20 +00:00
022622522f Fix section text color for dark theme 2020-04-11 12:48:36 +00:00
89513ca4e5 Secrets config for CI/CD 2020-04-09 17:10:21 +00:00
a934ee2335 Hide zha panel 2020-04-09 16:25:35 +00:00
49aeea634f 0.8.2 2020-04-09 16:24:37 +00:00
e18b9ebe14 Replace VideoPlayer with web player 2020-04-08 16:48:13 +00:00
08ee3f3d80 Fix inactive gauge color 2020-04-07 20:30:39 +00:00
62d07bf8b9 Gauge card refactoring 2020-04-07 20:20:57 +00:00
ab398cbdc3 Remove debugg banner 2020-04-06 21:43:38 +00:00
007d12719c WIP #102 Moving all colors to theme 2020-04-06 21:39:16 +00:00
524d195800 WIP #102 Colors from theme 2020-04-06 20:03:41 +00:00
405de64249 Fix double events handling 2020-04-04 22:34:54 +00:00
f53554702e WIP Themes: new dark theme 2020-04-04 22:08:50 +00:00
379e1a4a7e WIP Themes: State colors from themes 2020-04-04 21:39:12 +00:00
d6f7096055 WIP Themes: New light theme 2020-04-04 20:54:32 +00:00
37c721e4f6 WIP Themes: Entity page heade color 2020-04-04 16:19:25 +00:00
d94235ef6d WIP Themes: Dark theme fonts 2020-04-04 16:13:12 +00:00
eb4184713f WIP Themes: badges colors 2020-04-04 15:44:06 +00:00
a0a0cb4612 WIP Themes: Make all fonts depend on theme 2020-04-04 15:13:55 +00:00
f448a20784 WIP Themes: primary colors fix 2020-04-04 13:36:32 +00:00
36eff26862 WIP themes: fix sudhead style 2020-04-04 13:27:23 +00:00
5b2a1163b9 Merge pull request #529 from estevez-dev/hotfix/0.8.1
Fix for older HA versions
2020-04-04 16:22:24 +03:00
e627a8b963 Fix for older HA versions 2020-04-04 13:17:01 +00:00
4432124e8c WIP Themes: font size standartization 2020-04-04 12:47:40 +00:00
b8ba3c59e9 WIP Themes: climate controls and entity name 2020-04-04 12:00:15 +00:00
c40a496b6b Replace Spectrum with Discord 2020-04-04 10:12:21 +00:00
a7c3b46061 Update pubspec.yaml 2020-04-03 17:25:59 +03:00
dfbaaeb06b 0.8.0 2020-04-03 13:32:23 +00:00
f6ab20c6e8 Add whats new manu item 2020-04-03 13:29:02 +00:00
7625099d74 Disable background location log 2020-04-03 13:17:24 +00:00
32c8e76855 Fix for non-string climate mode values 2020-04-03 13:07:26 +00:00
0aa2c974d5 Disable crashlytics in debug mode 2020-04-03 12:43:48 +00:00
9524c8587b Update WHats new link 2020-04-03 12:36:16 +00:00
c075db8b1a 883 2020-04-02 22:05:36 +00:00
d0b7cc1929 Initial UI generation if no lovelace config 2020-04-02 22:04:56 +00:00
d8df32f140 Resolves #524 Geolocator update 2020-04-02 21:16:34 +00:00
293b5e0242 zha_map WIP: get inital data 2020-04-01 17:04:32 +00:00
2f517a3ad5 ui build refactoring 1 2020-03-21 23:11:00 +00:00
56d8e389db Do not update plugins on each gitpod start 2020-03-21 20:45:01 +00:00
1377843350 882 2020-03-21 14:06:58 +00:00
8e31eaf8bb Fix dashboard switching and dashboard icons 2020-03-20 10:14:14 +00:00
5ced01463f Resolves #526 Subscribe to Lovelace update events 2020-03-19 23:16:59 +00:00
a3548455eb Resolves #525 Support Lovelace dashboards 2020-03-19 22:59:53 +00:00
c40fceea4f add log output on location error in background 2020-03-15 17:51:01 +00:00
6ad3938a91 Resolves #369 Get actual entity data in entity wrapper 2020-03-15 16:28:45 +00:00
bc642f81ad Fix caching 2020-03-15 15:26:03 +00:00
14ce608bbb Add data caching 2020-03-15 13:47:51 +00:00
c4c67747c5 Remove services requesting 2020-03-15 12:34:16 +00:00
5b3ceecb0e Update crashlytics 2020-03-15 12:27:32 +00:00
bf53e4b9df Add fit option for EntityPicture widget 2020-03-14 18:23:39 +00:00
7e09d92fdf Add double_tap_action support 2020-03-14 18:12:11 +00:00
1ba9106d0b Show entity picture while camera stream loading 2020-03-14 17:56:07 +00:00
d727a29991 881 2020-03-12 21:20:46 +00:00
c5d617477f Fix state filter when state is number 2020-03-12 21:19:39 +00:00
244a1984cc Resolves #512 Support all state_filter options 2020-03-12 21:16:07 +00:00
b00b745f27 Camera fullscreen view 2020-03-12 12:22:38 +00:00
959ff21b9b Fix checking log file size if it doesnt exist 2020-03-09 15:01:53 +00:00
e6a7fd2dfe workmanager 0.2.2, geolocator 5.3.0, crashlytics 0.1.3 2020-03-09 14:57:29 +00:00
216276e5f3 Limit log file size to 5MB. Background location fixes 2020-03-09 14:53:12 +00:00
3e6229cf3e Add log file for background service 2020-03-09 13:11:16 +00:00
fc4cb80b74 Fix background task execution 2020-03-05 09:08:47 +00:00
b907ff1e82 Location isolate return 2020-03-04 17:16:16 +00:00
7536a52771 Clear camera viewer 2020-03-04 16:59:08 +00:00
73a8c111d1 Camera stream controls 2020-02-21 15:36:03 +00:00
86a19eeec2 Camera stream web view improvements 2020-02-21 11:23:39 +00:00
fba4459977 Camera stream view fixes 2020-02-20 22:42:19 +00:00
06f994a827 Camera stream aspect ratio calculations 2020-02-20 21:48:22 +00:00
35d8607484 Resolves #370 Camera stream support 2020-02-20 14:33:03 +00:00
2f4c06e9b5 Resolves #420 Camera view on entity page 2020-02-20 11:18:09 +00:00
92e008a380 Resolves #522 Fix temperature change for thermostat 2020-02-19 14:20:44 +00:00
14c272af92 Resolves #501 Entities card header toggle 2020-02-19 10:17:08 +00:00
710de9f2b8 Resolves #514 Media player volume buttons placement 2020-02-19 09:27:01 +00:00
d9ad3b3083 Location tracking... 2020-02-17 19:23:00 +00:00
b2686cb105 Update README.md 2020-02-17 16:39:30 +02:00
959e89de2b Remove web UI presentation option 2020-02-17 13:50:24 +00:00
6e448d3458 JS interface improvements 2020-02-12 22:42:43 +00:00
6695756727 Embedded webview 2020-02-12 21:13:49 +00:00
ed732e9b77 Remove bottom bar for webview 2020-02-12 13:37:52 +00:00
f495a6affc Move to ha-client.app 2020-02-12 13:22:48 +00:00
c8d7e1a95f Update README.md 2020-02-12 12:39:36 +02:00
e1ca2638e3 External bus and configuration opening for Web UI 2020-02-11 22:55:16 +00:00
01226cb9eb WebVIew UI settings 2020-02-11 20:53:29 +00:00
8a80d0c5d1 WebView UI experiment 001 2020-02-11 14:02:32 +00:00
f26f3e87c7 Remove Crashlytics debug 2020-02-11 12:57:31 +00:00
b750417415 Replace Sentry with Crashlytics 2020-02-11 12:06:19 +00:00
2c35dd7c21 Update battery plugin 2020-02-11 11:25:58 +00:00
cff4a4feed Update workmanager and migrate to Flutter embedding v2 2020-02-11 11:22:59 +00:00
62174b0651 Update geolocator plugin 2020-02-11 11:07:13 +00:00
d3ea4210c1 Update local notifications library 2020-02-11 11:03:39 +00:00
1c782bf64d Update firebase library and downgrade gradle 2020-02-11 10:57:15 +00:00
bc96dab339 Update gradle and Google services 2020-02-11 09:52:04 +00:00
0f7179b944 In app purchase update and optimizations 2020-02-11 09:39:11 +00:00
1e3bfa8ff7 Install Java in gitpod with sdkman 2020-02-07 09:46:34 +00:00
2bce86f905 gitpod config 2020-02-07 09:27:23 +00:00
0be00acc3a do 2020-01-30 20:50:42 +00:00
4e61adaeb1 Fixind java version switch 2020-01-30 20:42:29 +00:00
49a8f08153 Update .gitpod.dockerfile 2020-01-30 22:34:10 +02:00
ce15658462 Add jdk version set 2020-01-30 20:28:34 +00:00
16d73ba7dd Update .gitpod.yml 2020-01-30 22:20:54 +02:00
9f3e3c1917 Update .gitpod.dockerfile 2020-01-30 22:20:01 +02:00
f29e382a19 Update .gitpod.yml 2020-01-30 21:53:29 +02:00
073562373a Update .gitpod.dockerfile 2020-01-30 21:43:28 +02:00
4298ebcd66 Bump Flutter version to 1.12.13 2020-01-29 20:13:28 +02:00
a121295bef Update .gitignore 2020-01-29 20:12:46 +02:00
9303e4c0a5 Force https for oauth 2020-01-29 17:38:51 +00:00
831fc98ab1 Change domain for ha-client website 2020-01-29 17:31:16 +00:00
2003005e56 Update README.md 2020-01-29 11:48:32 +02:00
fda8fb7182 Webview for external panels and everything 2020-01-27 21:25:55 +00:00
cf6039b279 Back to webview oauth 2020-01-27 21:12:05 +00:00
41e552dce5 New webview test 2020-01-27 20:06:02 +00:00
90043b5806 Remove text sharing feature 2020-01-27 19:21:21 +00:00
9eb74b5a8d WIP Share media refactoring 2019-12-11 18:24:09 +00:00
9cc60a136b 0.7.7 2019-12-10 22:05:12 +00:00
78eb1e779c Fix state event handling before fetch complete 2019-12-10 22:04:24 +00:00
8db2d8508e Resolves #515 tabs controller issue 2019-12-10 22:00:17 +00:00
3f1ece26ec Remove background task logging and reporting 2019-12-10 21:19:29 +00:00
d1912a44c6 Replace Discord with Spectrum 2019-12-10 19:54:02 +00:00
36a05eb390 Update README.md 2019-12-06 10:58:23 +02:00
4f39ea1ad8 Update README.md 2019-12-04 16:31:03 +02:00
116 changed files with 4054 additions and 3533 deletions

3
.gitignore vendored
View File

@ -15,7 +15,8 @@ build/
.settings/
flutter_export_environment.sh
.flutter-plugins-dependencies
key.properties
premium_features_manager.class.dart
.secrets.dart
pubspec.lock

View File

@ -4,9 +4,5 @@ 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
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
&& sdk install java 8.0.242.j9-adpt"

View File

@ -3,12 +3,12 @@ image:
tasks:
- before: |
export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/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
cd /workspace && wget -qO flutter_sdk.tar.xz https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.12.13+hotfix.7-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"
@ -18,7 +18,6 @@ tasks:
flutter doctor --android-licenses
flutter pub get
command: |
flutter pub upgrade
echo "Ready to go!"
flutter doctor
vscode:

Binary file not shown.

Binary file not shown.

View File

View File

@ -1,13 +1,14 @@
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
# HA Client
## Native Android client for Home Assistant
### With notifications and Lovelace UI support
Visit [homemade.systems](http://ha-client.homemade.systems/) for more info.
Visit [ha-client.app](http://ha-client.app/) for more info.
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
Discuss it 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 on [Discord](https://discord.gg/nd6FZQ) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
#### Pre-release CI build
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)

View File

@ -78,10 +78,11 @@ flutter {
}
dependencies {
implementation 'com.google.firebase:firebase-core:16.0.8'
implementation 'com.google.firebase:firebase-analytics:17.2.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
apply plugin: 'io.fabric'
apply plugin: 'com.google.gms.google-services'

View File

@ -6,9 +6,9 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
@ -17,11 +17,15 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name=".Application"
android:name="io.flutter.app.FlutterApplication"
android:label="HA Client"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="ha_notify" />
@ -33,13 +37,12 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme).
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />-->
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
@ -48,14 +51,6 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</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>
<service

View File

@ -1,20 +0,0 @@
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);
}
}

View File

@ -1,15 +1,15 @@
package com.keyboardcrumbs.hassclient;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.share.FlutterShareReceiverActivity;
public class MainActivity extends FlutterShareReceiverActivity {
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
}

View File

@ -5,4 +5,7 @@
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

View File

@ -2,11 +2,15 @@ buildscript {
repositories {
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'io.fabric.tools:gradle:1.26.1'
}
}
@ -14,6 +18,9 @@ allprojects {
repositories {
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
}

View File

@ -3,3 +3,4 @@ org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true

View File

@ -0,0 +1 @@
include ':app'

View File

@ -0,0 +1,61 @@
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style>
body {
padding: 0;
margin: 0;
widows: 100%;
height: 100%;
}
video {
width: 100%;
}
</style>
<script>
var messageChannel = '{{message_channel}}';
</script>
</head>
<body>
<video id="screen" width="100%" controls></video>
<script>
if (Hls.isSupported()) {
var video = document.getElementById('screen');
var hls = new Hls();
hls.on(Hls.Events.ERROR, function (event, data) {
if (data.fatal) {
switch(data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
// try to recover network error
console.log("fatal network error encountered, try to recover");
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log("fatal media error encountered, try to recover");
hls.recoverMediaError();
break;
default:
// cannot recover
hls.destroy();
break;
}
}
});
// bind them together
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
console.log("video and hls.js are now bound together !");
hls.loadSource("{{stream_url}}");
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
console.log("manifest loaded, found " + data.levels.length + " quality level");
video.play();
video.onloadedmetadata = function() {
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
};
});
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,28 @@
<html>
<head>
<style>
body {
padding: 0;
margin: 0;
widows: 100%;
height: 100%;
}
img {
width: 100%;
}
</style>
<script>
var messageChannel = '{{message_channel}}';
window.onload = function() {
var img = document.getElementById('screen');
if (img) {
window[messageChannel].postMessage(document.body.clientWidth / img.offsetHeight);
}
};
</script>
</head>
<body>
<img id="screen" src="{{stream_url}}">
</body>
</html>

View File

@ -14,3 +14,22 @@ window.externalApp.getExternalAuth = function(options) {
}, 500);
}
};
window.externalApp.externalBus = function(message) {
console.log("External bus message: " + message);
var messageObj = JSON.parse(message);
if (messageObj.type == "config/get") {
var responseData = {
id: messageObj.id,
type: "result",
success: true,
result: {
hasSettingsScreen: true
}
};
setTimeout(function(){
window.externalBus(responseData);
}, 500);
} else if (messageObj.type == "config_screen/show") {
HAClient.postMessage('show-settings');
}
};

View File

@ -0,0 +1,62 @@
part of '../main.dart';
class AlarmPanelCard extends StatelessWidget {
final AlarmPanelCardData card;
const AlarmPanelCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
}
List<Widget> body = [];
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.entity.entity.displayState}",
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
EntityIcon(
size: 50.0,
),
Container(
width: 26.0,
child: IconButton(
padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.entity.entity))
)
)
]
),
));
body.add(
AlarmControlPanelControlsWidget(
extended: true,
states: card.states,
)
);
return CardWrapper(
child: EntityModel(
entityWrapper: card.entity,
handleTap: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: body
)
)
);
}
}

View File

@ -1,64 +1,510 @@
part of '../main.dart';
class HACard {
List<EntityWrapper> entities = [];
List<HACard> childCards = [];
EntityWrapper linkedEntityWrapper;
String name;
String id;
class CardData {
String type;
bool showName;
bool showState;
bool showEmpty;
int columnsCount;
List stateFilter;
List states;
List<EntityWrapper> entities = [];
List conditions;
String content;
bool showEmpty;
List stateFilter;
bool stateColor = true;
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
factory CardData.parse(Map<String, dynamic> rawData) {
switch (rawData['type']) {
case CardType.ENTITIES:
return EntitiesCardData(rawData);
break;
case CardType.ALARM_PANEL:
return AlarmPanelCardData(rawData);
break;
case CardType.BUTTON:
return ButtonCardData(rawData);
break;
case CardType.ENTITY_BUTTON:
return ButtonCardData(rawData);
break;
case CardType.CONDITIONAL:
return CardData.parse(rawData['card']);
break;
case CardType.ENTITY_FILTER:
Map<String, dynamic> cardData = Map.from(rawData);
cardData.remove('type');
if (rawData.containsKey('card')) {
cardData.addAll(rawData['card']);
}
cardData['type'] ??= CardType.ENTITIES;
return CardData.parse(cardData);
break;
case CardType.GAUGE:
return GaugeCardData(rawData);
break;
case CardType.GLANCE:
return GlanceCardData(rawData);
break;
case CardType.HORIZONTAL_STACK:
return HorizontalStackCardData(rawData);
break;
case CardType.VERTICAL_STACK:
return VerticalStackCardData(rawData);
break;
case CardType.MARKDOWN:
return MarkdownCardData(rawData);
break;
case CardType.MEDIA_CONTROL:
return MediaControlCardData(rawData);
break;
default:
if (rawData.containsKey('entities')) {
return EntitiesCardData(rawData);
} else if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
return EntitiesCardData(rawData);
}
return CardData(rawData);
}
}
CardData(Map<String, dynamic> rawData) {
type = rawData['type'] ?? CardType.ENTITIES;
conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true;
stateFilter = rawData['state_filter'] ?? [];
}
Widget buildCardWidget() {
return UnsupportedCard(card: this);
}
List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) {
if (HomeAssistant().autoUi && entityWrapper.entity.isHidden) {
return false;
}
List currentStateFilter;
if (entityWrapper.stateFilter != null && entityWrapper.stateFilter.isNotEmpty) {
currentStateFilter = entityWrapper.stateFilter;
} else {
currentStateFilter = stateFilter;
}
bool showByFilter = currentStateFilter.isEmpty;
for (var allowedState in currentStateFilter) {
if (allowedState is String && allowedState == entityWrapper.entity.state) {
showByFilter = true;
break;
} else if (allowedState is Map) {
try {
var tmpVal = allowedState['attribute'] != null ? entityWrapper.entity.getAttribute(allowedState['attribute']) : entityWrapper.entity.state;
var valToCompareWith = allowedState['value'];
var valToCompare;
if (valToCompareWith is! String && tmpVal is String) {
valToCompare = double.tryParse(tmpVal);
} else {
valToCompare = tmpVal;
}
if (valToCompare != null) {
bool result;
switch (allowedState['operator']) {
case '<=': { result = valToCompare <= valToCompareWith;}
break;
case '<': { result = valToCompare < valToCompareWith;}
break;
case '>=': { result = valToCompare >= valToCompareWith;}
break;
case '>': { result = valToCompare > valToCompareWith;}
break;
case '!=': { result = valToCompare != valToCompareWith;}
break;
case 'regex': {
RegExp regExp = RegExp(valToCompareWith.toString());
result = regExp.hasMatch(valToCompare.toString());
}
break;
default: {
result = valToCompare == valToCompareWith;
}
}
if (result) {
showByFilter = true;
break;
}
}
} catch (e) {
Logger.e('Error filtering ${entityWrapper.entity.entityId} by $allowedState');
Logger.e('$e');
}
}
}
return showByFilter;
}).toList();
}
}
class EntitiesCardData extends CardData {
String title;
String icon;
bool showHeaderToggle;
@override
Widget buildCardWidget() {
return EntitiesCard(card: this);
}
EntitiesCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
title = rawData["title"];
icon = rawData['icon'];
stateColor = rawData['state_color'] ?? false;
showHeaderToggle = rawData['show_header_toggle'] ?? false;
//Parsing entities
var rawEntities = rawData["entities"] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (rawEntity["type"] == "divider") {
entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
entities.add(
EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
stateColor: rawEntity["state_color"] ?? stateColor,
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
stateColor: rawEntity["state_color"] ?? stateColor,
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
entities.add(
EntityWrapper(
entity: e,
stateColor: rawEntity["state_color"] ?? stateColor,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
}
}
class AlarmPanelCardData extends CardData {
String name;
List<dynamic> states;
@override
Widget buildCardWidget() {
return AlarmPanelCard(card: this);
}
AlarmPanelCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
states = rawData['states'];
//Parsing entity
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
stateColor: true,
overrideName: name
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
}
}
}
class ButtonCardData extends CardData {
String name;
String icon;
bool showName;
bool showIcon;
double iconHeightPx = 0;
double iconHeightRem = 0;
@override
Widget buildCardWidget() {
return EntityButtonCard(card: this);
}
ButtonCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
icon = rawData['icon'];
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
stateColor = rawData['state_color'] ?? true;
var rawHeight = rawData['icon_height'];
if (rawHeight != null && rawHeight is String) {
if (rawHeight.contains('px')) {
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
} else if (rawHeight.contains('rem')) {
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
} else if (rawHeight.contains('em')) {
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
}
}
//Parsing entity
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
overrideName: name,
overrideIcon: icon,
stateColor: stateColor,
uiAction: EntityUIAction(
rawEntityData: rawData
)
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
} else if (entitiId == null) {
entities.add(
EntityWrapper(
entity: Entity.ghost(
name,
icon,
),
stateColor: stateColor,
uiAction: EntityUIAction(
rawEntityData: rawData
)
)
);
}
}
}
class GaugeCardData extends CardData {
String name;
String unit;
int min;
int max;
Map severity;
HACard({
this.name,
this.id,
this.linkedEntityWrapper,
this.columnsCount: 4,
this.showName: true,
this.showState: true,
this.stateFilter: const [],
this.showEmpty: true,
this.content,
this.states,
this.conditions: const [],
this.unit,
this.min,
this.max,
this.severity,
@required this.type
}) {
if (this.columnsCount <= 0) {
this.columnsCount = 4;
@override
Widget buildCardWidget() {
return GaugeCard(card: this);
}
GaugeCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
unit = rawData['unit'];
min = rawData['min'] ?? 0;
max = rawData['max'] ?? 100;
severity = rawData['severity'];
//Parsing entity
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
overrideName: name
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
}
List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) {
if (!ConnectionManager().useLovelace && entityWrapper.entity.isHidden) {
return false;
}
if (stateFilter.isNotEmpty) {
return stateFilter.contains(entityWrapper.entity.state);
}
return true;
}).toList();
}
Widget build(BuildContext context) {
return CardWidget(
card: this,
);
}
}
class GlanceCardData extends CardData {
String title;
bool showName;
bool showIcon;
bool showState;
bool stateColor;
int columnsCount;
@override
Widget buildCardWidget() {
return GlanceCard(card: this);
}
GlanceCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
title = rawData["title"];
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
showState = rawData['show_state'] ?? true;
stateColor = rawData['state_color'] ?? true;
columnsCount = rawData['columns'] ?? 4;
//Parsing entities
var rawEntities = rawData["entities"] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
entities.add(
EntityWrapper(
entity: e,
stateColor: stateColor,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
}
}
class HorizontalStackCardData extends CardData {
List<CardData> childCards;
@override
Widget buildCardWidget() {
return HorizontalStackCard(card: this);
}
HorizontalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
if (rawData.containsKey('cards')) {
childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard);
}).toList();
} else {
childCards = [];
}
}
}
class VerticalStackCardData extends CardData {
List<CardData> childCards;
@override
Widget buildCardWidget() {
return VerticalStackCard(card: this);
}
VerticalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
if (rawData.containsKey('cards')) {
childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard);
}).toList();
} else {
childCards = [];
}
}
}
class MarkdownCardData extends CardData {
String title;
String content;
@override
Widget buildCardWidget() {
return MarkdownCard(card: this);
}
MarkdownCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
title = rawData['title'];
content = rawData['content'];
}
}
class MediaControlCardData extends CardData {
@override
Widget buildCardWidget() {
return MediaControlsCard(card: this);
}
MediaControlCardData(Map<String, dynamic> rawData) : super(rawData) {
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
}
}
}

View File

@ -1,363 +0,0 @@
part of '../main.dart';
class CardWidget extends StatelessWidget {
final HACard card;
const CardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.linkedEntityWrapper!= null) {
if (card.linkedEntityWrapper.entity.isHidden) {
return Container(width: 0.0, height: 0.0,);
}
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: MissedEntityWidget(),
handleTap: false,
);
}
}
if (card.conditions.isNotEmpty) {
bool showCardByConditions = true;
for (var condition in card.conditions) {
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
if (conditionEntity != null &&
((condition['state'] != null && conditionEntity.state != condition['state']) ||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
) {
showCardByConditions = false;
break;
}
}
if (!showCardByConditions) {
return Container(width: 0.0, height: 0.0,);
}
}
switch (card.type) {
case CardType.ENTITIES: {
return _buildEntitiesCard(context);
}
case CardType.GLANCE: {
return _buildGlanceCard(context);
}
case CardType.MEDIA_CONTROL: {
return _buildMediaControlsCard(context);
}
case CardType.ENTITY_BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.GAUGE: {
return _buildGaugeCard(context);
}
/* case CardType.LIGHT: {
return _buildLightCard(context);
}*/
case CardType.MARKDOWN: {
return _buildMarkdownCard(context);
}
case CardType.ALARM_PANEL: {
return _buildAlarmPanelCard(context);
}
case CardType.HORIZONTAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
children.add(
Flexible(
fit: FlexFit.tight,
child: card.build(context),
)
);
}
});
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
children.add(
card.build(context)
);
});
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
default: {
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
return _buildEntitiesCard(context);
} else {
return _buildUnsupportedCard(context);
}
}
}
}
Widget _buildEntitiesCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeader(name: card.name));
entitiesToShow.forEach((EntityWrapper entity) {
body.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
));
});
return Card(
child: Padding(
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
child: Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildMarkdownCard(BuildContext context) {
if (card.content == null) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeader(name: card.name));
body.add(MarkdownBody(data: card.content));
return Card(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildAlarmPanelCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
style: TextStyle(
color: Colors.grey
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
EntityIcon(
size: 50.0,
),
Container(
width: 26.0,
child: IconButton(
padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
)
)
]
),
));
body.add(
AlarmControlPanelControlsWidget(
extended: true,
states: card.states,
)
);
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: body
)
)
);
}
Widget _buildGlanceCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> rows = [];
rows.add(CardHeader(name: card.name));
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) {
buttons.add(
SizedBox(
width: buttonWidth,
child: EntityModel(
entityWrapper: entity,
child: GlanceCardEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
return Wrap(
//spacing: 5.0,
//alignment: WrapAlignment.spaceEvenly,
runSpacing: Sizes.doubleRowPadding,
children: buttons,
);
}
),
),
)
);
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: rows
)
);
}
Widget _buildMediaControlsCard(BuildContext context) {
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
Widget _buildEntityButtonCard(BuildContext context) {
card.linkedEntityWrapper.displayName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase();
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: EntityButtonCardBody(
showName: card.showName,
),
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
)
);
}
Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeader(name: card.name ?? ""));
List<Widget> result = [];
if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: true,
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
),
)
]);
} else {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
),
]);
}
body.addAll(result);
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
}

View File

@ -0,0 +1,79 @@
part of '../main.dart';
class EntitiesCard extends StatelessWidget {
final EntitiesCardData card;
const EntitiesCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
Widget headerSwitch;
if (card.showHeaderToggle) {
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
}).map((EntityWrapper en) {
return en.entity.entityId;
}).toList();
headerSwitch = Switch(
value: headerToggleVal,
onChanged: (val) {
if (entitiesToToggle.isNotEmpty) {
ConnectionManager().callService(
domain: "homeassistant",
service: val ? "turn_on" : "turn_off",
entityId: entitiesToToggle
);
}
},
);
}
body.add(
CardHeader(
name: card.title,
trailing: headerSwitch,
emptyPadding: Sizes.rowPadding,
leading: card.icon != null ? Icon(
MaterialDesignIcons.getIconDataFromIconName(card.icon),
size: Sizes.iconSize,
color: Theme.of(context).textTheme.headline.color
) : null,
)
);
body.addAll(
entitiesToShow.map((EntityWrapper entity) {
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
);
})
);
return CardWrapper(
child: Padding(
padding: EdgeInsets.only(
right: Sizes.rightWidgetPadding,
left: Sizes.leftWidgetPadding,
bottom: Sizes.rowPadding,
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: body
)
),
)
);
}
}

View File

@ -0,0 +1,93 @@
part of '../main.dart';
class EntityButtonCard extends StatelessWidget {
final ButtonCardData card;
EntityButtonCard({
Key key, this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = card.entity;
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
return Container(width: 0.0, height: 0.0,);
}
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
Widget buttonIcon;
if (!card.showIcon) {
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
} else if (iconSize > 0) {
buttonIcon = SizedBox(
height: iconSize,
child: FractionallySizedBox(
widthFactor: 0.5,
child: FittedBox(
fit: BoxFit.contain,
child: EntityIcon(
//padding: EdgeInsets.only(top: 6),
),
)
),
);
} else {
buttonIcon = AspectRatio(
aspectRatio: 2,
child: FractionallySizedBox(
widthFactor: 0.5,
child: FittedBox(
fit: BoxFit.fitWidth,
child: EntityIcon(
//padding: EdgeInsets.only(top: 6),
),
)
),
);
}
return CardWrapper(
child: EntityModel(
entityWrapper: card.entity,
child: InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buttonIcon,
_buildName(context)
],
)
),
),
handleTap: true
)
);
}
Widget _buildName(BuildContext context) {
if (card.showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
textStyle: Theme.of(context).textTheme.subhead,
wordsWrap: true,
textAlign: TextAlign.center
);
}
return Container(width: 0, height: 0);
}
}

198
lib/cards/gauge_card.dart Normal file
View File

@ -0,0 +1,198 @@
part of '../main.dart';
class GaugeCard extends StatelessWidget {
final GaugeCardData card;
GaugeCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = card.entity;
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
}
entityWrapper.overrideName = card.name ??
entityWrapper.displayName;
entityWrapper.unitOfMeasurementOverride = card.unit ??
entityWrapper.unitOfMeasurement;
double fixedValue;
double value = entityWrapper.entity.doubleState;
if (value > card.max) {
fixedValue = card.max.toDouble();
} else if (value < card.min) {
fixedValue = card.min.toDouble();
} else {
fixedValue = value;
}
List<GaugeRange> ranges;
Color currentColor;
if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) {
List<RangeContainer> rangesList = <RangeContainer>[
RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()),
RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()),
RangeContainer(card.severity["yellow"], HAClientTheme().getYellowGaugeColor())
];
rangesList.sort((current, next) {
if (current.startFrom > next.startFrom) {
return 1;
}
if (current.startFrom < next.startFrom) {
return -1;
}
return 0;
});
if (fixedValue < rangesList[1].startFrom) {
currentColor = rangesList[0].color;
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
currentColor = rangesList[1].color;
} else {
currentColor = rangesList[2].color;
}
ranges = [
GaugeRange(
startValue: rangesList[0].startFrom.toDouble(),
endValue: rangesList[1].startFrom.toDouble(),
color: rangesList[0].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[1].startFrom.toDouble(),
endValue: rangesList[2].startFrom.toDouble(),
color: rangesList[1].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[2].startFrom.toDouble(),
endValue: card.max.toDouble(),
color: rangesList[2].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
)
];
}
if (ranges == null) {
currentColor = Theme.of(context).primaryColorDark;
ranges = <GaugeRange>[
GaugeRange(
startValue: card.min.toDouble(),
endValue: card.max.toDouble(),
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3,
)
];
}
return CardWrapper(
padding: EdgeInsets.all(4),
child: EntityModel(
entityWrapper: entityWrapper,
child: InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: AspectRatio(
aspectRatio: 1.8,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
maximum: card.max.toDouble(),
minimum: card.min.toDouble(),
showLabels: false,
useRangeColorForAxis: true,
showTicks: false,
canScaleToFit: true,
ranges: ranges,
axisLineStyle: AxisLineStyle(
thickness: 0.3,
thicknessUnit: GaugeSizeUnit.factor,
color: Colors.transparent
),
startAngle: 180,
endAngle: 0,
pointers: <GaugePointer>[
RangePointer(
value: fixedValue,
sizeUnit: GaugeSizeUnit.factor,
width: 0.3,
color: currentColor,
enableAnimation: true,
animationType: AnimationType.bounceOut,
)
]
)
],
),
Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
flex: 8,
fit: FlexFit.tight,
child: Container()
),
Flexible(
flex: 6,
fit: FlexFit.tight,
child: FractionallySizedBox(
widthFactor: 0.4,
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.bottomCenter,
child: SimpleEntityState(
padding: EdgeInsets.all(0),
expanded: false,
maxLines: 1,
textAlign: TextAlign.center
),
)
)
),
Flexible(
flex: 3,
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.contain,
child: EntityName(
padding: EdgeInsets.all(0),
textStyle: Theme.of(context).textTheme.subhead
),
)
),
],
)
],
)
),
),
handleTap: true
)
);
}
}
class RangeContainer {
final int startFrom;
Color color;
RangeContainer(this.startFrom, this.color);
}

120
lib/cards/glance_card.dart Normal file
View File

@ -0,0 +1,120 @@
part of '../main.dart';
class GlanceCard extends StatelessWidget {
final GlanceCardData card;
const GlanceCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
int length = entitiesToShow.length;
int rowsCount;
int columnsCount;
if (length == 0) {
columnsCount = 0;
rowsCount = 0;
} else {
columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
rowsCount = (length / columnsCount).round();
}
List<TableRow> rows = [];
for (int i = 0; i < rowsCount; i++) {
int start = i*columnsCount;
int end = start + math.min(columnsCount, length - start);
List<Widget> rowChildren = [];
rowChildren.addAll(entitiesToShow.sublist(
start, end
).map(
(EntityWrapper entity){
return EntityModel(
entityWrapper: entity,
child: _buildEntityContainer(context, entity),
handleTap: true
);
}
).toList()
);
while (rowChildren.length < columnsCount) {
rowChildren.add(
Container()
);
}
rows.add(
TableRow(
children: rowChildren
)
);
}
return CardWrapper(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardHeader(name: card.title),
Padding(
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
child: Table(
children: rows
)
)
],
)
);
}
Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) {
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return MissedEntityWidget();
} else if (entityWrapper.entity.statelessType != StatelessEntityType.none) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = [];
if (card.showName) {
result.add(_buildName(context));
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: Sizes.iconSize,
)
);
if (card.showState) {
result.add(_buildState());
}
return Center(
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
),
);
}
Widget _buildName(BuildContext context) {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: false,
textAlign: TextAlign.center,
textStyle: Theme.of(context).textTheme.body1,
);
}
Widget _buildState() {
return SimpleEntityState(
textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
padding: EdgeInsets.only(top: Sizes.rowPadding),
);
}
}

View File

@ -0,0 +1,30 @@
part of '../main.dart';
class HorizontalStackCard extends StatelessWidget {
final HorizontalStackCardData card;
const HorizontalStackCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
children = card.childCards.map((childCard) => Flexible(
fit: FlexFit.tight,
child: childCard.buildCardWidget()
)
).toList();
return IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
);
}
return Container(height: 0.0, width: 0.0,);
}
}

View File

@ -0,0 +1,33 @@
part of '../main.dart';
class MarkdownCard extends StatelessWidget {
final MarkdownCardData card;
const MarkdownCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.content == null) {
return Container(height: 0.0, width: 0.0,);
} else if (card.content == '***') {
return Container(height: Sizes.rowPadding, width: 0.0,);
}
return CardWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
CardHeader(name: card.title),
MarkdownBody(
data: card.content,
)
],
),
)
);
}
}

View File

@ -0,0 +1,27 @@
part of '../main.dart';
class MediaControlsCard extends StatelessWidget {
final MediaControlCardData card;
const MediaControlsCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
}
return CardWrapper(
child: EntityModel(
entityWrapper: card.entity,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
}

View File

@ -0,0 +1,17 @@
part of '../main.dart';
class UnsupportedCard extends StatelessWidget {
final CardData card;
const UnsupportedCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
return CardWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
)
);
}
}

View File

@ -0,0 +1,23 @@
part of '../main.dart';
class VerticalStackCard extends StatelessWidget {
final VerticalStackCardData card;
const VerticalStackCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.childCards.isNotEmpty) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: card.childCards.map<Widget>(
(childCard) => childCard.buildCardWidget()
).toList(),
);
}
return Container(height: 0.0, width: 0.0,);
}
}

View File

@ -4,9 +4,11 @@ class CardHeader extends StatelessWidget {
final String name;
final Widget trailing;
final Widget leading;
final Widget subtitle;
final double emptyPadding;
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -14,14 +16,15 @@ class CardHeader extends StatelessWidget {
if ((name != null) && (name.trim().length > 0)) {
result = new ListTile(
trailing: trailing,
leading: leading,
subtitle: subtitle,
title: Text("$name",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
style: Theme.of(context).textTheme.headline),
);
} else {
result = new Container(width: 0.0, height: 0.0);
result = new Container(width: 0.0, height: emptyPadding);
}
return result;
}

View File

@ -0,0 +1,21 @@
part of '../../main.dart';
class CardWrapper extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: padding,
child: child
),
);
}
}

View File

@ -1,56 +0,0 @@
part of '../../main.dart';
class EntityButtonCardBody extends StatelessWidget {
final bool showName;
EntityButtonCardBody({
Key key, this.showName: true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
children: <Widget>[
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
size: constraints.maxWidth / 2.5,
);
}
),
_buildName()
],
),
),
);
}
Widget _buildName() {
if (showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center,
fontSize: Sizes.nameFontSize,
);
}
return Container(width: 0, height: 0);
}
}

View File

@ -1,153 +0,0 @@
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);
}

View File

@ -1,85 +0,0 @@
part of '../../main.dart';
class GlanceCardEntityContainer extends StatelessWidget {
final bool showName;
final bool showState;
final bool nameInTheBottom;
final double iconSize;
final double nameFontSize;
final bool wordsWrapInName;
GlanceCardEntityContainer({
Key key,
@required this.showName,
@required this.showState,
this.nameInTheBottom: false,
this.iconSize: Sizes.iconSize,
this.nameFontSize: Sizes.smallFontSize,
this.wordsWrapInName: false
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = [];
if (!nameInTheBottom) {
if (showName) {
result.add(_buildName());
}
} else {
if (showState) {
result.add(_buildState());
}
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
)
);
if (!nameInTheBottom) {
if (showState) {
result.add(_buildState());
}
} else {
result.add(_buildName());
}
return Center(
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
),
);
}
Widget _buildName() {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: wordsWrapInName,
textAlign: TextAlign.center,
fontSize: nameFontSize,
);
}
Widget _buildState() {
return SimpleEntityState(
textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
padding: EdgeInsets.only(top: Sizes.rowPadding),
);
}
}

View File

@ -1,90 +0,0 @@
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,
),
);
}
),
)
]
)
),
);
}
}

View File

@ -51,6 +51,10 @@ class EntityUIAction {
String holdNavigationPath;
String holdService;
Map<String, dynamic> holdServiceData;
String doubleTapAction = EntityUIAction.none;
String doubleTapNavigationPath;
String doubleTapService;
Map<String, dynamic> doubleTapServiceData;
EntityUIAction({rawEntityData}) {
if (rawEntityData != null) {
@ -76,6 +80,17 @@ class EntityUIAction {
holdServiceData = rawEntityData["hold_action"]["service_data"];
}
}
if (rawEntityData["double_tap_action"] != null) {
if (rawEntityData["double_tap_action"] is String) {
doubleTapAction = rawEntityData["double_tap_action"];
} else {
doubleTapAction =
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
doubleTapService = rawEntityData["double_tap_action"]["service"];
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
}
}
}
}
@ -98,10 +113,12 @@ class CardType {
static const IFRAME = "iframe";
static const GAUGE = "gauge";
static const ENTITY_BUTTON = "entity-button";
static const BUTTON = "button";
static const CONDITIONAL = "conditional";
static const ALARM_PANEL = "alarm-panel";
static const MARKDOWN = "markdown";
static const LIGHT = "light";
static const ENTITY_FILTER = "entity-filter";
}
class Sizes {
@ -111,10 +128,10 @@ class Sizes {
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 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;

View File

@ -248,7 +248,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
FlatButton(
child: Text(
"TRIGGER",
style: TextStyle(color: Colors.redAccent)
style: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context).errorColor
)
),
onPressed: () => _askToTrigger(entity),
)

View File

@ -7,8 +7,7 @@ class BadgeWidget extends StatelessWidget {
double iconSize = 26.0;
Widget badgeIcon;
String onBadgeTextValue;
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
EntityColor.badgeColors["default"];
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
switch (entityModel.entityWrapper.entity.domain) {
case "sun":
{
@ -30,7 +29,7 @@ class BadgeWidget extends StatelessWidget {
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
color: Theme.of(context).textTheme.body1.color
);
break;
}
@ -40,7 +39,7 @@ class BadgeWidget extends StatelessWidget {
badgeIcon = EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
color: Colors.black
color: Theme.of(context).textTheme.body1.color
);
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
break;
@ -64,7 +63,9 @@ class BadgeWidget extends StatelessWidget {
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: stateFontSize),
style: Theme.of(context).textTheme.body1.copyWith(
fontSize: stateFontSize
)
),
);
break;
@ -77,7 +78,9 @@ class BadgeWidget extends StatelessWidget {
onBadgeText = Container(
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
child: Text("$onBadgeTextValue",
style: TextStyle(fontSize: 12.0, color: Colors.white),
style: Theme.of(context).textTheme.overline.copyWith(
color: HAClientTheme().getOnBadgeTextColor()
),
textAlign: TextAlign.center,
softWrap: false,
overflow: TextOverflow.fade),
@ -98,7 +101,7 @@ class BadgeWidget extends StatelessWidget {
decoration: new BoxDecoration(
// Circle shape
shape: BoxShape.circle,
color: Colors.white,
color: Theme.of(context).cardColor,
// The border you want
border: new Border.all(
width: 2.0,
@ -131,7 +134,7 @@ class BadgeWidget extends StatelessWidget {
child: Text(
"${entityModel.entityWrapper.displayName}",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12.0),
style: Theme.of(context).textTheme.caption,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,

View File

@ -3,12 +3,16 @@ part of '../../main.dart';
class CameraEntity extends Entity {
static const SUPPORT_ON_OFF = 1;
static const SUPPORT_STREAM = 2;
CameraEntity(Map rawData, String webHost) : super(rawData, webHost);
bool get supportOnOff => ((supportedFeatures &
CameraEntity.SUPPORT_ON_OFF) ==
CameraEntity.SUPPORT_ON_OFF);
bool get supportStream => ((supportedFeatures &
CameraEntity.SUPPORT_STREAM) ==
CameraEntity.SUPPORT_STREAM);
@override
Widget _buildAdditionalControlsForPage(BuildContext context) {

View File

@ -2,7 +2,9 @@ part of '../../../main.dart';
class CameraStreamView extends StatefulWidget {
CameraStreamView({Key key}) : super(key: key);
final bool withControls;
CameraStreamView({Key key, this.withControls: true}) : super(key: key);
@override
_CameraStreamViewState createState() => _CameraStreamViewState();
@ -10,45 +12,176 @@ class CameraStreamView extends StatefulWidget {
class _CameraStreamViewState extends State<CameraStreamView> {
CameraEntity _entity;
String _streamUrl = "";
bool _isLoaded = false;
double _aspectRatio = 1.33;
String _webViewHtml;
String _jsMessageChannelName = 'unknown';
Completer _loading;
@override
void initState() {
super.initState();
}
CameraEntity _entity;
bool started = false;
String streamUrl = "";
Future _loadResources() {
if (_loading != null && !_loading.isCompleted) {
Logger.d("[Camera Player] Resources loading is not finished yet");
return _loading.future;
}
Logger.d("[Camera Player] Loading resources");
_loading = Completer();
_entity = EntityModel
.of(context)
.entityWrapper
.entity;
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
HomeAssistant().getCameraStream(_entity.entityId)
.then((data) {
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
_webViewHtml = Uri.dataFromString(
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8')
).toString();
_loading.complete();
});
})
.catchError((e) {
if (e == 'start_stream_failed') {
Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e");
_loadMJPEG().then((_) {
_loading.complete();
});
} else {
_loading.completeError(e);
Logger.e("[Camera Player] Error loading stream: $e");
}
});
} else {
_loadMJPEG().then((_) {
_loading.complete();
});
}
return _loading.future;
}
launchStream() {
Launcher.launchURLInCustomTab(
context: context,
url: streamUrl
Future _loadMJPEG() async {
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
var file = await rootBundle.loadString('assets/html/cameraView.html');
_webViewHtml = Uri.dataFromString(
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8')
).toString();
}
Widget _buildScreen() {
Widget screenWidget;
if (!_isLoaded) {
screenWidget = Center(
child: EntityPicture(
fit: BoxFit.contain,
)
);
} else {
screenWidget = WebView(
initialUrl: _webViewHtml,
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
debuggingEnabled: Logger.isInDebugMode,
gestureNavigationEnabled: false,
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: {
JavascriptChannel(
name: _jsMessageChannelName,
onMessageReceived: ((message) {
Logger.d('[Camera Player] Message from page: $message');
setState((){
_aspectRatio = double.tryParse(message.message) ?? 1.33;
});
})
)
}
);
}
return AspectRatio(
aspectRatio: _aspectRatio,
child: screenWidget
);
}
Widget _buildControls() {
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
iconSize: 40,
color: Theme.of(context).accentColor,
onPressed: _isLoaded ? () {
setState(() {
_isLoaded = false;
});
} : null,
),
Expanded(
child: Container(),
),
IconButton(
icon: Icon(Icons.fullscreen),
iconSize: 40,
color: Theme.of(context).accentColor,
onPressed: _isLoaded ? () {
eventBus.fire(ShowEntityPageEvent());
Navigator.of(context).push(
MaterialPageRoute(
builder: (conext) => FullScreenPage(
child: EntityModel(
child: CameraStreamView(
withControls: false
),
handleTap: false,
entityWrapper: EntityWrapper(
entity: _entity
),
),
),
fullscreenDialog: true
)
).then((_) {
eventBus.fire(ShowEntityPageEvent(entity: _entity));
});
} : null,
)
],
);
}
@override
Widget build(BuildContext context) {
if (!started) {
_entity = EntityModel
.of(context)
.entityWrapper
.entity;
started = true;
if (!_isLoaded && (_loading == null || _loading.isCompleted)) {
_loadResources().then((_) => setState((){ _isLoaded = true; }));
}
streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
.entityId}?token=${_entity.attributes['access_token']}';
return Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(20.0),
child: IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:monitor-screenshot"), color: Colors.amber),
iconSize: 50.0,
onPressed: () => launchStream(),
)
)
],
);
if (widget.withControls) {
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildScreen(),
_buildControls()
],
),
);
} else {
return _buildScreen();
}
}
@override

View File

@ -10,9 +10,8 @@ class ClimateControlWidget extends StatefulWidget {
class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _showPending = false;
bool _temperaturePending = false;
bool _changedHere = false;
Timer _resetTimer;
Timer _tempThrottleTimer;
Timer _targetTempThrottleTimer;
double _tmpTemperature = 0.0;
@ -27,9 +26,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
bool _tmpAuxHeat = false;
void _resetVars(ClimateEntity entity) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
if (!_temperaturePending) {
_tmpTemperature = entity.temperature;
_tmpTargetHigh = entity.targetHigh;
_tmpTargetLow = entity.targetLow;
}
_tmpHVACMode = entity.state;
_tmpFanMode = entity.fanMode;
_tmpSwingMode = entity.swingMode;
@ -38,7 +39,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_tmpAuxHeat = entity.auxHeat;
_tmpTargetHumidity = entity.targetHumidity;
_showPending = false;
_changedHere = false;
}
@ -73,46 +73,44 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
void _setTemperature(ClimateEntity entity) {
if (_tempThrottleTimer!=null) {
_tempThrottleTimer.cancel();
}
_tempThrottleTimer?.cancel();
setState(() {
_changedHere = true;
_temperaturePending = true;
_tmpTemperature = double.parse(_tmpTemperature.toStringAsFixed(1));
});
_tempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
_temperaturePending = false;
ConnectionManager().callService(
domain: entity.domain,
service: "set_temperature",
entityId: entity.entityId,
data: {"temperature": "${_tmpTemperature.toStringAsFixed(1)}"}
);
_resetStateTimer(entity);
});
});
}
void _setTargetTemp(ClimateEntity entity) {
if (_targetTempThrottleTimer!=null) {
_targetTempThrottleTimer.cancel();
}
_targetTempThrottleTimer?.cancel();
setState(() {
_changedHere = true;
_temperaturePending = true;
_tmpTargetLow = double.parse(_tmpTargetLow.toStringAsFixed(1));
_tmpTargetHigh = double.parse(_tmpTargetHigh.toStringAsFixed(1));
});
_targetTempThrottleTimer = Timer(Duration(seconds: 2), () {
setState(() {
_changedHere = true;
_temperaturePending = false;
ConnectionManager().callService(
domain: entity.domain,
service: "set_temperature",
entityId: entity.entityId,
data: {"target_temp_high": "${_tmpTargetHigh.toStringAsFixed(1)}", "target_temp_low": "${_tmpTargetLow.toStringAsFixed(1)}"}
);
_resetStateTimer(entity);
});
});
}
@ -127,7 +125,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
entityId: entity.entityId,
data: {"humidity": "$_tmpTargetHumidity"}
);
_resetStateTimer(entity);
});
}
@ -141,7 +138,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
entityId: entity.entityId,
data: {"hvac_mode": "$_tmpHVACMode"}
);
_resetStateTimer(entity);
});
}
@ -155,7 +151,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
entityId: entity.entityId,
data: {"swing_mode": "$_tmpSwingMode"}
);
_resetStateTimer(entity);
});
}
@ -164,7 +159,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_tmpFanMode = value;
_changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_fan_mode", entityId: entity.entityId, data: {"fan_mode": "$_tmpFanMode"});
_resetStateTimer(entity);
});
}
@ -173,7 +167,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_tmpPresetMode = value;
_changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_preset_mode", entityId: entity.entityId, data: {"preset_mode": "$_tmpPresetMode"});
_resetStateTimer(entity);
});
}
@ -191,17 +184,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
_tmpAuxHeat = value;
_changedHere = true;
ConnectionManager().callService(domain: entity.domain, service: "set_aux_heat", entityId: entity.entityId, data: {"aux_heat": "$_tmpAuxHeat"});
_resetStateTimer(entity);
});
}
void _resetStateTimer(ClimateEntity entity) {
if (_resetTimer!=null) {
_resetTimer.cancel();
}
_resetTimer = Timer(Duration(seconds: 3), () {
setState(() {});
_resetVars(entity);
});
}
@ -209,11 +191,11 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Widget build(BuildContext context) {
final entityModel = EntityModel.of(context);
final ClimateEntity entity = entityModel.entityWrapper.entity;
Logger.d("[Climate widget build] changed here = $_changedHere");
if (_changedHere) {
_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
//_showPending = (_tmpTemperature != entity.temperature || _tmpTargetHigh != entity.targetHigh || _tmpTargetLow != entity.targetLow);
_changedHere = false;
} else {
_resetTimer?.cancel();
_resetVars(entity);
}
return Padding(
@ -222,20 +204,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//_buildOnOffControl(entity),
_buildTemperatureControls(entity),
_buildTargetTemperatureControls(entity),
_buildHumidityControls(entity),
_buildOperationControl(entity),
_buildFanControl(entity),
_buildSwingControl(entity),
_buildPresetModeControl(entity),
_buildAuxHeatControl(entity)
_buildTemperatureControls(entity, context),
_buildTargetTemperatureControls(entity, context),
_buildHumidityControls(entity, context),
_buildOperationControl(entity, context),
_buildFanControl(entity, context),
_buildSwingControl(entity, context),
_buildPresetModeControl(entity, context),
_buildAuxHeatControl(entity, context)
],
),
);
}
Widget _buildPresetModeControl(ClimateEntity entity) {
Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) {
if (entity.supportPresetMode) {
return ModeSelectorWidget(
options: entity.presetModes,
@ -260,7 +242,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}*/
Widget _buildAuxHeatControl(ClimateEntity entity) {
Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) {
if (entity.supportAuxHeat ) {
return ModeSwitchWidget(
caption: "Aux heat",
@ -272,7 +254,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildOperationControl(ClimateEntity entity) {
Widget _buildOperationControl(ClimateEntity entity, BuildContext context) {
if (entity.hvacModes != null) {
return ModeSelectorWidget(
onChange: (mode) => _setHVACMode(entity, mode),
@ -285,7 +267,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildFanControl(ClimateEntity entity) {
Widget _buildFanControl(ClimateEntity entity, BuildContext context) {
if (entity.supportFanMode) {
return ModeSelectorWidget(
options: entity.fanModes,
@ -298,7 +280,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildSwingControl(ClimateEntity entity) {
Widget _buildSwingControl(ClimateEntity entity, BuildContext context) {
if (entity.supportSwingMode) {
return ModeSelectorWidget(
onChange: (mode) => _setSwingMode(entity, mode),
@ -311,17 +293,15 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildTemperatureControls(ClimateEntity entity) {
Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) {
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
Text("Target temperature", style: Theme.of(context).textTheme.body1),
TemperatureControlWidget(
value: _tmpTemperature,
fontColor: _showPending ? Colors.red : Colors.black,
active: _temperaturePending,
onDec: () => _temperatureDown(entity),
onInc: () => _temperatureUp(entity),
)
@ -332,13 +312,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) {
List<Widget> controls = [];
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
controls.addAll(<Widget>[
TemperatureControlWidget(
value: _tmpTargetLow,
fontColor: _showPending ? Colors.red : Colors.black,
active: _temperaturePending,
onDec: () => _targetLowDown(entity),
onInc: () => _targetLowUp(entity),
),
@ -351,7 +331,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
controls.add(
TemperatureControlWidget(
value: _tmpTargetHigh,
fontColor: _showPending ? Colors.red : Colors.black,
active: _temperaturePending,
onDec: () => _targetHighDown(entity),
onInc: () => _targetHighUp(entity),
)
@ -361,9 +341,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Target temperature range", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
Text("Target temperature range", style: Theme.of(context).textTheme.body1),
Row(
children: controls,
)
@ -374,13 +352,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
}
}
Widget _buildHumidityControls(ClimateEntity entity) {
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
List<Widget> result = [];
if (entity.supportTargetHumidity) {
result.addAll(<Widget>[
Text(
"$_tmpTargetHumidity%",
style: TextStyle(fontSize: Sizes.largeFontSize),
style: Theme.of(context).textTheme.display1,
),
Expanded(
child: Slider(
@ -405,9 +383,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Target humidity", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
@ -429,7 +405,6 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
@override
void dispose() {
_resetTimer?.cancel();
super.dispose();
}

View File

@ -33,23 +33,16 @@ class ClimateStateWidget extends StatelessWidget {
children: <Widget>[
Text("$displayState",
textAlign: TextAlign.right,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: Sizes.stateFontSize,
)),
style: Theme.of(context).textTheme.body2),
Text(" $targetTemp",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
))
style: Theme.of(context).textTheme.body1)
],
),
entity.currentTemperature != null ?
Text("Currently: ${entity.currentTemperature}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
color: Colors.black45)
style: Theme.of(context).textTheme.subtitle
) :
Container(height: 0.0,)
],

View File

@ -3,10 +3,8 @@ part of '../../../main.dart';
class ModeSelectorWidget extends StatelessWidget {
final String caption;
final List<String> options;
final List options;
final String value;
final double captionFontSize;
final double valueFontSize;
final onChange;
final EdgeInsets padding;
@ -16,8 +14,6 @@ class ModeSelectorWidget extends StatelessWidget {
@required this.options,
this.value,
@required this.onChange,
this.captionFontSize,
this.valueFontSize,
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
}) : super(key: key);
@ -28,9 +24,7 @@ class ModeSelectorWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("$caption", style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
)),
Text("$caption", style: Theme.of(context).textTheme.body1),
Row(
children: <Widget>[
Expanded(
@ -40,15 +34,12 @@ class ModeSelectorWidget extends StatelessWidget {
value: value,
iconSize: 30.0,
isExpanded: true,
style: TextStyle(
fontSize: valueFontSize ?? Sizes.largeFontSize,
color: Colors.black,
),
style: Theme.of(context).textTheme.title,
hint: Text("Select ${caption.toLowerCase()}"),
items: options.map((String value) {
items: options.map((value) {
return new DropdownMenuItem<String>(
value: value,
child: Text(value),
value: '$value',
child: Text('$value'),
);
}).toList(),
onChanged: (mode) => onChange(mode),

View File

@ -4,7 +4,6 @@ class ModeSwitchWidget extends StatelessWidget {
final String caption;
final onChange;
final double captionFontSize;
final bool value;
final bool expanded;
final EdgeInsets padding;
@ -13,7 +12,6 @@ class ModeSwitchWidget extends StatelessWidget {
Key key,
@required this.caption,
@required this.onChange,
this.captionFontSize,
this.value,
this.expanded: true,
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
@ -25,7 +23,7 @@ class ModeSwitchWidget extends StatelessWidget {
padding: this.padding,
child: Row(
children: <Widget>[
_buildCaption(),
_buildCaption(context),
Switch(
onChanged: (value) => onChange(value),
value: value ?? false,
@ -35,12 +33,10 @@ class ModeSwitchWidget extends StatelessWidget {
);
}
Widget _buildCaption() {
Widget _buildCaption(BuildContext context) {
Widget captionWidget = Text(
"$caption",
style: TextStyle(
fontSize: captionFontSize ?? Sizes.stateFontSize
),
style: Theme.of(context).textTheme.body1,
);
if (expanded) {
return Expanded(

View File

@ -2,8 +2,7 @@ part of '../../../main.dart';
class TemperatureControlWidget extends StatelessWidget {
final double value;
final double fontSize;
final Color fontColor;
final bool active;
final onInc;
final onDec;
@ -12,8 +11,9 @@ class TemperatureControlWidget extends StatelessWidget {
@required this.value,
@required this.onInc,
@required this.onDec,
this.fontSize,
this.fontColor})
//this.fontSize,
this.active: false
})
: super(key: key);
@override
@ -23,10 +23,7 @@ class TemperatureControlWidget extends StatelessWidget {
children: <Widget>[
Text(
"$value",
style: TextStyle(
fontSize: fontSize ?? 24.0,
color: fontColor ?? Colors.black
),
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1,
),
Column(
children: <Widget>[

View File

@ -64,9 +64,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
child: Text("Position"),
),
Slider(
value: _tmpPosition,
@ -118,9 +116,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
controls.insert(0, Padding(
padding: EdgeInsets.fromLTRB(
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: Text("Tilt position", style: TextStyle(
fontSize: Sizes.stateFontSize
)),
child: Text("Tilt position"),
));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -9,10 +9,8 @@ class DateTimeStateWidget extends StatelessWidget {
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
child: GestureDetector(
child: Text("${entity.formattedState}",
textAlign: TextAlign.right,
style: new TextStyle(
fontSize: Sizes.stateFontSize,
)),
textAlign: TextAlign.right
),
onTap: () => _handleStateTap(context, entity),
));
}

View File

@ -11,25 +11,23 @@ class DefaultEntityContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return MissedEntityWidget();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
return Divider(
color: Colors.black45,
);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) {
return Divider();
}
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(
color: Colors.black45,
),
Divider(),
Text(
"${entityModel.entityWrapper.entity.displayName}",
style: TextStyle(color: Colors.blue),
style: HAClientTheme().getLinkTextStyle(context).copyWith(
decoration: TextDecoration.none
)
)
],
);
@ -61,6 +59,11 @@ class DefaultEntityContainer extends StatelessWidget {
entityModel.entityWrapper.handleTap();
}
},
onDoubleTap: () {
if (entityModel.handleTap) {
entityModel.entityWrapper.handleDoubleTap();
}
},
child: result,
);
} else {

View File

@ -1,13 +1,6 @@
part of '../main.dart';
class StatelessEntityType {
static const NONE = 0;
static const MISSED = 1;
static const DIVIDER = 2;
static const SECTION = 3;
static const CALL_SERVICE = 4;
static const WEBLINK = 5;
}
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
class Entity {
@ -76,8 +69,8 @@ class Entity {
String entityPicture;
String state;
String displayState;
DateTime _lastUpdated;
int statelessType = 0;
DateTime lastUpdatedTimestamp;
StatelessEntityType statelessType = StatelessEntityType.none;
List<Entity> childEntities = [];
String deviceClass;
@ -120,31 +113,36 @@ class Entity {
}
Entity.missed(String entityId) {
statelessType = StatelessEntityType.MISSED;
statelessType = StatelessEntityType.missed;
attributes = {"hidden": false};
this.entityId = entityId;
}
Entity.divider() {
statelessType = StatelessEntityType.DIVIDER;
statelessType = StatelessEntityType.divider;
attributes = {"hidden": false};
}
Entity.section(String label) {
statelessType = StatelessEntityType.SECTION;
statelessType = StatelessEntityType.section;
attributes = {"hidden": false, "friendly_name": "$label"};
}
Entity.ghost(String name, String icon) {
statelessType = StatelessEntityType.ghost;
attributes = {"icon": icon, "hidden": false, "friendly_name": name};
}
Entity.callService({String icon, String name, String service, String actionName}) {
statelessType = StatelessEntityType.CALL_SERVICE;
statelessType = StatelessEntityType.callService;
entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
}
Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.WEBLINK;
entityId = "custom.custom"; //TODO wtf??
statelessType = StatelessEntityType.webLink;
entityId = "custom.custom";
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
}
@ -155,7 +153,7 @@ class Entity {
deviceClass = attributes["device_class"];
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost);
}
@ -221,17 +219,17 @@ class Entity {
String getAttribute(String attributeName) {
if (attributes != null) {
return attributes["$attributeName"];
return attributes["$attributeName"].toString();
}
return null;
}
String _getLastUpdatedFormatted() {
if (_lastUpdated == null) {
if (lastUpdatedTimestamp == null) {
return "-";
} else {
DateTime now = DateTime.now();
Duration d = now.difference(_lastUpdated);
Duration d = now.difference(lastUpdatedTimestamp);
String text;
int v;
if (d.inDays == 0) {

View File

@ -1,77 +0,0 @@
part of '../main.dart';
class EntityColor {
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
static const _stateColors = {
EntityState.on: Colors.amber,
"auto": Colors.amber,
EntityState.active: Colors.amber,
EntityState.playing: Colors.amber,
EntityState.paused: Colors.amber,
"above_horizon": Colors.amber,
EntityState.home: Colors.amber,
EntityState.open: Colors.amber,
EntityState.cleaning: Colors.amber,
EntityState.returning: Colors.amber,
EntityState.off: defaultStateColor,
EntityState.closed: defaultStateColor,
"below_horizon": defaultStateColor,
"default": defaultStateColor,
EntityState.idle: defaultStateColor,
"heat": Colors.redAccent,
"cool": Colors.lightBlue,
EntityState.unavailable: Colors.black26,
EntityState.unknown: Colors.black26,
EntityState.alarm_disarmed: Colors.green,
EntityState.alarm_armed_away: Colors.redAccent,
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
EntityState.alarm_armed_home: Colors.redAccent,
EntityState.alarm_armed_night: Colors.redAccent,
EntityState.alarm_triggered: Colors.redAccent,
EntityState.alarm_arming: Colors.amber,
EntityState.alarm_disarming: Colors.amber,
EntityState.alarm_pending: Colors.amber,
};
static Color stateColor(String state) {
return _stateColors[state] ?? _stateColors["default"];
}
static charts.Color chartHistoryStateColor(String state, int id) {
Color c = _stateColors[state];
if (c != null) {
return charts.Color(
r: c.red,
g: c.green,
b: c.blue,
a: c.alpha
);
} else {
double r = id.toDouble() % 10;
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
}
}
static Color historyStateColor(String state, int id) {
Color c = _stateColors[state];
if (c != null) {
return c;
} else {
if (id > -1) {
double r = id.toDouble() % 10;
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
} else {
return _stateColors[EntityState.on];
}
}
}
}

View File

@ -50,24 +50,29 @@ class EntityIcon extends StatelessWidget {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
return Padding(
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
)
return Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size,
color: color,
);
}
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
Color iconColor;
if (color != null) {
iconColor = color;
} else if (entityWrapper.stateColor) {
iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context);
} else {
iconColor = HAClientTheme().getOffStateColor(context);
}
return Padding(
padding: padding,
child: buildIcon(
entityWrapper,
color ?? EntityColor.stateColor(entityWrapper.entity.state)
entityWrapper,
iconColor
),
);
}

View File

@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
final bool handleTap;
static EntityModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(EntityModel);
return context.dependOnInheritedWidgetOfExactType<EntityModel>();
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
bool updateShouldNotify(EntityModel oldWidget) {
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
}
}

View File

@ -5,18 +5,24 @@ class EntityName extends StatelessWidget {
final EdgeInsetsGeometry padding;
final TextOverflow textOverflow;
final bool wordsWrap;
final double fontSize;
final TextAlign textAlign;
final int maxLines;
final TextStyle textStyle;
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.textStyle, this.wordsWrap: true, this.textAlign: TextAlign.left}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
TextStyle textStyle = TextStyle(fontSize: fontSize);
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
TextStyle tStyle;
if (textStyle == null) {
if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) {
tStyle = HAClientTheme().getLinkTextStyle(context);
} else {
tStyle = Theme.of(context).textTheme.body1;
}
} else {
tStyle = textStyle;
}
return Padding(
padding: padding,
@ -25,7 +31,7 @@ class EntityName extends StatelessWidget {
overflow: textOverflow,
softWrap: wordsWrap,
maxLines: maxLines,
style: textStyle,
style: tStyle,
textAlign: textAlign,
),
);

View File

@ -16,7 +16,7 @@ class EntityPageLayout extends StatelessWidget {
children: <Widget>[
showClose ?
Container(
color: Colors.blue[300],
color: Theme.of(context).primaryColor,
height: 40,
child: Row(
children: <Widget>[
@ -25,18 +25,14 @@ class EntityPageLayout extends StatelessWidget {
padding: EdgeInsets.only(left: 8),
child: Text(
entity.displayName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 22
),
style: Theme.of(context).primaryTextTheme.headline
),
),
),
IconButton(
padding: EdgeInsets.all(0),
icon: Icon(Icons.close),
color: Colors.white,
color: Theme.of(context).primaryTextTheme.headline.color,
iconSize: 36.0,
onPressed: () {
eventBus.fire(ShowEntityPageEvent());

View File

@ -0,0 +1,71 @@
part of '../main.dart';
class EntityPicture extends StatelessWidget {
final EdgeInsetsGeometry padding;
final BoxFit fit;
const EntityPicture({Key key, this.padding: const EdgeInsets.all(0.0), this.fit: BoxFit.cover}) : super(key: key);
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
String domain = entityId.split(".")[0];
String iconNameByDomain = MaterialDesignIcons.defaultIconsByDomains["$domain.$state"] ?? MaterialDesignIcons.defaultIconsByDomains["$domain"];
String iconNameByDeviceClass;
if (deviceClass != null) {
iconNameByDeviceClass = MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass.$state"] ?? MaterialDesignIcons.defaultIconsByDeviceClass["$domain.$deviceClass"];
}
String iconName = iconNameByDeviceClass ?? iconNameByDomain;
if (iconName != null) {
return MaterialDesignIcons.iconsDataMap[iconName] ?? 0;
} else {
return 0;
}
}
Widget buildIcon(EntityWrapper data, BuildContext context) {
if (data == null) {
return null;
}
String iconName = data.icon;
int iconCode = 0;
if (iconName.length > 0) {
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
} else {
iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); //
}
Widget iconPicture = Container(
child: Center(
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'),
size: Sizes.largeIconSize,
color: HAClientTheme().getOffStateColor(context),
)
)
);
if (data.entityPicture != null) {
return CachedNetworkImage(
imageUrl: data.entityPicture,
fit: this.fit,
errorWidget: (context, _, __) => iconPicture,
placeholder: (context, _) => iconPicture,
);
}
return iconPicture;
}
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
return Padding(
padding: padding,
child: buildIcon(
entityWrapper,
context
),
);
}
}

View File

@ -2,30 +2,31 @@ part of '../main.dart';
class EntityWrapper {
String displayName;
String icon;
String unitOfMeasurement;
String entityPicture;
String overrideName;
final String overrideIcon;
final bool stateColor;
EntityUIAction uiAction;
Entity entity;
String unitOfMeasurementOverride;
final List stateFilter;
String get icon => overrideIcon ?? entity.icon;
String get entityPicture => entity.entityPicture;
String get displayName => overrideName ?? entity.displayName;
String get unitOfMeasurement => unitOfMeasurementOverride ?? entity.unitOfMeasurement;
EntityWrapper({
this.entity,
String icon,
String displayName,
this.uiAction
this.overrideIcon,
this.overrideName,
this.stateColor: true,
this.uiAction,
this.stateFilter
}) {
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
this.icon = icon ?? entity.icon;
if (icon == null) {
entityPicture = entity.entityPicture;
}
this.displayName = displayName ?? entity.displayName;
if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || entity.statelessType == StatelessEntityType.webLink) {
if (uiAction == null) {
uiAction = EntityUIAction();
}
unitOfMeasurement = entity.unitOfMeasurement;
}
}
@ -113,4 +114,44 @@ class EntityWrapper {
}
}
void handleDoubleTap() {
switch (uiAction.doubleTapAction) {
case EntityUIAction.toggle: {
ConnectionManager().callService(domain: "homeassistant", service: "toggle", entityId: entity.entityId);
break;
}
case EntityUIAction.callService: {
if (uiAction.doubleTapService != null) {
ConnectionManager().callService(
domain: uiAction.doubleTapService.split(".")[0],
service: uiAction.doubleTapService.split(".")[1],
data: uiAction.doubleTapServiceData
);
}
break;
}
case EntityUIAction.moreInfo: {
eventBus.fire(
new ShowEntityPageEvent(entity: entity));
break;
}
case EntityUIAction.navigate: {
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
//TODO handle local urls
Logger.w("Local urls is not supported yet");
} else {
Launcher.launchURL(uiAction.doubleTapService);
}
break;
}
default: {
break;
}
}
}
}

View File

@ -6,7 +6,6 @@ class FlatServiceButton extends StatelessWidget {
final String serviceName;
final String entityId;
final String text;
final double fontSize;
FlatServiceButton({
Key key,
@ -14,7 +13,6 @@ class FlatServiceButton extends StatelessWidget {
@required this.serviceName,
@required this.entityId,
@required this.text,
this.fontSize: Sizes.stateFontSize
}) : super(key: key);
void _setNewState() {
@ -24,7 +22,7 @@ class FlatServiceButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: fontSize*2.5,
height: Theme.of(context).textTheme.subhead.fontSize*2.5,
child: FlatButton(
onPressed: (() {
_setNewState();
@ -32,8 +30,7 @@ class FlatServiceButton extends StatelessWidget {
child: Text(
text,
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: fontSize, color: Colors.blue),
style: HAClientTheme().getActionTextStyle(context),
),
)
);

View File

@ -183,7 +183,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
}
return UniversalSlider(
title: "Color temperature",
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)),
value: val,
onChangeEnd: (value) => _setColorTemp(entity, value),
max: entity.maxMireds,
@ -194,7 +194,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
_tmpColorTemp = value.round();
});
},
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),),
);
} else {
return Container(width: 0.0, height: 0.0);
@ -224,7 +224,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
},
),
FlatButton(
color: savedColor?.toColor() ?? Colors.transparent,
color: savedColor?.toColor() ?? Theme.of(context).backgroundColor,
child: Text('Paste color'),
onPressed: savedColor == null ? null : () {
_setColor(entity, savedColor);

View File

@ -28,8 +28,7 @@ class LockStateWidget extends StatelessWidget {
onPressed: () => _unlock(entity),
child: Text("UNLOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
style: HAClientTheme().getActionTextStyle(context)
),
)
),
@ -39,8 +38,7 @@ class LockStateWidget extends StatelessWidget {
onPressed: () => _lock(entity),
child: Text("LOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
style: HAClientTheme().getActionTextStyle(context),
),
)
)
@ -56,8 +54,7 @@ class LockStateWidget extends StatelessWidget {
child: Text(
entity.isLocked ? "UNLOCK" : "LOCK",
textAlign: TextAlign.right,
style:
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
style: HAClientTheme().getActionTextStyle(context),
),
)
);

View File

@ -33,7 +33,7 @@ class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)),
);
}

View File

@ -13,12 +13,6 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
double _currentPosition = 0;
int _savedPosition = 0;
final TextStyle _seekTextStyle = TextStyle(
fontSize: 20,
color: Colors.blue,
fontWeight: FontWeight.bold
);
@override
initState() {
super.initState();
@ -53,8 +47,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
buttons.add(
RaisedButton(
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
color: Colors.orange,
focusColor: Colors.white,
color: Theme.of(context).accentColor,
onPressed: () {
ConnectionManager().callService(
domain: "media_player",
@ -79,7 +72,13 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
children: <Widget>[
Text("00:00"),
Expanded(
child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
child: Text(
"${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.title.copyWith(
color: Colors.blue
)
),
),
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
],
@ -87,8 +86,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
Container(height: 10,),
Slider(
min: 0,
activeColor: Colors.amber,
inactiveColor: Colors.black26,
activeColor: Theme.of(context).accentColor,
max: entity.durationSeconds.toDouble(),
value: _currentPosition,
onChangeStart: (val) {

View File

@ -12,14 +12,14 @@ class MediaPlayerWidget extends StatelessWidget {
Stack(
alignment: AlignmentDirectional.topEnd,
children: <Widget>[
_buildImage(entity),
_buildImage(entity, context),
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Container(
color: Colors.black45,
child: _buildState(entity),
child: _buildState(entity, context),
),
),
Positioned(
@ -35,12 +35,9 @@ class MediaPlayerWidget extends StatelessWidget {
);
}
Widget _buildState(MediaPlayerEntity entity) {
TextStyle style = TextStyle(
fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.normal,
height: 1.2
Widget _buildState(MediaPlayerEntity entity, BuildContext context) {
TextStyle style = Theme.of(context).textTheme.body1.copyWith(
color: Colors.white
);
List<Widget> states = [];
states.add(Text("${entity.displayName}", style: style));
@ -71,7 +68,7 @@ class MediaPlayerWidget extends StatelessWidget {
);
}
Widget _buildImage(MediaPlayerEntity entity) {
Widget _buildImage(MediaPlayerEntity entity, BuildContext context) {
String state = entity.state;
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
return Container(
@ -97,7 +94,7 @@ class MediaPlayerWidget extends StatelessWidget {
Icon(
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
size: 150.0,
color: EntityColor.stateColor("$state"),
color: HAClientTheme().getColorByEntityState("$state", context),
)
],
);
@ -356,13 +353,13 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
volumeStepWidget = Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId)
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:minus")),
onPressed: () => _setVolumeDown(entity.entityId)
),
IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:plus")),
onPressed: () => _setVolumeUp(entity.entityId)
)
],
);

View File

@ -7,10 +7,10 @@ class SimpleEntityState extends StatelessWidget {
final EdgeInsetsGeometry padding;
final int maxLines;
final String customValue;
final double fontSize;
final bool bold;
final TextStyle textStyle;
//final bool bold;
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);
const SimpleEntityState({Key key,/*this.bold: false,*/ this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.textStyle, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -22,16 +22,19 @@ class SimpleEntityState extends StatelessWidget {
} else {
state = customValue;
}
TextStyle textStyle = TextStyle(
fontSize: this.fontSize,
fontWeight: FontWeight.normal
);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
textStyle = textStyle.apply(color: Colors.blue);
TextStyle tStyle;
if (textStyle != null) {
tStyle = textStyle;
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
tStyle = Theme.of(context).textTheme.subhead.copyWith(
color: Colors.blue
);
} else {
tStyle = Theme.of(context).textTheme.body1;
}
if (this.bold) {
/*if (this.bold) {
textStyle = textStyle.apply(fontWeightDelta: 100);
}
}*/
while (state.contains(" ")){
state = state.replaceAll(" ", " ");
}
@ -43,7 +46,7 @@ class SimpleEntityState extends StatelessWidget {
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: textStyle
style: tStyle
)
);
if (expanded) {

View File

@ -62,8 +62,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
children: <Widget>[
Text(
"$_newValue",
style: TextStyle(
fontSize: Sizes.largeFontSize,
style: Theme.of(context).textTheme.display1.copyWith(
color: Colors.blue
),
),

View File

@ -27,7 +27,7 @@ class _TimerStateState extends State<TimerState> {
try {
int passed = DateTime
.now()
.difference(entity._lastUpdated)
.difference(entity.lastUpdatedTimestamp)
.inSeconds;
remaining = Duration(seconds: entity.duration.inSeconds - passed);
} catch (e) {

View File

@ -1,6 +1,6 @@
part of '../main.dart';
class UniversalSlider extends StatelessWidget {
class UniversalSlider extends StatefulWidget {
final onChanged;
final onChangeEnd;
@ -14,36 +14,69 @@ class UniversalSlider extends StatelessWidget {
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
@override
State<StatefulWidget> createState() {
return UniversalSliderState();
}
}
class UniversalSliderState extends State<UniversalSlider> {
double _value;
bool _changeStarted = false;
@override
void initState() {
_value = widget.value;
super.initState();
}
@override
Widget build(BuildContext context) {
List <Widget> row = [];
if (leading != null) {
row.add(leading);
if (widget.leading != null) {
row.add(widget.leading);
}
row.add(
Flexible(
child: Slider(
value: value,
min: min,
max: max,
onChanged: (value) => onChanged(value),
onChangeEnd: (value) => onChangeEnd(value),
),
)
Flexible(
child: Slider(
value: _value,
min: widget.min,
max: widget.max,
onChangeStart: (_) {
_changeStarted = true;
},
onChanged: (value) {
setState(() {
_value = value;
});
widget.onChanged(value);
},
onChangeEnd: (value) {
_changeStarted = false;
Timer(Duration(milliseconds: 500), () {
if (!_changeStarted) {
setState(() {
_value = value;
});
widget.onChangeEnd(value);
}
});
}
),
)
);
if (closing != null) {
row.add(closing);
if (widget.closing != null) {
row.add(widget.closing);
}
return Padding(
padding: padding,
padding: widget.padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: Sizes.rowPadding,),
Text(
"$title",
style: TextStyle(fontSize: Sizes.stateFontSize),
),
Text('${widget.title}'),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.min,
@ -54,5 +87,4 @@ class UniversalSlider extends StatelessWidget {
),
);
}
}

View File

@ -10,7 +10,7 @@ class VacuumControls extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildStatusAndBattery(entity),
_buildStatusAndBattery(entity, context),
_buildCommands(entity),
_buildFanSpeed(entity),
_buildAdditionalInfo(entity)
@ -19,12 +19,12 @@ class VacuumControls extends StatelessWidget {
);
}
Widget _buildStatusAndBattery(VacuumEntity entity) {
Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) {
List<Widget> result = [];
if (entity.supportStatus) {
result.addAll(
<Widget>[
Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
Text("Status:"),
Container(width: 6,),
Expanded(
//flex: 1,
@ -33,10 +33,7 @@ class VacuumControls extends StatelessWidget {
maxLines: 1,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: Sizes.stateFontSize,
fontWeight: FontWeight.bold
),
style: Theme.of(context).textTheme.body2,
),
),
]
@ -48,7 +45,7 @@ class VacuumControls extends StatelessWidget {
result.addAll(<Widget>[
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
Container(width: 6,),
Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize))
Text("$batteryLevel %")
]
);
}
@ -172,7 +169,7 @@ class VacuumControls extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)),
Text("Vacuum cleaner commands:"),
Container(height: Sizes.rowPadding,),
Row(
mainAxisSize: MainAxisSize.max,

View File

@ -27,10 +27,7 @@ class VacuumStateButton extends StatelessWidget {
text: "RETURN TO DOCK"
);
} else {
result = Text(entity.state.toUpperCase(), style: TextStyle(
fontSize: 16,
color: Colors.grey
));
result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead);
}
return Padding(
padding: EdgeInsets.only(right: 15),

View File

@ -152,39 +152,12 @@ class EntityCollection {
return _allEntities[entityId] != null;
}
List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) {
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {
return _allEntities.values.where((entity) {
return domains.contains(entity.domain) &&
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
return
(excludeDomains.isEmpty || !excludeDomains.contains(entity.domain)) &&
(includeDomains.isEmpty || includeDomains.contains(entity.domain)) &&
((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
}).toList();
}
List<Entity> filterEntitiesForDefaultView() {
List<Entity> result = [];
List<Entity> groups = [];
List<Entity> nonGroupEntities = [];
_allEntities.forEach((id, entity){
if (entity.isGroup && (entity.attributes['auto'] == null || (entity.attributes['auto'] && !entity.isHidden)) && (!entity.isView)) {
groups.add(entity);
}
if (!entity.isGroup) {
nonGroupEntities.add(entity);
}
});
nonGroupEntities.forEach((entity) {
bool foundInGroup = false;
groups.forEach((groupEntity) {
if (groupEntity.childEntityIds.contains(entity.entityId)) {
foundInGroup = true;
}
});
if (!foundInGroup) {
result.add(entity);
}
});
result.insertAll(0, groups);
return result;
}
}

View File

@ -2,6 +2,8 @@ part of 'main.dart';
class HomeAssistant {
static const DEFAULT_DASHBOARD = 'lovelace';
static final HomeAssistant _instance = HomeAssistant._internal();
factory HomeAssistant() {
@ -11,65 +13,73 @@ class HomeAssistant {
EntityCollection entities;
HomeAssistantUI ui;
Map _instanceConfig = {};
Map services;
String _userName;
bool childMode;
String _lovelaceDashbordUrl;
HSVColor savedColor;
int savedPlayerPosition;
String sendToPlayerId;
String sendFromPlayerId;
Map services;
bool autoUi = false;
String fcmToken;
Map _rawLovelaceData;
var _rawStates;
var _rawUserInfo;
var _rawPanels;
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
List<Panel> panels = [];
Duration fetchTimeout = Duration(seconds: 30);
String get locationName {
if (ConnectionManager().useLovelace) {
return ui?.title ?? "";
if (!autoUi) {
return ui?.title ?? "Home";
} else {
return _instanceConfig["location_name"] ?? "";
return _instanceConfig["location_name"] ?? "Home";
}
}
String get userName => _userName ?? locationName;
String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty;
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
HomeAssistant._internal() {
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
ConnectionManager().onLovelaceUpdatedCallback = _handleLovelaceUpdate;
DeviceInfoManager().loadDeviceInfo();
}
Completer _fetchCompleter;
Future fetchData() {
Future fetchData(bool uiOnly) {
if (_fetchCompleter != null && !_fetchCompleter.isCompleted) {
Logger.w("Previous data fetch is not completed yet");
return _fetchCompleter.future;
}
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
_fetchCompleter = Completer();
List<Future> futures = [];
futures.add(_getStates());
if (ConnectionManager().useLovelace) {
futures.add(_getLovelace());
if (!uiOnly) {
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
futures.add(_getStates(null));
futures.add(_getConfig(null));
futures.add(_getUserInfo(null));
futures.add(_getPanels(null));
futures.add(_getServices(null));
}
if (!autoUi) {
futures.add(_getLovelace(null));
}
futures.add(_getConfig());
futures.add(_getServices());
futures.add(_getUserInfo());
futures.add(_getPanels());
Future.wait(futures).then((_) {
if (isMobileAppEnabled) {
if (!childMode) _createUI();
if (isComponentEnabled('mobile_app')) {
_createUI();
_fetchCompleter.complete();
MobileAppIntegrationManager.checkAppRegistration();
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
} else {
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app-integration")]));
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
}
}).catchError((e) {
_fetchCompleter.completeError(e);
@ -77,6 +87,48 @@ class HomeAssistant {
return _fetchCompleter.future;
}
Future<void> fetchDataFromCache() async {
Logger.d('Loading cached data');
SharedPreferences prefs = await SharedPreferences.getInstance();
bool cached = prefs.getBool('cached');
if (cached != null && cached) {
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
try {
_getStates(prefs);
if (!autoUi) {
_getLovelace(prefs);
}
_getConfig(prefs);
_getUserInfo(prefs);
_getPanels(prefs);
_getServices(prefs);
if (isComponentEnabled('mobile_app')) {
_createUI();
}
} catch (e) {
Logger.d('Didnt get cached data: $e');
}
}
}
void saveCache() async {
Logger.d('Saving data to cache...');
SharedPreferences prefs = await SharedPreferences.getInstance();
try {
await prefs.setString('cached_states', json.encode(_rawStates));
await prefs.setString('cached_lovelace', json.encode(_rawLovelaceData));
await prefs.setString('cached_user', json.encode(_rawUserInfo));
await prefs.setString('cached_config', json.encode(_instanceConfig));
await prefs.setString('cached_panels', json.encode(_rawPanels));
await prefs.setString('cached_services', json.encode(services));
await prefs.setBool('cached', true);
} catch (e) {
await prefs.setBool('cached', false);
Logger.e('Error saving cache: $e');
}
Logger.d('Done saving cache');
}
Future logout() async {
Logger.d("Logging out...");
await ConnectionManager().logout().then((_) {
@ -86,80 +138,186 @@ class HomeAssistant {
});
}
Future _getConfig() async {
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
_instanceConfig = Map.from(data);
}).catchError((e) {
throw HAError("Error getting config: ${e}");
});
}
Future _getStates() async {
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => entities.parse(data)
).catchError((e) {
throw HAError("Error getting states: $e");
});
}
Future _getLovelace() {
Completer completer = Completer();
ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) {
_rawLovelaceData = data;
completer.complete();
}).catchError((e) {
if ("$e" == "config_not_found") {
ConnectionManager().useLovelace = false;
completer.complete();
} else {
completer.completeError(HAError("Error getting lovelace config: $e"));
Future _getConfig(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) {
try {
var data = json.decode(sharedPrefs.getString('cached_config'));
_parseConfig(data);
} catch (e) {
throw HAError("Error getting config: $e");
}
});
return completer.future;
} else {
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) => _parseConfig(data)).catchError((e) {
throw HAError("Error getting config: $e");
});
}
}
Future _getUserInfo() async {
_userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) {
_userName = data["name"];
childMode = _userName.startsWith("[child]");
}).catchError((e) {
Logger.w("Can't get user info: $e");
});
void _parseConfig(data) {
_instanceConfig = Map.from(data);
Logger.d('stream: ${_instanceConfig['components'].contains('stream')}');
}
Future _getServices() async {
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
Logger.d("Got ${data.length} services");
services = data;
}).catchError((e) {
Future _getStates(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) {
try {
var data = json.decode(sharedPrefs.getString('cached_states'));
_parseStates(data);
} catch (e) {
throw HAError("Error getting states: $e");
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => _parseStates(data)
).catchError((e) {
throw HAError("Error getting states: $e");
});
}
}
void _parseStates(data) {
_rawStates = data;
entities.parse(data);
}
Future _getLovelace(SharedPreferences sharedPrefs) {
if (sharedPrefs != null) {
try {
var data = json.decode(sharedPrefs.getString('cached_lovelace'));
_rawLovelaceData = data;
} catch (e) {
autoUi = true;
}
return Future.value();
} else {
Completer completer = Completer();
var additionalData;
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
additionalData = {
'url_path': _lovelaceDashbordUrl
};
}
ConnectionManager().sendSocketMessage(
type: 'lovelace/config',
additionalData: additionalData
).then((data) {
_rawLovelaceData = data;
completer.complete();
}).catchError((e) {
if ("$e" == "config_not_found") {
autoUi = true;
_rawLovelaceData = null;
completer.complete();
} else {
completer.completeError(HAError("Error getting lovelace config: $e"));
}
});
return completer.future;
}
}
Future _getServices(SharedPreferences prefs) async {
if (prefs != null) {
try {
var data = json.decode(prefs.getString('cached_services'));
_parseServices(data);
} catch (e) {
Logger.w("Can't get services: $e");
}
}
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
Logger.w("Can't get services: $e");
});
}
Future _getPanels() async {
panels.clear();
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) {
data.forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
panels.add(Panel(
id: k,
type: v["component_name"],
title: title,
urlPath: v["url_path"],
config: v["config"],
icon: v["icon"]
)
);
});
}).catchError((e) {
throw HAError("Error getting panels list: $e");
void _parseServices(data) {
services = data;
}
Future _getUserInfo(SharedPreferences sharedPrefs) async {
_userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) {
Logger.w("Can't get user info: $e");
});
}
void _parseUserInfo(data) {
_rawUserInfo = data;
_userName = data["name"];
}
Future _getPanels(SharedPreferences sharedPrefs) async {
if (sharedPrefs != null) {
try {
var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data);
} catch (e) {
throw HAError("Error getting panels list: $e");
}
} else {
await ConnectionManager().sendSocketMessage(type: "get_panels").then((data) => _parsePanels(data)).catchError((e) {
throw HAError("Error getting panels list: $e");
});
}
}
void _parsePanels(data) {
_rawPanels = data;
panels.clear();
List<Panel> dashboards = [];
data.forEach((k,v) {
String title = v['title'] == null ? "${k[0].toUpperCase()}${k.substring(1)}" : "${v['title'][0].toUpperCase()}${v['title'].substring(1)}";
if (v['component_name'] != null && v['component_name'] == 'lovelace') {
dashboards.add(
Panel(
id: k,
componentName: v['component_name'],
title: title,
urlPath: v['url_path'],
config: v['config'],
icon: (v['icon'] == null || v['icon'] == 'hass:view-dashboard') ? 'mdi:view-dashboard' : v['icon']
)
);
} else {
panels.add(
Panel(
id: k,
componentName: v['component_name'],
title: title,
urlPath: v['url_path'],
config: v['config'],
icon: v['icon']
)
);
}
});
panels.insertAll(0, dashboards);
}
Future getCameraStream(String entityId) {
Completer completer = Completer();
ConnectionManager().sendSocketMessage(type: "camera/stream", additionalData: {"entity_id": entityId}).then((data) {
completer.complete(data);
}).catchError((e) {
completer.completeError(e);
});
return completer.future;
}
bool isComponentEnabled(String name) {
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name");
}
void _handleLovelaceUpdate() {
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
eventBus.fire(new LovelaceChangedEvent());
}
}
void _handleEntityStateChange(Map eventData) {
//TheLogger.debug( "New state for ${eventData['entity_id']}");
if (_fetchCompleter.isCompleted) {
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
Map data = Map.from(eventData);
eventBus.fire(new StateChangedEvent(
entityId: data["entity_id"],
@ -168,211 +326,26 @@ class HomeAssistant {
}
}
void _parseLovelace() {
Logger.d("--Title: ${_rawLovelaceData["title"]}");
ui.title = _rawLovelaceData["title"];
int viewCounter = 0;
Logger.d("--Views count: ${_rawLovelaceData['views'].length}");
_rawLovelaceData["views"].forEach((rawView){
Logger.d("----view id: ${rawView['id']}");
HAView view = HAView(
count: viewCounter,
id: "${rawView['id']}",
name: rawView['title'],
iconName: rawView['icon'],
panel: rawView['panel'] ?? false,
);
if (rawView['badges'] != null && rawView['badges'] is List) {
rawView['badges'].forEach((entity) {
if (entity is String) {
if (entities.isExist(entity)) {
Entity e = entities.get(entity);
view.badges.add(e);
}
} else {
String eId = '${entity['entity']}';
if (entities.isExist(eId)) {
Entity e = entities.get(eId);
view.badges.add(e);
}
}
});
}
view.cards.addAll(_createLovelaceCards(rawView["cards"] ?? []));
ui.views.add(
view
);
viewCounter += 1;
});
}
List<HACard> _createLovelaceCards(List rawCards) {
List<HACard> result = [];
rawCards.forEach((rawCard){
try {
//bool isThereCardOptionsInside = rawCard["card"] != null;
var rawCardInfo = rawCard["card"] ?? rawCard;
HACard card = HACard(
id: "card",
name: rawCardInfo["title"] ?? rawCardInfo["name"],
type: rawCardInfo['type'] ?? CardType.ENTITIES,
columnsCount: rawCardInfo['columns'] ?? 4,
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
states: rawCardInfo['states'],
conditions: rawCard['conditions'] ?? [],
content: rawCardInfo['content'],
min: rawCardInfo['min'] ?? 0,
max: rawCardInfo['max'] ?? 100,
unit: rawCardInfo['unit'],
severity: rawCardInfo['severity']
);
if (rawCardInfo["cards"] != null) {
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
}
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
rawEntities?.forEach((rawEntity) {
if (rawEntity is String) {
if (entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: entities.get(rawEntity)));
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (rawEntity["type"] == "divider") {
card.entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (entities.isExist(rawEntity["entity"])) {
Entity e = entities.get(rawEntity["entity"]);
card.entities.add(
EntityWrapper(
entity: e,
displayName: rawEntity["name"],
icon: rawEntity["icon"],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
if (rawSingleEntity != null) {
var en = rawSingleEntity;
if (en is String) {
if (entities.isExist(en)) {
Entity e = entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: rawCardInfo["icon"],
displayName: rawCardInfo["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
}
} else {
if (entities.isExist(en["entity"])) {
Entity e = entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
icon: en["icon"],
displayName: en["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
}
}
}
result.add(card);
} catch (e) {
Logger.e("There was an error parsing card: ${e.toString()}");
}
});
return result;
bool isServiceExist(String service) {
return services != null &&
services.isNotEmpty &&
services.containsKey(service);
}
void _createUI() {
ui = HomeAssistantUI();
if ((ConnectionManager().useLovelace) && (_rawLovelaceData != null)) {
Logger.d("Creating Lovelace UI");
_parseLovelace();
} else {
Logger.d("Creating group-based UI");
int viewCounter = 0;
if (!entities.hasDefaultView) {
HAView view = HAView(
count: viewCounter,
id: "group.default_view",
name: "Home",
childEntities: entities.filterEntitiesForDefaultView()
Logger.d("Creating Lovelace UI");
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
/*if (isServiceExist('zha_map')) {
panels.add(
Panel(
id: 'haclient_zha',
componentName: 'haclient_zha',
title: 'ZHA',
urlPath: '/haclient_zha',
icon: 'mdi:zigbee'
)
);
ui.views.add(
view
);
viewCounter += 1;
}
entities.viewEntities.forEach((viewEntity) {
HAView view = HAView(
count: viewCounter,
id: viewEntity.entityId,
name: viewEntity.displayName,
childEntities: viewEntity.childEntities
);
view.linkedEntity = viewEntity;
ui.views.add(
view
);
viewCounter += 1;
});
}
}
Widget buildViews(BuildContext context, TabController tabController) {
return ui.build(context, tabController);
}*/
}
}

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:async';
import 'dart:math';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -13,7 +14,6 @@ import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
@ -21,18 +21,19 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/circular_slider/single_circular_slider.dart';
import 'package:share/receive_share_state.dart';
import 'package:share/share.dart';
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 'package:sentry/sentry.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
import 'package:webview_flutter/webview_flutter.dart';
import 'package:syncfusion_flutter_core/core.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import 'utils/logger.dart';
import '.secrets.dart';
part 'const.dart';
part 'utils/launcher.dart';
@ -61,8 +62,7 @@ part 'entities/badge.widget.dart';
part 'entities/entity_model.widget.dart';
part 'entities/default_entity_container.widget.dart';
part 'entities/missed_entity.widget.dart';
part 'cards/widgets/glance_card_entity_container.dart';
part 'cards/widgets/entity_button_card_body.widget.dart';
part 'cards/entity_button_card.dart';
part 'pages/widgets/entity_attributes_list.dart';
part 'entities/entity_icon.widget.dart';
part 'entities/entity_name.widget.dart';
@ -73,7 +73,6 @@ part 'entities/universal_slider.widget.dart';
part 'entities/flat_service_button.widget.dart';
part 'entities/light/widgets/light_color_picker.dart';
part 'entities/camera/widgets/camera_stream_view.dart';
part 'entities/entity_colors.class.dart';
part 'plugins/history_chart/entity_history.dart';
part 'plugins/history_chart/simple_state_history_chart.dart';
part 'plugins/history_chart/numeric_state_history_chart.dart';
@ -85,6 +84,7 @@ part 'entities/slider/widgets/slider_controls.dart';
part 'entities/text/widgets/text_input_state.dart';
part 'entities/select/widgets/select_state.dart';
part 'entities/simple_state.widget.dart';
part 'entities/entity_picture.widget.dart';
part 'entities/timer/widgets/timer_state.dart';
part 'entities/climate/widgets/climate_state.widget.dart';
part 'entities/cover/widgets/cover_state.dart';
@ -100,14 +100,18 @@ part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.d
part 'entities/vacuum/vacuum_entity.class.dart';
part 'entities/vacuum/widgets/vacuum_controls.dart';
part 'entities/vacuum/widgets/vacuum_state_button.dart';
part 'pages/settings.page.dart';
part 'pages/settings/connection_settings.part.dart';
part 'pages/purchase.page.dart';
part 'pages/widgets/product_purchase.widget.dart';
part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/bottom_info_bar.dart';
part 'pages/widgets/page_loading_error.dart';
part 'pages/panel.page.dart';
part 'pages/main.page.dart';
part 'pages/integration_settings.page.dart';
part 'pages/main/main.page.dart';
part 'pages/settings/integration_settings.part.dart';
part 'pages/settings/app_settings.page.dart';
part 'pages/settings/lookandfeel_settings.part.dart';
part 'pages/zha_page.dart';
part 'home_assistant.class.dart';
part 'pages/log.page.dart';
part 'pages/entity.page.dart';
@ -119,31 +123,39 @@ part 'managers/mobile_app_integration_manager.class.dart';
part 'managers/connection_manager.class.dart';
part 'managers/device_info_manager.class.dart';
part 'managers/startup_user_messages_manager.class.dart';
part 'managers/theme_manager.dart';
part 'ui.dart';
part 'view.class.dart';
part 'cards/card.class.dart';
part 'panels/panel_class.dart';
part 'viewWidget.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 'cards/gauge_card.dart';
part 'cards/widgets/card_wrapper.widget.dart';
part 'cards/entities_card.dart';
part 'cards/alarm_panel_card.dart';
part 'cards/horizontal_srack_card.dart';
part 'cards/markdown_card.dart';
part 'cards/media_control_card.dart';
part 'cards/unsupported_card.dart';
part 'cards/vertical_stack_card.dart';
part 'cards/glance_card.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';
part 'pages/fullscreen.page.dart';
EventBus eventBus = new EventBus();
final SentryClient _sentry = SentryClient(dsn: "https://03ef364745cc4c23a60ddbc874c69925@sentry.io/1836118");
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersionNumber = "0.7.6";
const appVersionNumber = "0.8.5";
const appVersionAdd = "";
const appVersion = "$appVersionNumber$appVersionAdd";
@ -152,59 +164,149 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
if (Logger.isInDebugMode) {
Logger.e('Caught error: $error');
Logger.p(stackTrace);
return;
} else {
Logger.e('Caught error: $error. Reporting to Senrty.');
// Send the Exception and Stacktrace to Sentry in Production mode.
_sentry.captureException(
exception: error,
stackTrace: stackTrace,
);
}
Crashlytics.instance.recordError(error, stackTrace);
}
void main() async {
Crashlytics.instance.enableInDevMode = true;
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
FlutterError.onError = (FlutterErrorDetails details) {
Logger.e(" Caut Flutter runtime error: ${details.exception}");
Logger.e("Caut Flutter runtime error: ${details.exception}");
if (Logger.isInDebugMode) {
FlutterError.dumpErrorToConsole(details);
} else {
// In production mode, report to the application zone to report to
// Sentry.
Zone.current.handleUncaughtError(details.exception, details.stack);
}
Crashlytics.instance.recordFlutterError(details);
};
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
runZoned(() {
runApp(new HAClientApp());
runApp(new HAClientApp(
theme: theme,
));
}, onError: (error, stack) {
_reportError(error, stack);
});
}
class HAClientApp extends StatelessWidget {
class HAClientApp extends StatefulWidget {
final AppTheme theme;
const HAClientApp({Key key, this.theme: AppTheme.defaultTheme}) : super(key: key);
@override
_HAClientAppState createState() => new _HAClientAppState();
}
class _HAClientAppState extends State<HAClientApp> {
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
StreamSubscription _themeChangeSubscription;
AppTheme _currentTheme = AppTheme.defaultTheme;
@override
void initState() {
InAppPurchaseConnection.enablePendingPurchases();
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_purchaseUpdateSubscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
_currentTheme = widget.theme;
_themeChangeSubscription = eventBus.on<ChangeThemeEvent>().listen((event){
setState(() {
_currentTheme = event.theme;
});
});
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
super.initState();
}
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");
}
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: appName,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
theme: HAClientTheme().getThemeData(_currentTheme),
darkTheme: HAClientTheme().darkTheme,
debugShowCheckedModeBanner: false,
initialRoute: "/",
routes: {
"/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/integration-settings": (context) => IntegrationSettingsPage(title: "Integration settings"),
"/app-settings": (context) => AppSettingsPage(),
"/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings),
"/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings),
"/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"),
"/whats-new": (context) => WhatsNewPage()
"/webview": (context) => standaloneWebview.WebviewScaffold(
url: "${(ModalRoute.of(context).settings.arguments as Map)['url']}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop()
),
title: new Text("${(ModalRoute.of(context).settings.arguments as Map)['title']}"),
),
),
"/whats-new": (context) => WhatsNewPage(),
"/haclient_zha": (context) => ZhaPage(),
"/auth": (context) => new standaloneWebview.WebviewScaffold(
url: "${ConnectionManager().oauthUrl}",
appBar: new AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#authentication")
),
title: new Text("Login with HA"),
actions: <Widget>[
FlatButton(
child: Text("Manual", style: Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
)),
onPressed: () {
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
},
)
],
),
)
},
);
}
@override
void dispose() {
_purchaseUpdateSubscription.cancel();
_themeChangeSubscription.cancel();
super.dispose();
}
}

View File

@ -9,46 +9,37 @@ class AuthManager {
}
AuthManager._internal();
StreamSubscription deepLinksSubscription;
Future start({String oauthUrl}) {
Completer completer = Completer();
deepLinksSubscription?.cancel();
deepLinksSubscription = getUriLinksStream().listen((Uri uri) {
Logger.d("[LINKED AUTH] We got something private");
_getTempToken(oauthUrl, uri.queryParameters["code"])
.then((tempToken) => completer.complete(tempToken))
.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",
contentType: "application/x-www-form-urlencoded",
includeAuthHeader: false,
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('http://ha-client.homemade.systems')}"
final flutterWebviewPlugin = new standaloneWebview.FlutterWebviewPlugin();
flutterWebviewPlugin.onUrlChanged.listen((String url) {
if (url.startsWith("https://ha-client.app/service/auth_callback.html")) {
Logger.d("url=$url");
String authCode = url.split("=")[1];
Logger.d("authCode=$authCode");
Logger.d("We have auth code. Getting temporary access token...");
ConnectionManager().sendHTTPPost(
endPoint: "/auth/token",
contentType: "application/x-www-form-urlencoded",
includeAuthHeader: false,
data: "grant_type=authorization_code&code=$authCode&client_id=${Uri.encodeComponent('https://ha-client.app')}"
).then((response) {
Logger.d("Got temp token");
String tempToken = json.decode(response)['access_token'];
Logger.d("Closing webview...");
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.complete(tempToken);
}).catchError((e) {
//flutterWebviewPlugin.close();
Logger.e("Error getting temp token: ${e.toString()}");
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.completeError(HAError("Error getting temp token"));
});
}).whenComplete(() => flutterWebviewPlugin.close());
}
});
Logger.d("Launching OAuth");
eventBus.fire(StartAuthEvent(oauthUrl, true));
return completer.future;
}
}
}

View File

@ -19,8 +19,8 @@ class ConnectionManager {
String _tempToken;
String oauthUrl;
String webhookId;
bool useLovelace = true;
bool settingsLoaded = false;
int appIntegrationVersion;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
@ -28,6 +28,7 @@ class ConnectionManager {
bool isConnected = false;
var onStateChangeCallback;
var onLovelaceUpdatedCallback;
IOWebSocketChannel _socket;
@ -38,12 +39,12 @@ class ConnectionManager {
Completer completer = Completer();
bool stopInit = false;
if (loadSettings) {
Logger.e("Loading settings...");
Logger.d("Loading settings...");
SharedPreferences prefs = await SharedPreferences.getInstance();
useLovelace = prefs.getBool('use-lovelace') ?? true;
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
displayHostname = "$_domain:$_port";
_webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
@ -59,9 +60,9 @@ class ConnectionManager {
_token = await storage.read(key: "hacl_llt");
Logger.e("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'http://ha-client.homemade.systems')}&redirect_uri=${Uri
'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent(
'haclient://auth')}";
'https://ha-client.app/service/auth_callback.html')}";
settingsLoaded = true;
} catch (e) {
completer.completeError(HAError("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
@ -149,6 +150,10 @@ class ConnectionManager {
} else if (data["type"] == "auth_ok") {
Logger.d("[Received] <== ${data.toString()}");
Logger.d("[Connection] Subscribing to events");
sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "lovelace_updated"},
);
sendSocketMessage(
type: "subscribe_events",
additionalData: {"event_type": "state_changed"},
@ -211,13 +216,14 @@ class ConnectionManager {
}
_messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]);
} else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
} else {
Logger.e("Event is null: $data");
if (data["event"] != null) {
if (data["event"]["event_type"] == "state_changed") {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]);
} else if (data["event"]["event_type"] == "lovelace_updated") {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: $data");
onLovelaceUpdatedCallback();
}
}
} else {
Logger.d("[Received unhandled] <== ${data.toString()}");
@ -351,7 +357,7 @@ class ConnectionManager {
_currentMessageId += 1;
}
Future callService({@required String domain, @required String service, String entityId, Map data}) {
Future callService({@required String domain, @required String service, entityId, Map data}) {
eventBus.fire(NotifyServiceCallEvent(domain, service, entityId));
Logger.d("Service call: $domain.$service, $entityId, $data");
Completer completer = Completer();

View File

@ -95,8 +95,8 @@ class LocationManager {
backoffPolicy: workManager.BackoffPolicy.linear,
backoffPolicyDelay: interval,
constraints: workManager.Constraints(
networkType: workManager.NetworkType.connected
)
networkType: workManager.NetworkType.connected,
),
);
}
}
@ -104,7 +104,7 @@ class LocationManager {
_stopLocationService() async {
Logger.d("Canceling previous schedule if any...");
await workManager.Workmanager.cancelByTag(backgroundTaskTag);
await workManager.Workmanager.cancelAll();
}
updateDeviceLocation() async {
@ -148,52 +148,90 @@ class LocationManager {
}
void updateDeviceLocationIsolate() {
workManager.Workmanager.executeTask((backgroundTask, data) {
print("[Background $backgroundTask] Started");
final SentryClient sentryBackgroundClient = SentryClient(dsn: "https://5c868e5ef26947e2b61b189e391ec31b@sentry.io/1836366");
workManager.Workmanager.executeTask((backgroundTask, data) async {
//print("[Background $backgroundTask] Started");
Geolocator geolocator = Geolocator();
var battery = Battery();
int batteryLevel = 100;
String webhookId = data["webhookId"];
String httpWebHost = data["httpWebHost"];
//String logData = '==> ${DateTime.now()} [Background $backgroundTask]:';
//print("[Background $backgroundTask] Getting path for log file...");
//final logFileDirectory = await getExternalStorageDirectory();
//print("[Background $backgroundTask] Opening log file...");
//File logFile = File('${logFileDirectory.path}/ha-client-background-log.txt');
//print("[Background $backgroundTask] Log file path: ${logFile.path}");
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.high, locationPermissionLevel: GeolocationPermission.locationAlways).then((location) {
if (location != null) {
print("[Background $backgroundTask] Got location: ${location.latitude} ${location.longitude}");
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)
);
} else {
throw "Can't get device location. Location is null";
}
}).catchError((e) {
sentryBackgroundClient.captureException(
exception: "${e.toString()}"
String url = "$httpWebHost/api/webhook/$webhookId";
Map<String, String> headers = {};
headers["Content-Type"] = "application/json";
Map data = {
"type": "update_location",
"data": {
"gps": [],
"gps_accuracy": 0,
"battery": 100
}
};
//print("[Background $backgroundTask] Getting battery level...");
int batteryLevel;
try {
batteryLevel = await battery.batteryLevel;
//print("[Background $backgroundTask] Got battery level: $batteryLevel");
} catch(e) {
//print("[Background $backgroundTask] Error getting battery level: $e. Setting zero");
batteryLevel = 0;
//logData += 'Battery: error, $e';
}
if (batteryLevel != null) {
data["data"]["battery"] = batteryLevel;
//logData += 'Battery: success, $batteryLevel';
}/* else {
logData += 'Battery: error, level is null';
}*/
Position location;
try {
location = await geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high, locationPermissionLevel: GeolocationPermission.locationAlways);
if (location != null && location.latitude != null) {
//logData += ' || Location: success, ${location.latitude} ${location.longitude} (${location.timestamp})';
data["data"]["gps"] = [location.latitude, location.longitude];
data["data"]["gps_accuracy"] = location.accuracy;
try {
http.Response response = await http.post(
url,
headers: headers,
body: json.encode(data)
);
print("[Background $backgroundTask] Error getting current location: ${e.toString()}");
});
});
/*if (response.statusCode >= 200 && response.statusCode < 300) {
logData += ' || Post: success, ${response.statusCode}';
} else {
logData += ' || Post: error, ${response.statusCode}';
}*/
} catch(e) {
//logData += ' || Post: error, $e';
}
}/* else {
logData += ' || Location: error, location is null';
}*/
} catch (e) {
//print("[Background $backgroundTask] Location error: $e");
//logData += ' || Location: error, $e';
}
}/* else {
logData += 'Not configured';
}*/
//print("[Background $backgroundTask] Writing log data...");
/*try {
var fileMode;
if (logFile.existsSync() && logFile.lengthSync() < 5000000) {
fileMode = FileMode.append;
} else {
fileMode = FileMode.write;
}
await logFile.writeAsString('$logData\n', mode: fileMode);
} catch (e) {
print("[Background $backgroundTask] Error writing log: $e");
}
return Future.value(true);
print("[Background $backgroundTask] Finished.");*/
return true;
});
}

View File

@ -2,9 +2,11 @@ part of '../main.dart';
class MobileAppIntegrationManager {
static const INTEGRATION_VERSION = 3;
static final _appRegistrationData = {
"app_version": "$appVersion",
"device_name": "",
"app_version": "$appVersion",
"manufacturer": DeviceInfoManager().manufacturer,
"model": DeviceInfoManager().model,
"os_version": DeviceInfoManager().osVersion,
@ -22,6 +24,7 @@ class MobileAppIntegrationManager {
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}",
"app_id": "ha_client",
"app_name": "$appName",
"os_name": DeviceInfoManager().osName,
@ -34,9 +37,11 @@ class MobileAppIntegrationManager {
).then((response) {
Logger.d("Processing registration responce...");
var responseObject = json.decode(response);
ConnectionManager().webhookId = responseObject["webhook_id"];
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
SharedPreferences.getInstance().then((prefs) {
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
ConnectionManager().webhookId = responseObject["webhook_id"];
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
completer.complete();
eventBus.fire(ShowPopupDialogEvent(
@ -65,18 +70,29 @@ class MobileAppIntegrationManager {
includeAuthHeader: false,
data: json.encode(updateData)
).then((response) {
if (response == null || response.isEmpty) {
Logger.d("No registration data in response. MobileApp integration was removed");
var registrationData;
try {
registrationData = json.decode(response);
} catch (e) {
registrationData = null;
}
if (registrationData == null || registrationData.isEmpty) {
Logger.d("No registration data in response. MobileApp integration was removed or broken");
_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"
));
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
Logger.d('App registration needs to be updated');
_askToRemoveAndRegisterApp();
} else {
Logger.d('App registration works fine');
if (showOkDialog) {
eventBus.fire(ShowPopupDialogEvent(
title: "All good",
body: "HA Client integration with your Home Assistant server works fine",
positiveText: "Nice!",
negativeText: "Ok"
));
}
}
}
completer.complete();
@ -105,10 +121,22 @@ class MobileAppIntegrationManager {
}
}
static void _askToRemoveAndRegisterApp() {
eventBus.fire(ShowPopupDialogEvent(
title: "Mobile app integration needs to be updated",
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
positiveText: "Ok",
negativeText: "Report an issue",
onNegative: () {
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
},
));
}
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.",
title: "App integration is broken",
body: "Looks like app integration was removed from your Home Assistant or it needs to be updated. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant before registering and restart Home Assistant. Then go back here.",
positiveText: "Register now",
negativeText: "Cancel",
onPositive: () {

View File

@ -9,12 +9,12 @@ class StartupUserMessagesManager {
return _instance;
}
StartupUserMessagesManager._internal() {}
StartupUserMessagesManager._internal();
bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-706";
static final _whatsNewMessageKey = "user-message-shown-whats-new-894";
void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance();

View File

@ -0,0 +1,272 @@
part of '../main.dart';
enum AppTheme {darkTheme, defaultTheme, haTheme}
class HAClientTheme {
static const TextTheme textTheme = TextTheme(
display1: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
display2: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
headline: TextStyle(fontSize: 24, fontWeight: FontWeight.normal),
title: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
subhead: TextStyle(fontSize: 16, fontWeight: FontWeight.normal),
body1: TextStyle(fontSize: 15, fontWeight: FontWeight.normal),
body2: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
subtitle: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
caption: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
overline: TextStyle(
fontSize: 10,
fontWeight: FontWeight.normal,
letterSpacing: 1,
),
button: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
);
static const offEntityStates = [
EntityState.off,
EntityState.closed,
"below_horizon",
"default",
EntityState.idle,
EntityState.alarm_disarmed,
];
static const onEntityStates = [
EntityState.on,
"auto",
EntityState.active,
EntityState.playing,
EntityState.paused,
"above_horizon",
EntityState.home,
EntityState.open,
EntityState.cleaning,
EntityState.returning,
"cool",
EntityState.alarm_arming,
EntityState.alarm_disarming,
EntityState.alarm_pending,
];
static const disabledEntityStates = [
EntityState.unavailable,
EntityState.unknown,
];
static const alarmEntityStates = [
EntityState.alarm_armed_away,
EntityState.alarm_armed_custom_bypass,
EntityState.alarm_armed_home,
EntityState.alarm_armed_night,
EntityState.alarm_triggered,
"heat",
];
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
static const badgeColors = {
"default": Color.fromRGBO(223, 76, 30, 1.0),
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
};
static final HAClientTheme _instance = HAClientTheme
._internal();
factory HAClientTheme() {
return _instance;
}
HAClientTheme._internal();
ThemeData getThemeData(AppTheme theme) {
switch (theme) {
case AppTheme.darkTheme:
return darkTheme;
break;
case AppTheme.defaultTheme:
return defaultTheme;
break;
case AppTheme.haTheme:
return homeAssistantTheme;
break;
default:
return defaultTheme;
}
}
final ThemeData defaultTheme = ThemeData.from(
colorScheme: ColorScheme(
primary: Color.fromRGBO(112, 154, 193, 1),
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
secondary: Color.fromRGBO(253, 216, 53, 1),
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
background: Color.fromRGBO(250, 250, 250, 1),
surface: Colors.white,
error: Colors.red,
onPrimary: Colors.white,
onSecondary: Colors.black87,
onBackground: Colors.black87,
onSurface: Colors.black87,
onError: Colors.white,
brightness: Brightness.light
),
textTheme: ThemeData.light().textTheme.copyWith(
display1: textTheme.display1.copyWith(color: Colors.black54),
display2: textTheme.display2.copyWith(color: Colors.redAccent),
headline: textTheme.headline.copyWith(color: Colors.black87),
title: textTheme.title.copyWith(color: Colors.black87),
subhead: textTheme.subhead.copyWith(color: Colors.black54),
body1: textTheme.body1.copyWith(color: Colors.black87),
body2: textTheme.body2.copyWith(color: Colors.black87),
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
caption: textTheme.caption.copyWith(color: Colors.black45),
overline: textTheme.overline.copyWith(color: Colors.black26),
button: textTheme.button.copyWith(color: Colors.white),
)
);
final ThemeData homeAssistantTheme = ThemeData.from(
colorScheme: ColorScheme(
primary: Color.fromRGBO(2, 165, 238, 1),
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
secondary: Color.fromRGBO(253, 216, 53, 1),
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
background: Color.fromRGBO(250, 250, 250, 1),
surface: Colors.white,
error: Colors.red,
onPrimary: Colors.white,
onSecondary: Colors.black87,
onBackground: Colors.black87,
onSurface: Colors.black87,
onError: Colors.white,
brightness: Brightness.light
),
textTheme: ThemeData.light().textTheme.copyWith(
display1: textTheme.display1.copyWith(color: Colors.black54),
display2: textTheme.display2.copyWith(color: Colors.redAccent),
headline: textTheme.headline.copyWith(color: Colors.black87),
title: textTheme.title.copyWith(color: Colors.black87),
subhead: textTheme.subhead.copyWith(color: Colors.black54),
body1: textTheme.body1.copyWith(color: Colors.black87),
body2: textTheme.body2.copyWith(color: Colors.black87),
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
caption: textTheme.caption.copyWith(color: Colors.black45),
overline: textTheme.overline.copyWith(color: Colors.black26),
button: textTheme.button.copyWith(color: Colors.white),
)
);
final ThemeData darkTheme = ThemeData.from(
colorScheme: ColorScheme(
primary: Color.fromRGBO(112, 154, 193, 1),
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
secondary: Color.fromRGBO(253, 216, 53, 1),
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
background: Color.fromRGBO(47, 49, 54, 1),
surface: Color.fromRGBO(54, 57, 63, 1),
error: Color.fromRGBO(183, 109, 109, 1),
onPrimary: Colors.white,
onSecondary: Colors.black87,
onBackground: Color.fromRGBO(220, 221, 222, 1),
onSurface: Colors.white,
onError: Colors.white,
brightness: Brightness.dark
),
textTheme: textTheme
);
Color getOnStateColor(BuildContext context) {
return Theme.of(context).colorScheme.secondary;
}
Color getOffStateColor(BuildContext context) {
return Theme.of(context).colorScheme.primaryVariant;
}
Color getDisabledStateColor(BuildContext context) {
return Theme.of(context).disabledColor;
}
Color getAlertStateColor(BuildContext context) {
return Theme.of(context).colorScheme.error;
}
Color getColorByEntityState(String state, BuildContext context) {
if (onEntityStates.contains(state)) {
return getOnStateColor(context);
} else if (disabledEntityStates.contains(state)) {
return getDisabledStateColor(context);
} else if (alarmEntityStates.contains(state)) {
return getAlertStateColor(context);
} else {
return getOffStateColor(context);
}
}
Color getGreenGaugeColor() {
return Colors.green;
}
Color getYellowGaugeColor() {
return Colors.yellow;
}
Color getRedGaugeColor() {
return Colors.red;
}
TextStyle getLinkTextStyle(BuildContext context) {
ThemeData theme = Theme.of(context);
return theme.textTheme.body1.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline
);
}
TextStyle getActionTextStyle(BuildContext context) {
ThemeData theme = Theme.of(context);
return theme.textTheme.subhead.copyWith(
color: Colors.blue
);
}
Color getBadgeColor(String entityDomain) {
return badgeColors[entityDomain] ??
badgeColors["default"];
}
Color getOnBadgeTextColor() {
return Colors.white;
}
charts.Color chartHistoryStateColor(String state, int id, BuildContext context) {
Color c = getColorByEntityState(state, context);
if (c != null) {
return charts.Color(
r: c.red,
g: c.green,
b: c.blue,
a: c.alpha
);
} else {
double r = id.toDouble() % 10;
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
}
}
Color historyStateColor(String state, int id, BuildContext context) {
Color c = getColorByEntityState(state, context);
if (c != null) {
return c;
} else {
if (id > -1) {
double r = id.toDouble() % 10;
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
} else {
return getOnStateColor(context);
}
}
}
}

View File

@ -12,61 +12,52 @@ class EntityViewPage extends StatefulWidget {
class _EntityViewPageState extends State<EntityViewPage> {
StreamSubscription _refreshDataSubscription;
StreamSubscription _stateSubscription;
Entity entity;
Entity forwardToMainPage;
bool _popScheduled = false;
Entity _entity;
@override
void initState() {
super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId == widget.entityId) {
entity = HomeAssistant().entities.get(widget.entityId);
Logger.d("[Entity page] State change event handled: ${event.entityId}");
setState(() {});
setState(() {
_getEntity();
});
}
});
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
entity = HomeAssistant().entities.get(widget.entityId);
setState(() {});
Logger.d("[Entity page] Refresh data event handled");
setState(() {
_getEntity();
});
});
entity = HomeAssistant().entities.get(widget.entityId);
_getEntity();
}
_getEntity() {
_entity = HomeAssistant().entities.get(widget.entityId);
}
@override
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);
}
String entityNameToDisplay = '${(_entity?.displayName ?? widget.entityId) ?? ''}';
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text("${entity.displayName}"),
title: new Text(entityNameToDisplay),
),
body: body,
body: _entity == null ? PageLoadingError(
errorText: 'Entity is not available $entityNameToDisplay',
) : EntityPageLayout(entity: _entity),
);
}
_popAfterBuild() async {
forwardToMainPage = entity;
await Future.delayed(Duration(milliseconds: 300));
Navigator.of(context).pop();
}
@override
void dispose(){
if (_stateSubscription != null) _stateSubscription.cancel();
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
eventBus.fire(ShowEntityPageEvent(entity: forwardToMainPage));
super.dispose();
}
}

View File

@ -0,0 +1,18 @@
part of '../main.dart';
class FullScreenPage extends StatelessWidget {
final Widget child;
const FullScreenPage({Key key, this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Center(
child: this.child,
),
);
}
}

View File

@ -1,200 +0,0 @@
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;
if (_locationInterval % 5 != 0) {
_locationInterval = 5 * (_locationInterval ~/ 5);
}
});
});
}
void incLocationInterval() {
if (_locationInterval < 720) {
setState(() {
_locationInterval = _locationInterval + 5;
});
}
}
void decLocationInterval() {
if (_locationInterval > 5) {
setState(() {
_locationInterval = _locationInterval - 5;
});
}
}
restart() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to restart Home Assistant?",
body: "This will restart your Home Assistant server.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart");
},
));
}
stop() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to STOP Home Assistant?",
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "stop");
},
));
}
updateRegistration() {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowPopupDialogEvent(
title: "Waaaait",
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
positiveText: "Done it already",
negativeText: "Ok, I will",
onPositive: () {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
_switchLocationTrackingState(bool state) async {
if (state) {
await LocationManager().updateDeviceLocation();
}
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
setState(() {
_wait = false;
});
}
@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();
}
}

View File

@ -1,4 +1,4 @@
part of '../main.dart';
part of '../../main.dart';
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
@ -9,10 +9,10 @@ class MainPage extends StatefulWidget {
_MainPageState createState() => new _MainPageState();
}
class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription<List<PurchaseDetails>> _subscription;
StreamSubscription _stateSubscription;
StreamSubscription _lovelaceSubscription;
StreamSubscription _settingsSubscription;
StreamSubscription _serviceCallSubscription;
StreamSubscription _showEntityPageSubscription;
@ -21,26 +21,16 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
StreamSubscription _showPopupDialogSubscription;
StreamSubscription _showPopupMessageSubscription;
StreamSubscription _reloadUISubscription;
StreamSubscription _fullReloadSubscription;
StreamSubscription _showPageSubscription;
BottomInfoBarController _bottomInfoBarController;
int _previousViewCount;
bool _showLoginButton = false;
bool _preventAppRefresh = false;
String _savedSharedText;
Entity _entityToShow;
@override
void initState() {
super.initState();
final Stream purchaseUpdates =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
_subscription = purchaseUpdates.listen((purchases) {
_handlePurchaseUpdates(purchases);
});
workManager.Workmanager.initialize(
updateDeviceLocationIsolate,
isInDebugMode: false
);
enableShareReceiving();
WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure(
@ -58,6 +48,8 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
}
);
_bottomInfoBarController = BottomInfoBarController();
_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
@ -81,12 +73,6 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
_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);
@ -108,46 +94,42 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
);
}
void _fullLoad() async {
_showInfoBottomBar(progress: true,);
void _fullLoad() {
_bottomInfoBarController.showInfoBottomBar(progress: true,);
_subscribe().then((_) {
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
_fetchData();
LocationManager();
StartupUserMessagesManager().checkMessagesToShow();
SharedPreferences.getInstance().then((prefs) {
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
_fetchData(useCache: true);
LocationManager();
StartupUserMessagesManager().checkMessagesToShow();
});
}, onError: (e) {
_setErrorState(e);
});
});
}
void _quickLoad() {
_hideBottomBar();
_showInfoBottomBar(progress: true,);
void _quickLoad({bool uiOnly: false}) {
_bottomInfoBarController.showInfoBottomBar(progress: true,);
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData();
_fetchData(useCache: false, uiOnly: uiOnly);
}, 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;
_fetchData({useCache: false, uiOnly: false}) async {
if (useCache && !uiOnly) {
HomeAssistant().fetchDataFromCache().then((_) {
setState((){});
});
}
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;
}
if (_entityToShow != null) {
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
}
await HomeAssistant().fetchData(uiOnly).then((_) {
setState((){
_bottomInfoBarController.hideBottomBar();
});
HomeAssistant().saveCache();
}).catchError((e) {
if (e is HAError) {
_setErrorState(e);
@ -163,40 +145,37 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
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");
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
//HomeAssistant().saveCache();
}
}
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");
Logger.d("Need to rebuild UI");
_quickLoad();
} else {
setState(() {});
}
});
}
if (_lovelaceSubscription == null) {
_lovelaceSubscription = eventBus.on<LovelaceChangedEvent>().listen((event) {
_quickLoad();
});
}
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_quickLoad();
_quickLoad(uiOnly: true);
});
}
if (_fullReloadSubscription == null) {
_fullReloadSubscription = eventBus.on<FullReloadEvent>().listen((event){
_fullLoad();
});
}
if (_showPopupDialogSubscription == null) {
@ -245,7 +224,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(event.error);
_bottomInfoBarController.showErrorBottomBar(event.error);
});
}
@ -258,6 +237,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
_showOAuth();
} else {
_preventAppRefresh = false;
Navigator.of(context).pop();
}
});
}
@ -271,18 +251,16 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
void _showOAuth() {
_preventAppRefresh = true;
Launcher.launchURLInCustomTab(
url: ConnectionManager().oauthUrl
);
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
}
_setErrorState(HAError e) {
if (e == null) {
_showErrorBottomBar(
_bottomInfoBarController.showErrorBottomBar(
HAError("Unknown error")
);
} else {
_showErrorBottomBar(e);
_bottomInfoBarController.showErrorBottomBar(e);
}
}
@ -323,20 +301,20 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
);
}
void _notifyServiceCalled(String domain, String service, String entityId) {
_showInfoBottomBar(
void _notifyServiceCalled(String domain, String service, entityId) {
_bottomInfoBarController.showInfoBottomBar(
message: "Calling $domain.$service",
duration: Duration(seconds: 4)
);
}
void _showEntityPage(String entityId) {
setState(() {
_entityToShow = HomeAssistant().entities?.get(entityId);
if (_entityToShow != null) {
_mainScrollController?.jumpTo(0);
}
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId),
)
);
/*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
Navigator.push(
context,
@ -376,11 +354,10 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
accountName: Text(HomeAssistant().userName),
accountEmail: Text(HomeAssistant().locationName ?? ""),
currentAccountPicture: CircleAvatar(
backgroundColor: Theme.of(context).backgroundColor,
child: Text(
HomeAssistant().userAvatarText,
style: TextStyle(
fontSize: 32.0
),
style: Theme.of(context).textTheme.display1
),
),
)
@ -389,41 +366,19 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
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);
}
)
panel.getMenuItemWidget(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"),
title: Text("App settings"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/integration-settings');
Navigator.of(context).pushNamed('/app-settings');
},
)
]);
@ -460,29 +415,36 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
title: Text("Help"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.homemade.systems/docs");
Launcher.launchURL("http://ha-client.app/docs");
},
),
new ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
title: Text("Join Discord channel"),
title: Text("Contacts/Discussion"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("https://discord.gg/AUzEvwn");
Launcher.launchURL("https://discord.gg/nd6FZQ");
},
),
new ListTile(
title: Text("What's new?"),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed('/whats-new');
}
),
new AboutListTile(
aboutBoxChildren: <Widget>[
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURL("http://ha-client.homemade.systems/");
Launcher.launchURL("http://ha-client.app/");
},
child: Text(
"ha-client.homemade.systems",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
"ha-client.app",
style: Theme.of(context).textTheme.body1.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
),
@ -492,13 +454,13 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions");
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/terms_and_conditions");
},
child: Text(
"Terms and Conditions",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
style: Theme.of(context).textTheme.body1.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
),
@ -508,13 +470,13 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy");
Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/privacy_policy");
},
child: Text(
"Privacy Policy",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
style: Theme.of(context).textTheme.body1.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
)
@ -530,119 +492,19 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
);
}
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>();
final ScrollController _mainScrollController = ScrollController();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> serviceMenuItems = [];
List<PopupMenuItem<String>> mediaMenuItems = [];
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;
}
serviceMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
@ -658,15 +520,15 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
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]);
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["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)
),
style: Theme.of(context).textTheme.body1.copyWith(
color: HAClientTheme().getColorByEntityState(entity.state, context)
)
),
value: "${entity.entityId}",
)).toList()
@ -695,7 +557,12 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
shape: BoxShape.circle,
),
child: Center(
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
child: Text(
"$playersCount",
style: Theme.of(context).textTheme.caption.copyWith(
color: Colors.white
)
),
),
),
)
@ -713,7 +580,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
child: Text("Login with Home Assistant", style: Theme.of(context).textTheme.button),
color: Colors.blue,
onPressed: () => _fullLoad(),
)
@ -731,28 +598,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
);
}
} else {
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
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: _entityToShow, showClose: true,),
)
],
);
} else if (_entityToShow != null) {
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
} else {
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
}
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
}
return NestedScrollView(
@ -789,7 +635,9 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
context: context,
items: serviceMenuItems
).then((String val) {
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
@ -806,7 +654,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
_scaffoldKey.currentState.openDrawer();
},
),
bottom: (empty || _entityToShow != null) ? null : TabBar(
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
isScrollable: true,
@ -816,7 +664,6 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
];
},
body: mainScrollBody,
controller: _mainScrollController,
);
}
@ -824,89 +671,37 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
@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 WillPopScope(
child: Scaffold(
return Scaffold(
key: _scaffoldKey,
primary: false,
drawer: _buildAppDrawer(),
bottomNavigationBar: BottomInfoBar(
controller: _bottomInfoBarController,
),
body: _buildScaffoldBody(true)
);
} else {
return Scaffold(
key: _scaffoldKey,
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
bottomNavigationBar: BottomInfoBar(
controller: _bottomInfoBarController,
),
body: _buildScaffoldBody(false)
),
onWillPop: () {
if (_entityToShow != null) {
eventBus.fire(ShowEntityPageEvent());
return Future.value(false);
} else {
return Future.value(true);
}
},
);
}
);
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
//flutterWebviewPlugin.dispose();
_viewsTabController?.dispose();
_stateSubscription?.cancel();
_lovelaceSubscription?.cancel();
_settingsSubscription?.cancel();
_serviceCallSubscription?.cancel();
_showPopupDialogSubscription?.cancel();
@ -914,11 +709,9 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
_showEntityPageSubscription?.cancel();
_showErrorSubscription?.cancel();
_startAuthSubscription?.cancel();
_subscription?.cancel();
_showPageSubscription?.cancel();
_fullReloadSubscription?.cancel();
_reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose();
}
}

View File

@ -57,9 +57,8 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
_loaded = false;
});
} else {
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
//_useMediaExtractor = _isMediaExtractorExist;
_players = HomeAssistant().entities.getByDomains(domains: ["media_player"]);
_isMediaExtractorExist = HomeAssistant().isComponentEnabled("media_extractor");
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
setState(() {
if (_players.isNotEmpty) {
_loaded = true;
@ -135,7 +134,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
if (_validationMessage.isNotEmpty) {
children.add(Text(
"$_validationMessage",
style: TextStyle(color: Colors.red)
style: Theme.of(context).textTheme.body1.copyWith(
color: Theme.of(context).errorColor
)
));
}
children.addAll(<Widget>[
@ -193,9 +194,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
},
child: Text(
"How?",
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline
style: Theme.of(context).textTheme.body1.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline
),
),
),

View File

@ -62,7 +62,7 @@ class _PurchasePageState extends State<PurchasePage> {
}
}
Widget _buildProducts() {
List<Widget> _buildProducts() {
List<Widget> productWidgets = [];
for (ProductDetails product in _products) {
productWidgets.add(
@ -72,10 +72,7 @@ class _PurchasePageState extends State<PurchasePage> {
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
);
}
return ListView(
scrollDirection: Axis.vertical,
children: productWidgets
);
return productWidgets;
}
void _buyProduct(ProductDetails product) {
@ -87,12 +84,28 @@ class _PurchasePageState extends State<PurchasePage> {
@override
Widget build(BuildContext context) {
Widget body;
List<Widget> body;
if (!_loaded) {
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
body = [_error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error)];
} else {
body = _buildProducts();
}
body.add(
Card(
child: Container(
height: 80,
child: InkWell(
child: Image.network('https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif'),
onTap: () {
Launcher.launchURLInCustomTab(
context: context,
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARWGETZD2D83Q&source=url'
);
},
)
),
)
);
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
@ -100,7 +113,10 @@ class _PurchasePageState extends State<PurchasePage> {
}),
title: new Text(widget.title),
),
body: body,
body: ListView(
scrollDirection: Axis.vertical,
children: body
),
);
}

View File

@ -1,234 +0,0 @@
part of '../main.dart';
class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
}
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = "";
String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _longLivedToken = "";
String _newLongLivedToken = "";
bool _useLovelace = true;
bool _newUseLovelace = true;
String oauthUrl;
bool useOAuth = false;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
try {
useOAuth = prefs.getBool("oauth-used") ?? true;
} catch (e) {
useOAuth = true;
}
if (!useOAuth) {
try {
_longLivedToken = _newLongLivedToken =
await storage.read(key: "hacl_llt");
} catch (e) {
_longLivedToken = _newLongLivedToken = "";
await storage.delete(key: "hacl_llt");
}
}
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
try {
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
} catch (e) {
_useLovelace = _newUseLovelace = true;
}
});
}
bool _checkConfigChanged() {
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newUseLovelace != _useLovelace) ||
(_newLongLivedToken != _longLivedToken));
}
_saveSettings() async {
_newHassioDomain = _newHassioDomain.trim();
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
_newHassioDomain = _newHassioDomain.split("//")[1];
}
_newHassioDomain = _newHassioDomain.split("/")[0];
if (_newHassioDomain.contains(":")) {
List<String> domainAndPort = _newHassioDomain.split(":");
_newHassioDomain = domainAndPort[0];
_newHassioPort = domainAndPort[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
if (_newLongLivedToken.isNotEmpty) {
_newLongLivedToken = _newLongLivedToken.trim();
prefs.setBool("oauth-used", false);
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
} else if (!useOAuth) {
await storage.delete(key: "hacl_llt");
}
prefs.setString("hassio-domain", _newHassioDomain);
if (_newHassioPort == null || _newHassioPort.isEmpty) {
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
} else {
_newHassioPort = _newHassioPort.trim();
}
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
prefs.setBool("use-lovelace", _newUseLovelace);
}
@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),
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: (){
if (_checkConfigChanged()) {
Logger.d("Settings changed. Saving...");
_saveSettings().then((r) {
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} else {
Logger.d("Settings was not changed");
Navigator.pop(context);
}
}
)
],
),
body: ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text(
"Connection settings",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
new Row(
children: [
Text("Use ssl (HTTPS)"),
Switch(
value: (_newSocketProtocol == "wss"),
onChanged: (value) {
setState(() {
_newSocketProtocol = value ? "wss" : "ws";
});
},
)
],
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
onChanged: (value) {
_newHassioDomain = value;
}
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port (default is 8123)"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
onChanged: (value) {
_newHassioPort = value;
}
),
new Text(
"Try ports 80 and 443 if default is not working and you don't know why.",
style: TextStyle(color: Colors.grey),
),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Text(
"UI",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
),
new Row(
children: [
Text("Use Lovelace UI"),
Switch(
value: _newUseLovelace,
onChanged: (value) {
setState(() {
_newUseLovelace = value;
});
},
)
],
),
Text(
"Authentication settings",
style: TextStyle(
color: Colors.black45,
fontSize: 20.0
),
),
Container(height: 10.0,),
Text(
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
style: TextStyle(color: Colors.redAccent),
),
new TextField(
decoration: InputDecoration(
labelText: "Long-lived token"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
onChanged: (value) {
_newLongLivedToken = value;
}
),
],
),
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -0,0 +1,104 @@
part of '../../main.dart';
enum AppSettingsSection {menu, connectionSettings, integrationSettings, lookAndFeel}
class AppSettingsPage extends StatefulWidget {
final AppSettingsSection showSection;
AppSettingsPage({Key key, this.showSection: AppSettingsSection.menu}) : super(key: key);
@override
_AppSettingsPageState createState() => new _AppSettingsPageState();
}
class _AppSettingsPageState extends State<AppSettingsPage> {
var _currentSection;
@override
void initState() {
super.initState();
_currentSection = widget.showSection;
}
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
return ListTile(
title: Text(title, style: Theme.of(context).textTheme.subhead),
leading: Icon(icon),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
setState(() {
_currentSection = section;
});
},
);
}
Widget _buildMenu(BuildContext context) {
return ListView(
children: <Widget>[
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
],
);
}
@override
Widget build(BuildContext context) {
Widget section;
String title;
switch (_currentSection) {
case AppSettingsSection.menu: {
section = _buildMenu(context);
title = 'App settings';
break;
}
case AppSettingsSection.connectionSettings: {
section = ConnectionSettingsPage();
title = 'App settings - Connection';
break;
}
case AppSettingsSection.integrationSettings: {
section = IntegrationSettingsPage();
title = 'App settings - Integration';
break;
}
case AppSettingsSection.lookAndFeel: {
section = LookAndFeelSettingsPage();
title = 'App settings - Look&Feel';
break;
}
default:
title = ':(';
section = PageLoadingIndicator();
}
return WillPopScope(
child: Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
if (_currentSection == AppSettingsSection.menu) {
Navigator.pop(context);
} else {
setState(() {
_currentSection = AppSettingsSection.menu;
});
}
}),
title: Text(title),
),
body: section
),
onWillPop: () {
if (_currentSection == AppSettingsSection.menu) {
return Future.value(true);
} else {
setState(() {
_currentSection = AppSettingsSection.menu;
});
return Future.value(false);
}
},
);
}
}

View File

@ -0,0 +1,192 @@
part of '../../main.dart';
class ConnectionSettingsPage extends StatefulWidget {
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
}
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
String _hassioDomain = "";
String _newHassioDomain = "";
String _hassioPort = "";
String _newHassioPort = "";
String _socketProtocol = "wss";
String _newSocketProtocol = "wss";
String _longLivedToken = "";
String _newLongLivedToken = "";
String oauthUrl;
bool useOAuth = false;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
try {
useOAuth = prefs.getBool("oauth-used") ?? true;
} catch (e) {
useOAuth = true;
}
if (!useOAuth) {
try {
_longLivedToken = _newLongLivedToken =
await storage.read(key: "hacl_llt");
} catch (e) {
_longLivedToken = _newLongLivedToken = "";
await storage.delete(key: "hacl_llt");
}
}
setState(() {
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
});
}
bool _checkConfigChanged() {
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
(_newLongLivedToken != _longLivedToken));
}
_saveSettings() async {
_newHassioDomain = _newHassioDomain.trim();
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
_newHassioDomain = _newHassioDomain.split("//")[1];
}
_newHassioDomain = _newHassioDomain.split("/")[0];
if (_newHassioDomain.contains(":")) {
List<String> domainAndPort = _newHassioDomain.split(":");
_newHassioDomain = domainAndPort[0];
_newHassioPort = domainAndPort[1];
}
SharedPreferences prefs = await SharedPreferences.getInstance();
final storage = new FlutterSecureStorage();
if (_newLongLivedToken.isNotEmpty) {
_newLongLivedToken = _newLongLivedToken.trim();
prefs.setBool("oauth-used", false);
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
} else if (!useOAuth) {
await storage.delete(key: "hacl_llt");
}
prefs.setString("hassio-domain", _newHassioDomain);
if (_newHassioPort == null || _newHassioPort.isEmpty) {
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
} else {
_newHassioPort = _newHassioPort.trim();
}
prefs.setString("hassio-port", _newHassioPort);
prefs.setString("hassio-protocol", _newSocketProtocol);
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
}
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text(
"Connection settings",
style: Theme.of(context).textTheme.headline,
),
new Row(
children: [
Text("Use ssl (HTTPS)"),
Switch(
value: (_newSocketProtocol == "wss"),
onChanged: (value) {
setState(() {
_newSocketProtocol = value ? "wss" : "ws";
});
},
)
],
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant domain or ip address"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
onChanged: (value) {
_newHassioDomain = value;
}
),
new TextField(
decoration: InputDecoration(
labelText: "Home Assistant port (default is 8123)"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
onChanged: (value) {
_newHassioPort = value;
}
),
new Text(
"Try ports 80 and 443 if default is not working and you don't know why.",
style: Theme.of(context).textTheme.caption,
),
Text(
"Authentication settings",
style: Theme.of(context).textTheme.headline,
),
Container(height: 10.0,),
Text(
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
style: Theme.of(context).textTheme.body1.copyWith(
color: Colors.redAccent
),
),
new TextField(
decoration: InputDecoration(
labelText: "Long-lived token"
),
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
onChanged: (value) {
_newLongLivedToken = value;
}
),
Container(
height: Sizes.rowPadding,
),
RaisedButton(
child: Text('Apply', style: Theme.of(context).textTheme.button),
color: Theme.of(context).primaryColorDark,
onPressed: () {
if (_checkConfigChanged()) {
Logger.d("Settings changed. Saving...");
_saveSettings().then((r) {
Navigator.pop(context);
eventBus.fire(SettingsChangedEvent(true));
});
} else {
Logger.d("Settings was not changed");
Navigator.pop(context);
}
},
)
],
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -0,0 +1,194 @@
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;
if (_locationInterval % 5 != 0) {
_locationInterval = 5 * (_locationInterval ~/ 5);
}
});
});
}
void incLocationInterval() {
if (_locationInterval < 720) {
setState(() {
_locationInterval = _locationInterval + 5;
});
}
}
void decLocationInterval() {
if (_locationInterval > 5) {
setState(() {
_locationInterval = _locationInterval - 5;
});
}
}
restart() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to restart Home Assistant?",
body: "This will restart your Home Assistant server.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "restart");
},
));
}
stop() {
eventBus.fire(ShowPopupDialogEvent(
title: "Are you sure you want to STOP Home Assistant?",
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
positiveText: "Sure. Make it so",
negativeText: "What?? No!",
onPositive: () {
ConnectionManager().callService(domain: "homeassistant", service: "stop");
},
));
}
updateRegistration() {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
}
resetRegistration() {
eventBus.fire(ShowPopupDialogEvent(
title: "Waaaait",
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
positiveText: "Done it already",
negativeText: "Ok, I will",
onPositive: () {
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
},
));
}
_switchLocationTrackingState(bool state) async {
if (state) {
await LocationManager().updateDeviceLocation();
}
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
setState(() {
_wait = false;
});
}
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text("Location tracking", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding,),
InkWell(
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
child: Text(
"Please read documentation!",
style: Theme.of(context).textTheme.subhead.copyWith(
color: Colors.blue,
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: Theme.of(context).textTheme.title),
onPressed: () => decLocationInterval(),
),
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
FlatButton(
padding: EdgeInsets.all(0.0),
child: Text("+", style: Theme.of(context).textTheme.title),
onPressed: () => incLocationInterval(),
),
],
),
Divider(),
Text("Integration status", style: Theme.of(context).textTheme.title),
Container(height: Sizes.rowPadding,),
Text(
"${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}",
style: Theme.of(context).textTheme.subtitle,
),
Container(height: 6.0,),
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
//Divider(),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
RaisedButton(
color: Colors.blue,
onPressed: () => updateRegistration(),
child: Text("Check integration", style: Theme.of(context).textTheme.button)
),
Container(width: 10.0,),
RaisedButton(
color: Colors.redAccent,
onPressed: () => resetRegistration(),
child: Text("Reset integration", style: Theme.of(context).textTheme.button)
)
],
),
]
);
}
@override
void dispose() {
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
super.dispose();
}
}

View File

@ -0,0 +1,79 @@
part of '../../main.dart';
class LookAndFeelSettingsPage extends StatefulWidget {
LookAndFeelSettingsPage({Key key, this.title}) : super(key: key);
final String title;
@override
_LookAndFeelSettingsPageState createState() => new _LookAndFeelSettingsPageState();
}
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
AppTheme _currentTheme;
bool _changed = false;
@override
void initState() {
super.initState();
_loadSettings();
}
_loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
SharedPreferences.getInstance().then((prefs) {
setState(() {
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme];
});
});
}
_saveSettings(AppTheme theme) {
SharedPreferences.getInstance().then((prefs) {
prefs.setInt('app-theme', theme.index);
setState(() {
_currentTheme = theme;
eventBus.fire(ChangeThemeEvent(_currentTheme));
});
});
}
Map appThemeName = {
AppTheme.defaultTheme: 'Default',
AppTheme.haTheme: 'Home Assistant theme',
AppTheme.darkTheme: 'Dark theme'
};
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.vertical,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
Text("Application theme:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<AppTheme>(
value: _currentTheme,
iconSize: 30.0,
isExpanded: true,
style: Theme.of(context).textTheme.title,
//hint: Text("Select ${caption.toLowerCase()}"),
items: AppTheme.values.map((value) {
return new DropdownMenuItem<AppTheme>(
value: value,
child: Text('${appThemeName[value]}'),
);
}).toList(),
onChanged: (theme) => _saveSettings(theme),
)
]
);
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
error = "";
});
http.Response response;
response = await http.get("http://ha-client.homemade.systems/service/whats_new_0.7.0.md");
response = await http.get("http://ha-client.app/service/whats_new_0.8.5.md");
if (response.statusCode == 200) {
setState(() {
data = response.body;

View File

@ -0,0 +1,210 @@
part of '../../main.dart';
class BottomInfoBarController {
Function show;
Function hide;
String bottomBarText;
bool bottomBarProgress;
bool bottomBarErrorColor;
Timer _bottomBarTimer;
bool initialState = false;
List<HAErrorAction> actions = [];
void hideBottomBar() {
_bottomBarTimer?.cancel();
if (hide == null) {
initialState = false;
} else {
hide();
}
}
void showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
actions.clear();
bottomBarErrorColor = false;
bottomBarText = message;
bottomBarProgress = progress;
if (show == null) {
initialState = true;
} else {
show();
}
if (duration != null) {
_bottomBarTimer = Timer(duration, () {
hideBottomBar();
});
}
}
void showErrorBottomBar(HAError error) {
actions.clear();
actions.addAll(error.actions);
bottomBarErrorColor = true;
bottomBarProgress = false;
bottomBarText = "${error.message}";
if (show == null) {
initialState = true;
} else {
show();
}
}
}
class BottomInfoBar extends StatefulWidget {
final BottomInfoBarController controller;
const BottomInfoBar({Key key, this.controller}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _BottomInfoBarState();
}
}
class _BottomInfoBarState extends State<BottomInfoBar> {
bool _show;
@override
void initState() {
_show = widget.controller.initialState;
widget.controller.show = () {
setState(() {
_show = true;
});
};
widget.controller.hide = () {
setState(() {
_show = false;
});
};
super.initState();
}
@override
Widget build(BuildContext context) {
if (!_show) {
return Container(width: 0, height: 0,);
} else {
Widget bottomBar;
List<Widget> bottomBarChildren = [];
Widget actionsWidget;
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
);
List<Widget> actions = [];
widget.controller.actions.forEach((HAErrorAction action) {
switch (action.type) {
case HAErrorActionType.FULL_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
eventBus.fire(FullReloadEvent());
},
));
break;
}
case HAErrorActionType.QUICK_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
eventBus.fire(ReloadUIEvent());
},
));
break;
}
case HAErrorActionType.RELOGIN: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
ConnectionManager().logout().then((_) => eventBus.fire(FullReloadEvent()));
},
));
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) {
actionsWidget = Row(
mainAxisSize: MainAxisSize.min,
children: actions,
mainAxisAlignment: MainAxisAlignment.end,
);
} else {
actionsWidget = Container(height: 0.0, width: 0.0,);
}
if (widget.controller.bottomBarText != null) {
bottomBarChildren.add(
Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
Sizes.rowPadding),
child: Text(
"${widget.controller.bottomBarText}",
textAlign: TextAlign.left,
softWrap: true,
),
)
);
}
if (widget.controller.bottomBarProgress) {
bottomBarChildren.add(
LinearProgressIndicator(),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: widget.controller.bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: widget.controller.bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: bottomBarChildren,
),
),
actionsWidget
],
),
);
} else {
bottomBar = Container(height: 0,);
}
return bottomBar;
}
}
}

View File

@ -10,8 +10,7 @@ class LastUpdatedWidget extends StatelessWidget {
child: Text(
'${entityModel.entityWrapper.entity.lastUpdated}',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: Sizes.smallFontSize, color: Colors.black26),
style: Theme.of(context).textTheme.caption
),
);
}

View File

@ -23,7 +23,7 @@ class PageLoadingError extends StatelessWidget {
size: 48.0
)
),
Text(this.errorText, style: TextStyle(color: Colors.black45))
Text(this.errorText, style: Theme.of(context).textTheme.subtitle)
],
)
],

View File

@ -14,7 +14,7 @@ class PageLoadingIndicator extends StatelessWidget {
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
child: CircularProgressIndicator()
),
Text("Loading...", style: TextStyle(color: Colors.black45))
Text("Loading...", style: Theme.of(context).textTheme.subtitle)
],
)
],

View File

@ -40,10 +40,7 @@ class ProductPurchase extends StatelessWidget {
children: <Widget>[
Text(
"${product.title}",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0
),
style: Theme.of(context).textTheme.body2,
),
Container(height: Sizes.rowPadding,),
Text(
@ -53,7 +50,9 @@ class ProductPurchase extends StatelessWidget {
softWrap: true,
),
Container(height: Sizes.rowPadding,),
Text("${product.price} $period", style: TextStyle(color: priceColor)),
Text("${product.price} $period", style: Theme.of(context).textTheme.body1.copyWith(
color: priceColor
)),
],
)
),
@ -61,7 +60,7 @@ class ProductPurchase extends StatelessWidget {
Expanded(
flex: 2,
child: RaisedButton(
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
child: Text(this.purchased ? buttonTextInactive : buttonText, style: Theme.of(context).textTheme.button),
color: Colors.blue,
onPressed: this.purchased ? null : () => this.onBuy(this.product),
),

90
lib/pages/zha_page.dart Normal file
View File

@ -0,0 +1,90 @@
part of '../main.dart';
class ZhaPage extends StatefulWidget {
ZhaPage({Key key}) : super(key: key);
@override
_ZhaPageState createState() => new _ZhaPageState();
}
class _ZhaPageState extends State<ZhaPage> {
List data = [];
String error = "";
@override
void initState() {
super.initState();
_loadData();
}
_loadData() async {
setState(() {
data.clear();
error = "";
});
ConnectionManager().sendSocketMessage(
type: 'zha_map/devices'
).then((response){
setState(() {
data = response['devices'];
});
}).catchError((e){
setState(() {
error = '$e';
});
});
}
@override
Widget build(BuildContext context) {
Widget body;
if (error.isNotEmpty) {
body = PageLoadingError(errorText: error,);
} else if (data.isEmpty) {
body = PageLoadingIndicator();
} else {
List<Widget> devicesListWindet = [];
data.forEach((device) {
devicesListWindet.add(
Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardHeader(
name: '${device['ieee']}',
subtitle: Text('${device['manufacturer']}'),
),
Text('${device['device_type']}'),
Text('model: ${device['model']}'),
Text('offline: ${device['offline']}'),
Text('neighbours: ${device['neighbours'].length}'),
Text('raw: $device'),
],
),
)
);
});
body = ListView(
children: devicesListWindet
);
}
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('ZHA'),
),
body: body
);
}
}

View File

@ -11,7 +11,7 @@ class Panel {
};
final String id;
final String type;
final String componentName;
final String title;
final String urlPath;
final Map config;
@ -19,34 +19,61 @@ class Panel {
bool isHidden = true;
bool isWebView = false;
Panel({this.id, this.type, this.title, this.urlPath, this.icon, this.config}) {
Panel({this.id, this.componentName, this.title, this.urlPath, this.icon, this.config}) {
if (icon == null || !icon.startsWith("mdi:")) {
icon = Panel.iconsByComponent[type];
icon = Panel.iconsByComponent[componentName];
}
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
isWebView = (type != 'config');
isHidden = (componentName == 'kiosk' || componentName == 'states' || componentName == 'profile' || componentName == 'developer-tools');
isWebView = (componentName != 'config' && componentName != 'lovelace' && !componentName.startsWith('haclient'));
}
void handleOpen(BuildContext context) {
if (type == "config") {
if (componentName == "config") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PanelPage(title: "$title", panel: this),
)
);
} else if (componentName.startsWith('haclient')) {
Navigator.of(context).pushNamed(urlPath);
} else if (componentName == 'lovelace') {
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
HomeAssistant().autoUi = false;
SharedPreferences.getInstance().then((prefs) {
prefs.setString('lovelace_dashboard_url', this.urlPath);
eventBus.fire(ReloadUIEvent());
});
} else {
Launcher.launchURLInCustomTab(url: "${ConnectionManager().httpWebHost}/$urlPath");
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
}
}
Widget getMenuItemWidget(BuildContext context) {
return ListTile(
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(this.icon)),
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("${this.title}"),
Container(width: 4.0,),
isWebView ? Text("webview", style: Theme.of(context).textTheme.overline) : Container(width: 1.0,)
],
),
onTap: () {
Navigator.of(context).pop();
this.handleOpen(context);
}
);
}
Widget getWidget() {
switch (type) {
switch (componentName) {
case "config": {
return ConfigPanelWidget();
}
default: {
return Text("Unsupported panel component: $type");
return Text("Unsupported panel component: $componentName");
}
}
}

View File

@ -16,10 +16,10 @@ class LinkToWebConfig extends StatelessWidget {
title: Text("${this.name}",
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
style: Theme.of(context).textTheme.headline),
subtitle: Text("Tap to open web version"),
onTap: () {
Launcher.launchURLInCustomTab(url: this.url);
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
},
)
],

View File

@ -1,77 +0,0 @@
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;
}
}

View File

@ -1,366 +0,0 @@
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) {}
}

Some files were not shown because too many files have changed in this diff Show More