diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart index 8007e8e..b9d3dfb 100644 --- a/lib/home_assistant.class.dart +++ b/lib/home_assistant.class.dart @@ -42,7 +42,7 @@ class HomeAssistant { return _instanceConfig["location_name"] ?? "Home"; } } - String get userName => _userName ?? locationName; + String get userName => _userName ?? ''; String get userAvatarText => userName.length > 0 ? userName[0] : ""; bool get isNoEntities => entities == null || entities.isEmpty; bool get isNoViews => ui == null || ui.isEmpty; diff --git a/lib/main.dart b/lib/main.dart index bafad64..639c47c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -113,6 +113,7 @@ part 'pages/settings/integration_settings.part.dart'; part 'pages/settings/app_settings.page.dart'; part 'pages/settings/lookandfeel_settings.part.dart'; part 'pages/zha_page.dart'; +part 'pages/quick_start.page.dart'; part 'home_assistant.class.dart'; part 'pages/entity.page.dart'; part 'utils/mdi.class.dart'; @@ -260,7 +261,7 @@ class _HAClientAppState extends State { routes: { "/": (context) => MainPage(title: 'HA Client'), "/app-settings": (context) => AppSettingsPage(), - "/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings), + "/connection-settings": (context) => AppSettingsPage(), "/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings), "/putchase": (context) => PurchasePage(title: "Support app development"), "/play-media": (context) => PlayMediaPage( @@ -278,22 +279,23 @@ class _HAClientAppState extends State { ), ), "/whats-new": (context) => WhatsNewPage(), + "/quick-start": (context) => QuickStartPage(), "/haclient_zha": (context) => ZhaPage(), "/auth": (context) => new standaloneWebview.WebviewScaffold( url: "${ConnectionManager().oauthUrl}", appBar: new AppBar( leading: IconButton( - icon: Icon(Icons.help), - onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#authentication") + icon: Icon(Icons.help), + onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "https://ha-client.app/help/connection") ), - title: new Text("Login with HA"), + title: new Text("Login"), actions: [ FlatButton( - child: Text("Manual", style: Theme.of(context).textTheme.button.copyWith( + child: Text("Long-lived token", style: Theme.of(context).textTheme.button.copyWith( decoration: TextDecoration.underline )), onPressed: () { - eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true)); + eventBus.fire(ShowTokenLoginPopupEvent(goBackFirst: true)); }, ) ], diff --git a/lib/managers/connection_manager.class.dart b/lib/managers/connection_manager.class.dart index b2cfe10..3c555ec 100644 --- a/lib/managers/connection_manager.class.dart +++ b/lib/managers/connection_manager.class.dart @@ -53,7 +53,10 @@ class ConnectionManager { httpWebHost = "${prefs.getString('hassio-res-protocol')}://$_domain:$_port"; Logger.d('$_domain$_port'); - if ((_domain == null) || (_port == null) || + if (_domain == null && _port == null && webhookId == null && mobileAppDeviceName == null) { + completer.completeError(HACNotSetUpException()); + stopInit = true; + } else if ((_domain == null) || (_port == null) || (_domain.isEmpty) || (_port.isEmpty)) { completer.completeError(HACException.checkConnectionSettings()); stopInit = true; diff --git a/lib/managers/mobile_app_integration_manager.class.dart b/lib/managers/mobile_app_integration_manager.class.dart index 20aebb2..fc1d6d9 100644 --- a/lib/managers/mobile_app_integration_manager.class.dart +++ b/lib/managers/mobile_app_integration_manager.class.dart @@ -17,6 +17,9 @@ class MobileAppIntegrationManager { }; static String getDefaultDeviceName() { + if (HomeAssistant().userName.isEmpty) { + return '${DeviceInfoManager().model}'; + } return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}'; } diff --git a/lib/managers/theme_manager.dart b/lib/managers/theme_manager.dart index 6b5ac21..062f247 100644 --- a/lib/managers/theme_manager.dart +++ b/lib/managers/theme_manager.dart @@ -19,7 +19,7 @@ class HAClientTheme { fontWeight: FontWeight.normal, letterSpacing: 1, ), - button: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + button: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ); static const offEntityStates = [ diff --git a/lib/pages/main/main.page.dart b/lib/pages/main/main.page.dart index 67c1bda..0c661d7 100644 --- a/lib/pages/main/main.page.dart +++ b/lib/pages/main/main.page.dart @@ -20,6 +20,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker StreamSubscription _startAuthSubscription; StreamSubscription _showPopupDialogSubscription; StreamSubscription _showPopupMessageSubscription; + StreamSubscription _showTokenLoginPopupSubscription; StreamSubscription _reloadUISubscription; StreamSubscription _fullReloadSubscription; StreamSubscription _showPageSubscription; @@ -105,7 +106,11 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker StartupUserMessagesManager().checkMessagesToShow(); }); }, onError: (e) { - _setErrorState(e); + if (e is HACNotSetUpException) { + Navigator.of(context).pushReplacementNamed('/quick-start'); + } else { + _setErrorState(e); + } }); }); } @@ -201,6 +206,14 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker ); }); } + if (_showTokenLoginPopupSubscription == null) { + _showTokenLoginPopupSubscription = eventBus.on().listen((event){ + if (event.goBackFirst) { + Navigator.of(context).pop(); + } + _showTokenLoginDialog(); + }); + } if (_serviceCallSubscription == null) { _serviceCallSubscription = eventBus.on().listen((event) { @@ -302,6 +315,78 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker ); } + final _tokenLoginFormKey = GlobalKey(); + + void _showTokenLoginDialog() { + // flutter defined function + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) { + // return object of type Dialog + return SimpleDialog( + title: new Text('Login with long-lived token'), + children: [ + Form( + key: _tokenLoginFormKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.all(20), + child: TextFormField( + onSaved: (newValue) { + final storage = new FlutterSecureStorage(); + storage.write(key: "hacl_llt", value: newValue).then((_) { + Navigator.of(context).pop(); + eventBus.fire(SettingsChangedEvent(true)); + }); + }, + decoration: InputDecoration( + hintText: 'Please enter long-lived token', + contentPadding: EdgeInsets.all(0), + hintStyle: Theme.of(context).textTheme.subhead.copyWith( + color: Theme.of(context).textTheme.overline.color + ) + ), + validator: (value) { + if (value.isEmpty) { + return 'Long-lived token can\'t be emty'; + } + return null; + }, + ) + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: Text('Login', style: Theme.of(context).textTheme.button.copyWith(fontSize: 20)), + color: Theme.of(context).primaryColor, + onPressed: () { + if (_tokenLoginFormKey.currentState.validate()) { + _tokenLoginFormKey.currentState.save(); + } + }, + ), + Container(width: 10), + RaisedButton( + child: Text('Cancel', style: Theme.of(context).textTheme.button.copyWith(fontSize: 20)), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + ) + ], + ), + ) + ], + ); + }, + ); + } + void _notifyServiceCalled(String domain, String service, entityId) { _bottomInfoBarController.showInfoBottomBar( message: "Calling $domain.$service", @@ -577,9 +662,15 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker mainAxisAlignment: MainAxisAlignment.center, children: [ FlatButton( - child: Text("Login with Home Assistant", style: Theme.of(context).textTheme.button), - color: Colors.blue, + child: Text("Login", style: Theme.of(context).textTheme.button), + color: Theme.of(context).primaryColor, onPressed: () => _fullLoad(), + ), + Container(height: 20,), + FlatButton( + child: Text("Login with long-lived token", style: Theme.of(context).textTheme.button), + color: Theme.of(context).primaryColor, + onPressed: () => eventBus.fire(ShowTokenLoginPopupEvent(goBackFirst: false)) ) ] ) @@ -697,6 +788,7 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker //final flutterWebviewPlugin = new FlutterWebviewPlugin(); //flutterWebviewPlugin.dispose(); _viewsTabController?.dispose(); + _showTokenLoginPopupSubscription?.cancel(); _stateSubscription?.cancel(); _lovelaceSubscription?.cancel(); _settingsSubscription?.cancel(); diff --git a/lib/pages/quick_start.page.dart b/lib/pages/quick_start.page.dart new file mode 100644 index 0000000..5697ee1 --- /dev/null +++ b/lib/pages/quick_start.page.dart @@ -0,0 +1,53 @@ +part of '../main.dart'; + +class QuickStartPage extends StatefulWidget { + QuickStartPage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _QuickStartPageState createState() => new _QuickStartPageState(); +} + +class _QuickStartPageState extends State { + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: new AppBar( + leading: IconButton( + icon: Icon(Icons.close), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text('Quick start'), + actions: [ + IconButton( + icon: Icon(Icons.help), + onPressed: () { + Launcher.launchURLInCustomTab( + context: context, + url: 'https://ha-client.app/help' + ); + }, + ) + ], + ), + body: ConnectionSettingsPage( + quickStart: true, + ) + ); + + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/lib/pages/settings/app_settings.page.dart b/lib/pages/settings/app_settings.page.dart index 39b5d2c..5503d06 100644 --- a/lib/pages/settings/app_settings.page.dart +++ b/lib/pages/settings/app_settings.page.dart @@ -35,12 +35,15 @@ class _AppSettingsPageState extends State { } Widget _buildMenu(BuildContext context) { + List items = [ + _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings), + _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel), + ]; + if (ConnectionManager().webhookId != null) { + items.insert(1, _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings)); + } return ListView( - children: [ - _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings), - _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings), - _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel), - ], + children: items, ); } diff --git a/lib/pages/settings/connection_settings.part.dart b/lib/pages/settings/connection_settings.part.dart index ed19fb7..028d636 100644 --- a/lib/pages/settings/connection_settings.part.dart +++ b/lib/pages/settings/connection_settings.part.dart @@ -1,187 +1,194 @@ part of '../../main.dart'; class ConnectionSettingsPage extends StatefulWidget { - ConnectionSettingsPage({Key key, this.title}) : super(key: key); + ConnectionSettingsPage({Key key, this.title, this.quickStart: false}) : super(key: key); final String title; + final bool quickStart; @override _ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState(); } class _ConnectionSettingsPageState extends State { - String _hassioDomain = ""; - String _newHassioDomain = ""; - String _hassioPort = ""; - String _newHassioPort = ""; - String _socketProtocol = "wss"; - String _newSocketProtocol = "wss"; - String _longLivedToken = ""; - String _newLongLivedToken = ""; + String _homeAssistantUrl = ''; + String _deviceName; + bool _loaded = false; + bool _includeDeviceName = false; - String oauthUrl; - bool useOAuth = false; + final _formKey = GlobalKey(); @override void initState() { super.initState(); - _loadSettings(); - + if (!widget.quickStart) { + _loadSettings(); + } else { + _deviceName = MobileAppIntegrationManager.getDefaultDeviceName(); + _loaded = true; + } } _loadSettings() async { + Logger.d('Loading settings...'); + _includeDeviceName = widget.quickStart || ConnectionManager().webhookId == null; SharedPreferences prefs = await SharedPreferences.getInstance(); - final storage = new FlutterSecureStorage(); - - try { - useOAuth = prefs.getBool("oauth-used") ?? true; - } catch (e) { - useOAuth = true; - } - - if (!useOAuth) { - try { - _longLivedToken = _newLongLivedToken = - await storage.read(key: "hacl_llt"); - } catch (e) { - _longLivedToken = _newLongLivedToken = ""; - await storage.delete(key: "hacl_llt"); - } - } - + String domain = prefs.getString('hassio-domain')?? ''; + String port = prefs.getString('hassio-port') ?? ''; + String urlProtocol = prefs.getString('hassio-res-protocol') ?? 'https'; + _homeAssistantUrl = '$urlProtocol://$domain:$port'; + _deviceName = prefs.getString('app-integration-device-name') ?? MobileAppIntegrationManager.getDefaultDeviceName(); setState(() { - _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; - _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; - _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; + _loaded = true; }); } - bool _checkConfigChanged() { - return ( - (_newHassioPort != _hassioPort) || - (_newHassioDomain != _hassioDomain) || - (_newSocketProtocol != _socketProtocol) || - (_newLongLivedToken != _longLivedToken)); - - } - _saveSettings() async { - _newHassioDomain = _newHassioDomain.trim(); - if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) { - _newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws"; - _newHassioDomain = _newHassioDomain.split("//")[1]; + _homeAssistantUrl = _homeAssistantUrl.trim(); + String socketProtocol; + String domain; + String port; + if (_homeAssistantUrl.startsWith("http") && _homeAssistantUrl.indexOf("//") > 0) { + _homeAssistantUrl.startsWith("https") ? socketProtocol = "wss" : socketProtocol = "ws"; + domain = _homeAssistantUrl.split("//")[1]; + } else { + domain = _homeAssistantUrl; } - _newHassioDomain = _newHassioDomain.split("/")[0]; - if (_newHassioDomain.contains(":")) { - List domainAndPort = _newHassioDomain.split(":"); - _newHassioDomain = domainAndPort[0]; - _newHassioPort = domainAndPort[1]; + domain = domain.split("/")[0]; + if (domain.contains(":")) { + List domainAndPort = domain.split(":"); + domain = domainAndPort[0]; + port = domainAndPort[1]; } SharedPreferences prefs = await SharedPreferences.getInstance(); - final storage = new FlutterSecureStorage(); - if (_newLongLivedToken.isNotEmpty) { - _newLongLivedToken = _newLongLivedToken.trim(); - prefs.setBool("oauth-used", false); - await storage.write(key: "hacl_llt", value: _newLongLivedToken); - } else if (!useOAuth) { - await storage.delete(key: "hacl_llt"); - } - prefs.setString("hassio-domain", _newHassioDomain); - if (_newHassioPort == null || _newHassioPort.isEmpty) { - _newHassioPort = _newSocketProtocol == "wss" ? "443" : "80"; + await prefs.setString("hassio-domain", domain); + if (port == null || port.isEmpty) { + port = socketProtocol == "wss" ? "443" : "80"; } else { - _newHassioPort = _newHassioPort.trim(); + port = port.trim(); + } + await prefs.setString("hassio-port", port); + await prefs.setString("hassio-protocol", socketProtocol); + await prefs.setString("hassio-res-protocol", socketProtocol == "wss" ? "https" : "http"); + if (_includeDeviceName) { + await prefs.setString('app-integration-device-name', _deviceName); } - prefs.setString("hassio-port", _newHassioPort); - prefs.setString("hassio-protocol", _newSocketProtocol); - prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); } @override Widget build(BuildContext context) { - return ListView( - scrollDirection: Axis.vertical, - padding: const EdgeInsets.all(20.0), - children: [ + if (!_loaded) { + return PageLoadingIndicator(); + } + List formChildren = [ + Text( + "Home Assistant url:", + style: Theme.of(context).textTheme.headline, + ), + TextFormField( + initialValue: _homeAssistantUrl, + decoration: InputDecoration( + hintText: "Please enter url", + contentPadding: EdgeInsets.all(0), + hintStyle: Theme.of(context).textTheme.subhead.copyWith( + color: Theme.of(context).textTheme.overline.color + ) + ), + onSaved: (newValue) { + _homeAssistantUrl = newValue; + }, + validator: (value) { + if (value.isEmpty) { + return 'Url is required'; + } + return null; + }, + ), + Container( + height: 10, + ), + Text( + "For example:", + style: Theme.of(context).textTheme.body1, + ), + Text( + "192.186.2.14:8123", + style: Theme.of(context).textTheme.subhead, + ), + Text( + "http://myhome.duckdns.org:8123", + style: Theme.of(context).textTheme.subhead, + ), + Text( + "https://efkmfrwk3r4fsfwrfrg5.ui.nabu.casa/", + style: Theme.of(context).textTheme.subhead, + ), + ]; + + if (_includeDeviceName) { + formChildren.addAll([ + Container( + height: 30, + ), Text( - "Connection settings", + "Device name:", style: Theme.of(context).textTheme.headline, ), - new Row( - children: [ - Text("Use ssl (HTTPS)"), - Switch( - value: (_newSocketProtocol == "wss"), - onChanged: (value) { - setState(() { - _newSocketProtocol = value ? "wss" : "ws"; - }); - }, + TextFormField( + initialValue: _deviceName, + onSaved: (newValue) { + _deviceName = newValue; + }, + decoration: InputDecoration( + hintText: 'Please enter device name', + contentPadding: EdgeInsets.all(0), + hintStyle: Theme.of(context).textTheme.subhead.copyWith( + color: Theme.of(context).textTheme.overline.color ) - ], - ), - new TextField( - decoration: InputDecoration( - labelText: "Home Assistant domain or ip address" ), - controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)), - onChanged: (value) { - _newHassioDomain = value; - } - ), - new TextField( - decoration: InputDecoration( - labelText: "Home Assistant port (default is 8123)" - ), - controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)), - onChanged: (value) { - _newHassioPort = value; - } - ), - new Text( - "Try ports 80 and 443 if default is not working and you don't know why.", - style: Theme.of(context).textTheme.caption, - ), - Text( - "Authentication settings", - style: Theme.of(context).textTheme.headline, - ), - Container(height: 10.0,), - Text( - "You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.", - style: Theme.of(context).textTheme.body1.copyWith( - color: Colors.redAccent - ), - ), - new TextField( - decoration: InputDecoration( - labelText: "Long-lived token" - ), - controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)), - onChanged: (value) { - _newLongLivedToken = value; + validator: (value) { + if (value.isEmpty) { + return 'Device name is required'; } + return null; + }, ), - Container( - height: Sizes.rowPadding, - ), - RaisedButton( - child: Text('Apply', style: Theme.of(context).textTheme.button), - color: Theme.of(context).primaryColorDark, + ]); + } + + formChildren.addAll([ + Container( + height: 30, + ), + ButtonTheme( + height: 60, + child: RaisedButton( + child: Text(widget.quickStart ? 'Engage' : 'Apply', style: Theme.of(context).textTheme.button.copyWith(fontSize: 20)), + color: Theme.of(context).primaryColor, onPressed: () { - if (_checkConfigChanged()) { - Logger.d("Settings changed. Saving..."); + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); _saveSettings().then((r) { - Navigator.pop(context); + if (widget.quickStart) { + Navigator.pushReplacementNamed(context, '/'); + } else { + Navigator.pop(context); + } eventBus.fire(SettingsChangedEvent(true)); }); - } else { - Logger.d("Settings was not changed"); - Navigator.pop(context); } }, ) - ], + ) + ]); + + return Form( + key: _formKey, + child: ListView( + scrollDirection: Axis.vertical, + padding: const EdgeInsets.all(20.0), + children: formChildren, + ), ); } diff --git a/lib/types/event_bus_events.dart b/lib/types/event_bus_events.dart index f22e369..24f880d 100644 --- a/lib/types/event_bus_events.dart +++ b/lib/types/event_bus_events.dart @@ -76,6 +76,12 @@ class ShowPopupMessageEvent { ShowPopupMessageEvent({this.title, this.body, this.buttonText: "Ok", this.onButtonClick}); } +class ShowTokenLoginPopupEvent { + final bool goBackFirst; + + ShowTokenLoginPopupEvent({this.goBackFirst: false}); +} + class ShowEntityPageEvent { final String entityId; diff --git a/lib/types/ha_error.dart b/lib/types/ha_error.dart index 4d01afb..9060aa9 100644 --- a/lib/types/ha_error.dart +++ b/lib/types/ha_error.dart @@ -24,6 +24,13 @@ class HACException implements Exception { } } +class HACNotSetUpException implements Exception { + @override + String toString() { + return 'HA Client is not set up'; + } +} + class HAErrorAction { final String title; final int type;