Compare commits

...

13 Commits

Author SHA1 Message Date
6afbd37d71 892 2020-04-15 15:46:06 +03:00
0e8869878f 0.8.4 2020-04-15 12:06:06 +00:00
7c2cfe3215 MobileApp integration force update 2020-04-15 11:59:40 +00:00
c376c0e952 Minor fix 2020-04-14 21:08:10 +00:00
da5f663396 Gauge card optimizations 2020-04-14 18:39:21 +00:00
0e92418a33 Handle missed entity on Entity page 2020-04-14 18:15:28 +00:00
2eef7cfe5e Improve Horizontal and Vertical stack building 2020-04-13 19:24:37 +00:00
de4e0bfb3a Add new Button card support 2020-04-13 18:17:14 +00:00
8bf2d31e72 Bring back separate entity page 2020-04-13 18:15:14 +00:00
2125c46143 Gauge style improvements 2020-04-13 14:01:17 +00:00
5402eb84df Some default icons update 2020-04-13 13:13:49 +00:00
ad5aa0898f Set header toggle default to false 2020-04-13 13:05:59 +00:00
040d40b614 PayPal donate button 2020-04-11 19:26:25 +00:00
13 changed files with 160 additions and 163 deletions

View File

@ -59,6 +59,10 @@ class CardWidget extends StatelessWidget {
return _buildEntityButtonCard(context);
}
case CardType.BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.GAUGE: {
return _buildGaugeCard(context);
}
@ -78,20 +82,15 @@ class CardWidget extends StatelessWidget {
case CardType.HORIZONTAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
children.add(
Flexible(
children = card.childCards.map((childCard) => Flexible(
fit: FlexFit.tight,
child: card.build(context),
child: childCard.build(context),
)
);
}
});
).toList();
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
}
@ -100,12 +99,7 @@ class CardWidget extends StatelessWidget {
case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
card.childCards.forEach((card) {
children.add(
card.build(context)
);
});
List<Widget> children = card.childCards.map((childCard) => childCard.build(context)).toList();
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,

View File

@ -25,6 +25,7 @@ class EntityButtonCardBody extends StatelessWidget {
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {

View File

@ -1,6 +1,6 @@
part of '../../main.dart';
class GaugeCardBody extends StatefulWidget {
class GaugeCardBody extends StatelessWidget {
final int min;
final int max;
@ -8,31 +8,26 @@ class GaugeCardBody extends StatefulWidget {
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
_GaugeCardBodyState createState() => _GaugeCardBodyState();
}
class _GaugeCardBodyState extends State<GaugeCardBody> {
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
double fixedValue;
double value = entityWrapper.entity.doubleState;
if (value > widget.max) {
fixedValue = widget.max.toDouble();
} else if (value < widget.min) {
fixedValue = widget.min.toDouble();
if (value > max) {
fixedValue = max.toDouble();
} else if (value < min) {
fixedValue = min.toDouble();
} else {
fixedValue = value;
}
List<GaugeRange> ranges;
if (widget.severity != null && widget.severity["green"] is int && widget.severity["red"] is int && widget.severity["yellow"] is int) {
Color currentColor;
if (severity != null && severity["green"] is int && severity["red"] is int && severity["yellow"] is int) {
List<RangeContainer> rangesList = <RangeContainer>[
RangeContainer(widget.severity["green"], HAClientTheme().getGreenGaugeColor()),
RangeContainer(widget.severity["red"], HAClientTheme().getRedGaugeColor()),
RangeContainer(widget.severity["yellow"], HAClientTheme().getYellowGaugeColor())
RangeContainer(severity["green"], HAClientTheme().getGreenGaugeColor()),
RangeContainer(severity["red"], HAClientTheme().getRedGaugeColor()),
RangeContainer(severity["yellow"], HAClientTheme().getYellowGaugeColor())
];
rangesList.sort((current, next) {
if (current.startFrom > next.startFrom) {
@ -43,11 +38,20 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
}
return 0;
});
if (fixedValue < rangesList[1].startFrom) {
currentColor = rangesList[0].color;
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
currentColor = rangesList[1].color;
} else {
currentColor = rangesList[2].color;
}
ranges = [
GaugeRange(
startValue: rangesList[0].startFrom.toDouble(),
endValue: rangesList[1].startFrom.toDouble(),
color: fixedValue < rangesList[1].startFrom ? rangesList[0].color : rangesList[0].color.withOpacity(0.1),
color: rangesList[0].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
@ -55,15 +59,15 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
GaugeRange(
startValue: rangesList[1].startFrom.toDouble(),
endValue: rangesList[2].startFrom.toDouble(),
color: (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) ? rangesList[1].color : rangesList[1].color.withOpacity(0.1),
color: rangesList[1].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[2].startFrom.toDouble(),
endValue: widget.max.toDouble(),
color: fixedValue >= rangesList[2].startFrom ? rangesList[2].color : rangesList[2].color.withOpacity(0.1),
endValue: max.toDouble(),
color: rangesList[2].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
@ -71,14 +75,15 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
];
}
if (ranges == null) {
currentColor = Theme.of(context).primaryColorDark;
ranges = <GaugeRange>[
GaugeRange(
startValue: widget.min.toDouble(),
endValue: widget.max.toDouble(),
color: Theme.of(context).primaryColorDark,
startValue: min.toDouble(),
endValue: max.toDouble(),
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
startWidth: 0.3,
)
];
}
@ -104,12 +109,18 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
return SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
maximum: widget.max.toDouble(),
minimum: widget.min.toDouble(),
maximum: max.toDouble(),
minimum: min.toDouble(),
showLabels: false,
useRangeColorForAxis: true,
showTicks: false,
canScaleToFit: true,
ranges: ranges,
axisLineStyle: AxisLineStyle(
thickness: 0.3,
thicknessUnit: GaugeSizeUnit.factor,
color: Colors.transparent
),
annotations: <GaugeAnnotation>[
GaugeAnnotation(
angle: -90,
@ -135,27 +146,16 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
),
)
],
axisLineStyle: AxisLineStyle(
thickness: 0.3,
thicknessUnit: GaugeSizeUnit.factor
),
startAngle: 180,
endAngle: 0,
pointers: <GaugePointer>[
NeedlePointer(
RangePointer(
value: fixedValue,
lengthUnit: GaugeSizeUnit.factor,
needleLength: 0.9,
needleColor: Theme.of(context).accentColor,
enableAnimation: true,
needleStartWidth: 1,
animationType: AnimationType.bounceOut,
needleEndWidth: 3,
knobStyle: KnobStyle(
sizeUnit: GaugeSizeUnit.factor,
color: Theme.of(context).buttonColor,
knobRadius: 0.1
)
width: 0.3,
color: currentColor,
enableAnimation: true,
animationType: AnimationType.bounceOut,
)
]
)
@ -166,6 +166,7 @@ class _GaugeCardBodyState extends State<GaugeCardBody> {
),
);
}
}
class RangeContainer {

View File

@ -113,6 +113,7 @@ class CardType {
static const IFRAME = "iframe";
static const GAUGE = "gauge";
static const ENTITY_BUTTON = "entity-button";
static const BUTTON = "button";
static const CONDITIONAL = "conditional";
static const ALARM_PANEL = "alarm-panel";
static const MARKDOWN = "markdown";

View File

@ -149,7 +149,7 @@ EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client";
const appVersionNumber = "0.8.3";
const appVersionNumber = "0.8.4";
const appVersionAdd = "";
const appVersion = "$appVersionNumber$appVersionAdd";

View File

@ -20,6 +20,7 @@ class ConnectionManager {
String oauthUrl;
String webhookId;
bool settingsLoaded = false;
int appIntegrationVersion;
bool get isAuthenticated => _token != null;
StreamSubscription _socketSubscription;
Duration connectTimeout = Duration(seconds: 15);
@ -43,6 +44,7 @@ class ConnectionManager {
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
displayHostname = "$_domain:$_port";
_webSocketAPIEndpoint =
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";

View File

@ -2,9 +2,11 @@ part of '../main.dart';
class MobileAppIntegrationManager {
static const INTEGRATION_VERSION = 3;
static final _appRegistrationData = {
"app_version": "$appVersion",
"device_name": "",
"app_version": "$appVersion",
"manufacturer": DeviceInfoManager().manufacturer,
"model": DeviceInfoManager().model,
"os_version": DeviceInfoManager().osVersion,
@ -22,6 +24,7 @@ class MobileAppIntegrationManager {
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
var registrationData = Map.from(_appRegistrationData);
registrationData.addAll({
"device_id": "${DeviceInfoManager().unicDeviceId}",
"app_id": "ha_client",
"app_name": "$appName",
"os_name": DeviceInfoManager().osName,
@ -34,9 +37,11 @@ class MobileAppIntegrationManager {
).then((response) {
Logger.d("Processing registration responce...");
var responseObject = json.decode(response);
ConnectionManager().webhookId = responseObject["webhook_id"];
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
SharedPreferences.getInstance().then((prefs) {
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
ConnectionManager().webhookId = responseObject["webhook_id"];
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
completer.complete();
eventBus.fire(ShowPopupDialogEvent(
@ -65,11 +70,21 @@ class MobileAppIntegrationManager {
includeAuthHeader: false,
data: json.encode(updateData)
).then((response) {
if (response == null || response.isEmpty) {
Logger.d("No registration data in response. MobileApp integration was removed");
var registrationData;
try {
registrationData = json.decode(response);
} catch (e) {
registrationData = null;
}
if (registrationData == null || registrationData.isEmpty) {
Logger.d("No registration data in response. MobileApp integration was removed or broken");
_askToRegisterApp();
} else {
Logger.d("App registration works fine");
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
Logger.d('App registration needs to be updated');
_askToRemoveAndRegisterApp();
} else {
Logger.d('App registration works fine');
if (showOkDialog) {
eventBus.fire(ShowPopupDialogEvent(
title: "All good",
@ -79,6 +94,7 @@ class MobileAppIntegrationManager {
));
}
}
}
completer.complete();
}).catchError((e) {
if (e is http.Response && e.statusCode == 410) {
@ -105,10 +121,22 @@ class MobileAppIntegrationManager {
}
}
static void _askToRemoveAndRegisterApp() {
eventBus.fire(ShowPopupDialogEvent(
title: "Mobile app integration needs to be updated",
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
positiveText: "Ok",
negativeText: "Report an issue",
onNegative: () {
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
},
));
}
static void _askToRegisterApp() {
eventBus.fire(ShowPopupDialogEvent(
title: "App integration was removed",
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
title: "App integration is broken",
body: "Looks like app integration was removed from your Home Assistant or it needs to be updated. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant before registering and restart Home Assistant. Then go back here.",
positiveText: "Register now",
negativeText: "Cancel",
onPositive: () {

View File

@ -12,61 +12,52 @@ class EntityViewPage extends StatefulWidget {
class _EntityViewPageState extends State<EntityViewPage> {
StreamSubscription _refreshDataSubscription;
StreamSubscription _stateSubscription;
Entity entity;
Entity forwardToMainPage;
bool _popScheduled = false;
Entity _entity;
@override
void initState() {
super.initState();
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId == widget.entityId) {
entity = HomeAssistant().entities.get(widget.entityId);
Logger.d("[Entity page] State change event handled: ${event.entityId}");
setState(() {});
setState(() {
_getEntity();
});
}
});
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
entity = HomeAssistant().entities.get(widget.entityId);
setState(() {});
Logger.d("[Entity page] Refresh data event handled");
setState(() {
_getEntity();
});
entity = HomeAssistant().entities.get(widget.entityId);
});
_getEntity();
}
_getEntity() {
_entity = HomeAssistant().entities.get(widget.entityId);
}
@override
Widget build(BuildContext context) {
Widget body;
if (MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
if (!_popScheduled) {
_popScheduled = true;
_popAfterBuild();
}
body = PageLoadingIndicator();
} else {
body = EntityPageLayout(entity: entity);
}
String entityNameToDisplay = '${(_entity?.displayName ?? widget.entityId) ?? ''}';
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
title: new Text("${entity.displayName}"),
title: new Text(entityNameToDisplay),
),
body: body,
body: _entity == null ? PageLoadingError(
errorText: 'Entity is not available $entityNameToDisplay',
) : EntityPageLayout(entity: _entity),
);
}
_popAfterBuild() async {
forwardToMainPage = entity;
await Future.delayed(Duration(milliseconds: 300));
Navigator.of(context).pop();
}
@override
void dispose(){
if (_stateSubscription != null) _stateSubscription.cancel();
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
eventBus.fire(ShowEntityPageEvent(entity: forwardToMainPage));
super.dispose();
}
}

View File

@ -25,7 +25,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
int _previousViewCount;
bool _showLoginButton = false;
bool _preventAppRefresh = false;
Entity _entityToShow;
@override
void initState() {
@ -125,9 +124,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
await HomeAssistant().fetchData(uiOnly).then((_) {
_hideBottomBar();
if (_entityToShow != null) {
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
}
}).catchError((e) {
if (e is HAError) {
_setErrorState(e);
@ -302,12 +298,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
void _showEntityPage(String entityId) {
setState(() {
_entityToShow = HomeAssistant().entities?.get(entityId);
if (_entityToShow != null) {
_mainScrollController?.jumpTo(0);
}
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EntityViewPage(entityId: entityId),
)
);
/*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
Navigator.push(
context,
@ -591,7 +587,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final ScrollController _mainScrollController = ScrollController();
Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> serviceMenuItems = [];
@ -696,30 +691,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
),
);
}
} else {
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
mainScrollBody = Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
child: HomeAssistant().ui.build(context, _viewsTabController),
),
Container(
width: Sizes.mainPageScreenSeparatorWidth,
color: Colors.blue,
),
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
child: EntityPageLayout(entity: _entityToShow, showClose: true,),
)
],
);
} else if (_entityToShow != null) {
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
} else {
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
}
}
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
@ -774,7 +748,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_scaffoldKey.currentState.openDrawer();
},
),
bottom: (empty || _entityToShow != null) ? null : TabBar(
bottom: empty ? null : TabBar(
controller: _viewsTabController,
tabs: buildUIViewTabs(),
isScrollable: true,
@ -784,7 +758,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
];
},
body: mainScrollBody,
controller: _mainScrollController,
);
}
@ -849,22 +822,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
body: _buildScaffoldBody(true)
);
} else {
return WillPopScope(
child: Scaffold(
return Scaffold(
key: _scaffoldKey,
drawer: _buildAppDrawer(),
primary: false,
bottomNavigationBar: bottomBar,
body: _buildScaffoldBody(false)
),
onWillPop: () {
if (_entityToShow != null) {
eventBus.fire(ShowEntityPageEvent());
return Future.value(false);
} else {
return Future.value(true);
}
},
);
}
}

View File

@ -62,7 +62,7 @@ class _PurchasePageState extends State<PurchasePage> {
}
}
Widget _buildProducts() {
List<Widget> _buildProducts() {
List<Widget> productWidgets = [];
for (ProductDetails product in _products) {
productWidgets.add(
@ -72,10 +72,7 @@ class _PurchasePageState extends State<PurchasePage> {
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
);
}
return ListView(
scrollDirection: Axis.vertical,
children: productWidgets
);
return productWidgets;
}
void _buyProduct(ProductDetails product) {
@ -87,12 +84,28 @@ class _PurchasePageState extends State<PurchasePage> {
@override
Widget build(BuildContext context) {
Widget body;
List<Widget> body;
if (!_loaded) {
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
body = [_error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error)];
} else {
body = _buildProducts();
}
body.add(
Card(
child: Container(
height: 80,
child: InkWell(
child: Image.network('https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif'),
onTap: () {
Launcher.launchURLInCustomTab(
context: context,
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARWGETZD2D83Q&source=url'
);
},
)
),
)
);
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
@ -100,7 +113,10 @@ class _PurchasePageState extends State<PurchasePage> {
}),
title: new Text(widget.title),
),
body: body,
body: ListView(
scrollDirection: Axis.vertical,
children: body
),
);
}

View File

@ -7,9 +7,9 @@ class MaterialDesignIcons {
"binary_sensor": "mdi:checkbox-blank-circle-outline",
"group": "mdi:google-circles-communities",
"sensor": "mdi:eye",
"automation": "mdi:playlist-play",
"script": "mdi:file-document",
"input_boolean": "mdi:drawing",
"automation": "mdi:robot",
"script": "mdi:script-text",
"input_boolean": "mdi:toggle-switch-outline",
"input_datetime": "mdi:clock",
"input_number": "mdi:ray-vertex",
"input_select": "mdi:format-list-bulleted",

View File

@ -48,7 +48,7 @@ class HAView {
type: rawCardInfo['type'] ?? CardType.ENTITIES,
columnsCount: rawCardInfo['columns'] ?? 4,
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
showHeaderToggle: (rawCardInfo['show_header_toggle'] ?? rawCard['show_header_toggle']) ?? true,
showHeaderToggle: (rawCardInfo['show_header_toggle'] ?? rawCard['show_header_toggle']) ?? false,
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.8.3+890
version: 0.8.4+892
environment: