Compare commits
	
		
			20 Commits
		
	
	
		
			rc/1.1.0-b
			...
			1.1.0-beta
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | fec3c525e1 | ||
|  | b1bbed6d80 | ||
|  | 13878cfc51 | ||
|  | be49180205 | ||
|  | c4a0b16553 | ||
|  | caacd5e9f4 | ||
|  | 5fa28abb6c | ||
|  | e0a28c0b59 | ||
|  | 096e714a04 | ||
|  | 78893ea01f | ||
|  | 90efb29be5 | ||
|  | fca323c56b | ||
|  | e5fe6af5f3 | ||
|  | f0090d522d | ||
|  | edbfd8359b | ||
|  | 2702bb254a | ||
|  | ca7b6ed550 | ||
|  | fb00b5d9ff | ||
|  | 7ffba397ce | ||
|  | 1080076e3b | 
| @@ -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,6 +16,8 @@ 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; | ||||
| @@ -31,28 +33,36 @@ public class MainActivity extends FlutterActivity { | ||||
|             new MethodChannel.MethodCallHandler() { | ||||
|                 @Override | ||||
|                 public void onMethodCall(MethodCall call, MethodChannel.Result result) { | ||||
|                     Context context = getActivity(); | ||||
|                     if (call.method.equals("getFCMToken")) { | ||||
|                         FirebaseInstanceId.getInstance().getInstanceId() | ||||
|                         if (checkPlayServices()) { | ||||
|                             FirebaseInstanceId.getInstance().getInstanceId() | ||||
|                             .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() { | ||||
|                                 @Override | ||||
|                                 public void onComplete(@NonNull Task<InstanceIdResult> task) { | ||||
|                                     if (task.isSuccessful()) { | ||||
|                                         Context context = getActivity(); | ||||
|                                         String token = task.getResult().getToken(); | ||||
|                                         UpdateTokenTask updateTokenTask = new UpdateTokenTask(context); | ||||
|                                         updateTokenTask.execute(token); | ||||
|                                         result.success(token); | ||||
|                                     } else { | ||||
|                                         result.error("fcm_error", task.getException().getMessage(), task.getException()); | ||||
|                                         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); | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -16,7 +16,8 @@ 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: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:in_app_purchase/in_app_purchase.dart'; | ||||
| import 'plugins/dynamic_multi_column_layout.dart'; | ||||
| @@ -184,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( | ||||
|   | ||||
| @@ -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,6 +28,7 @@ class AppSettings { | ||||
|   String webhookId; | ||||
|   double haVersion; | ||||
|   bool scrollBadges; | ||||
|   DisplayMode displayMode; | ||||
|   AppTheme appTheme; | ||||
|   final int defaultLocationUpdateIntervalMinutes = 20; | ||||
|   Duration locationUpdateInterval; | ||||
| @@ -30,13 +37,15 @@ 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'); | ||||
| @@ -52,18 +61,11 @@ 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')}"; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -103,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() { | ||||
|   | ||||
| @@ -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")])); | ||||
|     }); | ||||
|   | ||||
| @@ -139,7 +139,7 @@ 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"); | ||||
|         } | ||||
|       ) | ||||
|     )); | ||||
|   | ||||
| @@ -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(); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -301,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(), | ||||
| @@ -484,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( | ||||
| @@ -493,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>( | ||||
| @@ -654,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(); | ||||
|   | ||||
| @@ -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: (){ | ||||
|   | ||||
| @@ -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: () { | ||||
|   | ||||
| @@ -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); | ||||
|             }, | ||||
|           ), | ||||
|         ] | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| name: hass_client | ||||
| description: Home Assistant Android Client | ||||
|  | ||||
| version: 0.0.0+1151 | ||||
| version: 1.1.0+1156 | ||||
|  | ||||
|  | ||||
| environment: | ||||
| @@ -23,7 +23,8 @@ dependencies: | ||||
|   flutter_custom_tabs: ^0.6.0 | ||||
|   flutter_webview_plugin: ^0.3.10+1 | ||||
|   webview_flutter: ^0.3.19+7 | ||||
|   flutter_secure_storage: ^3.3.3 | ||||
|   hive: ^1.4.1+1 | ||||
|   hive_flutter: ^0.3.0+2 | ||||
|   device_info: ^0.4.1+4 | ||||
|   geolocator: ^5.3.1 | ||||
|   workmanager: ^0.2.2 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user