Revert all rash decisions

This commit is contained in:
estevez-dev 2019-09-04 22:46:14 +03:00
parent 8d1b159f56
commit 02717332f7
15 changed files with 1000 additions and 1210 deletions

View File

@ -4,11 +4,13 @@ import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.androidalarmmanager.AlarmService;
public class Application extends FlutterApplication implements PluginRegistrantCallback {
@Override
public void onCreate() {
super.onCreate();
AlarmService.setPluginRegistrant(this);
}
@Override

View File

@ -96,29 +96,4 @@ class CardType {
static const conditional = "conditional";
static const alarmPanel = "alarm-panel";
static const markdown = "markdown";
}
class UserError {
final int code;
final String message;
UserError({@required this.code, this.message: ""});
}
class ErrorCode {
static const UNKNOWN = 0;
static const NOT_CONFIGURED = 1;
static const AUTH_INVALID = 2;
static const NO_MOBILE_APP_COMPONENT = 3;
static const ERROR_GETTING_CONFIG = 4;
static const ERROR_GETTING_STATES = 5;
static const ERROR_GETTING_LOVELACE_CONFIG = 6;
static const ERROR_GETTING_PANELS = 7;
static const CONNECTION_TIMEOUT = 8;
static const DISCONNECTED = 9;
static const UNABLE_TO_CONNECT = 10;
static const GENERAL_AUTH_ERROR = 11;
static const AUTH_ERROR = 12;
static const NOT_LOGGED_IN = 13;
static const SECURE_STORAGE_READ_ERROR = 14;
}

View File

@ -68,7 +68,7 @@ class HomeAssistant {
_fetchCompleter.complete();
MobileAppIntegrationManager.checkAppRegistration();
} else {
_fetchCompleter.completeError(UserError(code: ErrorCode.NO_MOBILE_APP_COMPONENT));
_fetchCompleter.completeError(HAError("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.homemade.systems/docs#mobile-app")]));
}
}).catchError((e) {
_fetchCompleter.completeError(e);
@ -89,7 +89,7 @@ class HomeAssistant {
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
_instanceConfig = Map.from(data);
}).catchError((e) {
throw UserError(code: ErrorCode.ERROR_GETTING_CONFIG, message: "$e");
throw HAError("Error getting config: ${e}");
});
}
@ -97,26 +97,26 @@ class HomeAssistant {
await ConnectionManager().sendSocketMessage(type: "get_states").then(
(data) => entities.parse(data)
).catchError((e) {
throw UserError(code: ErrorCode.ERROR_GETTING_STATES, message: "$e");
throw HAError("Error getting states: $e");
});
}
Future _getLovelace() async {
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
throw UserError(code: ErrorCode.ERROR_GETTING_LOVELACE_CONFIG, message: "$e");
throw HAError("Error getting lovelace config: $e");
});
}
Future _getUserInfo() async {
_userName = null;
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
Logger.w("Can't get user info: $e");
Logger.w("Can't get user info: ${e}");
});
}
Future _getServices() async {
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
Logger.w("Can't get services: $e");
Logger.w("Can't get services: ${e}");
});
}
@ -136,7 +136,7 @@ class HomeAssistant {
);
});
}).catchError((e) {
throw UserError(code: ErrorCode.ERROR_GETTING_PANELS, message: "$e");
throw HAError("Error getting panels list: $e");
});
}

View File

@ -93,13 +93,9 @@ part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/page_loading_error.dart';
part 'pages/panel.page.dart';
part 'home_assistant.class.dart';
part 'pages/main.page.dart';
part 'pages/log.page.dart';
part 'pages/entity.page.dart';
part 'pages/widgets/app_drawer.dart';
part 'pages/widgets/main_page_body.dart';
part 'utils/logger.dart';
part 'utils/event_bus_events.dart';
part 'utils.class.dart';
part 'mdi.class.dart';
part 'entity_collection.class.dart';
part 'managers/auth_manager.class.dart';
@ -118,7 +114,6 @@ part 'ui_widgets/card_widget.dart';
part 'ui_widgets/card_header_widget.dart';
part 'panels/config_panel_widget.dart';
part 'panels/widgets/link_to_web_config.dart';
part 'pages/widgets/user_error_panel.widget.dart';
EventBus eventBus = new EventBus();
@ -197,4 +192,795 @@ 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<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription<List<PurchaseDetails>> _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<SettingsChangedEvent>().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<PurchaseDetails>) {
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<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI");
_quickLoad();
} else {
setState(() {});
}
});
}
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
_quickLoad();
});
}
if (_showPopupDialogSubscription == null) {
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().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<ShowPopupMessageEvent>().listen((event){
_showPopupDialog(
title: event.title,
body: event.body,
onPositive: event.onButtonClick,
positiveText: event.buttonText,
negativeText: null
);
});
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId,
event.additionalParams);
});
}
if (_showEntityPageSubscription == null) {
_showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity.entityId);
});
}
if (_showPageSubscription == null) {
_showPageSubscription =
eventBus.on<ShowPageEvent>().listen((event) {
_showPage(event.path, event.goBackFirst);
});
}
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(event.error);
});
}
if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().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<Widget> 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<Tab> buildUIViewTabs() {
List<Tab> result = [];
if (HomeAssistant().ui.views.isNotEmpty) {
HomeAssistant().ui.views.forEach((HAView view) {
result.add(view.buildTab());
});
}
return result;
}
Drawer _buildAppDrawer() {
List<Widget> 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: <Widget>[
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: <Widget>[
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<Widget> 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.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<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
List<Widget> emptyBody = [
Text("."),
];
if (ConnectionManager().isAuthenticated) {
_showLoginButton = false;
popupMenuItems.add(
PopupMenuItem<String>(
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 <Widget>[
SliverAppBar(
floating: true,
pinned: true,
primary: true,
title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[
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<Widget> 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: <Widget>[
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: <Widget>[
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();
}
}

View File

@ -33,7 +33,7 @@ class AuthManager {
//flutterWebviewPlugin.close();
Logger.e("Error getting temp token: ${e.toString()}");
eventBus.fire(StartAuthEvent(oauthUrl, false));
completer.completeError(UserError(code: ErrorCode.AUTH_ERROR));
completer.completeError(HAError("Error getting temp token"));
});
}
});

View File

@ -40,7 +40,6 @@ class ConnectionManager {
if (loadSettings) {
Logger.e("Loading settings...");
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.reload();
useLovelace = prefs.getBool('use-lovelace') ?? true;
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
@ -52,27 +51,29 @@ class ConnectionManager {
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(UserError(code: ErrorCode.NOT_CONFIGURED));
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
} else {
//_token = prefs.getString('hassio-token');
final storage = new FlutterSecureStorage();
try {
_token = await storage.read(key: "hacl_llt");
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
.encodeComponent(
'http://ha-client.homemade.systems/service/auth_callback.html')}";
settingsLoaded = true;
Logger.e("Long-lived token read successful");
} catch (e) {
Logger.e("Cannt read secure storage. Need to relogin.");
completer.completeError(UserError(code: ErrorCode.SECURE_STORAGE_READ_ERROR));
stopInit = true;
_token = null;
await storage.delete(key: "hacl_llt");
}
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
.encodeComponent(
'http://ha-client.homemade.systems/service/auth_callback.html')}";
settingsLoaded = true;
}
} else {
if ((_domain == null) || (_port == null) ||
(_domain.isEmpty) || (_port.isEmpty)) {
completer.completeError(UserError(code: ErrorCode.NOT_CONFIGURED));
completer.completeError(HAError.checkConnectionSettings());
stopInit = true;
}
}
@ -100,7 +101,7 @@ class ConnectionManager {
if (forceReconnect || !isConnected) {
_connect().timeout(connectTimeout, onTimeout: () {
_disconnect().then((_) {
completer?.completeError(UserError(code: ErrorCode.CONNECTION_TIMEOUT));
completer?.completeError(HAError("Connection timeout"));
});
}).then((_) {
completer?.complete();
@ -145,9 +146,11 @@ class ConnectionManager {
}
} else if (data["type"] == "auth_invalid") {
Logger.d("[Received] <== ${data.toString()}");
_messageResolver["auth"]?.completeError(UserError(code: ErrorCode.AUTH_INVALID, message: "${data["message"]}"));
_messageResolver["auth"]?.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
_messageResolver.remove("auth");
if (!connecting.isCompleted) connecting.completeError(UserError(code: ErrorCode.AUTH_INVALID, message: "${data["message"]}"));
logout().then((_) {
if (!connecting.isCompleted) connecting.completeError(HAError("${data["message"]}", actions: [HAErrorAction.loginAgain()]));
});
} else {
_handleMessage(data);
}
@ -211,14 +214,14 @@ class ConnectionManager {
Logger.d("Socket disconnected.");
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(UserError(code: ErrorCode.DISCONNECTED));
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(UserError(code: ErrorCode.UNABLE_TO_CONNECT));
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
@ -229,14 +232,14 @@ class ConnectionManager {
Logger.e("Socket stream Error: $e");
if (!connectionCompleter.isCompleted) {
isConnected = false;
connectionCompleter.completeError(UserError(code: ErrorCode.UNABLE_TO_CONNECT));
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
} else {
_disconnect().then((_) {
Timer(Duration(seconds: 5), () {
Logger.d("Trying to reconnect...");
_connect().catchError((e) {
isConnected = false;
eventBus.fire(ShowErrorEvent(UserError(code: ErrorCode.UNABLE_TO_CONNECT)));
eventBus.fire(ShowErrorEvent(HAError("Unable to connect to Home Assistant")));
});
});
});
@ -272,7 +275,7 @@ class ConnectionManager {
});
}).catchError((e) => completer.completeError(e));
} else {
completer.completeError(UserError(code: ErrorCode.GENERAL_AUTH_ERROR));
completer.completeError(HAError("General login error"));
}
return completer.future;
}
@ -306,7 +309,8 @@ class ConnectionManager {
throw e;
});
}).catchError((e) {
completer.completeError(UserError(code: ErrorCode.AUTH_ERROR, message: "$e"));
logout();
completer.completeError(HAError("Authentication error: $e", actions: [HAErrorAction.loginAgain()]));
});
return completer.future;
}
@ -329,7 +333,7 @@ class ConnectionManager {
String rawMessage = json.encode(dataObject);
if (!isConnected) {
_connect().timeout(connectTimeout, onTimeout: (){
_completer.completeError(UserError(code: ErrorCode.UNABLE_TO_CONNECT));
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
}).then((_) {
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
_socket.sink.add(rawMessage);

View File

@ -26,7 +26,7 @@ class StartupUserMessagesManager {
void _showSupportAppDevelopmentMessage() {
eventBus.fire(ShowPopupDialogEvent(
title: "Hi!",
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. Following the principles of free and open source softwere this will not be changed in nearest future. But still you can support this application development materially. There is one-time payment available as well as some subscription options, please check them in main menu -> Support app development. Thanks.",
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. Following the principles of free and open source softwere this will not be changed in nearest future. But still you can support this application development materially. There is several options available, please check them in main menu -> Support app development. Thanks.",
positiveText: "Take me there",
negativeText: "Nope",
onPositive: () {

View File

@ -1,486 +0,0 @@
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<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
StreamSubscription<List<PurchaseDetails>> _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;
UserError _userError;
@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<SettingsChangedEvent>().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 {
//TODO show loading somewhere somehow
//_showInfoBottomBar(progress: true,);
_subscribe().then((_) {
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
_fetchData();
}, onError: (error) {
_setErrorState(error);
});
});
}
void _quickLoad() {
//_hideBottomBar();
//TODO show loading somewhere somehow
//_showInfoBottomBar(progress: true,);
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData();
}, onError: (error) {
_setErrorState(error);
});
}
_fetchData() async {
setState(() {
_userError = null;
});
await HomeAssistant().fetchData().then((_) {
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((code) {
_setErrorState(code);
});
eventBus.fire(RefreshDataFinishedEvent());
StartupUserMessagesManager().checkMessagesToShow();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
Logger.d("$state");
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
_quickLoad();
}
}
void _handlePurchaseUpdates(purchase) {
if (purchase is List<PurchaseDetails>) {
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<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
Logger.d("New entity. Need to rebuild UI");
_quickLoad();
} else {
setState(() {});
}
});
}
if (_reloadUISubscription == null) {
_reloadUISubscription = eventBus.on<ReloadUIEvent>().listen((event){
if (event.full)
_fullLoad();
else
_quickLoad();
});
}
if (_showPopupDialogSubscription == null) {
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().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<ShowPopupMessageEvent>().listen((event){
_showPopupDialog(
title: event.title,
body: event.body,
onPositive: event.onButtonClick,
positiveText: event.buttonText,
negativeText: null
);
});
}
if (_serviceCallSubscription == null) {
_serviceCallSubscription =
eventBus.on<ServiceCallEvent>().listen((event) {
_callService(event.domain, event.service, event.entityId,
event.additionalParams);
});
}
if (_showEntityPageSubscription == null) {
_showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity.entityId);
});
}
if (_showPageSubscription == null) {
_showPageSubscription =
eventBus.on<ShowPageEvent>().listen((event) {
_showPage(event.path, event.goBackFirst);
});
}
if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_setErrorState(event.error);
});
}
if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().listen((event){
if (event.starting) {
_showOAuth();
} else {
_preventAppRefresh = false;
Navigator.of(context).pop();
setState(() {
_userError = null;
});
}
});
}
_firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token;
completer.complete();
});
return completer.future;
}
void _showOAuth() {
_preventAppRefresh = true;
_setErrorState(UserError(code: ErrorCode.NOT_LOGGED_IN));
Navigator.of(context).pushNamed('/login');
}
_setErrorState(error) {
if (error is UserError) {
setState(() {
//_showBottomBar = false;
_userError = error;
});
} else {
setState(() {
//_showBottomBar = false;
_userError = UserError(code: ErrorCode.UNKNOWN);
});
}
/*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<Widget> 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,
);
},
);
}
void _callService(String domain, String service, String entityId, Map additionalParams) {
//TODO show SnackBar
/*_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
);
}
/*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();
});
}
}*/
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
TabController _viewsTabController;
@override
Widget build(BuildContext context) {
Widget bottomBar;
if (_userError != null) {
bottomBar = UserErrorPanel(error: _userError,);
/*List<Widget> 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: <Widget>[
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: <Widget>[
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: AppDrawer(),
bottomNavigationBar: bottomBar,
body: MainPageBody(
empty: true,
onReload: () => _quickLoad(),
tabController: _viewsTabController,
onMenu: () => _scaffoldKey.currentState.openDrawer(),
)
);
} else {
return Scaffold(
key: _scaffoldKey,
drawer: AppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: MainPageBody(
empty: false,
onReload: () => _quickLoad(),
tabController: _viewsTabController,
onMenu: () => _scaffoldKey.currentState.openDrawer(),
),
);
}
}
@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();
}
}

View File

@ -1,175 +0,0 @@
part of '../../main.dart';
class AppDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<Widget> 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: <Widget>[
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: <Widget>[
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,
),
);
}
}

View File

@ -1,111 +0,0 @@
part of '../../main.dart';
class MainPageBody extends StatelessWidget {
final bool empty;
final onReload;
final onMenu;
final TabController tabController;
const MainPageBody({Key key, this.empty, this.onReload, this.tabController, this.onMenu}) : super(key: key);
List<Tab> buildUIViewTabs() {
List<Tab> result = [];
if (HomeAssistant().ui.views.isNotEmpty) {
HomeAssistant().ui.views.forEach((HAView view) {
//TODO Create a widget for that and pass view to it. An opposit way as it is implemented now
result.add(view.buildTab());
});
}
return result;
}
@override
Widget build(BuildContext context) {
List<PopupMenuItem<String>> popupMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"),
value: "reload",
));
/*List<Widget> emptyBody = [
Text("."),
];*/
if (ConnectionManager().isAuthenticated) {
//_showLoginButton = false;
popupMenuItems.add(
PopupMenuItem<String>(
child: new Text("Logout"),
value: "logout",
));
}
Widget bodyWidget;
if (empty) {
bodyWidget = Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator()
],
);
} else {
bodyWidget = HomeAssistant().buildViews(context, tabController);
}
/*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 <Widget>[
SliverAppBar(
floating: true,
pinned: true,
primary: true,
title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[
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") {
onReload();
} else if (val == "logout") {
HomeAssistant().logout().then((_) {
onReload();
});
}
});
}
)
],
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () => onMenu(),
),
bottom: empty ? null : TabBar(
controller: tabController,
tabs: buildUIViewTabs(),
isScrollable: true,
),
),
];
},
body: bodyWidget,
);
}
}

View File

@ -1,242 +0,0 @@
part of '../../main.dart';
class UserErrorActionButton extends StatelessWidget {
final onPressed;
final String text;
const UserErrorActionButton({Key key, this.onPressed, this.text}) : super(key: key);
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () => this.onPressed(),
color: Colors.blue,
child: Text(
"${this.text}",
style: TextStyle(color: Colors.white),
),
);
}
}
class UserErrorPanel extends StatelessWidget {
final UserError error;
const UserErrorPanel({Key key, this.error}) : super(key: key);
void _goToAppSettings(BuildContext context) {
Navigator.pushNamed(context, '/connection-settings');
}
void _reload() {
eventBus.fire(ReloadUIEvent(true));
}
void _disableLovelace() {
SharedPreferences.getInstance().then((prefs){
prefs.setBool("use-lovelace", false);
eventBus.fire(ReloadUIEvent(true));
});
}
void _reLogin() {
ConnectionManager().logout().then((_) => eventBus.fire(ReloadUIEvent(true)));
}
@override
Widget build(BuildContext context) {
String errorText;
List<Widget> buttons = [];
switch (this.error.code) {
case ErrorCode.AUTH_ERROR: {
errorText = "There was an error logging in to Home Assistant";
buttons.add(UserErrorActionButton(
onPressed: () => _reload(),
text: "Retry",
));
buttons.add(UserErrorActionButton(
onPressed: () => _reLogin(),
text: "Login again",
));
break;
}
case ErrorCode.UNABLE_TO_CONNECT: {
errorText = "Unable to connect to Home Assistant";
buttons.addAll(<Widget>[
UserErrorActionButton(
onPressed: () => _reload(),
text: "Retry"
),
UserErrorActionButton(
onPressed: () => _goToAppSettings(context),
text: "Check application settings",
)
]
);
break;
}
case ErrorCode.AUTH_INVALID: {
errorText = "${error.message ?? "Can't login to Home Assistant"}";
buttons.addAll(<Widget>[
UserErrorActionButton(
onPressed: () => _reload(),
text: "Retry"
),
UserErrorActionButton(
onPressed: () => _reLogin(),
text: "Login again",
)
]
);
break;
}
case ErrorCode.GENERAL_AUTH_ERROR: {
errorText = "There was some error logging in. ${this.error.message ?? ""}";
buttons.addAll(<Widget>[
UserErrorActionButton(
onPressed: () => _reload(),
text: "Retry"
),
UserErrorActionButton(
onPressed: () => _reLogin(),
text: "Login again",
)
]
);
break;
}
case ErrorCode.SECURE_STORAGE_READ_ERROR: {
errorText = "There was an error reading secure storage. You can try again or clear saved auth data and login again.";
buttons.addAll(<Widget>[
UserErrorActionButton(
onPressed: () => _reload(),
text: "Retry"
),
UserErrorActionButton(
onPressed: () => _reLogin(),
text: "Clear and login again",
)
]
);
break;
}
case ErrorCode.DISCONNECTED: {
errorText = "Disconnected";
buttons.addAll(<Widget>[
UserErrorActionButton(
onPressed: () => _reload(),
text: "Reconnect"
),
UserErrorActionButton(
onPressed: () => _goToAppSettings(context),
text: "Check application settings",
)
]
);
break;
}
case ErrorCode.CONNECTION_TIMEOUT: {
errorText = "Connection timeout";
buttons.addAll(<Widget>[
UserErrorActionButton(
onPressed: () => _reload(),
text: "Reconnect"
),
UserErrorActionButton(
onPressed: () => _goToAppSettings(context),
text: "Check application settings",
)
]
);
break;
}
case ErrorCode.NOT_CONFIGURED: {
errorText = "Looks like HA Client is not configured yet.";
buttons.add(UserErrorActionButton(
onPressed: () => _goToAppSettings(context),
text: "Open application settings",
));
break;
}
case ErrorCode.ERROR_GETTING_PANELS:
case ErrorCode.ERROR_GETTING_CONFIG:
case ErrorCode.ERROR_GETTING_STATES: {
errorText = "Couldn't get data from Home Assistant. ${error.message ?? ""}";
buttons.add(UserErrorActionButton(
onPressed: () => _reload(),
text: "Try again",
));
break;
}
case ErrorCode.ERROR_GETTING_LOVELACE_CONFIG: {
errorText = "Couldn't get Lovelace UI config. You can try to disable it and use group-based UI istead.";
buttons.addAll(<Widget>[
UserErrorActionButton(
onPressed: () => _reload(),
text: "Retry",
),
UserErrorActionButton(
onPressed: () => _disableLovelace(),
text: "Disable Lovelace UI",
)
]);
break;
}
case ErrorCode.NOT_LOGGED_IN: {
errorText = "You are not logged in yet. Please login.";
buttons.add(UserErrorActionButton(
onPressed: () => _reload(),
text: "Login",
));
break;
}
case ErrorCode.NO_MOBILE_APP_COMPONENT: {
errorText = "Looks like mobile_app component is not enabled on your Home Assistant instance. Please add it to your configuration.yaml";
buttons.add(UserErrorActionButton(
onPressed: () => Launcher.launchURLInCustomTab(context: context, url: "https://www.home-assistant.io/components/mobile_app/"),
text: "Help",
));
break;
}
default: {
errorText = "There was an error. Code ${this.error.code}";
buttons.add(UserErrorActionButton(
onPressed: () => _reload(),
text: "Reload",
));
}
}
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Divider(
color: Colors.deepOrange,
height: 1.0,
indent: 8.0,
endIndent: 8.0,
),
Padding(
padding: EdgeInsets.fromLTRB(8.0, 14.0, 8.0, 0.0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text(
errorText,
textAlign: TextAlign.start,
style: TextStyle(color: Colors.black87, fontSize: 18.0),
softWrap: true,
maxLines: 3,
)
],
),
),
ButtonBar(
children: buttons,
)
],
);
}
}

View File

@ -29,11 +29,7 @@ class HACard {
this.states,
this.conditions: const [],
@required this.type
}) {
if (this.columnsCount <= 0) {
this.columnsCount = 4;
}
}
});
List<EntityWrapper> getEntitiesToShow() {
return entities.where((entityWrapper) {

171
lib/utils.class.dart Normal file
View File

@ -0,0 +1,171 @@
part of 'main.dart';
class Logger {
static List<String> _log = [];
static String getLog() {
String res = '';
_log.forEach((line) {
res += "$line\n";
});
return res;
}
static bool get isInDebugMode {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
static void e(String message) {
_writeToLog("Error", message);
}
static void w(String message) {
_writeToLog("Warning", message);
}
static void d(String message) {
_writeToLog("Debug", message);
}
static void _writeToLog(String level, String message) {
if (isInDebugMode) {
debugPrint('$message');
}
DateTime t = DateTime.now();
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
if (_log.length > 100) {
_log.removeAt(0);
}
}
}
class HAError {
String message;
final List<HAErrorAction> actions;
HAError(this.message, {this.actions: const [HAErrorAction.tryAgain()]});
HAError.unableToConnect({this.actions = const [HAErrorAction.tryAgain()]}) {
this.message = "Unable to connect to Home Assistant";
}
HAError.disconnected({this.actions = const [HAErrorAction.reconnect()]}) {
this.message = "Disconnected";
}
HAError.checkConnectionSettings({this.actions = const [HAErrorAction.reload(), HAErrorAction(title: "Settings", type: HAErrorActionType.OPEN_CONNECTION_SETTINGS)]}) {
this.message = "Check connection settings";
}
}
class HAErrorAction {
final String title;
final int type;
final String url;
const HAErrorAction({@required this.title, this.type: HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.tryAgain({this.title = "Try again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.reconnect({this.title = "Reconnect", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.reload({this.title = "Reload", this.type = HAErrorActionType.FULL_RELOAD, this.url});
const HAErrorAction.loginAgain({this.title = "Login again", this.type = HAErrorActionType.FULL_RELOAD, this.url});
}
class HAErrorActionType {
static const FULL_RELOAD = 0;
static const QUICK_RELOAD = 1;
static const LOGOUT = 2;
static const URL = 3;
static const OPEN_CONNECTION_SETTINGS = 4;
}
class StateChangedEvent {
String entityId;
String newState;
bool needToRebuildUI;
StateChangedEvent({
this.entityId,
this.newState,
this.needToRebuildUI: false
});
}
class SettingsChangedEvent {
bool reconnect;
SettingsChangedEvent(this.reconnect);
}
class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}
class ReloadUIEvent {
ReloadUIEvent();
}
class StartAuthEvent {
String oauthUrl;
bool showButton;
StartAuthEvent(this.oauthUrl, this.showButton);
}
class ServiceCallEvent {
String domain;
String service;
String entityId;
Map<String, dynamic> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
}
class ShowPopupDialogEvent {
final String title;
final String body;
final String positiveText;
final String negativeText;
final onPositive;
final onNegative;
ShowPopupDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
}
class ShowPopupMessageEvent {
final String title;
final String body;
final String buttonText;
final onButtonClick;
ShowPopupMessageEvent({this.title, this.body, this.buttonText: "Ok", this.onButtonClick});
}
class ShowEntityPageEvent {
Entity entity;
ShowEntityPageEvent(this.entity);
}
class ShowPageEvent {
final String path;
final bool goBackFirst;
ShowPageEvent({@required this.path, this.goBackFirst: false});
}
class ShowErrorEvent {
final HAError error;
ShowErrorEvent(this.error);
}

View File

@ -1,84 +0,0 @@
part of '../main.dart';
class StateChangedEvent {
String entityId;
String newState;
bool needToRebuildUI;
StateChangedEvent({
this.entityId,
this.newState,
this.needToRebuildUI: false
});
}
class SettingsChangedEvent {
bool reconnect;
SettingsChangedEvent(this.reconnect);
}
class RefreshDataFinishedEvent {
RefreshDataFinishedEvent();
}
class ReloadUIEvent {
final bool full;
ReloadUIEvent(this.full);
}
class StartAuthEvent {
String oauthUrl;
bool starting;
StartAuthEvent(this.oauthUrl, this.starting);
}
class ServiceCallEvent {
String domain;
String service;
String entityId;
Map<String, dynamic> additionalParams;
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
}
class ShowPopupDialogEvent {
final String title;
final String body;
final String positiveText;
final String negativeText;
final onPositive;
final onNegative;
ShowPopupDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
}
class ShowPopupMessageEvent {
final String title;
final String body;
final String buttonText;
final onButtonClick;
ShowPopupMessageEvent({this.title, this.body, this.buttonText: "Ok", this.onButtonClick});
}
class ShowEntityPageEvent {
Entity entity;
ShowEntityPageEvent(this.entity);
}
class ShowPageEvent {
final String path;
final bool goBackFirst;
ShowPageEvent({@required this.path, this.goBackFirst: false});
}
class ShowErrorEvent {
final UserError error;
ShowErrorEvent(this.error);
}

View File

@ -1,46 +0,0 @@
part of '../main.dart';
class Logger {
static List<String> _log = [];
static String getLog() {
String res = '';
_log.forEach((line) {
res += "$line\n";
});
return res;
}
static bool get isInDebugMode {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
static void e(String message) {
_writeToLog("Error", message);
}
static void w(String message) {
_writeToLog("Warning", message);
}
static void d(String message) {
_writeToLog("Debug", message);
}
static void _writeToLog(String level, String message) {
if (isInDebugMode) {
debugPrint('$message');
}
DateTime t = DateTime.now();
_log.add("${formatDate(t, ["mm","dd"," ","HH",":","nn",":","ss"])} [$level] : $message");
if (_log.length > 100) {
_log.removeAt(0);
}
}
}