Compare commits

...

54 Commits

Author SHA1 Message Date
5f4a3fbdfc Update device_info to 0.4.2+4 2020-06-03 19:27:26 +00:00
312ed99e9f Remove irrelevant errors log 2020-06-03 18:42:26 +00:00
25e6d51c17 Ignore map state filters in cards 2020-06-03 18:35:06 +00:00
b501574bab Bump version code 2020-06-03 18:25:54 +00:00
53b31d8e90 Fix HTTP exceptions handling for notification images 2020-06-03 18:25:07 +00:00
6d80420a9b Fix cover null attributes handling 2020-06-02 22:07:37 +00:00
e977054139 Fix mobile app registration with unknown manufacturer or model 2020-06-02 21:54:54 +00:00
6367d38524 Fix timer duration parsing 2020-06-02 21:43:32 +00:00
f9b2d7d84c Fix light card with wrong domain entity. Show custom cards if there is entitites 2020-06-02 21:36:45 +00:00
44c28ad106 isExist fix 2020-05-30 10:39:00 +00:00
fec3c525e1 Bump version code 2020-05-30 10:23:21 +00:00
b1bbed6d80 Bump version code 2020-05-30 10:09:48 +00:00
13878cfc51 Fix FCM token crash 2020-05-30 10:02:50 +00:00
be49180205 Update github urls 2020-05-29 18:38:57 +00:00
c4a0b16553 Resolves #567 Login connection timeout issue 2020-05-29 18:26:20 +00:00
caacd5e9f4 Fix display mode name 2020-05-29 18:20:25 +00:00
5fa28abb6c Bump version code 2020-05-29 18:18:38 +00:00
e0a28c0b59 Resolves #563 Fullscreen mode 2020-05-29 18:16:59 +00:00
096e714a04 Real fullscreen for camera view 2020-05-29 16:52:57 +00:00
78893ea01f Fix for cropped screans 2020-05-29 16:35:34 +00:00
90efb29be5 Resolves #555 PayPal donate button removed 2020-05-29 16:25:07 +00:00
fca323c56b Resolves #559 2020-05-29 16:14:39 +00:00
e5fe6af5f3 Resolves #513 Allow to install on SD card 2020-05-29 09:24:23 +00:00
f0090d522d Merge pull request #566 from estevez-dev/rc/1.1.0-b3
Bump version code
2020-05-29 00:16:12 +03:00
edbfd8359b Bump version code 2020-05-28 21:15:26 +00:00
2702bb254a Bump version code 2020-05-28 20:55:08 +00:00
ca7b6ed550 Resolves #564 - Show picture-elements as button if camera_image provided 2020-05-28 20:52:23 +00:00
fb00b5d9ff Replace secure storage with encripted db 2020-05-28 20:23:13 +00:00
7ffba397ce Fix crash whne no google play services available 2020-05-28 19:17:32 +00:00
1080076e3b Merge pull request #562 from estevez-dev/rc/1.1.0-b2
Rc/1.1.0 b2
2020-05-25 18:59:27 +03:00
e295a36465 1151 2020-05-25 15:57:57 +00:00
9a09a83dc6 Request FCM token from native 2020-05-25 15:41:29 +00:00
95ca80949f bump version code 2020-05-25 14:10:29 +00:00
80b5763530 FCM token update and waiting 2020-05-25 14:09:45 +00:00
9a5e35b024 Fix notification event data 2020-05-25 12:34:53 +00:00
4493975676 defer fcm token load 2020-05-25 11:58:14 +00:00
141a68faf7 Update readme 2020-05-25 11:34:55 +00:00
a8efe7dbb6 Remove mobile app integration version check 2020-05-25 11:16:22 +00:00
9608983994 Bump version code 2020-05-25 11:06:08 +00:00
8eb15ab9a4 Notification channel description 2020-05-25 11:05:16 +00:00
aac0cfbb56 Dismiss and auto dismiss for notifications 2020-05-25 10:20:48 +00:00
343494ece0 Fix notification action receiver 2020-05-25 09:54:00 +00:00
b1e5e73278 Fix display name getting issue 2020-05-25 08:52:16 +00:00
9b5a0068fd Fix integer entity names handling in cards 2020-05-25 08:39:30 +00:00
aa26212ddd bump version code 2020-05-25 08:22:52 +00:00
1c45f96706 Remove Firebase hotfix
Firebase messages now handled by native code
2020-05-25 10:39:08 +03:00
c2d5192c51 Filter strange card data 2020-05-25 10:37:02 +03:00
88ae80507c Merge pull request #561 from estevez-dev/actionable_notifications
Actionable notifications
2020-05-25 02:34:18 +03:00
55868d1dfe Resolves #471 Actionable notification 2020-05-24 23:33:20 +00:00
92a1230267 WIP #471 Handling basic notifications in native code 2020-05-24 15:04:55 +00:00
d3f99fb262 WIP #471 Native FCM init 2020-05-24 12:42:31 +00:00
3fdf016c39 Merge pull request #560 from estevez-dev/rc/1.1.0
Rc/1.1.0
2020-05-24 11:26:53 +03:00
8ce0e8aafa remove ndk filters 2020-05-24 08:26:09 +00:00
54f6fb28ef add ndk platform filters 2020-05-24 07:57:00 +00:00
30 changed files with 636 additions and 201 deletions

View File

@ -1,6 +1,6 @@
# HA Client
## Native Android client for Home Assistant
### With notifications and Lovelace UI support
### With actionable notifications, location tracking and Lovelace UI support
Visit [ha-client.app](http://ha-client.app/) for more info.
@ -12,3 +12,7 @@ Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant commun
#### Last release build status
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
#### Special thanks to
- [Crewski](https://github.com/Crewski) for his [HANotify](https://github.com/Crewski/HANotify)
- [Home Assistant](https://github.com/home-assistant) for some support and [Home Assistant](https://www.home-assistant.io/)

View File

@ -79,6 +79,8 @@ flutter {
dependencies {
implementation 'com.google.firebase:firebase-analytics:17.2.2'
implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'androidx.work:work-runtime:2.3.4'
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'

View File

@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keyboardcrumbs.hassclient">
package="com.keyboardcrumbs.hassclient"
android:installLocation="auto">
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
@ -16,7 +17,6 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="HA Client"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
@ -43,16 +43,26 @@
<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" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".MessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver android:name=".NotificationActionReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
</intent-filter>
</receiver>
<service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"

View File

@ -5,11 +5,67 @@ import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.ConnectionResult;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;
import com.google.firebase.messaging.FirebaseMessaging;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
Context context = getActivity();
if (call.method.equals("getFCMToken")) {
if (checkPlayServices()) {
FirebaseInstanceId.getInstance().getInstanceId()
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
@Override
public void onComplete(@NonNull Task<InstanceIdResult> task) {
if (task.isSuccessful()) {
String token = task.getResult().getToken();
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
updateTokenTask.execute(token);
result.success(token);
} else {
result.error("fcm_error", task.getException().getMessage(), null);
}
}
});
} else {
result.error("google_play_service_error", "Google Play Services unavailable", null);
}
}
}
}
);
}
private boolean checkPlayServices() {
return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

View File

@ -0,0 +1,152 @@
package com.keyboardcrumbs.hassclient;
import java.util.Map;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
import java.io.InputStream;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.webkit.URLUtil;
public class MessagingService extends FirebaseMessagingService {
private static final String TAG = "MessagingService";
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Map<String, String> data = remoteMessage.getData();
if (data.size() > 0) {
if (data.containsKey("body") || data.containsKey("title")) {
sendNotification(data);
}
}
}
@Override
public void onNewToken(String token) {
UpdateTokenTask updateTokenTask = new UpdateTokenTask(this);
updateTokenTask.execute(token);
}
private void sendNotification(Map<String, String> data) {
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
boolean autoCancel;
if (!data.containsKey("channelId")) {
channelId = "ha_notify";
channelDescription = "Default notification channel";
} else {
channelId = data.get("channelId");
channelDescription = channelId;
}
if (!data.containsKey("body")) {
messageBody = "";
} else {
messageBody = data.get("body");
}
if (!data.containsKey("title")) {
messageTitle = "HA Client";
} else {
messageTitle = data.get("title");
}
if (!data.containsKey("tag")) {
nTag = String.valueOf(System.currentTimeMillis());
} else {
nTag = data.get("tag");
}
if (data.containsKey("dismiss")) {
try {
boolean dismiss = Boolean.parseBoolean(data.get("dismiss"));
if (dismiss) {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(nTag, 0);
return;
}
} catch (Exception e) {
//nope
}
}
if (data.containsKey("autoDismiss")) {
try {
autoCancel = Boolean.parseBoolean(data.get("autoDismiss"));
} catch (Exception e) {
autoCancel = true;
}
} else {
autoCancel = true;
}
imageUrl = data.get("image");
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
PendingIntent.FLAG_ONE_SHOT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.mini_icon)
.setContentTitle(messageTitle)
.setContentText(messageBody)
.setAutoCancel(autoCancel)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
if (URLUtil.isValidUrl(imageUrl)) {
Bitmap image = getBitmapFromURL(imageUrl);
if (image != null) {
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
notificationBuilder.setLargeIcon(image);
}
}
for (int i = 1; i <= 3; i++) {
if (data.containsKey("action" + i)) {
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class);
if (autoCancel) {
broadcastIntent.putExtra("tag", nTag);
}
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT);
notificationBuilder.addAction(R.drawable.mini_icon, data.get("action" + i), actionIntent);
}
}
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Since android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId,
channelDescription,
NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
}
notificationManager.notify(nTag, 0 /* ID of notification */, notificationBuilder.build());
}
private Bitmap getBitmapFromURL(String imageUrl) {
try {
URL url = new URL(imageUrl);
URLConnection connection = url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
return BitmapFactory.decodeStream(input);
} catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,69 @@
package com.keyboardcrumbs.hassclient;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.app.NotificationManager;
import android.webkit.URLUtil;
import org.json.JSONObject;
import android.content.SharedPreferences;
public class NotificationActionReceiver extends BroadcastReceiver {
private static final String TAG = "NotificationActionReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String rawActionData = intent.getStringExtra("actionData");
if (intent.hasExtra("tag")) {
String notificationTag = intent.getStringExtra("tag");
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationTag, 0);
}
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
String webhookId = prefs.getString("flutter.app-webhook-id", null);
if (webhookId != null) {
try {
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
"://" +
prefs.getString("flutter.hassio-domain", "") +
":" +
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
JSONObject actionData = new JSONObject(rawActionData);
if (URLUtil.isValidUrl(requestUrl)) {
JSONObject dataToSend = new JSONObject();
JSONObject requestData = new JSONObject();
if (actionData.getString("action").equals("call-service")) {
dataToSend.put("type", "call_service");
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
requestData.put("service", actionData.getString("service").split("\\.")[1]);
if (actionData.has("service_data")) {
requestData.put("service_data", actionData.get("service_data"));
}
} else {
dataToSend.put("type", "fire_event");
requestData.put("event_type", "ha_client_event");
JSONObject eventData = new JSONObject();
eventData.put("action", actionData.getString("action"));
requestData.put("event_data", eventData);
}
dataToSend.put("data", requestData);
String stringRequest = dataToSend.toString();
SendTask sendTask = new SendTask();
sendTask.execute(requestUrl, stringRequest);
} else {
Log.w(TAG, "Invalid HA url");
}
} catch (Exception e) {
Log.e(TAG, "Error handling notification action", e);
}
} else {
Log.w(TAG, "Webhook id not found");
}
}
}

View File

@ -0,0 +1,46 @@
package com.keyboardcrumbs.hassclient;
import android.util.Log;
import android.os.AsyncTask;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
public class SendTask extends AsyncTask<String, String, String> {
private static final String TAG = "SendTask";
public SendTask(){
//set context variables if required
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
String urlString = params[0];
String data = params[1];
try {
URL url = new URL(urlString);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
byte[] outputBytes = data.getBytes("UTF-8");
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
urlConnection.disconnect();
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
}
return null;
}
}

View File

@ -0,0 +1,46 @@
package com.keyboardcrumbs.hassclient;
import android.util.Log;
import android.os.AsyncTask;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
import android.webkit.URLUtil;
import org.json.JSONObject;
import android.content.SharedPreferences;
import android.content.Context;
import java.lang.ref.WeakReference;
public class UpdateTokenTask extends AsyncTask<String, String, String> {
private static final String TAG = "UpdateTokenTask";
private WeakReference<Context> contextRef;
public UpdateTokenTask(Context context){
contextRef = new WeakReference<>(context);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
Log.d(TAG, "Updating push token");
Context context = contextRef.get();
if (context != null) {
String token = params[0];
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("flutter.npush-token", token);
editor.commit();
}
return null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

View File

@ -38,6 +38,15 @@ class CardData {
case CardType.LIGHT:
return LightCardData(rawData);
break;
case CardType.PICTURE_ELEMENTS:
//TODO temporary solution
if (rawData.containsKey('camera_image')) {
rawData['entity'] = rawData['camera_image'];
return ButtonCardData(rawData);
} else {
return CardData(null);
}
break;
case CardType.ENTITY_BUTTON:
case CardType.BUTTON:
case CardType.PICTURE_ENTITY:
@ -81,6 +90,12 @@ class CardData {
return BadgesData(rawData);
break;
default:
if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
}
if (rawData.containsKey('entities') && rawData['entities'] is List) {
return EntitiesCardData(rawData);
}
return CardData(null);
}
} catch (error, stacktrace) {
@ -94,7 +109,11 @@ class CardData {
type = rawData['type'];
conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true;
stateFilter = rawData['state_filter'] ?? [];
if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) {
stateFilter = rawData['state_filter'];
} else {
stateFilter = [];
}
} else {
type = CardType.UNKNOWN;
conditions = [];
@ -197,7 +216,7 @@ class BadgesData extends CardData {
entities.add(
EntityWrapper(
entity: HomeAssistant().entities.get(rawBadge['entity']),
overrideName: rawBadge["name"],
overrideName: rawBadge["name"]?.toString(),
overrideIcon: rawBadge["icon"],
)
);
@ -223,7 +242,7 @@ class BadgesData extends CardData {
entities.add(
EntityWrapper(
entity: e,
overrideName: rawEntity["name"],
overrideName: rawEntity["name"]?.toString(),
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -248,7 +267,7 @@ class EntitiesCardData extends CardData {
EntitiesCardData(rawData) : super(rawData) {
//Parsing card data
title = rawData['title'];
title = rawData['title']?.toString();
icon = rawData['icon'] is String ? rawData['icon'] : null;
stateColor = rawData['state_color'] ?? false;
showHeaderToggle = rawData['show_header_toggle'] ?? false;
@ -279,7 +298,7 @@ class EntitiesCardData extends CardData {
EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
name: rawEntity["name"]?.toString(),
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
@ -298,7 +317,7 @@ class EntitiesCardData extends CardData {
entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
name: rawEntity["name"]?.toString(),
url: rawEntity["url"]
),
stateColor: rawEntity["state_color"] ?? stateColor,
@ -311,7 +330,7 @@ class EntitiesCardData extends CardData {
EntityWrapper(
entity: e,
stateColor: rawEntity["state_color"] ?? stateColor,
overrideName: rawEntity["name"],
overrideName: rawEntity["name"]?.toString(),
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
@ -338,7 +357,7 @@ class AlarmPanelCardData extends CardData {
AlarmPanelCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
name = rawData['name']?.toString();
states = rawData['states'];
//Parsing entity
var entitiId = rawData["entity"];
@ -365,12 +384,18 @@ class LightCardData extends CardData {
@override
Widget buildCardWidget() {
return LightCard(card: this);
if (this.entity != null && this.entity.entity is LightEntity) {
return LightCard(card: this);
}
return ErrorCard(
errorText: 'Specify an entity from within the light domain.',
showReportButton: false,
);
}
LightCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
name = rawData['name']?.toString();
icon = rawData['icon'] is String ? rawData['icon'] : null;
//Parsing entity
var entitiId = rawData["entity"];
@ -407,7 +432,7 @@ class ButtonCardData extends CardData {
ButtonCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
name = rawData['name']?.toString();
icon = rawData['icon'] is String ? rawData['icon'] : null;
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
@ -470,7 +495,7 @@ class GaugeCardData extends CardData {
GaugeCardData(rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
name = rawData['name']?.toString();
unit = rawData['unit'];
if (rawData['min'] is int) {
min = rawData['min'].toDouble();
@ -522,7 +547,7 @@ class GlanceCardData extends CardData {
GlanceCardData(rawData) : super(rawData) {
//Parsing card data
title = rawData["title"];
title = rawData["title"]?.toString();
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
showState = rawData['show_state'] ?? true;
@ -544,7 +569,7 @@ class GlanceCardData extends CardData {
EntityWrapper(
entity: e,
stateColor: stateColor,
overrideName: rawEntity["name"],
overrideName: rawEntity["name"]?.toString(),
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)

View File

@ -2,12 +2,21 @@ part of '../main.dart';
class ErrorCard extends StatelessWidget {
final ErrorCardData card;
final String errorText;
final bool showReportButton;
const ErrorCard({Key key, this.card}) : super(key: key);
const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key);
@override
Widget build(BuildContext context) {
String error;
if (errorText == null) {
error = 'There was an error showing ${card?.type}';
} else {
error = errorText;
}
return CardWrapper(
color: Theme.of(context).errorColor,
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
@ -15,21 +24,25 @@ class ErrorCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
error,
textAlign: TextAlign.center,
),
card != null ?
RaisedButton(
onPressed: () {
Clipboard.setData(new ClipboardData(text: card.cardConfig));
},
child: Text('Copy card config'),
),
) :
Container(width: 0, height: 0),
showReportButton ?
RaisedButton(
onPressed: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
},
child: Text('Report issue'),
)
) :
Container(width: 0, height: 0)
],
),
)

View File

@ -7,6 +7,6 @@ class UnsupportedCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
return Container(height: 20);
}
}

View File

@ -4,12 +4,14 @@ class CardWrapper extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
final Color color;
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
color: color,
child: Padding(
padding: padding,
child: child

View File

@ -40,8 +40,8 @@ class CoverEntity extends Entity {
CoverEntity.SUPPORT_SET_TILT_POSITION);
double get currentPosition => _getDoubleAttributeValue('current_position');
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position');
double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0;
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0;
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
bool get canTiltBeOpened => currentTiltPosition < 100;

View File

@ -78,9 +78,21 @@ class Entity {
chartType: EntityHistoryWidgetType.simple
);
String get displayName =>
attributes["friendly_name"] ??
(attributes["name"] ?? (entityId != null && entityId.contains('.')) ? entityId.split(".")[1].replaceAll("_", " ") : "");
String get displayName {
if (attributes.containsKey('friendly_name')) {
return attributes['friendly_name'];
}
if (attributes.containsKey('name')) {
return attributes['name'];
}
if (entityId == null) {
return "";
}
if (entityId.contains(".")) {
return entityId.split(".")[1].replaceAll("_", " ");
}
return entityId;
}
bool get isView =>
(domain == "group") &&

View File

@ -8,8 +8,8 @@ class TimerEntity extends Entity {
@override
void update(Map rawData, String webHost) {
super.update(rawData, webHost);
String durationSource = "${attributes["duration"]}";
if (durationSource != null && durationSource.isNotEmpty) {
if (attributes.containsKey('duration')) {
String durationSource = "${attributes["duration"]}";
try {
List<String> durationList = durationSource.split(":");
if (durationList.length == 1) {

View File

@ -149,7 +149,7 @@ class EntityCollection {
}
bool isExist(String entityId) {
return _allEntities[entityId] != null;
return _allEntities.containsKey(entityId);
}
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {

View File

@ -22,8 +22,6 @@ class HomeAssistant {
Map services;
bool autoUi = false;
String fcmToken;
Map _rawLovelaceData;
var _rawStates;
var _rawUserInfo;
@ -223,7 +221,7 @@ class HomeAssistant {
var data = json.decode(prefs.getString('cached_services'));
_parseServices(data ?? {});
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
}
}
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
@ -263,7 +261,7 @@ class HomeAssistant {
var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data ?? {});
} catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace);
Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
panels.clear();
}
} else {

View File

@ -16,10 +16,9 @@ import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.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/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
@ -157,11 +156,11 @@ part 'cards/badges.dart';
part 'managers/app_settings.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
//FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = 'HA Client';
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0.md';
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0-b2.md';
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console.
@ -186,7 +185,13 @@ void main() async {
};
WidgetsFlutterBinding.ensureInitialized();
AppSettings().loadAppTheme();
await AppSettings().loadStartupSettings();
await Hive.initFlutter();
if (AppSettings().displayMode == DisplayMode.fullscreen) {
SystemChrome.setEnabledSystemUIOverlays([]);
} else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
runZoned(() {
runApp(new HAClientApp(

View File

@ -1,7 +1,13 @@
part of '../main.dart';
enum DisplayMode {normal, fullscreen}
class AppSettings {
static const DEFAULT_HIVE_BOX = 'defaultSettingsBox';
static const AUTH_TOKEN_KEY = 'llt';
static final AppSettings _instance = AppSettings._internal();
factory AppSettings() {
@ -22,7 +28,7 @@ class AppSettings {
String webhookId;
double haVersion;
bool scrollBadges;
int appIntegrationVersion;
DisplayMode displayMode;
AppTheme appTheme;
final int defaultLocationUpdateIntervalMinutes = 20;
Duration locationUpdateInterval;
@ -31,20 +37,21 @@ class AppSettings {
bool get isAuthenticated => longLivedToken != null;
bool get isTempAuthenticated => tempToken != null;
loadAppTheme() async {
loadStartupSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
appTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
}
Future load(bool full) async {
if (full) {
await Hive.openBox(DEFAULT_HIVE_BOX);
Logger.d('Loading settings...');
SharedPreferences prefs = await SharedPreferences.getInstance();
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
mobileAppDeviceName = prefs.getString('app-integration-device-name');
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
scrollBadges = prefs.getBool('scroll-badges') ?? true;
displayHostname = "$_domain:$_port";
webSocketAPIEndpoint =
@ -54,21 +61,19 @@ class AppSettings {
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
defaultLocationUpdateIntervalMinutes);
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
Logger.d('Done. $_domain:$_port');
try {
final storage = new FlutterSecureStorage();
longLivedToken = await storage.read(key: "hacl_llt");
Logger.d("Long-lived token read successful");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent(
'https://ha-client.app/service/auth_callback.html')}";
} catch (e, stacktrace) {
Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
}
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri
.encodeComponent(
'https://ha-client.app/service/auth_callback.html')}";
}
}
Future<dynamic> loadSingle(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.get('$key');
}
Future save(Map<String, dynamic> settings) async {
if (settings != null && settings.isNotEmpty) {
SharedPreferences prefs = await SharedPreferences.getInstance();
@ -100,25 +105,13 @@ class AppSettings {
Future clearTokens() async {
longLivedToken = null;
tempToken = null;
try {
final storage = new FlutterSecureStorage();
await storage.delete(key: "hacl_llt");
} catch(e, stacktrace) {
Logger.e("Error clearing tokens: $e", stacktrace: stacktrace);
}
Hive.box(DEFAULT_HIVE_BOX).delete(AUTH_TOKEN_KEY);
}
Future saveLongLivedToken(token) async {
void saveLongLivedToken(token) {
longLivedToken = token;
tempToken = null;
try {
final storage = new FlutterSecureStorage();
await storage.write(key: "hacl_llt", value: "$longLivedToken");
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("oauth-used", true);
} catch(e, stacktrace) {
Logger.e("Error saving long-lived token: $e", stacktrace: stacktrace);
}
Hive.box(DEFAULT_HIVE_BOX).put(AUTH_TOKEN_KEY, longLivedToken);
}
bool isNotConfigured() {

View File

@ -254,6 +254,7 @@ class ConnectionManager {
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
Logger.d("Got long-lived token.");
AppSettings().saveLongLivedToken(data);
completer.complete();
}).catchError((e) {
completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
});

View File

@ -2,17 +2,15 @@ part of '../main.dart';
class MobileAppIntegrationManager {
static const INTEGRATION_VERSION = 3;
static final _appRegistrationData = {
"device_name": "",
"app_version": "$appVersion",
"manufacturer": DeviceInfoManager().manufacturer,
"model": DeviceInfoManager().model,
"os_version": DeviceInfoManager().osVersion,
"manufacturer": DeviceInfoManager().manufacturer ?? "unknown",
"model": DeviceInfoManager().model ?? "unknown",
"os_version": DeviceInfoManager().osVersion ?? "0",
"app_data": {
"push_token": "",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV2"
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
}
};
@ -23,10 +21,28 @@ class MobileAppIntegrationManager {
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
}
static Future checkAppRegistration() {
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
static Future checkAppRegistration() async {
String fcmToken = await AppSettings().loadSingle('npush-token');
if (fcmToken != null) {
Logger.d("[MobileAppIntegrationManager] token exist");
await _doCheck(fcmToken);
} else {
Logger.d("[MobileAppIntegrationManager] no fcm token. Requesting...");
try {
fcmToken = await platform.invokeMethod('getFCMToken');
await _doCheck(fcmToken);
} on PlatformException catch (e) {
Logger.e('[MobileAppIntegrationManager] Failed to get FCM token from native: ${e.message}');
}
}
}
static Future _doCheck(String fcmToken) {
Completer completer = Completer();
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
(_appRegistrationData["app_data"] as Map)["push_token"] = "$fcmToken";
if (AppSettings().webhookId == null) {
Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData);
@ -49,10 +65,8 @@ class MobileAppIntegrationManager {
Logger.d("Processing registration responce...");
var responseObject = json.decode(response);
AppSettings().webhookId = responseObject["webhook_id"];
AppSettings().appIntegrationVersion = INTEGRATION_VERSION;
AppSettings().save({
'app-webhook-id': responseObject["webhook_id"],
'app-integration-version': INTEGRATION_VERSION
'app-webhook-id': responseObject["webhook_id"]
}).then((prefs) {
completer.complete();
eventBus.fire(ShowPopupEvent(
@ -76,7 +90,6 @@ class MobileAppIntegrationManager {
}
_showError();
});
return completer.future;
} else {
Logger.d("App was previously registered. Checking...");
var updateData = {
@ -98,12 +111,7 @@ class MobileAppIntegrationManager {
Logger.w("No registration data in response. MobileApp integration was removed or broken");
_askToRegisterApp();
} else {
if (INTEGRATION_VERSION > AppSettings().appIntegrationVersion) {
Logger.d('App registration needs to be updated');
_askToRemoveAndRegisterApp();
} else {
Logger.d('App registration works fine');
}
Logger.d('App registration works fine');
}
completer.complete();
}).catchError((e) {
@ -119,8 +127,8 @@ class MobileAppIntegrationManager {
}
completer.complete();
});
return completer.future;
}
return completer.future;
}
static void _showError() {
@ -131,26 +139,12 @@ class MobileAppIntegrationManager {
positiveText: "Report issue",
negativeText: "Close",
onPositive: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
}
)
));
}
static void _askToRemoveAndRegisterApp() {
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Mobile app integration needs to be updated",
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
positiveText: "Ok",
negativeText: "Report an issue",
onNegative: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
},
)
));
}
static void _askToRegisterApp() {
eventBus.fire(ShowPopupEvent(
popup: RegisterAppPopup(

View File

@ -1,18 +1,37 @@
part of '../main.dart';
class FullScreenPage extends StatelessWidget {
class FullScreenPage extends StatefulWidget {
final Widget child;
const FullScreenPage({Key key, this.child}) : super(key: key);
@override
_FullScreenPageState createState() => _FullScreenPageState();
}
class _FullScreenPageState extends State<FullScreenPage> {
@override
void initState() {
SystemChrome.setEnabledSystemUIOverlays([]);
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Center(
child: this.child,
child: this.widget.child,
),
);
}
@override
void dispose() {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
super.dispose();
}
}

View File

@ -33,35 +33,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
super.initState();
WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
return Future.value();
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
return Future.value();
}
);
_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
var initializationSettingsAndroid =
new AndroidInitializationSettings('mini_icon');
var initializationSettingsIOS = new IOSInitializationSettings(
onDidReceiveLocalNotification: null);
var initializationSettings = new InitializationSettings(
initializationSettingsAndroid, initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
@ -73,31 +46,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_fullLoad();
}
Future onSelectNotification(String payload) async {
if (payload != null) {
Logger.d('Notification clicked: ' + payload);
}
}
Future _showNotification({String title, String text}) async {
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title ?? appName,
text,
platformChannelSpecifics
);
}
void _fullLoad() {
_bottomInfoBarController.showInfoBottomBar(progress: true,);
Logger.d('[loading] fullLoad');
_subscribe().then((_) {
Logger.d('[loading] subscribed');
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
Logger.d('[loading] COnnection manager initialized');
SharedPreferences.getInstance().then((prefs) {
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
_fetchData(useCache: true);
@ -155,9 +110,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
}
Future _subscribe() {
Completer completer = Completer();
Future _subscribe() async {
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
@ -238,11 +191,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
});
}
_firebaseMessaging.getToken().then((String token) {
/*_firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token;
completer.complete();
});
return completer.future;
});*/
}
void _showOAuth() {
@ -349,7 +301,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
title: Text("Report an issue"),
onTap: () {
Navigator.of(context).pop();
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new/choose");
},
),
Divider(),
@ -532,7 +484,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
floating: true,
pinned: true,
snap: false,
primary: true,
primary: AppSettings().displayMode == DisplayMode.normal,
title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[
PopupMenuButton(
@ -541,6 +493,15 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
child: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color)
),
onSelected: (String val) {
if (val == "reload") {
_quickLoad();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
_quickLoad();
});
}
},
itemBuilder: (BuildContext context) {
List<PopupMenuEntry<String>> result = [
PopupMenuItem<String>(
@ -702,6 +663,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
Hive.close();
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
//flutterWebviewPlugin.dispose();
_viewsTabController?.dispose();

View File

@ -31,7 +31,7 @@ class _PurchasePageState extends State<PurchasePage> {
} else {
const Set<String> _kIds = {'one_time_support','just_few_bucks_per_year', 'app_fan_support_per_year', 'grateful_user_support_per_year'};
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
if (!response.notFoundIDs.isEmpty) {
if (response.notFoundIDs.isNotEmpty) {
Logger.d("Products not found: ${response.notFoundIDs}");
}
_products = response.productDetails;
@ -90,22 +90,6 @@ class _PurchasePageState extends State<PurchasePage> {
} 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: (){

View File

@ -23,7 +23,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
return ListTile(
title: Text(title, style: Theme.of(context).textTheme.subhead),
title: Text(title),
leading: Icon(icon),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {

View File

@ -13,6 +13,7 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
AppTheme _currentTheme;
bool _scrollBadges = false;
DisplayMode _displayMode;
@override
void initState() {
@ -25,7 +26,8 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
await prefs.reload();
SharedPreferences.getInstance().then((prefs) {
setState(() {
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index];
_currentTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
_displayMode = DisplayMode.values[prefs.getInt('display-mode') ?? DisplayMode.normal.index];
_scrollBadges = prefs.getBool('scroll-badges') ?? true;
});
});
@ -42,18 +44,34 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
});
}
Future _saveOther() async {
Future _saveBadgesSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
AppSettings().scrollBadges = _scrollBadges;
await prefs.setBool('scroll-badges', _scrollBadges);
}
Future _saveDisplayMode(DisplayMode mode) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
AppSettings().displayMode = mode;
await prefs.setInt('display-mode', mode.index);
if (mode == DisplayMode.fullscreen) {
SystemChrome.setEnabledSystemUIOverlays([]);
} else {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}
}
Map appThemeName = {
AppTheme.defaultTheme: 'Default',
AppTheme.haTheme: 'Home Assistant theme',
AppTheme.darkTheme: 'Dark theme'
};
Map DisplayModeName = {
DisplayMode.normal: 'Normal',
DisplayMode.fullscreen: 'Fullscreen'
};
@override
Widget build(BuildContext context) {
return ListView(
@ -93,7 +111,28 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
setState(() {
_scrollBadges = val;
});
_saveOther();
_saveBadgesSettings();
},
),
Container(height: Sizes.doubleRowPadding),
Text("Display mode:", style: Theme.of(context).textTheme.body2),
Container(height: Sizes.rowPadding),
DropdownButton<DisplayMode>(
value: _displayMode,
iconSize: 30.0,
isExpanded: true,
style: Theme.of(context).textTheme.title,
items: DisplayMode.values.map((value) {
return new DropdownMenuItem<DisplayMode>(
value: value,
child: Text('${DisplayModeName[value]}'),
);
}).toList(),
onChanged: (DisplayMode val) {
setState(() {
_displayMode = val;
});
_saveDisplayMode(val);
},
),
]

View File

@ -73,11 +73,9 @@ class TokenLoginPopup extends Popup {
padding: EdgeInsets.all(20),
child: TextFormField(
onSaved: (newValue) {
final storage = new FlutterSecureStorage();
storage.write(key: "hacl_llt", value: newValue.trim()).then((_) {
Navigator.of(context).pop();
eventBus.fire(SettingsChangedEvent(true));
});
Hive.box(AppSettings.DEFAULT_HIVE_BOX).put(AppSettings.AUTH_TOKEN_KEY, newValue.trim());
Navigator.of(context).pop();
eventBus.fire(SettingsChangedEvent(true));
},
decoration: InputDecoration(
hintText: 'Please enter long-lived token',

View File

@ -26,7 +26,13 @@ class HAView {
}
(rawData['cards'] ?? []).forEach((rawCardData) {
cards.add(CardData.parse(rawCardData));
if (rawCardData != null) {
if (rawCardData is Map) {
cards.add(CardData.parse(rawCardData));
} else if (rawCardData is List && rawCardData.length == 1) {
cards.add(CardData.parse(rawCardData[0]));
}
}
});
}
@ -71,4 +77,4 @@ class HAView {
view: this,
);
}
}
}

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.0.0+1144
version: 1.1.0+1157
environment:
@ -23,10 +23,9 @@ dependencies:
flutter_custom_tabs: ^0.6.0
flutter_webview_plugin: ^0.3.10+1
webview_flutter: ^0.3.19+7
firebase_messaging: ^6.0.15
flutter_secure_storage: ^3.3.3
device_info: ^0.4.1+4
flutter_local_notifications: ^1.1.6
hive: ^1.4.1+1
hive_flutter: ^0.3.0+2
device_info: ^0.4.2+4
geolocator: ^5.3.1
workmanager: ^0.2.2
battery: ^1.0.0