diff --git a/lib/main.dart b/lib/main.dart index bc9377e..fae417d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -92,6 +92,7 @@ part 'pages/widgets/product_purchase.widget.dart'; part 'pages/widgets/page_loading_indicator.dart'; part 'pages/widgets/page_loading_error.dart'; part 'pages/panel.page.dart'; +part 'pages/main.page.dart'; part 'home_assistant.class.dart'; part 'pages/log.page.dart'; part 'pages/entity.page.dart'; @@ -194,805 +195,4 @@ class HAClientApp extends StatelessWidget { }, ); } -} - -class MainPage extends StatefulWidget { - MainPage({Key key, this.title}) : super(key: key); - - final String title; - - @override - _MainPageState createState() => new _MainPageState(); -} - -class _MainPageState extends State with WidgetsBindingObserver, TickerProviderStateMixin { - - StreamSubscription> _subscription; - StreamSubscription _stateSubscription; - StreamSubscription _settingsSubscription; - StreamSubscription _serviceCallSubscription; - StreamSubscription _showEntityPageSubscription; - StreamSubscription _showErrorSubscription; - StreamSubscription _startAuthSubscription; - StreamSubscription _showPopupDialogSubscription; - StreamSubscription _showPopupMessageSubscription; - StreamSubscription _reloadUISubscription; - StreamSubscription _showPageSubscription; - int _previousViewCount; - bool _showLoginButton = false; - bool _preventAppRefresh = false; - - @override - void initState() { - final Stream purchaseUpdates = - InAppPurchaseConnection.instance.purchaseUpdatedStream; - _subscription = purchaseUpdates.listen((purchases) { - _handlePurchaseUpdates(purchases); - }); - super.initState(); - WidgetsBinding.instance.addObserver(this); - - _firebaseMessaging.configure( - onLaunch: (data) { - Logger.d("Notification [onLaunch]: $data"); - return Future.value(); - }, - onMessage: (data) { - Logger.d("Notification [onMessage]: $data"); - return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]); - }, - onResume: (data) { - Logger.d("Notification [onResume]: $data"); - return Future.value(); - } - ); - - _firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true)); - - // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project - var initializationSettingsAndroid = - new AndroidInitializationSettings('mini_icon'); - var initializationSettingsIOS = new IOSInitializationSettings( - onDidReceiveLocalNotification: null); - var initializationSettings = new InitializationSettings( - initializationSettingsAndroid, initializationSettingsIOS); - flutterLocalNotificationsPlugin.initialize(initializationSettings, - onSelectNotification: onSelectNotification); - - _settingsSubscription = eventBus.on().listen((event) { - Logger.d("Settings change event: reconnect=${event.reconnect}"); - if (event.reconnect) { - _preventAppRefresh = false; - _fullLoad(); - } - }); - - _fullLoad(); - - - } - - Future onSelectNotification(String payload) async { - if (payload != null) { - Logger.d('Notification clicked: ' + payload); - } - } - - Future _showNotification({String title, String text}) async { - var androidPlatformChannelSpecifics = new AndroidNotificationDetails( - 'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service', - importance: Importance.Max, priority: Priority.High); - var iOSPlatformChannelSpecifics = new IOSNotificationDetails(); - var platformChannelSpecifics = new NotificationDetails( - androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); - await flutterLocalNotificationsPlugin.show( - 0, - title ?? appName, - text, - platformChannelSpecifics - ); - } - - void _fullLoad() async { - _showInfoBottomBar(progress: true,); - _subscribe().then((_) { - ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){ - _fetchData(); - StartupUserMessagesManager().checkMessagesToShow(); - }, onError: (e) { - _setErrorState(e); - }); - }); - } - - void _quickLoad() { - _hideBottomBar(); - _showInfoBottomBar(progress: true,); - ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){ - _fetchData(); - //StartupUserMessagesManager().checkMessagesToShow(); - }, onError: (e) { - _setErrorState(e); - }); - } - - _fetchData() async { - await HomeAssistant().fetchData().then((_) { - _hideBottomBar(); - int currentViewCount = HomeAssistant().ui?.views?.length ?? 0; - if (_previousViewCount != currentViewCount) { - Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller."); - _viewsTabController = TabController(vsync: this, length: currentViewCount); - _previousViewCount = currentViewCount; - } - }).catchError((e) { - if (e is HAError) { - _setErrorState(e); - } else { - _setErrorState(HAError(e.toString())); - } - }); - eventBus.fire(RefreshDataFinishedEvent()); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - Logger.d("$state"); - if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) { - _quickLoad(); - } - } - - void _handlePurchaseUpdates(purchase) { - if (purchase is List) { - if (purchase[0].status == PurchaseStatus.purchased) { - eventBus.fire(ShowPopupMessageEvent( - title: "Thanks a lot!", - body: "Thank you for supporting HA Client development!", - buttonText: "Ok" - )); - } else { - Logger.d("Purchase change handler: ${purchase[0].status}"); - } - } else { - Logger.e("Something wrong with purchase handling. Got: $purchase"); - } - } - - Future _subscribe() { - Completer completer = Completer(); - if (_stateSubscription == null) { - _stateSubscription = eventBus.on().listen((event) { - if (event.needToRebuildUI) { - Logger.d("New entity. Need to rebuild UI"); - _quickLoad(); - } else { - setState(() {}); - } - }); - } - if (_reloadUISubscription == null) { - _reloadUISubscription = eventBus.on().listen((event){ - _quickLoad(); - }); - } - if (_showPopupDialogSubscription == null) { - _showPopupDialogSubscription = eventBus.on().listen((event){ - _showPopupDialog( - title: event.title, - body: event.body, - onPositive: event.onPositive, - onNegative: event.onNegative, - positiveText: event.positiveText, - negativeText: event.negativeText - ); - }); - } - if (_showPopupMessageSubscription == null) { - _showPopupMessageSubscription = eventBus.on().listen((event){ - _showPopupDialog( - title: event.title, - body: event.body, - onPositive: event.onButtonClick, - positiveText: event.buttonText, - negativeText: null - ); - }); - } - if (_serviceCallSubscription == null) { - _serviceCallSubscription = - eventBus.on().listen((event) { - _callService(event.domain, event.service, event.entityId, - event.additionalParams); - }); - } - - if (_showEntityPageSubscription == null) { - _showEntityPageSubscription = - eventBus.on().listen((event) { - _showEntityPage(event.entity.entityId); - }); - } - - if (_showPageSubscription == null) { - _showPageSubscription = - eventBus.on().listen((event) { - _showPage(event.path, event.goBackFirst); - }); - } - - if (_showErrorSubscription == null) { - _showErrorSubscription = eventBus.on().listen((event){ - _showErrorBottomBar(event.error); - }); - } - - if (_startAuthSubscription == null) { - _startAuthSubscription = eventBus.on().listen((event){ - setState(() { - _showLoginButton = event.showButton; - }); - if (event.showButton) { - _showOAuth(); - } else { - _preventAppRefresh = false; - Navigator.of(context).pop(); - } - }); - } - - _firebaseMessaging.getToken().then((String token) { - HomeAssistant().fcmToken = token; - completer.complete(); - }); - return completer.future; - } - - void _showOAuth() { - _preventAppRefresh = true; - Navigator.of(context).pushNamed('/login'); - } - - _setErrorState(HAError e) { - if (e == null) { - _showErrorBottomBar( - HAError("Unknown error") - ); - } else { - _showErrorBottomBar(e); - } - } - - void _showPopupDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) { - List buttons = []; - buttons.add(FlatButton( - child: new Text("$positiveText"), - onPressed: () { - Navigator.of(context).pop(); - if (onPositive != null) { - onPositive(); - } - }, - )); - if (negativeText != null) { - buttons.add(FlatButton( - child: new Text("$negativeText"), - onPressed: () { - Navigator.of(context).pop(); - if (onNegative != null) { - onNegative(); - } - }, - )); - } - // flutter defined function - showDialog( - barrierDismissible: false, - context: context, - builder: (BuildContext context) { - // return object of type Dialog - return AlertDialog( - title: new Text("$title"), - content: new Text("$body"), - actions: buttons, - ); - }, - ); - } - - //TODO remove this shit - void _callService(String domain, String service, String entityId, Map additionalParams) { - _showInfoBottomBar( - message: "Calling $domain.$service", - duration: Duration(seconds: 3) - ); - ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e)); - } - - void _showEntityPage(String entityId) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EntityViewPage(entityId: entityId), - ) - ); - } - - void _showPage(String path, bool goBackFirst) { - if (goBackFirst) { - Navigator.pop(context); - } - Navigator.pushNamed( - context, - path - ); - } - - List buildUIViewTabs() { - List result = []; - - if (HomeAssistant().ui.views.isNotEmpty) { - HomeAssistant().ui.views.forEach((HAView view) { - result.add(view.buildTab()); - }); - } - - return result; - } - - Drawer _buildAppDrawer() { - List menuItems = []; - menuItems.add( - UserAccountsDrawerHeader( - accountName: Text(HomeAssistant().userName), - accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"), - onDetailsPressed: () { - final flutterWebViewPlugin = new FlutterWebviewPlugin(); - flutterWebViewPlugin.onStateChanged.listen((viewState) async { - if (viewState.type == WebViewState.startLoad) { - Logger.d("[WebView] Injecting external auth JS"); - rootBundle.loadString('assets/js/externalAuth.js').then((js){ - flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token)); - }); - } - }); - Navigator.of(context).pushNamed( - "/webview", - arguments: { - "url": "${ConnectionManager().httpWebHost}/profile?external_auth=1", - "title": "Profile" - } - ); - }, - currentAccountPicture: CircleAvatar( - child: Text( - HomeAssistant().userAvatarText, - style: TextStyle( - fontSize: 32.0 - ), - ), - ), - ) - ); - if (HomeAssistant().panels.isNotEmpty) { - HomeAssistant().panels.forEach((Panel panel) { - if (!panel.isHidden) { - menuItems.add( - new ListTile( - leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)), - title: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("${panel.title}"), - Container(width: 4.0,), - panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,) - ], - ), - onTap: () { - Navigator.of(context).pop(); - panel.handleOpen(context); - } - ) - ); - } - }); - } - menuItems.addAll([ - Divider(), - ListTile( - leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")), - title: Text("Connection settings"), - onTap: () { - Navigator.of(context).pop(); - Navigator.of(context).pushNamed('/connection-settings'); - }, - ) - ]); - menuItems.addAll([ - Divider(), - new ListTile( - leading: Icon(Icons.insert_drive_file), - title: Text("Log"), - onTap: () { - Navigator.of(context).pop(); - Navigator.of(context).pushNamed('/log-view'); - }, - ), - new ListTile( - leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")), - title: Text("Report an issue"), - onTap: () { - Navigator.of(context).pop(); - Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new"); - }, - ), - Divider(), - new ListTile( - leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")), - title: Text("Support app development"), - onTap: () { - Navigator.of(context).pop(); - Navigator.of(context).pushNamed('/putchase'); - }, - ), - Divider(), - new ListTile( - leading: Icon(Icons.help), - title: Text("Help"), - onTap: () { - Navigator.of(context).pop(); - Launcher.launchURL("http://ha-client.homemade.systems/docs"); - }, - ), - new ListTile( - leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")), - title: Text("Join Discord channel"), - onTap: () { - Navigator.of(context).pop(); - Launcher.launchURL("https://discord.gg/AUzEvwn"); - }, - ), - new AboutListTile( - aboutBoxChildren: [ - GestureDetector( - onTap: () { - Navigator.of(context).pop(); - Launcher.launchURL("http://ha-client.homemade.systems/"); - }, - child: Text( - "ha-client.homemade.systems", - style: TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline - ), - ), - ), - Container( - height: 10.0, - ), - GestureDetector( - onTap: () { - Navigator.of(context).pop(); - Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions"); - }, - child: Text( - "Terms and Conditions", - style: TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline - ), - ), - ), - Container( - height: 10.0, - ), - GestureDetector( - onTap: () { - Navigator.of(context).pop(); - Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy"); - }, - child: Text( - "Privacy Policy", - style: TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline - ), - ), - ) - ], - applicationName: appName, - applicationVersion: appVersion - ) - ]); - return new Drawer( - child: ListView( - children: menuItems, - ), - ); - } - - void _hideBottomBar() { - //_scaffoldKey?.currentState?.hideCurrentSnackBar(); - setState(() { - _showBottomBar = false; - }); - } - - Widget _bottomBarAction; - bool _showBottomBar = false; - String _bottomBarText; - bool _bottomBarProgress; - Color _bottomBarColor; - Timer _bottomBarTimer; - - void _showInfoBottomBar({String message, bool progress: false, Duration duration}) { - _bottomBarTimer?.cancel(); - _bottomBarAction = Container(height: 0.0, width: 0.0,); - _bottomBarColor = Colors.grey.shade50; - setState(() { - _bottomBarText = message; - _bottomBarProgress = progress; - _showBottomBar = true; - }); - if (duration != null) { - _bottomBarTimer = Timer(duration, () { - _hideBottomBar(); - }); - } - } - - void _showErrorBottomBar(HAError error) { - TextStyle textStyle = TextStyle( - color: Colors.blue, - fontSize: Sizes.nameFontSize - ); - _bottomBarColor = Colors.red.shade100; - List 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 _scaffoldKey = new GlobalKey(); - - Widget _buildScaffoldBody(bool empty) { - List> popupMenuItems = []; - - popupMenuItems.add(PopupMenuItem( - child: new Text("Reload"), - value: "reload", - )); - List emptyBody = [ - Text("."), - ]; - if (ConnectionManager().isAuthenticated) { - _showLoginButton = false; - popupMenuItems.add( - PopupMenuItem( - child: new Text("Logout"), - value: "logout", - )); - } - if (_showLoginButton) { - emptyBody = [ - FlatButton( - child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)), - color: Colors.blue, - onPressed: () => _fullLoad(), - ) - ]; - } - return NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverAppBar( - floating: true, - pinned: true, - primary: true, - title: Text(HomeAssistant().locationName ?? ""), - actions: [ - IconButton( - icon: Icon(MaterialDesignIcons.getIconDataFromIconName( - "mdi:dots-vertical"), color: Colors.white,), - onPressed: () { - showMenu( - position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), - context: context, - items: popupMenuItems - ).then((String val) { - if (val == "reload") { - _quickLoad(); - } else if (val == "logout") { - HomeAssistant().logout().then((_) { - _quickLoad(); - }); - } - }); - } - ) - ], - leading: IconButton( - icon: Icon(Icons.menu), - onPressed: () { - _scaffoldKey.currentState.openDrawer(); - }, - ), - bottom: empty ? null : TabBar( - controller: _viewsTabController, - tabs: buildUIViewTabs(), - isScrollable: true, - ), - ), - - ]; - }, - body: empty ? - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: emptyBody - ), - ) - : - HomeAssistant().buildViews(context, _viewsTabController), - ); - } - - TabController _viewsTabController; - - @override - Widget build(BuildContext context) { - Widget bottomBar; - if (_showBottomBar) { - List 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: [ - 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: [ - Expanded( - child: Column( - crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: bottomBarChildren, - ), - ), - _bottomBarAction - ], - ), - ); - } - } - // This method is rerun every time setState is called. - if (HomeAssistant().isNoViews) { - return Scaffold( - key: _scaffoldKey, - primary: false, - drawer: _buildAppDrawer(), - bottomNavigationBar: bottomBar, - body: _buildScaffoldBody(true) - ); - } else { - return Scaffold( - key: _scaffoldKey, - drawer: _buildAppDrawer(), - primary: false, - bottomNavigationBar: bottomBar, - body: _buildScaffoldBody(false), - ); - } - } - - @override - void dispose() { - final flutterWebviewPlugin = new FlutterWebviewPlugin(); - flutterWebviewPlugin.dispose(); - WidgetsBinding.instance.removeObserver(this); - _viewsTabController?.dispose(); - _stateSubscription?.cancel(); - _settingsSubscription?.cancel(); - _serviceCallSubscription?.cancel(); - _showPopupDialogSubscription?.cancel(); - _showPopupMessageSubscription?.cancel(); - _showEntityPageSubscription?.cancel(); - _showErrorSubscription?.cancel(); - _startAuthSubscription?.cancel(); - _subscription?.cancel(); - _showPageSubscription?.cancel(); - _reloadUISubscription?.cancel(); - //TODO disconnect - //widget.homeAssistant?.disconnect(); - super.dispose(); - } -} +} \ No newline at end of file diff --git a/lib/pages/main.page.dart b/lib/pages/main.page.dart new file mode 100644 index 0000000..48c3d3d --- /dev/null +++ b/lib/pages/main.page.dart @@ -0,0 +1,802 @@ +part of '../main.dart'; + +class MainPage extends StatefulWidget { + MainPage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _MainPageState createState() => new _MainPageState(); +} + +class _MainPageState extends State with WidgetsBindingObserver, TickerProviderStateMixin { + + StreamSubscription> _subscription; + StreamSubscription _stateSubscription; + StreamSubscription _settingsSubscription; + StreamSubscription _serviceCallSubscription; + StreamSubscription _showEntityPageSubscription; + StreamSubscription _showErrorSubscription; + StreamSubscription _startAuthSubscription; + StreamSubscription _showPopupDialogSubscription; + StreamSubscription _showPopupMessageSubscription; + StreamSubscription _reloadUISubscription; + StreamSubscription _showPageSubscription; + int _previousViewCount; + bool _showLoginButton = false; + bool _preventAppRefresh = false; + + @override + void initState() { + final Stream purchaseUpdates = + InAppPurchaseConnection.instance.purchaseUpdatedStream; + _subscription = purchaseUpdates.listen((purchases) { + _handlePurchaseUpdates(purchases); + }); + super.initState(); + WidgetsBinding.instance.addObserver(this); + + _firebaseMessaging.configure( + onLaunch: (data) { + Logger.d("Notification [onLaunch]: $data"); + return Future.value(); + }, + onMessage: (data) { + Logger.d("Notification [onMessage]: $data"); + return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]); + }, + onResume: (data) { + Logger.d("Notification [onResume]: $data"); + return Future.value(); + } + ); + + _firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true)); + + // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project + var initializationSettingsAndroid = + new AndroidInitializationSettings('mini_icon'); + var initializationSettingsIOS = new IOSInitializationSettings( + onDidReceiveLocalNotification: null); + var initializationSettings = new InitializationSettings( + initializationSettingsAndroid, initializationSettingsIOS); + flutterLocalNotificationsPlugin.initialize(initializationSettings, + onSelectNotification: onSelectNotification); + + _settingsSubscription = eventBus.on().listen((event) { + Logger.d("Settings change event: reconnect=${event.reconnect}"); + if (event.reconnect) { + _preventAppRefresh = false; + _fullLoad(); + } + }); + + _fullLoad(); + + + } + + Future onSelectNotification(String payload) async { + if (payload != null) { + Logger.d('Notification clicked: ' + payload); + } + } + + Future _showNotification({String title, String text}) async { + var androidPlatformChannelSpecifics = new AndroidNotificationDetails( + 'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service', + importance: Importance.Max, priority: Priority.High); + var iOSPlatformChannelSpecifics = new IOSNotificationDetails(); + var platformChannelSpecifics = new NotificationDetails( + androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + 0, + title ?? appName, + text, + platformChannelSpecifics + ); + } + + void _fullLoad() async { + _showInfoBottomBar(progress: true,); + _subscribe().then((_) { + ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){ + _fetchData(); + StartupUserMessagesManager().checkMessagesToShow(); + }, onError: (e) { + _setErrorState(e); + }); + }); + } + + void _quickLoad() { + _hideBottomBar(); + _showInfoBottomBar(progress: true,); + ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){ + _fetchData(); + //StartupUserMessagesManager().checkMessagesToShow(); + }, onError: (e) { + _setErrorState(e); + }); + } + + _fetchData() async { + await HomeAssistant().fetchData().then((_) { + _hideBottomBar(); + int currentViewCount = HomeAssistant().ui?.views?.length ?? 0; + if (_previousViewCount != currentViewCount) { + Logger.d("Views count changed ($_previousViewCount->$currentViewCount). Creating new tabs controller."); + _viewsTabController = TabController(vsync: this, length: currentViewCount); + _previousViewCount = currentViewCount; + } + }).catchError((e) { + if (e is HAError) { + _setErrorState(e); + } else { + _setErrorState(HAError(e.toString())); + } + }); + eventBus.fire(RefreshDataFinishedEvent()); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + Logger.d("$state"); + if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) { + _quickLoad(); + } + } + + void _handlePurchaseUpdates(purchase) { + if (purchase is List) { + if (purchase[0].status == PurchaseStatus.purchased) { + eventBus.fire(ShowPopupMessageEvent( + title: "Thanks a lot!", + body: "Thank you for supporting HA Client development!", + buttonText: "Ok" + )); + } else { + Logger.d("Purchase change handler: ${purchase[0].status}"); + } + } else { + Logger.e("Something wrong with purchase handling. Got: $purchase"); + } + } + + Future _subscribe() { + Completer completer = Completer(); + if (_stateSubscription == null) { + _stateSubscription = eventBus.on().listen((event) { + if (event.needToRebuildUI) { + Logger.d("New entity. Need to rebuild UI"); + _quickLoad(); + } else { + setState(() {}); + } + }); + } + if (_reloadUISubscription == null) { + _reloadUISubscription = eventBus.on().listen((event){ + _quickLoad(); + }); + } + if (_showPopupDialogSubscription == null) { + _showPopupDialogSubscription = eventBus.on().listen((event){ + _showPopupDialog( + title: event.title, + body: event.body, + onPositive: event.onPositive, + onNegative: event.onNegative, + positiveText: event.positiveText, + negativeText: event.negativeText + ); + }); + } + if (_showPopupMessageSubscription == null) { + _showPopupMessageSubscription = eventBus.on().listen((event){ + _showPopupDialog( + title: event.title, + body: event.body, + onPositive: event.onButtonClick, + positiveText: event.buttonText, + negativeText: null + ); + }); + } + if (_serviceCallSubscription == null) { + _serviceCallSubscription = + eventBus.on().listen((event) { + _callService(event.domain, event.service, event.entityId, + event.additionalParams); + }); + } + + if (_showEntityPageSubscription == null) { + _showEntityPageSubscription = + eventBus.on().listen((event) { + _showEntityPage(event.entity.entityId); + }); + } + + if (_showPageSubscription == null) { + _showPageSubscription = + eventBus.on().listen((event) { + _showPage(event.path, event.goBackFirst); + }); + } + + if (_showErrorSubscription == null) { + _showErrorSubscription = eventBus.on().listen((event){ + _showErrorBottomBar(event.error); + }); + } + + if (_startAuthSubscription == null) { + _startAuthSubscription = eventBus.on().listen((event){ + setState(() { + _showLoginButton = event.showButton; + }); + if (event.showButton) { + _showOAuth(); + } else { + _preventAppRefresh = false; + Navigator.of(context).pop(); + } + }); + } + + _firebaseMessaging.getToken().then((String token) { + HomeAssistant().fcmToken = token; + completer.complete(); + }); + return completer.future; + } + + void _showOAuth() { + _preventAppRefresh = true; + Navigator.of(context).pushNamed('/login'); + } + + _setErrorState(HAError e) { + if (e == null) { + _showErrorBottomBar( + HAError("Unknown error") + ); + } else { + _showErrorBottomBar(e); + } + } + + void _showPopupDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) { + List buttons = []; + buttons.add(FlatButton( + child: new Text("$positiveText"), + onPressed: () { + Navigator.of(context).pop(); + if (onPositive != null) { + onPositive(); + } + }, + )); + if (negativeText != null) { + buttons.add(FlatButton( + child: new Text("$negativeText"), + onPressed: () { + Navigator.of(context).pop(); + if (onNegative != null) { + onNegative(); + } + }, + )); + } + // flutter defined function + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) { + // return object of type Dialog + return AlertDialog( + title: new Text("$title"), + content: new Text("$body"), + actions: buttons, + ); + }, + ); + } + + //TODO remove this shit + void _callService(String domain, String service, String entityId, Map additionalParams) { + _showInfoBottomBar( + message: "Calling $domain.$service", + duration: Duration(seconds: 3) + ); + ConnectionManager().callService(domain: domain, service: service, entityId: entityId, additionalServiceData: additionalParams).catchError((e) => _setErrorState(e)); + } + + void _showEntityPage(String entityId) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EntityViewPage(entityId: entityId), + ) + ); + } + + void _showPage(String path, bool goBackFirst) { + if (goBackFirst) { + Navigator.pop(context); + } + Navigator.pushNamed( + context, + path + ); + } + + List buildUIViewTabs() { + List result = []; + + if (HomeAssistant().ui.views.isNotEmpty) { + HomeAssistant().ui.views.forEach((HAView view) { + result.add(view.buildTab()); + }); + } + + return result; + } + + Drawer _buildAppDrawer() { + List menuItems = []; + menuItems.add( + UserAccountsDrawerHeader( + accountName: Text(HomeAssistant().userName), + accountEmail: Text(ConnectionManager().displayHostname ?? "Not configured"), + onDetailsPressed: () { + final flutterWebViewPlugin = new FlutterWebviewPlugin(); + flutterWebViewPlugin.onStateChanged.listen((viewState) async { + if (viewState.type == WebViewState.startLoad) { + Logger.d("[WebView] Injecting external auth JS"); + rootBundle.loadString('assets/js/externalAuth.js').then((js){ + flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token)); + }); + } + }); + Navigator.of(context).pushNamed( + "/webview", + arguments: { + "url": "${ConnectionManager().httpWebHost}/profile?external_auth=1", + "title": "Profile" + } + ); + }, + currentAccountPicture: CircleAvatar( + child: Text( + HomeAssistant().userAvatarText, + style: TextStyle( + fontSize: 32.0 + ), + ), + ), + ) + ); + if (HomeAssistant().panels.isNotEmpty) { + HomeAssistant().panels.forEach((Panel panel) { + if (!panel.isHidden) { + menuItems.add( + new ListTile( + leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)), + title: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("${panel.title}"), + Container(width: 4.0,), + panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,) + ], + ), + onTap: () { + Navigator.of(context).pop(); + panel.handleOpen(context); + } + ) + ); + } + }); + } + menuItems.addAll([ + Divider(), + ListTile( + leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:login-variant")), + title: Text("Connection settings"), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushNamed('/connection-settings'); + }, + ) + ]); + menuItems.addAll([ + Divider(), + new ListTile( + leading: Icon(Icons.insert_drive_file), + title: Text("Log"), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushNamed('/log-view'); + }, + ), + new ListTile( + leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:github-circle")), + title: Text("Report an issue"), + onTap: () { + Navigator.of(context).pop(); + Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new"); + }, + ), + Divider(), + new ListTile( + leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")), + title: Text("Support app development"), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushNamed('/putchase'); + }, + ), + Divider(), + new ListTile( + leading: Icon(Icons.help), + title: Text("Help"), + onTap: () { + Navigator.of(context).pop(); + Launcher.launchURL("http://ha-client.homemade.systems/docs"); + }, + ), + new ListTile( + leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")), + title: Text("Join Discord channel"), + onTap: () { + Navigator.of(context).pop(); + Launcher.launchURL("https://discord.gg/AUzEvwn"); + }, + ), + new AboutListTile( + aboutBoxChildren: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + Launcher.launchURL("http://ha-client.homemade.systems/"); + }, + child: Text( + "ha-client.homemade.systems", + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline + ), + ), + ), + Container( + height: 10.0, + ), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/terms_and_conditions"); + }, + child: Text( + "Terms and Conditions", + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline + ), + ), + ), + Container( + height: 10.0, + ), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.homemade.systems/privacy_policy"); + }, + child: Text( + "Privacy Policy", + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline + ), + ), + ) + ], + applicationName: appName, + applicationVersion: appVersion + ) + ]); + return new Drawer( + child: ListView( + children: menuItems, + ), + ); + } + + void _hideBottomBar() { + //_scaffoldKey?.currentState?.hideCurrentSnackBar(); + setState(() { + _showBottomBar = false; + }); + } + + Widget _bottomBarAction; + bool _showBottomBar = false; + String _bottomBarText; + bool _bottomBarProgress; + Color _bottomBarColor; + Timer _bottomBarTimer; + + void _showInfoBottomBar({String message, bool progress: false, Duration duration}) { + _bottomBarTimer?.cancel(); + _bottomBarAction = Container(height: 0.0, width: 0.0,); + _bottomBarColor = Colors.grey.shade50; + setState(() { + _bottomBarText = message; + _bottomBarProgress = progress; + _showBottomBar = true; + }); + if (duration != null) { + _bottomBarTimer = Timer(duration, () { + _hideBottomBar(); + }); + } + } + + void _showErrorBottomBar(HAError error) { + TextStyle textStyle = TextStyle( + color: Colors.blue, + fontSize: Sizes.nameFontSize + ); + _bottomBarColor = Colors.red.shade100; + List 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 _scaffoldKey = new GlobalKey(); + + Widget _buildScaffoldBody(bool empty) { + List> popupMenuItems = []; + + popupMenuItems.add(PopupMenuItem( + child: new Text("Reload"), + value: "reload", + )); + List emptyBody = [ + Text("."), + ]; + if (ConnectionManager().isAuthenticated) { + _showLoginButton = false; + popupMenuItems.add( + PopupMenuItem( + child: new Text("Logout"), + value: "logout", + )); + } + if (_showLoginButton) { + emptyBody = [ + FlatButton( + child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)), + color: Colors.blue, + onPressed: () => _fullLoad(), + ) + ]; + } + return NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + floating: true, + pinned: true, + primary: true, + title: Text(HomeAssistant().locationName ?? ""), + actions: [ + IconButton( + icon: Icon(MaterialDesignIcons.getIconDataFromIconName( + "mdi:dots-vertical"), color: Colors.white,), + onPressed: () { + showMenu( + position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), + context: context, + items: popupMenuItems + ).then((String val) { + if (val == "reload") { + _quickLoad(); + } else if (val == "logout") { + HomeAssistant().logout().then((_) { + _quickLoad(); + }); + } + }); + } + ) + ], + leading: IconButton( + icon: Icon(Icons.menu), + onPressed: () { + _scaffoldKey.currentState.openDrawer(); + }, + ), + bottom: empty ? null : TabBar( + controller: _viewsTabController, + tabs: buildUIViewTabs(), + isScrollable: true, + ), + ), + + ]; + }, + body: empty ? + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: emptyBody + ), + ) + : + HomeAssistant().buildViews(context, _viewsTabController), + ); + } + + TabController _viewsTabController; + + @override + Widget build(BuildContext context) { + Widget bottomBar; + if (_showBottomBar) { + List 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: [ + 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: [ + Expanded( + child: Column( + crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: bottomBarChildren, + ), + ), + _bottomBarAction + ], + ), + ); + } + } + // This method is rerun every time setState is called. + if (HomeAssistant().isNoViews) { + return Scaffold( + key: _scaffoldKey, + primary: false, + drawer: _buildAppDrawer(), + bottomNavigationBar: bottomBar, + body: _buildScaffoldBody(true) + ); + } else { + return Scaffold( + key: _scaffoldKey, + drawer: _buildAppDrawer(), + primary: false, + bottomNavigationBar: bottomBar, + body: _buildScaffoldBody(false), + ); + } + } + + @override + void dispose() { + final flutterWebviewPlugin = new FlutterWebviewPlugin(); + flutterWebviewPlugin.dispose(); + WidgetsBinding.instance.removeObserver(this); + _viewsTabController?.dispose(); + _stateSubscription?.cancel(); + _settingsSubscription?.cancel(); + _serviceCallSubscription?.cancel(); + _showPopupDialogSubscription?.cancel(); + _showPopupMessageSubscription?.cancel(); + _showEntityPageSubscription?.cancel(); + _showErrorSubscription?.cancel(); + _startAuthSubscription?.cancel(); + _subscription?.cancel(); + _showPageSubscription?.cancel(); + _reloadUISubscription?.cancel(); + //TODO disconnect + //widget.homeAssistant?.disconnect(); + super.dispose(); + } +}