WIP #523 and connection settings refactoring

This commit is contained in:
Yegor Vialov 2020-05-02 23:02:18 +00:00
parent 96c8338890
commit 725ec9291d
11 changed files with 329 additions and 153 deletions

View File

@ -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;

View File

@ -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<HAClientApp> {
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<HAClientApp> {
),
),
"/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: <Widget>[
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));
},
)
],

View File

@ -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;

View File

@ -17,6 +17,9 @@ class MobileAppIntegrationManager {
};
static String getDefaultDeviceName() {
if (HomeAssistant().userName.isEmpty) {
return '${DeviceInfoManager().model}';
}
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
}

View File

@ -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 = [

View File

@ -20,6 +20,7 @@ class _MainPageState extends State<MainPage> 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<MainPage> 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<MainPage> with WidgetsBindingObserver, Ticker
);
});
}
if (_showTokenLoginPopupSubscription == null) {
_showTokenLoginPopupSubscription = eventBus.on<ShowTokenLoginPopupEvent>().listen((event){
if (event.goBackFirst) {
Navigator.of(context).pop();
}
_showTokenLoginDialog();
});
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<NotifyServiceCallEvent>().listen((event) {
@ -302,6 +315,78 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
);
}
final _tokenLoginFormKey = GlobalKey<FormState>();
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: <Widget>[
Form(
key: _tokenLoginFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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: <Widget>[
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<MainPage> with WidgetsBindingObserver, Ticker
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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<MainPage> with WidgetsBindingObserver, Ticker
//final flutterWebviewPlugin = new FlutterWebviewPlugin();
//flutterWebviewPlugin.dispose();
_viewsTabController?.dispose();
_showTokenLoginPopupSubscription?.cancel();
_stateSubscription?.cancel();
_lovelaceSubscription?.cancel();
_settingsSubscription?.cancel();

View File

@ -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<QuickStartPage> {
@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: <Widget>[
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();
}
}

View File

@ -35,12 +35,15 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
}
Widget _buildMenu(BuildContext context) {
List<Widget> 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: <Widget>[
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings),
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
],
children: items,
);
}

View File

@ -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<ConnectionSettingsPage> {
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<FormState>();
@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<String> domainAndPort = _newHassioDomain.split(":");
_newHassioDomain = domainAndPort[0];
_newHassioPort = domainAndPort[1];
domain = domain.split("/")[0];
if (domain.contains(":")) {
List<String> 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: <Widget>[
if (!_loaded) {
return PageLoadingIndicator();
}
List<Widget> formChildren = <Widget>[
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(<Widget>[
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(<Widget>[
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,
),
);
}

View File

@ -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;

View File

@ -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;