Revert all rash decisions
This commit is contained in:
parent
8d1b159f56
commit
02717332f7
@ -4,11 +4,13 @@ import io.flutter.app.FlutterApplication;
|
|||||||
import io.flutter.plugin.common.PluginRegistry;
|
import io.flutter.plugin.common.PluginRegistry;
|
||||||
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
|
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
import io.flutter.plugins.androidalarmmanager.AlarmService;
|
||||||
|
|
||||||
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
public class Application extends FlutterApplication implements PluginRegistrantCallback {
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
AlarmService.setPluginRegistrant(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -96,29 +96,4 @@ class CardType {
|
|||||||
static const conditional = "conditional";
|
static const conditional = "conditional";
|
||||||
static const alarmPanel = "alarm-panel";
|
static const alarmPanel = "alarm-panel";
|
||||||
static const markdown = "markdown";
|
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;
|
|
||||||
}
|
}
|
@ -68,7 +68,7 @@ class HomeAssistant {
|
|||||||
_fetchCompleter.complete();
|
_fetchCompleter.complete();
|
||||||
MobileAppIntegrationManager.checkAppRegistration();
|
MobileAppIntegrationManager.checkAppRegistration();
|
||||||
} else {
|
} 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) {
|
}).catchError((e) {
|
||||||
_fetchCompleter.completeError(e);
|
_fetchCompleter.completeError(e);
|
||||||
@ -89,7 +89,7 @@ class HomeAssistant {
|
|||||||
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
await ConnectionManager().sendSocketMessage(type: "get_config").then((data) {
|
||||||
_instanceConfig = Map.from(data);
|
_instanceConfig = Map.from(data);
|
||||||
}).catchError((e) {
|
}).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(
|
await ConnectionManager().sendSocketMessage(type: "get_states").then(
|
||||||
(data) => entities.parse(data)
|
(data) => entities.parse(data)
|
||||||
).catchError((e) {
|
).catchError((e) {
|
||||||
throw UserError(code: ErrorCode.ERROR_GETTING_STATES, message: "$e");
|
throw HAError("Error getting states: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getLovelace() async {
|
Future _getLovelace() async {
|
||||||
await ConnectionManager().sendSocketMessage(type: "lovelace/config").then((data) => _rawLovelaceData = data).catchError((e) {
|
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 {
|
Future _getUserInfo() async {
|
||||||
_userName = null;
|
_userName = null;
|
||||||
await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _userName = data["name"]).catchError((e) {
|
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 {
|
Future _getServices() async {
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
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) {
|
}).catchError((e) {
|
||||||
throw UserError(code: ErrorCode.ERROR_GETTING_PANELS, message: "$e");
|
throw HAError("Error getting panels list: $e");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
800
lib/main.dart
800
lib/main.dart
@ -93,13 +93,9 @@ part 'pages/widgets/page_loading_indicator.dart';
|
|||||||
part 'pages/widgets/page_loading_error.dart';
|
part 'pages/widgets/page_loading_error.dart';
|
||||||
part 'pages/panel.page.dart';
|
part 'pages/panel.page.dart';
|
||||||
part 'home_assistant.class.dart';
|
part 'home_assistant.class.dart';
|
||||||
part 'pages/main.page.dart';
|
|
||||||
part 'pages/log.page.dart';
|
part 'pages/log.page.dart';
|
||||||
part 'pages/entity.page.dart';
|
part 'pages/entity.page.dart';
|
||||||
part 'pages/widgets/app_drawer.dart';
|
part 'utils.class.dart';
|
||||||
part 'pages/widgets/main_page_body.dart';
|
|
||||||
part 'utils/logger.dart';
|
|
||||||
part 'utils/event_bus_events.dart';
|
|
||||||
part 'mdi.class.dart';
|
part 'mdi.class.dart';
|
||||||
part 'entity_collection.class.dart';
|
part 'entity_collection.class.dart';
|
||||||
part 'managers/auth_manager.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 'ui_widgets/card_header_widget.dart';
|
||||||
part 'panels/config_panel_widget.dart';
|
part 'panels/config_panel_widget.dart';
|
||||||
part 'panels/widgets/link_to_web_config.dart';
|
part 'panels/widgets/link_to_web_config.dart';
|
||||||
part 'pages/widgets/user_error_panel.widget.dart';
|
|
||||||
|
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -33,7 +33,7 @@ class AuthManager {
|
|||||||
//flutterWebviewPlugin.close();
|
//flutterWebviewPlugin.close();
|
||||||
Logger.e("Error getting temp token: ${e.toString()}");
|
Logger.e("Error getting temp token: ${e.toString()}");
|
||||||
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
eventBus.fire(StartAuthEvent(oauthUrl, false));
|
||||||
completer.completeError(UserError(code: ErrorCode.AUTH_ERROR));
|
completer.completeError(HAError("Error getting temp token"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -40,7 +40,6 @@ class ConnectionManager {
|
|||||||
if (loadSettings) {
|
if (loadSettings) {
|
||||||
Logger.e("Loading settings...");
|
Logger.e("Loading settings...");
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.reload();
|
|
||||||
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
useLovelace = prefs.getBool('use-lovelace') ?? true;
|
||||||
_domain = prefs.getString('hassio-domain');
|
_domain = prefs.getString('hassio-domain');
|
||||||
_port = prefs.getString('hassio-port');
|
_port = prefs.getString('hassio-port');
|
||||||
@ -52,27 +51,29 @@ class ConnectionManager {
|
|||||||
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||||
if ((_domain == null) || (_port == null) ||
|
if ((_domain == null) || (_port == null) ||
|
||||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||||
completer.completeError(UserError(code: ErrorCode.NOT_CONFIGURED));
|
completer.completeError(HAError.checkConnectionSettings());
|
||||||
stopInit = true;
|
stopInit = true;
|
||||||
} else {
|
} else {
|
||||||
|
//_token = prefs.getString('hassio-token');
|
||||||
final storage = new FlutterSecureStorage();
|
final storage = new FlutterSecureStorage();
|
||||||
try {
|
try {
|
||||||
_token = await storage.read(key: "hacl_llt");
|
_token = await storage.read(key: "hacl_llt");
|
||||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
Logger.e("Long-lived token read successful");
|
||||||
'http://ha-client.homemade.systems/')}&redirect_uri=${Uri
|
|
||||||
.encodeComponent(
|
|
||||||
'http://ha-client.homemade.systems/service/auth_callback.html')}";
|
|
||||||
settingsLoaded = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.e("Cannt read secure storage. Need to relogin.");
|
Logger.e("Cannt read secure storage. Need to relogin.");
|
||||||
completer.completeError(UserError(code: ErrorCode.SECURE_STORAGE_READ_ERROR));
|
_token = null;
|
||||||
stopInit = true;
|
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 {
|
} else {
|
||||||
if ((_domain == null) || (_port == null) ||
|
if ((_domain == null) || (_port == null) ||
|
||||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
(_domain.isEmpty) || (_port.isEmpty)) {
|
||||||
completer.completeError(UserError(code: ErrorCode.NOT_CONFIGURED));
|
completer.completeError(HAError.checkConnectionSettings());
|
||||||
stopInit = true;
|
stopInit = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,7 +101,7 @@ class ConnectionManager {
|
|||||||
if (forceReconnect || !isConnected) {
|
if (forceReconnect || !isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: () {
|
_connect().timeout(connectTimeout, onTimeout: () {
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
completer?.completeError(UserError(code: ErrorCode.CONNECTION_TIMEOUT));
|
completer?.completeError(HAError("Connection timeout"));
|
||||||
});
|
});
|
||||||
}).then((_) {
|
}).then((_) {
|
||||||
completer?.complete();
|
completer?.complete();
|
||||||
@ -145,9 +146,11 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
} else if (data["type"] == "auth_invalid") {
|
} else if (data["type"] == "auth_invalid") {
|
||||||
Logger.d("[Received] <== ${data.toString()}");
|
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");
|
_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 {
|
} else {
|
||||||
_handleMessage(data);
|
_handleMessage(data);
|
||||||
}
|
}
|
||||||
@ -211,14 +214,14 @@ class ConnectionManager {
|
|||||||
Logger.d("Socket disconnected.");
|
Logger.d("Socket disconnected.");
|
||||||
if (!connectionCompleter.isCompleted) {
|
if (!connectionCompleter.isCompleted) {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
connectionCompleter.completeError(UserError(code: ErrorCode.DISCONNECTED));
|
connectionCompleter.completeError(HAError("Disconnected", actions: [HAErrorAction.reconnect()]));
|
||||||
} else {
|
} else {
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
Timer(Duration(seconds: 5), () {
|
Timer(Duration(seconds: 5), () {
|
||||||
Logger.d("Trying to reconnect...");
|
Logger.d("Trying to reconnect...");
|
||||||
_connect().catchError((e) {
|
_connect().catchError((e) {
|
||||||
isConnected = false;
|
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");
|
Logger.e("Socket stream Error: $e");
|
||||||
if (!connectionCompleter.isCompleted) {
|
if (!connectionCompleter.isCompleted) {
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
connectionCompleter.completeError(UserError(code: ErrorCode.UNABLE_TO_CONNECT));
|
connectionCompleter.completeError(HAError("Unable to connect to Home Assistant"));
|
||||||
} else {
|
} else {
|
||||||
_disconnect().then((_) {
|
_disconnect().then((_) {
|
||||||
Timer(Duration(seconds: 5), () {
|
Timer(Duration(seconds: 5), () {
|
||||||
Logger.d("Trying to reconnect...");
|
Logger.d("Trying to reconnect...");
|
||||||
_connect().catchError((e) {
|
_connect().catchError((e) {
|
||||||
isConnected = false;
|
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));
|
}).catchError((e) => completer.completeError(e));
|
||||||
} else {
|
} else {
|
||||||
completer.completeError(UserError(code: ErrorCode.GENERAL_AUTH_ERROR));
|
completer.completeError(HAError("General login error"));
|
||||||
}
|
}
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
@ -306,7 +309,8 @@ class ConnectionManager {
|
|||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
}).catchError((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;
|
return completer.future;
|
||||||
}
|
}
|
||||||
@ -329,7 +333,7 @@ class ConnectionManager {
|
|||||||
String rawMessage = json.encode(dataObject);
|
String rawMessage = json.encode(dataObject);
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
_connect().timeout(connectTimeout, onTimeout: (){
|
_connect().timeout(connectTimeout, onTimeout: (){
|
||||||
_completer.completeError(UserError(code: ErrorCode.UNABLE_TO_CONNECT));
|
_completer.completeError(HAError("No connection to Home Assistant", actions: [HAErrorAction.reconnect()]));
|
||||||
}).then((_) {
|
}).then((_) {
|
||||||
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
Logger.d("[Sending] ==> ${auth ? "type="+dataObject['type'] : rawMessage}");
|
||||||
_socket.sink.add(rawMessage);
|
_socket.sink.add(rawMessage);
|
||||||
|
@ -26,7 +26,7 @@ class StartupUserMessagesManager {
|
|||||||
void _showSupportAppDevelopmentMessage() {
|
void _showSupportAppDevelopmentMessage() {
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
title: "Hi!",
|
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",
|
positiveText: "Take me there",
|
||||||
negativeText: "Nope",
|
negativeText: "Nope",
|
||||||
onPositive: () {
|
onPositive: () {
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,11 +29,7 @@ class HACard {
|
|||||||
this.states,
|
this.states,
|
||||||
this.conditions: const [],
|
this.conditions: const [],
|
||||||
@required this.type
|
@required this.type
|
||||||
}) {
|
});
|
||||||
if (this.columnsCount <= 0) {
|
|
||||||
this.columnsCount = 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<EntityWrapper> getEntitiesToShow() {
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
return entities.where((entityWrapper) {
|
return entities.where((entityWrapper) {
|
||||||
|
171
lib/utils.class.dart
Normal file
171
lib/utils.class.dart
Normal 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);
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Reference in New Issue
Block a user