Compare commits
53 Commits
1.0.1
...
1.1.0-beta
Author | SHA1 | Date | |
---|---|---|---|
d53825f140 | |||
ea7e0f04ce | |||
cf75989447 | |||
f27d55869b | |||
a287f597ad | |||
0698950f3d | |||
aa58559ba6 | |||
94acc67383 | |||
701e6a46df | |||
1ed56ce8f1 | |||
a6d1baca77 | |||
dbeda6ea68 | |||
3dca28a7da | |||
da4264a409 | |||
302451e118 | |||
d19dbd389b | |||
05ae954b30 | |||
840e266381 | |||
01525a2929 | |||
2eb2596f37 | |||
699cab3498 | |||
353c80b6bc | |||
e4d1a4f823 | |||
78d6b38b92 | |||
1499a91ef7 | |||
9b7f7aa380 | |||
5683ab5158 | |||
a20dfaf05e | |||
24d42c9597 | |||
9078ad81e8 | |||
7cba6c8a10 | |||
c1f9c8c16d | |||
1d1d132b33 | |||
e258b3bc2c | |||
13508ee92f | |||
4fbf58e707 | |||
a3442f84ca | |||
6a6ab3b2cb | |||
d9fa553e2f | |||
cde5d9b912 | |||
3468446b5b | |||
326434273a | |||
470d3be946 | |||
d1032be6a6 | |||
cffac8e1f8 | |||
870bc25dd9 | |||
de713024f6 | |||
4d4add4581 | |||
1670c8e505 | |||
55eb1b5125 | |||
dbeaaaf91e | |||
8166d8ce6d | |||
35bcf0c1fa |
@ -4,15 +4,11 @@
|
|||||||
|
|
||||||
Visit [ha-client.app](http://ha-client.app/) for more info.
|
Visit [ha-client.app](http://ha-client.app/) for more info.
|
||||||
|
|
||||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
Download the app from [Google Play](https://play.google.com/store/apps/details?id=com.keyboardcrumbs.haclient)
|
||||||
|
|
||||||
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||||
|
|
||||||
#### Stable CI build
|
#### Last release build status
|
||||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5eaef46c513b9f9b25eb7f1a/latest_build)
|
|
||||||
#### Beta CI build
|
|
||||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
|
||||||
#### Pre-release CI build
|
|
||||||
[](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5da8bdab9f20ef798f7c2c64/latest_build)
|
|
||||||
|
@ -11,9 +11,10 @@ window.externalApp.getExternalAuth = function(options) {
|
|||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
console.log("Calling a callback");
|
console.log("Calling a callback");
|
||||||
window[options.callback](true, responseData);
|
window[options.callback](true, responseData);
|
||||||
}, 500);
|
}, 900);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
window.externalApp.externalBus = function(message) {
|
window.externalApp.externalBus = function(message) {
|
||||||
console.log("External bus message: " + message);
|
console.log("External bus message: " + message);
|
||||||
var messageObj = JSON.parse(message);
|
var messageObj = JSON.parse(message);
|
||||||
@ -32,4 +33,5 @@ window.externalApp.externalBus = function(message) {
|
|||||||
} else if (messageObj.type == "config_screen/show") {
|
} else if (messageObj.type == "config_screen/show") {
|
||||||
HAClient.postMessage('show-settings');
|
HAClient.postMessage('show-settings');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
196
lib/cards/badges.dart
Normal file
196
lib/cards/badges.dart
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class Badges extends StatelessWidget {
|
||||||
|
final BadgesData badges;
|
||||||
|
|
||||||
|
const Badges({Key key, this.badges}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow();
|
||||||
|
|
||||||
|
if (entitiesToShow.isNotEmpty) {
|
||||||
|
if (AppSettings().scrollBadges) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints.tightFor(height: 112),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: entitiesToShow.map((entity) =>
|
||||||
|
EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
|
child: BadgeWidget(),
|
||||||
|
),
|
||||||
|
handleTap: true,
|
||||||
|
)).toList()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 10.0,
|
||||||
|
runSpacing: 5,
|
||||||
|
children: entitiesToShow.map((entity) =>
|
||||||
|
EntityModel(
|
||||||
|
entityWrapper: entity,
|
||||||
|
child: BadgeWidget(),
|
||||||
|
handleTap: true,
|
||||||
|
)).toList(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Container(height: 0.0, width: 0.0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BadgeWidget extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entityModel = EntityModel.of(context);
|
||||||
|
Widget badgeIcon;
|
||||||
|
String onBadgeTextValue;
|
||||||
|
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||||
|
switch (entityModel.entityWrapper.entity.domain) {
|
||||||
|
case "sun":
|
||||||
|
{
|
||||||
|
IconData iconData;
|
||||||
|
if (entityModel.entityWrapper.entity.state == "below_horizon") {
|
||||||
|
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc);
|
||||||
|
} else {
|
||||||
|
iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8);
|
||||||
|
}
|
||||||
|
badgeIcon = Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: Icon(
|
||||||
|
iconData,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "camera":
|
||||||
|
case "media_player":
|
||||||
|
case "binary_sensor":
|
||||||
|
{
|
||||||
|
badgeIcon = EntityIcon(
|
||||||
|
imagePadding: EdgeInsets.all(0.0),
|
||||||
|
iconPadding: EdgeInsets.all(10),
|
||||||
|
color: Theme.of(context).textTheme.body2.color
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "device_tracker":
|
||||||
|
case "person":
|
||||||
|
{
|
||||||
|
badgeIcon = EntityIcon(
|
||||||
|
imagePadding: EdgeInsets.all(0.0),
|
||||||
|
iconPadding: EdgeInsets.all(10),
|
||||||
|
color: Theme.of(context).textTheme.body2.color
|
||||||
|
);
|
||||||
|
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||||
|
badgeIcon = Padding(
|
||||||
|
padding: EdgeInsets.all(4),
|
||||||
|
child: Text(
|
||||||
|
"${entityModel.entityWrapper.entity.displayState}",
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
softWrap: false,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.body1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Widget onBadgeText;
|
||||||
|
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
||||||
|
onBadgeText = Container(width: 0.0, height: 0.0);
|
||||||
|
} else {
|
||||||
|
onBadgeText = Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 50),
|
||||||
|
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||||
|
child: Text("$onBadgeTextValue",
|
||||||
|
style: Theme.of(context).textTheme.overline.copyWith(
|
||||||
|
color: HAClientTheme().getOnBadgeTextColor()
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.ellipsis
|
||||||
|
),
|
||||||
|
decoration: new BoxDecoration(
|
||||||
|
color: iconColor,
|
||||||
|
borderRadius: BorderRadius.circular(9.0),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Stack(
|
||||||
|
overflow: Overflow.visible,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 45,
|
||||||
|
height: 45,
|
||||||
|
decoration: new BoxDecoration(
|
||||||
|
// Circle shape
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
// The border you want
|
||||||
|
border: Border.all(
|
||||||
|
width: 2.0,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 41,
|
||||||
|
height: 41,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: badgeIcon,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: -6,
|
||||||
|
child: onBadgeText
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 45),
|
||||||
|
padding: EdgeInsets.only(top: 10),
|
||||||
|
child: Text(
|
||||||
|
"${entityModel.entityWrapper.displayName}",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.caption.copyWith(
|
||||||
|
fontSize: 10
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => entityModel.entityWrapper.handleTap(),
|
||||||
|
onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(),
|
||||||
|
onLongPress: () => entityModel.entityWrapper.handleHold(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -13,17 +13,34 @@ class CardData {
|
|||||||
|
|
||||||
factory CardData.parse(rawData) {
|
factory CardData.parse(rawData) {
|
||||||
try {
|
try {
|
||||||
|
if (rawData['type'] == null) {
|
||||||
|
rawData['type'] = CardType.ENTITIES;
|
||||||
|
} else if (!(rawData['type'] is String)) {
|
||||||
|
return CardData(null);
|
||||||
|
}
|
||||||
switch (rawData['type']) {
|
switch (rawData['type']) {
|
||||||
case CardType.ENTITIES:
|
case CardType.ENTITIES:
|
||||||
|
case CardType.HISTORY_GRAPH:
|
||||||
|
case CardType.MAP:
|
||||||
|
case CardType.PICTURE_GLANCE:
|
||||||
|
case CardType.SENSOR:
|
||||||
|
case CardType.ENTITY:
|
||||||
|
case CardType.WEATHER_FORECAST:
|
||||||
|
case CardType.PLANT_STATUS:
|
||||||
|
if (rawData['entity'] != null) {
|
||||||
|
rawData['entities'] = [rawData['entity']];
|
||||||
|
}
|
||||||
return EntitiesCardData(rawData);
|
return EntitiesCardData(rawData);
|
||||||
break;
|
break;
|
||||||
case CardType.ALARM_PANEL:
|
case CardType.ALARM_PANEL:
|
||||||
return AlarmPanelCardData(rawData);
|
return AlarmPanelCardData(rawData);
|
||||||
break;
|
break;
|
||||||
case CardType.BUTTON:
|
case CardType.LIGHT:
|
||||||
return ButtonCardData(rawData);
|
return LightCardData(rawData);
|
||||||
break;
|
break;
|
||||||
case CardType.ENTITY_BUTTON:
|
case CardType.ENTITY_BUTTON:
|
||||||
|
case CardType.BUTTON:
|
||||||
|
case CardType.PICTURE_ENTITY:
|
||||||
return ButtonCardData(rawData);
|
return ButtonCardData(rawData);
|
||||||
break;
|
break;
|
||||||
case CardType.CONDITIONAL:
|
case CardType.CONDITIONAL:
|
||||||
@ -42,6 +59,10 @@ class CardData {
|
|||||||
return GaugeCardData(rawData);
|
return GaugeCardData(rawData);
|
||||||
break;
|
break;
|
||||||
case CardType.GLANCE:
|
case CardType.GLANCE:
|
||||||
|
case CardType.THERMOSTAT:
|
||||||
|
if (rawData['entity'] != null) {
|
||||||
|
rawData['entities'] = [rawData['entity']];
|
||||||
|
}
|
||||||
return GlanceCardData(rawData);
|
return GlanceCardData(rawData);
|
||||||
break;
|
break;
|
||||||
case CardType.HORIZONTAL_STACK:
|
case CardType.HORIZONTAL_STACK:
|
||||||
@ -56,14 +77,11 @@ class CardData {
|
|||||||
case CardType.MEDIA_CONTROL:
|
case CardType.MEDIA_CONTROL:
|
||||||
return MediaControlCardData(rawData);
|
return MediaControlCardData(rawData);
|
||||||
break;
|
break;
|
||||||
|
case CardType.BADGES:
|
||||||
|
return BadgesData(rawData);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (rawData.containsKey('entities')) {
|
return CardData(null);
|
||||||
return EntitiesCardData(rawData);
|
|
||||||
} else if (rawData.containsKey('entity')) {
|
|
||||||
rawData['entities'] = [rawData['entity']];
|
|
||||||
return EntitiesCardData(rawData);
|
|
||||||
}
|
|
||||||
return CardData(rawData);
|
|
||||||
}
|
}
|
||||||
} catch (error, stacktrace) {
|
} catch (error, stacktrace) {
|
||||||
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
|
Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace);
|
||||||
@ -73,10 +91,10 @@ class CardData {
|
|||||||
|
|
||||||
CardData(rawData) {
|
CardData(rawData) {
|
||||||
if (rawData != null && rawData is Map) {
|
if (rawData != null && rawData is Map) {
|
||||||
type = rawData['type'] ?? CardType.ENTITIES;
|
type = rawData['type'];
|
||||||
conditions = rawData['conditions'] ?? [];
|
conditions = rawData['conditions'] ?? [];
|
||||||
showEmpty = rawData['show_empty'] ?? true;
|
showEmpty = rawData['show_empty'] ?? true;
|
||||||
stateFilter = rawData['state_filter'] ?? [];
|
stateFilter = rawData['state_filter'] ?? [];
|
||||||
} else {
|
} else {
|
||||||
type = CardType.UNKNOWN;
|
type = CardType.UNKNOWN;
|
||||||
conditions = [];
|
conditions = [];
|
||||||
@ -159,6 +177,64 @@ class CardData {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BadgesData extends CardData {
|
||||||
|
|
||||||
|
String title;
|
||||||
|
String icon;
|
||||||
|
bool showHeaderToggle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return Badges(badges: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
BadgesData(rawData) : super(rawData) {
|
||||||
|
if (rawData['badges'] is List) {
|
||||||
|
rawData['badges'].forEach((dynamic rawBadge) {
|
||||||
|
if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {
|
||||||
|
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge)));
|
||||||
|
} else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) {
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(rawBadge['entity']),
|
||||||
|
overrideName: rawBadge["name"],
|
||||||
|
overrideIcon: rawBadge["icon"],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (rawBadge is Map && rawBadge.containsKey('entities')) {
|
||||||
|
_parseEntities(rawBadge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseEntities(rawData) {
|
||||||
|
var rawEntities = rawData['entities'] ?? [];
|
||||||
|
rawEntities.forEach((rawEntity) {
|
||||||
|
if (rawEntity is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(rawEntity),
|
||||||
|
stateFilter: rawData['state_filter'] ?? [],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) {
|
||||||
|
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
overrideName: rawEntity["name"],
|
||||||
|
overrideIcon: rawEntity["icon"],
|
||||||
|
stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []),
|
||||||
|
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
class EntitiesCardData extends CardData {
|
class EntitiesCardData extends CardData {
|
||||||
|
|
||||||
String title;
|
String title;
|
||||||
@ -172,12 +248,12 @@ class EntitiesCardData extends CardData {
|
|||||||
|
|
||||||
EntitiesCardData(rawData) : super(rawData) {
|
EntitiesCardData(rawData) : super(rawData) {
|
||||||
//Parsing card data
|
//Parsing card data
|
||||||
title = rawData["title"];
|
title = rawData['title'];
|
||||||
icon = rawData['icon'];
|
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||||
stateColor = rawData['state_color'] ?? false;
|
stateColor = rawData['state_color'] ?? false;
|
||||||
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
||||||
//Parsing entities
|
//Parsing entities
|
||||||
var rawEntities = rawData["entities"] ?? [];
|
var rawEntities = rawData['entities'] ?? [];
|
||||||
rawEntities.forEach((rawEntity) {
|
rawEntities.forEach((rawEntity) {
|
||||||
if (rawEntity is String) {
|
if (rawEntity is String) {
|
||||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||||
@ -282,6 +358,39 @@ class AlarmPanelCardData extends CardData {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LightCardData extends CardData {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
String icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return LightCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
LightCardData(rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
name = rawData['name'];
|
||||||
|
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||||
|
//Parsing entity
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
overrideName: name,
|
||||||
|
overrideIcon: icon,
|
||||||
|
uiAction: EntityUIAction()..tapAction = EntityUIAction.toggle
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ButtonCardData extends CardData {
|
class ButtonCardData extends CardData {
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
@ -299,7 +408,7 @@ class ButtonCardData extends CardData {
|
|||||||
ButtonCardData(rawData) : super(rawData) {
|
ButtonCardData(rawData) : super(rawData) {
|
||||||
//Parsing card data
|
//Parsing card data
|
||||||
name = rawData['name'];
|
name = rawData['name'];
|
||||||
icon = rawData['icon'];
|
icon = rawData['icon'] is String ? rawData['icon'] : null;
|
||||||
showName = rawData['show_name'] ?? true;
|
showName = rawData['show_name'] ?? true;
|
||||||
showIcon = rawData['show_icon'] ?? true;
|
showIcon = rawData['show_icon'] ?? true;
|
||||||
stateColor = rawData['state_color'] ?? true;
|
stateColor = rawData['state_color'] ?? true;
|
||||||
@ -379,7 +488,7 @@ class GaugeCardData extends CardData {
|
|||||||
}
|
}
|
||||||
severity = rawData['severity'];
|
severity = rawData['severity'];
|
||||||
//Parsing entity
|
//Parsing entity
|
||||||
var entitiId = rawData["entity"];
|
var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"];
|
||||||
if (entitiId != null && entitiId is String) {
|
if (entitiId != null && entitiId is String) {
|
||||||
if (HomeAssistant().entities.isExist(entitiId)) {
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
entities.add(EntityWrapper(
|
entities.add(EntityWrapper(
|
||||||
@ -390,7 +499,7 @@ class GaugeCardData extends CardData {
|
|||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
entities.add(EntityWrapper(entity: Entity.missed('$entitiId')));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -460,7 +569,7 @@ class HorizontalStackCardData extends CardData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HorizontalStackCardData(rawData) : super(rawData) {
|
HorizontalStackCardData(rawData) : super(rawData) {
|
||||||
if (rawData.containsKey('cards')) {
|
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||||
return CardData.parse(childCard);
|
return CardData.parse(childCard);
|
||||||
}).toList();
|
}).toList();
|
||||||
@ -481,7 +590,7 @@ class VerticalStackCardData extends CardData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VerticalStackCardData(rawData) : super(rawData) {
|
VerticalStackCardData(rawData) : super(rawData) {
|
||||||
if (rawData.containsKey('cards')) {
|
if (rawData.containsKey('cards') && rawData['cards'] is List) {
|
||||||
childCards = rawData['cards'].map<CardData>((childCard) {
|
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||||
return CardData.parse(childCard);
|
return CardData.parse(childCard);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
@ -62,13 +62,16 @@ class EntityButtonCard extends StatelessWidget {
|
|||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Padding(
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: EdgeInsets.only(top: 5),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: Column(
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
buttonIcon,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
_buildName(context)
|
children: <Widget>[
|
||||||
],
|
buttonIcon,
|
||||||
|
_buildName(context)
|
||||||
|
],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -30,10 +30,13 @@ class GlanceCard extends StatelessWidget {
|
|||||||
start, end
|
start, end
|
||||||
).map(
|
).map(
|
||||||
(EntityWrapper entity){
|
(EntityWrapper entity){
|
||||||
return EntityModel(
|
return Padding(
|
||||||
entityWrapper: entity,
|
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
||||||
child: _buildEntityContainer(context, entity),
|
child: EntityModel(
|
||||||
handleTap: true
|
entityWrapper: entity,
|
||||||
|
child: _buildEntityContainer(context, entity),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
).toList()
|
).toList()
|
||||||
@ -50,17 +53,22 @@ class GlanceCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return CardWrapper(
|
return CardWrapper(
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Padding(
|
||||||
children: <Widget>[
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
||||||
CardHeader(name: card.title),
|
child: Column(
|
||||||
Padding(
|
mainAxisSize: MainAxisSize.min,
|
||||||
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
|
children: <Widget>[
|
||||||
child: Table(
|
CardHeader(
|
||||||
children: rows
|
name: card.title,
|
||||||
)
|
emptyPadding: Sizes.rowPadding,
|
||||||
|
),
|
||||||
|
Table(
|
||||||
|
children: rows
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
],
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -85,16 +93,14 @@ class GlanceCard extends StatelessWidget {
|
|||||||
result.add(_buildState());
|
result.add(_buildState());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return InkResponse(
|
||||||
child: InkResponse(
|
child: Column(
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: result,
|
||||||
children: result,
|
|
||||||
),
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
),
|
),
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
161
lib/cards/light_card.dart
Normal file
161
lib/cards/light_card.dart
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class LightCard extends StatefulWidget {
|
||||||
|
|
||||||
|
final LightCardData card;
|
||||||
|
|
||||||
|
LightCard({Key key, this.card}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _LightCardState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LightCardState extends State<LightCard> {
|
||||||
|
|
||||||
|
double _actualBrightness;
|
||||||
|
double _newBrightness;
|
||||||
|
bool _changedHere = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setBrightness(double value, LightEntity entity) {
|
||||||
|
setState((){
|
||||||
|
_newBrightness = value;
|
||||||
|
_changedHere = true;
|
||||||
|
});
|
||||||
|
ConnectionManager().callService(
|
||||||
|
domain: entity.domain,
|
||||||
|
service: "turn_on",
|
||||||
|
entityId: entity.entityId,
|
||||||
|
data: {"brightness": value.round()}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
EntityWrapper entityWrapper = widget.card.entity;
|
||||||
|
LightEntity entity = entityWrapper.entity;
|
||||||
|
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||||
|
return EntityModel(
|
||||||
|
entityWrapper: widget.card.entity,
|
||||||
|
child: MissedEntityWidget(),
|
||||||
|
handleTap: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
entityWrapper.overrideName = widget.card.name ??
|
||||||
|
entityWrapper.displayName;
|
||||||
|
entityWrapper.overrideIcon = widget.card.icon ??
|
||||||
|
entityWrapper.icon;
|
||||||
|
|
||||||
|
if (!_changedHere) {
|
||||||
|
_actualBrightness = (entity.brightness ?? 0).toDouble();
|
||||||
|
_newBrightness = _actualBrightness;
|
||||||
|
} else {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
|
Color lightColor = entity.color?.toColor();
|
||||||
|
Color color;
|
||||||
|
if (lightColor != null && lightColor != Colors.white) {
|
||||||
|
color = lightColor;
|
||||||
|
} else {
|
||||||
|
color = Theme.of(context).accentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardWrapper(
|
||||||
|
padding: EdgeInsets.all(4),
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: entityWrapper,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints.loose(Size(200, 200)),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
SfRadialGauge(
|
||||||
|
axes: <RadialAxis>[
|
||||||
|
RadialAxis(
|
||||||
|
onAxisTapped: (val) {
|
||||||
|
_setBrightness(val, entity);
|
||||||
|
},
|
||||||
|
maximum: 255,
|
||||||
|
minimum: 0,
|
||||||
|
showLabels: false,
|
||||||
|
showTicks: false,
|
||||||
|
axisLineStyle: AxisLineStyle(
|
||||||
|
thickness: 0.05,
|
||||||
|
thicknessUnit: GaugeSizeUnit.factor,
|
||||||
|
color: HAClientTheme().getDisabledStateColor(context)
|
||||||
|
),
|
||||||
|
pointers: <GaugePointer>[
|
||||||
|
RangePointer(
|
||||||
|
value: _actualBrightness,
|
||||||
|
sizeUnit: GaugeSizeUnit.factor,
|
||||||
|
width: 0.05,
|
||||||
|
color: color,
|
||||||
|
enableAnimation: true,
|
||||||
|
animationType: AnimationType.bounceOut,
|
||||||
|
),
|
||||||
|
MarkerPointer(
|
||||||
|
value: _newBrightness,
|
||||||
|
markerType: MarkerType.circle,
|
||||||
|
markerHeight: 20,
|
||||||
|
markerWidth: 20,
|
||||||
|
enableDragging: true,
|
||||||
|
onValueChangeEnd: (val) {
|
||||||
|
_setBrightness(val, entity);
|
||||||
|
},
|
||||||
|
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
||||||
|
//enableAnimation: true,
|
||||||
|
//animationType: AnimationType.bounceOut,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
FractionallySizedBox(
|
||||||
|
heightFactor: 0.4,
|
||||||
|
widthFactor: 0.4,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: InkResponse(
|
||||||
|
onTap: () => entityWrapper.handleTap(),
|
||||||
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
child: EntityIcon(
|
||||||
|
showBadge: false,
|
||||||
|
padding: EdgeInsets.all(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
EntityName(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
wordsWrap: true,
|
||||||
|
maxLines: 3,
|
||||||
|
textOverflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,11 +7,6 @@ class UnsupportedCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CardWrapper(
|
return Container();
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
|
|
||||||
child: Text("'${card.type}' card is not supported yet"),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -36,66 +36,6 @@ class EntityState {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntityUIAction {
|
|
||||||
static const moreInfo = 'more-info';
|
|
||||||
static const toggle = 'toggle';
|
|
||||||
static const callService = 'call-service';
|
|
||||||
static const navigate = 'navigate';
|
|
||||||
static const none = 'none';
|
|
||||||
|
|
||||||
String tapAction = EntityUIAction.moreInfo;
|
|
||||||
String tapNavigationPath;
|
|
||||||
String tapService;
|
|
||||||
Map<String, dynamic> tapServiceData;
|
|
||||||
String holdAction = EntityUIAction.none;
|
|
||||||
String holdNavigationPath;
|
|
||||||
String holdService;
|
|
||||||
Map<String, dynamic> holdServiceData;
|
|
||||||
String doubleTapAction = EntityUIAction.none;
|
|
||||||
String doubleTapNavigationPath;
|
|
||||||
String doubleTapService;
|
|
||||||
Map<String, dynamic> doubleTapServiceData;
|
|
||||||
|
|
||||||
EntityUIAction({rawEntityData}) {
|
|
||||||
if (rawEntityData != null) {
|
|
||||||
if (rawEntityData["tap_action"] != null) {
|
|
||||||
if (rawEntityData["tap_action"] is String) {
|
|
||||||
tapAction = rawEntityData["tap_action"];
|
|
||||||
} else {
|
|
||||||
tapAction =
|
|
||||||
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
|
|
||||||
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
|
|
||||||
tapService = rawEntityData["tap_action"]["service"];
|
|
||||||
tapServiceData = rawEntityData["tap_action"]["service_data"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rawEntityData["hold_action"] != null) {
|
|
||||||
if (rawEntityData["hold_action"] is String) {
|
|
||||||
holdAction = rawEntityData["hold_action"];
|
|
||||||
} else {
|
|
||||||
holdAction =
|
|
||||||
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
|
|
||||||
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
|
|
||||||
holdService = rawEntityData["hold_action"]["service"];
|
|
||||||
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rawEntityData["double_tap_action"] != null) {
|
|
||||||
if (rawEntityData["double_tap_action"] is String) {
|
|
||||||
doubleTapAction = rawEntityData["double_tap_action"];
|
|
||||||
} else {
|
|
||||||
doubleTapAction =
|
|
||||||
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
|
|
||||||
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
|
|
||||||
doubleTapService = rawEntityData["double_tap_action"]["service"];
|
|
||||||
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class CardType {
|
class CardType {
|
||||||
static const HORIZONTAL_STACK = "horizontal-stack";
|
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||||
static const VERTICAL_STACK = "vertical-stack";
|
static const VERTICAL_STACK = "vertical-stack";
|
||||||
@ -113,6 +53,7 @@ class CardType {
|
|||||||
static const IFRAME = "iframe";
|
static const IFRAME = "iframe";
|
||||||
static const GAUGE = "gauge";
|
static const GAUGE = "gauge";
|
||||||
static const ENTITY_BUTTON = "entity-button";
|
static const ENTITY_BUTTON = "entity-button";
|
||||||
|
static const ENTITY = "entity";
|
||||||
static const BUTTON = "button";
|
static const BUTTON = "button";
|
||||||
static const CONDITIONAL = "conditional";
|
static const CONDITIONAL = "conditional";
|
||||||
static const ALARM_PANEL = "alarm-panel";
|
static const ALARM_PANEL = "alarm-panel";
|
||||||
@ -120,6 +61,9 @@ class CardType {
|
|||||||
static const LIGHT = "light";
|
static const LIGHT = "light";
|
||||||
static const ENTITY_FILTER = "entity-filter";
|
static const ENTITY_FILTER = "entity-filter";
|
||||||
static const UNKNOWN = "unknown";
|
static const UNKNOWN = "unknown";
|
||||||
|
static const HISTORY_GRAPH = "history-graph";
|
||||||
|
static const PICTURE_GLANCE = "picture-glance";
|
||||||
|
static const BADGES = "badges";
|
||||||
}
|
}
|
||||||
|
|
||||||
class Sizes {
|
class Sizes {
|
||||||
|
@ -1,148 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class BadgeWidget extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final entityModel = EntityModel.of(context);
|
|
||||||
double iconSize = 26.0;
|
|
||||||
Widget badgeIcon;
|
|
||||||
String onBadgeTextValue;
|
|
||||||
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
|
||||||
switch (entityModel.entityWrapper.entity.domain) {
|
|
||||||
case "sun":
|
|
||||||
{
|
|
||||||
badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon"
|
|
||||||
? Icon(
|
|
||||||
MaterialDesignIcons.getIconDataFromIconCode(0xf0dc),
|
|
||||||
size: iconSize,
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
MaterialDesignIcons.getIconDataFromIconCode(0xf5a8),
|
|
||||||
size: iconSize,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "camera":
|
|
||||||
case "media_player":
|
|
||||||
case "binary_sensor":
|
|
||||||
{
|
|
||||||
badgeIcon = EntityIcon(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
size: iconSize,
|
|
||||||
color: Theme.of(context).textTheme.body1.color
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "device_tracker":
|
|
||||||
case "person":
|
|
||||||
{
|
|
||||||
badgeIcon = EntityIcon(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
size: iconSize,
|
|
||||||
color: Theme.of(context).textTheme.body1.color
|
|
||||||
);
|
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
double stateFontSize;
|
|
||||||
if (entityModel.entityWrapper.entity.displayState.length <= 3) {
|
|
||||||
stateFontSize = 18.0;
|
|
||||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 4) {
|
|
||||||
stateFontSize = 15.0;
|
|
||||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 6) {
|
|
||||||
stateFontSize = 10.0;
|
|
||||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
|
||||||
stateFontSize = 8.0;
|
|
||||||
}
|
|
||||||
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
|
||||||
badgeIcon = Center(
|
|
||||||
child: Text(
|
|
||||||
"${entityModel.entityWrapper.entity.displayState}",
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
softWrap: false,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
|
||||||
fontSize: stateFontSize
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Widget onBadgeText;
|
|
||||||
if (onBadgeTextValue == null || onBadgeTextValue.length == 0) {
|
|
||||||
onBadgeText = Container(width: 0.0, height: 0.0);
|
|
||||||
} else {
|
|
||||||
onBadgeText = Container(
|
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
|
||||||
child: Text("$onBadgeTextValue",
|
|
||||||
style: Theme.of(context).textTheme.overline.copyWith(
|
|
||||||
color: HAClientTheme().getOnBadgeTextColor()
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade),
|
|
||||||
decoration: new BoxDecoration(
|
|
||||||
// Circle shape
|
|
||||||
//shape: BoxShape.circle,
|
|
||||||
color: iconColor,
|
|
||||||
borderRadius: BorderRadius.circular(9.0),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return GestureDetector(
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
|
|
||||||
width: 50.0,
|
|
||||||
height: 50.0,
|
|
||||||
decoration: new BoxDecoration(
|
|
||||||
// Circle shape
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
// The border you want
|
|
||||||
border: new Border.all(
|
|
||||||
width: 2.0,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
overflow: Overflow.visible,
|
|
||||||
children: <Widget>[
|
|
||||||
Positioned(
|
|
||||||
width: 46.0,
|
|
||||||
height: 46.0,
|
|
||||||
top: 0.0,
|
|
||||||
left: 0.0,
|
|
||||||
child: badgeIcon,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
//width: 50.0,
|
|
||||||
bottom: -9.0,
|
|
||||||
left: -10.0,
|
|
||||||
right: -10.0,
|
|
||||||
child: Center(
|
|
||||||
child: onBadgeText,
|
|
||||||
))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 60.0,
|
|
||||||
child: Text(
|
|
||||||
"${entityModel.entityWrapper.displayName}",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.caption,
|
|
||||||
softWrap: true,
|
|
||||||
maxLines: 3,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () =>
|
|
||||||
eventBus.fire(new ShowEntityPageEvent(entityId: entityModel.entityWrapper.entity.entityId)));
|
|
||||||
}
|
|
||||||
}
|
|
@ -42,7 +42,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
||||||
_webViewHtml = Uri.dataFromString(
|
_webViewHtml = Uri.dataFromString(
|
||||||
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
file.replaceFirst('{{stream_url}}', '${AppSettings().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||||
mimeType: 'text/html',
|
mimeType: 'text/html',
|
||||||
encoding: Encoding.getByName('utf-8')
|
encoding: Encoding.getByName('utf-8')
|
||||||
).toString();
|
).toString();
|
||||||
@ -69,7 +69,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future _loadMJPEG() async {
|
Future _loadMJPEG() async {
|
||||||
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
_streamUrl = '${AppSettings().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||||
.entityId}?token=${_entity.attributes['access_token']}';
|
.entityId}?token=${_entity.attributes['access_token']}';
|
||||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
var file = await rootBundle.loadString('assets/html/cameraView.html');
|
var file = await rootBundle.loadString('assets/html/cameraView.html');
|
||||||
|
@ -11,7 +11,7 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
ModeSelectorWidget({
|
ModeSelectorWidget({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.caption,
|
@required this.caption,
|
||||||
@required this.options,
|
this.options: const [],
|
||||||
this.value,
|
this.value,
|
||||||
@required this.onChange,
|
@required this.onChange,
|
||||||
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
|
@ -79,7 +79,8 @@ class Entity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
String get displayName =>
|
String get displayName =>
|
||||||
attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " "));
|
attributes["friendly_name"] ??
|
||||||
|
(attributes["name"] ?? (entityId != null && entityId.contains('.')) ? entityId.split(".")[1].replaceAll("_", " ") : "");
|
||||||
|
|
||||||
bool get isView =>
|
bool get isView =>
|
||||||
(domain == "group") &&
|
(domain == "group") &&
|
||||||
@ -209,14 +210,6 @@ class Entity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBadgeWidget(BuildContext context) {
|
|
||||||
return EntityModel(
|
|
||||||
entityWrapper: EntityWrapper(entity: this),
|
|
||||||
child: BadgeWidget(),
|
|
||||||
handleTap: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getAttribute(String attributeName) {
|
String getAttribute(String attributeName) {
|
||||||
if (attributes != null) {
|
if (attributes != null) {
|
||||||
return attributes["$attributeName"].toString();
|
return attributes["$attributeName"].toString();
|
||||||
|
@ -3,10 +3,13 @@ part of '../main.dart';
|
|||||||
class EntityIcon extends StatelessWidget {
|
class EntityIcon extends StatelessWidget {
|
||||||
|
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
|
final EdgeInsetsGeometry iconPadding;
|
||||||
|
final EdgeInsetsGeometry imagePadding;
|
||||||
final double size;
|
final double size;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
final bool showBadge;
|
||||||
|
|
||||||
const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key);
|
const EntityIcon({Key key, this.color, this.showBadge: true, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key);
|
||||||
|
|
||||||
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
int getDefaultIconByEntityId(String entityId, String deviceClass, String state) {
|
||||||
if (entityId == null) {
|
if (entityId == null) {
|
||||||
@ -26,76 +29,6 @@ class EntityIcon extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildIcon(BuildContext context, EntityWrapper data, Color color) {
|
|
||||||
Widget result;
|
|
||||||
if (data == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (data.entityPicture != null) {
|
|
||||||
result = Container(
|
|
||||||
height: size+12,
|
|
||||||
width: size+12,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
image: DecorationImage(
|
|
||||||
fit:BoxFit.cover,
|
|
||||||
image: CachedNetworkImageProvider(
|
|
||||||
"${data.entityPicture}"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
String iconName = data.icon;
|
|
||||||
int iconCode = 0;
|
|
||||||
if (iconName.length > 0) {
|
|
||||||
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
|
||||||
} else {
|
|
||||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
|
||||||
data.entity.deviceClass, data.entity.state); //
|
|
||||||
}
|
|
||||||
result = Icon(
|
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
|
||||||
size: size,
|
|
||||||
color: color,
|
|
||||||
);
|
|
||||||
if (data.entity is LightEntity &&
|
|
||||||
(data.entity as LightEntity).supportColor &&
|
|
||||||
(data.entity as LightEntity).color != null
|
|
||||||
) {
|
|
||||||
Color lightColor = (data.entity as LightEntity).color.toColor();
|
|
||||||
if (lightColor == Colors.white) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
result = Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
result,
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Container(
|
|
||||||
width: size / 3,
|
|
||||||
height: size / 3,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: lightColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
boxShadow: <BoxShadow>[
|
|
||||||
BoxShadow(
|
|
||||||
spreadRadius: 0,
|
|
||||||
blurRadius: 0,
|
|
||||||
offset: Offset(0.3, 0.3)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
@ -107,13 +40,99 @@ class EntityIcon extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
iconColor = HAClientTheme().getOffStateColor(context);
|
iconColor = HAClientTheme().getOffStateColor(context);
|
||||||
}
|
}
|
||||||
|
Widget iconWidget;
|
||||||
|
bool isPicture = false;
|
||||||
|
if (entityWrapper == null) {
|
||||||
|
iconWidget = Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (entityWrapper.entityPicture != null) {
|
||||||
|
iconWidget = ClipOval(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: '${entityWrapper.entityPicture}',
|
||||||
|
width: size+12,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
height: size+12,
|
||||||
|
errorWidget: (context, str, dyn) {
|
||||||
|
return Padding(
|
||||||
|
padding: iconPadding ?? padding,
|
||||||
|
child: _buildIcon(entityWrapper, iconColor)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
isPicture = true;
|
||||||
|
} else {
|
||||||
|
iconWidget = _buildIcon(entityWrapper, iconColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EdgeInsetsGeometry computedPadding;
|
||||||
|
if (isPicture && imagePadding != null) {
|
||||||
|
computedPadding = imagePadding;
|
||||||
|
} else if (!isPicture && iconPadding != null) {
|
||||||
|
computedPadding = iconPadding;
|
||||||
|
} else {
|
||||||
|
computedPadding = padding;
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: computedPadding,
|
||||||
child: buildIcon(
|
child: iconWidget,
|
||||||
context,
|
|
||||||
entityWrapper,
|
|
||||||
iconColor
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildIcon(EntityWrapper entityWrapper, Color iconColor) {
|
||||||
|
Widget iconWidget;
|
||||||
|
String iconName = entityWrapper.icon;
|
||||||
|
int iconCode = 0;
|
||||||
|
if (iconName.length > 0) {
|
||||||
|
iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName);
|
||||||
|
} else {
|
||||||
|
iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId,
|
||||||
|
entityWrapper.entity.deviceClass, entityWrapper.entity.state); //
|
||||||
|
}
|
||||||
|
if (showBadge && entityWrapper.entity is LightEntity &&
|
||||||
|
(entityWrapper.entity as LightEntity).supportColor &&
|
||||||
|
(entityWrapper.entity as LightEntity).color != null &&
|
||||||
|
(entityWrapper.entity as LightEntity).color.toColor() != Colors.white
|
||||||
|
) {
|
||||||
|
Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();
|
||||||
|
iconWidget = Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
|
size: size,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
width: size / 3,
|
||||||
|
height: size / 3,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: lightColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
spreadRadius: 0,
|
||||||
|
blurRadius: 0,
|
||||||
|
offset: Offset(0.3, 0.3)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
iconWidget = Icon(
|
||||||
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
|
size: size,
|
||||||
|
color: iconColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return iconWidget;
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ part of '../main.dart';
|
|||||||
class EntityWrapper {
|
class EntityWrapper {
|
||||||
|
|
||||||
String overrideName;
|
String overrideName;
|
||||||
final String overrideIcon;
|
String overrideIcon;
|
||||||
final bool stateColor;
|
final bool stateColor;
|
||||||
EntityUIAction uiAction;
|
EntityUIAction uiAction;
|
||||||
Entity entity;
|
Entity entity;
|
||||||
@ -61,7 +61,7 @@ class EntityWrapper {
|
|||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
if (uiAction.tapService != null && uiAction.tapService.startsWith("/")) {
|
||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.tapService}');
|
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.tapService}');
|
||||||
} else {
|
} else {
|
||||||
Launcher.launchURLInBrowser(uiAction.tapService);
|
Launcher.launchURLInBrowser(uiAction.tapService);
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ class EntityWrapper {
|
|||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
if (uiAction.holdService != null && uiAction.holdService.startsWith("/")) {
|
||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.holdService}');
|
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.holdService}');
|
||||||
} else {
|
} else {
|
||||||
Launcher.launchURLInBrowser(uiAction.holdService);
|
Launcher.launchURLInBrowser(uiAction.holdService);
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ class EntityWrapper {
|
|||||||
case EntityUIAction.navigate: {
|
case EntityUIAction.navigate: {
|
||||||
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
|
if (uiAction.doubleTapService != null && uiAction.doubleTapService.startsWith("/")) {
|
||||||
//TODO handle local urls
|
//TODO handle local urls
|
||||||
Launcher.launchURLInBrowser('${ConnectionManager().httpWebHost}${uiAction.doubleTapService}');
|
Launcher.launchURLInBrowser('${AppSettings().httpWebHost}${uiAction.doubleTapService}');
|
||||||
} else {
|
} else {
|
||||||
Launcher.launchURLInBrowser(uiAction.doubleTapService);
|
Launcher.launchURLInBrowser(uiAction.doubleTapService);
|
||||||
}
|
}
|
||||||
@ -154,4 +154,64 @@ class EntityWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntityUIAction {
|
||||||
|
static const moreInfo = 'more-info';
|
||||||
|
static const toggle = 'toggle';
|
||||||
|
static const callService = 'call-service';
|
||||||
|
static const navigate = 'navigate';
|
||||||
|
static const none = 'none';
|
||||||
|
|
||||||
|
String tapAction = EntityUIAction.moreInfo;
|
||||||
|
String tapNavigationPath;
|
||||||
|
String tapService;
|
||||||
|
Map<String, dynamic> tapServiceData;
|
||||||
|
String holdAction = EntityUIAction.moreInfo;
|
||||||
|
String holdNavigationPath;
|
||||||
|
String holdService;
|
||||||
|
Map<String, dynamic> holdServiceData;
|
||||||
|
String doubleTapAction = EntityUIAction.none;
|
||||||
|
String doubleTapNavigationPath;
|
||||||
|
String doubleTapService;
|
||||||
|
Map<String, dynamic> doubleTapServiceData;
|
||||||
|
|
||||||
|
EntityUIAction({rawEntityData}) {
|
||||||
|
if (rawEntityData != null) {
|
||||||
|
if (rawEntityData["tap_action"] != null) {
|
||||||
|
if (rawEntityData["tap_action"] is String) {
|
||||||
|
tapAction = rawEntityData["tap_action"];
|
||||||
|
} else {
|
||||||
|
tapAction =
|
||||||
|
rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo;
|
||||||
|
tapNavigationPath = rawEntityData["tap_action"]["navigation_path"];
|
||||||
|
tapService = rawEntityData["tap_action"]["service"];
|
||||||
|
tapServiceData = rawEntityData["tap_action"]["service_data"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawEntityData["hold_action"] != null) {
|
||||||
|
if (rawEntityData["hold_action"] is String) {
|
||||||
|
holdAction = rawEntityData["hold_action"];
|
||||||
|
} else {
|
||||||
|
holdAction =
|
||||||
|
rawEntityData["hold_action"]["action"] ?? EntityUIAction.none;
|
||||||
|
holdNavigationPath = rawEntityData["hold_action"]["navigation_path"];
|
||||||
|
holdService = rawEntityData["hold_action"]["service"];
|
||||||
|
holdServiceData = rawEntityData["hold_action"]["service_data"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawEntityData["double_tap_action"] != null) {
|
||||||
|
if (rawEntityData["double_tap_action"] is String) {
|
||||||
|
doubleTapAction = rawEntityData["double_tap_action"];
|
||||||
|
} else {
|
||||||
|
doubleTapAction =
|
||||||
|
rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none;
|
||||||
|
doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"];
|
||||||
|
doubleTapService = rawEntityData["double_tap_action"]["service"];
|
||||||
|
doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -134,8 +134,10 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpBrightness = value.round();
|
_tmpBrightness = value.round();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
min: 1.0,
|
min: 1,
|
||||||
max: 255.0,
|
max: 255,
|
||||||
|
divisions: 254,
|
||||||
|
label: '${val?.toInt() ?? ''}',
|
||||||
onChangeEnd: (value) => _setBrightness(entity, value),
|
onChangeEnd: (value) => _setBrightness(entity, value),
|
||||||
value: val,
|
value: val,
|
||||||
leading: Icon(Icons.brightness_5),
|
leading: Icon(Icons.brightness_5),
|
||||||
@ -155,10 +157,12 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpWhiteValue = value.round();
|
_tmpWhiteValue = value.round();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
min: 0.0,
|
min: 0,
|
||||||
max: 255.0,
|
max: 255,
|
||||||
|
divisions: 255,
|
||||||
|
label: '$_tmpWhiteValue',
|
||||||
onChangeEnd: (value) => _setWhiteValue(entity, value),
|
onChangeEnd: (value) => _setWhiteValue(entity, value),
|
||||||
value: _tmpWhiteValue == null ? 0.0 : _tmpWhiteValue.toDouble(),
|
value: _tmpWhiteValue?.toDouble() ?? 0.0,
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:file-word-box")),
|
||||||
title: "White",
|
title: "White",
|
||||||
);
|
);
|
||||||
@ -188,6 +192,8 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||||
max: entity.maxMireds,
|
max: entity.maxMireds,
|
||||||
min: entity.minMireds,
|
min: entity.minMireds,
|
||||||
|
divisions: (entity.maxMireds - entity.minMireds).toInt(),
|
||||||
|
label: '$_tmpColorTemp',
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_changedHere = true;
|
_changedHere = true;
|
||||||
|
@ -26,11 +26,11 @@ class TimerEntity extends Entity {
|
|||||||
seconds: int.tryParse(durationList[2]) ?? 0
|
seconds: int.tryParse(durationList[2]) ?? 0
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Logger.e("Strange $entityId duration format: $durationSource");
|
Logger.e("Strange timer duration format: $durationSource");
|
||||||
duration = Duration(seconds: 0);
|
duration = Duration(seconds: 0);
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
Logger.e("Error parsing duration for $entityId: $e", stacktrace: stacktrace);
|
Logger.e("Error parsing timer duration \'$durationSource\': $e", stacktrace: stacktrace);
|
||||||
duration = Duration(seconds: 0);
|
duration = Duration(seconds: 0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -13,9 +13,10 @@ class UniversalSlider extends StatefulWidget {
|
|||||||
final double max;
|
final double max;
|
||||||
final double value;
|
final double value;
|
||||||
final int divisions;
|
final int divisions;
|
||||||
|
final String label;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
|
|
||||||
const UniversalSlider({Key key, this.onChanged, this.onChangeStart, this.activeColor, this.divisions, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
const UniversalSlider({Key key, this.onChanged, this.label, this.onChangeStart, this.activeColor, this.divisions, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() {
|
State<StatefulWidget> createState() {
|
||||||
@ -28,10 +29,10 @@ class UniversalSliderState extends State<UniversalSlider> {
|
|||||||
|
|
||||||
double _value;
|
double _value;
|
||||||
bool _changeStarted = false;
|
bool _changeStarted = false;
|
||||||
|
bool _changedHere = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_value = widget.value;
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +40,11 @@ class UniversalSliderState extends State<UniversalSlider> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List <Widget> row = [];
|
List <Widget> row = [];
|
||||||
List <Widget> col = [];
|
List <Widget> col = [];
|
||||||
|
if (!_changedHere) {
|
||||||
|
_value = widget.value;
|
||||||
|
} else {
|
||||||
|
_changedHere = false;
|
||||||
|
}
|
||||||
if (widget.leading != null) {
|
if (widget.leading != null) {
|
||||||
row.add(widget.leading);
|
row.add(widget.leading);
|
||||||
}
|
}
|
||||||
@ -49,6 +55,7 @@ class UniversalSliderState extends State<UniversalSlider> {
|
|||||||
min: widget.min ?? 0,
|
min: widget.min ?? 0,
|
||||||
max: widget.max ?? 100,
|
max: widget.max ?? 100,
|
||||||
activeColor: widget.activeColor,
|
activeColor: widget.activeColor,
|
||||||
|
label: widget.label,
|
||||||
onChangeStart: (value) {
|
onChangeStart: (value) {
|
||||||
_changeStarted = true;
|
_changeStarted = true;
|
||||||
widget.onChangeStart?.call(value);
|
widget.onChangeStart?.call(value);
|
||||||
@ -57,6 +64,7 @@ class UniversalSliderState extends State<UniversalSlider> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_value = value;
|
_value = value;
|
||||||
|
_changedHere = true;
|
||||||
});
|
});
|
||||||
widget.onChanged?.call(value);
|
widget.onChanged?.call(value);
|
||||||
},
|
},
|
||||||
@ -64,6 +72,7 @@ class UniversalSliderState extends State<UniversalSlider> {
|
|||||||
_changeStarted = false;
|
_changeStarted = false;
|
||||||
setState(() {
|
setState(() {
|
||||||
_value = value;
|
_value = value;
|
||||||
|
_changedHere = true;
|
||||||
});
|
});
|
||||||
Timer(Duration(milliseconds: 500), () {
|
Timer(Duration(milliseconds: 500), () {
|
||||||
if (!_changeStarted) {
|
if (!_changeStarted) {
|
||||||
|
@ -14,7 +14,7 @@ class HomeAssistant {
|
|||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
String _userName;
|
String _userName;
|
||||||
String _lovelaceDashbordUrl;
|
String currentDashboardPath;
|
||||||
HSVColor savedColor;
|
HSVColor savedColor;
|
||||||
int savedPlayerPosition;
|
int savedPlayerPosition;
|
||||||
String sendToPlayerId;
|
String sendToPlayerId;
|
||||||
@ -29,8 +29,6 @@ class HomeAssistant {
|
|||||||
var _rawUserInfo;
|
var _rawUserInfo;
|
||||||
var _rawPanels;
|
var _rawPanels;
|
||||||
|
|
||||||
set lovelaceDashboardUrl(String val) => _lovelaceDashbordUrl = val;
|
|
||||||
|
|
||||||
List<Panel> panels = [];
|
List<Panel> panels = [];
|
||||||
|
|
||||||
Duration fetchTimeout = Duration(seconds: 30);
|
Duration fetchTimeout = Duration(seconds: 30);
|
||||||
@ -63,7 +61,7 @@ class HomeAssistant {
|
|||||||
_fetchCompleter = Completer();
|
_fetchCompleter = Completer();
|
||||||
List<Future> futures = [];
|
List<Future> futures = [];
|
||||||
if (!uiOnly) {
|
if (!uiOnly) {
|
||||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost);
|
||||||
futures.add(_getStates(null));
|
futures.add(_getStates(null));
|
||||||
futures.add(_getConfig(null));
|
futures.add(_getConfig(null));
|
||||||
futures.add(_getUserInfo(null));
|
futures.add(_getUserInfo(null));
|
||||||
@ -77,7 +75,6 @@ class HomeAssistant {
|
|||||||
if (isComponentEnabled('mobile_app')) {
|
if (isComponentEnabled('mobile_app')) {
|
||||||
_createUI();
|
_createUI();
|
||||||
_fetchCompleter.complete();
|
_fetchCompleter.complete();
|
||||||
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
|
||||||
} else {
|
} else {
|
||||||
_fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
|
_fetchCompleter.completeError(HACException("Mobile app component not found", actions: [HAErrorAction.tryAgain(), HAErrorAction(type: HAErrorActionType.URL ,title: "Help",url: "http://ha-client.app/docs#mobile-app-integration")]));
|
||||||
}
|
}
|
||||||
@ -92,7 +89,7 @@ class HomeAssistant {
|
|||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
bool cached = prefs.getBool('cached');
|
bool cached = prefs.getBool('cached');
|
||||||
if (cached != null && cached) {
|
if (cached != null && cached) {
|
||||||
if (entities == null) entities = EntityCollection(ConnectionManager().httpWebHost);
|
if (entities == null) entities = EntityCollection(AppSettings().httpWebHost);
|
||||||
try {
|
try {
|
||||||
_getStates(prefs);
|
_getStates(prefs);
|
||||||
if (!autoUi) {
|
if (!autoUi) {
|
||||||
@ -163,7 +160,7 @@ class HomeAssistant {
|
|||||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) {
|
if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) {
|
||||||
try {
|
try {
|
||||||
var data = json.decode(sharedPrefs.getString('cached_states'));
|
var data = json.decode(sharedPrefs.getString('cached_states'));
|
||||||
_parseStates(data);
|
_parseStates(data ?? []);
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
Logger.e('Error getting cached states: $e', stacktrace: stacktrace);
|
Logger.e('Error getting cached states: $e', stacktrace: stacktrace);
|
||||||
}
|
}
|
||||||
@ -194,9 +191,9 @@ class HomeAssistant {
|
|||||||
} else {
|
} else {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
var additionalData;
|
var additionalData;
|
||||||
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
if (AppSettings().haVersion >= 107 && currentDashboardPath != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||||
additionalData = {
|
additionalData = {
|
||||||
'url_path': _lovelaceDashbordUrl
|
'url_path': currentDashboardPath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ConnectionManager().sendSocketMessage(
|
ConnectionManager().sendSocketMessage(
|
||||||
@ -224,7 +221,7 @@ class HomeAssistant {
|
|||||||
if (prefs != null && prefs.containsKey('cached_services')) {
|
if (prefs != null && prefs.containsKey('cached_services')) {
|
||||||
try {
|
try {
|
||||||
var data = json.decode(prefs.getString('cached_services'));
|
var data = json.decode(prefs.getString('cached_services'));
|
||||||
_parseServices(data);
|
_parseServices(data ?? {});
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
Logger.e(e, stacktrace: stacktrace);
|
Logger.e(e, stacktrace: stacktrace);
|
||||||
}
|
}
|
||||||
@ -243,7 +240,7 @@ class HomeAssistant {
|
|||||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) {
|
if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) {
|
||||||
try {
|
try {
|
||||||
var data = json.decode(sharedPrefs.getString('cached_user'));
|
var data = json.decode(sharedPrefs.getString('cached_user'));
|
||||||
_parseUserInfo(data);
|
_parseUserInfo(data ?? {});
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
Logger.e('Error getting cached user info: $e', stacktrace: stacktrace);
|
Logger.e('Error getting cached user info: $e', stacktrace: stacktrace);
|
||||||
}
|
}
|
||||||
@ -264,7 +261,7 @@ class HomeAssistant {
|
|||||||
if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) {
|
if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) {
|
||||||
try {
|
try {
|
||||||
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
var data = json.decode(sharedPrefs.getString('cached_panels'));
|
||||||
_parsePanels(data);
|
_parsePanels(data ?? {});
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
Logger.e(e, stacktrace: stacktrace);
|
Logger.e(e, stacktrace: stacktrace);
|
||||||
panels.clear();
|
panels.clear();
|
||||||
|
@ -58,7 +58,6 @@ part 'entities/fan/fan_entity.class.dart';
|
|||||||
part 'entities/automation/automation_entity.class.dart';
|
part 'entities/automation/automation_entity.class.dart';
|
||||||
part 'entities/camera/camera_entity.class.dart';
|
part 'entities/camera/camera_entity.class.dart';
|
||||||
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
|
||||||
part 'entities/badge.widget.dart';
|
|
||||||
part 'entities/entity_model.widget.dart';
|
part 'entities/entity_model.widget.dart';
|
||||||
part 'entities/default_entity_container.widget.dart';
|
part 'entities/default_entity_container.widget.dart';
|
||||||
part 'entities/missed_entity.widget.dart';
|
part 'entities/missed_entity.widget.dart';
|
||||||
@ -143,6 +142,7 @@ part 'cards/horizontal_srack_card.dart';
|
|||||||
part 'cards/markdown_card.dart';
|
part 'cards/markdown_card.dart';
|
||||||
part 'cards/media_control_card.dart';
|
part 'cards/media_control_card.dart';
|
||||||
part 'cards/unsupported_card.dart';
|
part 'cards/unsupported_card.dart';
|
||||||
|
part 'cards/light_card.dart';
|
||||||
part 'cards/error_card.dart';
|
part 'cards/error_card.dart';
|
||||||
part 'cards/vertical_stack_card.dart';
|
part 'cards/vertical_stack_card.dart';
|
||||||
part 'cards/glance_card.dart';
|
part 'cards/glance_card.dart';
|
||||||
@ -153,15 +153,15 @@ part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
|
|||||||
part 'pages/whats_new.page.dart';
|
part 'pages/whats_new.page.dart';
|
||||||
part 'pages/fullscreen.page.dart';
|
part 'pages/fullscreen.page.dart';
|
||||||
part 'popups.dart';
|
part 'popups.dart';
|
||||||
|
part 'cards/badges.dart';
|
||||||
|
part 'managers/app_settings.dart';
|
||||||
|
|
||||||
EventBus eventBus = new EventBus();
|
EventBus eventBus = new EventBus();
|
||||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||||
const String appName = 'HA Client';
|
const String appName = 'HA Client';
|
||||||
const appVersionNumber = '1.0.1';
|
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
|
||||||
final String appVersionAdd = secrets['version_type'] ?? '';
|
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0.md';
|
||||||
final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd';
|
|
||||||
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.md';
|
|
||||||
|
|
||||||
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||||
// Print the exception to the console.
|
// Print the exception to the console.
|
||||||
@ -186,12 +186,11 @@ void main() async {
|
|||||||
};
|
};
|
||||||
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
AppSettings().loadAppTheme();
|
||||||
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
|
||||||
|
|
||||||
runZoned(() {
|
runZoned(() {
|
||||||
runApp(new HAClientApp(
|
runApp(new HAClientApp(
|
||||||
theme: theme,
|
theme: AppSettings().appTheme,
|
||||||
));
|
));
|
||||||
}, onError: (error, stack) {
|
}, onError: (error, stack) {
|
||||||
_reportError(error, stack);
|
_reportError(error, stack);
|
||||||
@ -253,7 +252,6 @@ class _HAClientAppState extends State<HAClientApp> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This widget is the root of your application.
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new MaterialApp(
|
return new MaterialApp(
|
||||||
@ -286,7 +284,7 @@ class _HAClientAppState extends State<HAClientApp> {
|
|||||||
"/quick-start": (context) => QuickStartPage(),
|
"/quick-start": (context) => QuickStartPage(),
|
||||||
"/haclient_zha": (context) => ZhaPage(),
|
"/haclient_zha": (context) => ZhaPage(),
|
||||||
"/auth": (context) => new standaloneWebview.WebviewScaffold(
|
"/auth": (context) => new standaloneWebview.WebviewScaffold(
|
||||||
url: "${ConnectionManager().oauthUrl}",
|
url: "${AppSettings().oauthUrl}",
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.help),
|
icon: Icon(Icons.help),
|
||||||
|
132
lib/managers/app_settings.dart
Normal file
132
lib/managers/app_settings.dart
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class AppSettings {
|
||||||
|
|
||||||
|
static final AppSettings _instance = AppSettings._internal();
|
||||||
|
|
||||||
|
factory AppSettings() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettings._internal();
|
||||||
|
|
||||||
|
String mobileAppDeviceName;
|
||||||
|
String _domain;
|
||||||
|
String _port;
|
||||||
|
String displayHostname;
|
||||||
|
String webSocketAPIEndpoint;
|
||||||
|
String httpWebHost;
|
||||||
|
String longLivedToken;
|
||||||
|
String tempToken;
|
||||||
|
String oauthUrl;
|
||||||
|
String webhookId;
|
||||||
|
double haVersion;
|
||||||
|
bool scrollBadges;
|
||||||
|
int appIntegrationVersion;
|
||||||
|
AppTheme appTheme;
|
||||||
|
final int defaultLocationUpdateIntervalMinutes = 20;
|
||||||
|
Duration locationUpdateInterval;
|
||||||
|
bool locationTrackingEnabled = false;
|
||||||
|
|
||||||
|
bool get isAuthenticated => longLivedToken != null;
|
||||||
|
bool get isTempAuthenticated => tempToken != null;
|
||||||
|
|
||||||
|
loadAppTheme() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
appTheme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future load(bool full) async {
|
||||||
|
if (full) {
|
||||||
|
Logger.d('Loading settings...');
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
_domain = prefs.getString('hassio-domain');
|
||||||
|
_port = prefs.getString('hassio-port');
|
||||||
|
webhookId = prefs.getString('app-webhook-id');
|
||||||
|
mobileAppDeviceName = prefs.getString('app-integration-device-name');
|
||||||
|
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
||||||
|
scrollBadges = prefs.getBool('scroll-badges') ?? true;
|
||||||
|
displayHostname = "$_domain:$_port";
|
||||||
|
webSocketAPIEndpoint =
|
||||||
|
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||||
|
httpWebHost =
|
||||||
|
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
||||||
|
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
||||||
|
defaultLocationUpdateIntervalMinutes);
|
||||||
|
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||||
|
Logger.d('Done. $_domain:$_port');
|
||||||
|
try {
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
longLivedToken = await storage.read(key: "hacl_llt");
|
||||||
|
Logger.d("Long-lived token read successful");
|
||||||
|
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
||||||
|
'https://ha-client.app')}&redirect_uri=${Uri
|
||||||
|
.encodeComponent(
|
||||||
|
'https://ha-client.app/service/auth_callback.html')}";
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future save(Map<String, dynamic> settings) async {
|
||||||
|
if (settings != null && settings.isNotEmpty) {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
settings.forEach((k,v) async {
|
||||||
|
if (v is String) {
|
||||||
|
await prefs.setString(k, v);
|
||||||
|
} else if (v is bool) {
|
||||||
|
await prefs.setBool(k ,v);
|
||||||
|
} else if (v is int) {
|
||||||
|
await prefs.setInt(k ,v);
|
||||||
|
} else if (v is double) {
|
||||||
|
await prefs.setDouble(k, v);
|
||||||
|
} else {
|
||||||
|
Logger.e('Unknown setting type: <$k, $v>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future startAuth() {
|
||||||
|
return AuthManager().start(
|
||||||
|
oauthUrl: oauthUrl
|
||||||
|
).then((token) {
|
||||||
|
Logger.d("Token from AuthManager recived");
|
||||||
|
tempToken = token;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future clearTokens() async {
|
||||||
|
longLivedToken = null;
|
||||||
|
tempToken = null;
|
||||||
|
try {
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
await storage.delete(key: "hacl_llt");
|
||||||
|
} catch(e, stacktrace) {
|
||||||
|
Logger.e("Error clearing tokens: $e", stacktrace: stacktrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future saveLongLivedToken(token) async {
|
||||||
|
longLivedToken = token;
|
||||||
|
tempToken = null;
|
||||||
|
try {
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
await storage.write(key: "hacl_llt", value: "$longLivedToken");
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setBool("oauth-used", true);
|
||||||
|
} catch(e, stacktrace) {
|
||||||
|
Logger.e("Error saving long-lived token: $e", stacktrace: stacktrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isNotConfigured() {
|
||||||
|
return _domain == null && _port == null && webhookId == null && mobileAppDeviceName == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSomethingMissed() {
|
||||||
|
return (_domain == null) || (_port == null) || (_domain.isEmpty) || (_port.isEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,24 +10,11 @@ class ConnectionManager {
|
|||||||
|
|
||||||
ConnectionManager._internal();
|
ConnectionManager._internal();
|
||||||
|
|
||||||
String _domain;
|
|
||||||
String _port;
|
|
||||||
String displayHostname;
|
|
||||||
String _webSocketAPIEndpoint;
|
|
||||||
String httpWebHost;
|
|
||||||
String _token;
|
|
||||||
String _tempToken;
|
|
||||||
String oauthUrl;
|
|
||||||
String webhookId;
|
|
||||||
double haVersion;
|
|
||||||
String mobileAppDeviceName;
|
|
||||||
bool settingsLoaded = false;
|
|
||||||
int appIntegrationVersion;
|
|
||||||
bool get isAuthenticated => _token != null;
|
|
||||||
StreamSubscription _socketSubscription;
|
StreamSubscription _socketSubscription;
|
||||||
Duration connectTimeout = Duration(seconds: 15);
|
Duration connectTimeout = Duration(seconds: 15);
|
||||||
|
|
||||||
bool isConnected = false;
|
bool isConnected = false;
|
||||||
|
bool settingsLoaded = false;
|
||||||
|
|
||||||
var onStateChangeCallback;
|
var onStateChangeCallback;
|
||||||
var onLovelaceUpdatedCallback;
|
var onLovelaceUpdatedCallback;
|
||||||
@ -37,69 +24,27 @@ class ConnectionManager {
|
|||||||
int _currentMessageId = 0;
|
int _currentMessageId = 0;
|
||||||
Map<String, Completer> _messageResolver = {};
|
Map<String, Completer> _messageResolver = {};
|
||||||
|
|
||||||
Future init({bool loadSettings, bool forceReconnect: false}) async {
|
Future init({bool loadSettings, bool forceReconnect: false}) {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
bool stopInit = false;
|
AppSettings().load(loadSettings).then((_) {
|
||||||
if (loadSettings) {
|
Logger.d('Checking config...');
|
||||||
Logger.d("Loading settings...");
|
if (AppSettings().isNotConfigured()) {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
Logger.d('This is first start');
|
||||||
_domain = prefs.getString('hassio-domain');
|
|
||||||
_port = prefs.getString('hassio-port');
|
|
||||||
webhookId = prefs.getString('app-webhook-id');
|
|
||||||
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
|
||||||
mobileAppDeviceName = prefs.getString('app-integration-device-name');
|
|
||||||
displayHostname = "$_domain:$_port";
|
|
||||||
_webSocketAPIEndpoint =
|
|
||||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
|
||||||
httpWebHost =
|
|
||||||
"${prefs.getString('hassio-res-protocol')}://$_domain:$_port";
|
|
||||||
Logger.d('$_domain$_port');
|
|
||||||
if (_domain == null && _port == null && webhookId == null && mobileAppDeviceName == null) {
|
|
||||||
completer.completeError(HACNotSetUpException());
|
completer.completeError(HACNotSetUpException());
|
||||||
stopInit = true;
|
} else if (AppSettings().isSomethingMissed()) {
|
||||||
} else if ((_domain == null) || (_port == null) ||
|
|
||||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
|
||||||
completer.completeError(HACException.checkConnectionSettings());
|
completer.completeError(HACException.checkConnectionSettings());
|
||||||
stopInit = true;
|
} else if (!AppSettings().isAuthenticated) {
|
||||||
} else {
|
settingsLoaded = true;
|
||||||
final storage = new FlutterSecureStorage();
|
AppSettings().startAuth().then((_) {
|
||||||
try {
|
|
||||||
_token = await storage.read(key: "hacl_llt");
|
|
||||||
Logger.d("Long-lived token read successful");
|
|
||||||
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
|
|
||||||
'https://ha-client.app')}&redirect_uri=${Uri
|
|
||||||
.encodeComponent(
|
|
||||||
'https://ha-client.app/service/auth_callback.html')}";
|
|
||||||
settingsLoaded = true;
|
|
||||||
} catch (e, stacktrace) {
|
|
||||||
completer.completeError(HACException("Error reading login details", actions: [HAErrorAction.tryAgain(type: HAErrorActionType.FULL_RELOAD), HAErrorAction.loginAgain()]));
|
|
||||||
Logger.e("Error reading secure storage: $e", stacktrace: stacktrace);
|
|
||||||
stopInit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ((_domain == null) || (_port == null) ||
|
|
||||||
(_domain.isEmpty) || (_port.isEmpty)) {
|
|
||||||
completer.completeError(HACException.checkConnectionSettings());
|
|
||||||
stopInit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stopInit) {
|
|
||||||
if (_token == null) {
|
|
||||||
AuthManager().start(
|
|
||||||
oauthUrl: oauthUrl
|
|
||||||
).then((token) {
|
|
||||||
Logger.d("Token from AuthManager recived");
|
|
||||||
_tempToken = token;
|
|
||||||
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
completer.completeError(e);
|
completer.completeError(e);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
settingsLoaded = true;
|
||||||
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
_doConnect(completer: completer, forceReconnect: forceReconnect);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
@ -141,7 +86,7 @@ class ConnectionManager {
|
|||||||
Logger.d("Socket connecting...");
|
Logger.d("Socket connecting...");
|
||||||
try {
|
try {
|
||||||
_socket = IOWebSocketChannel.connect(
|
_socket = IOWebSocketChannel.connect(
|
||||||
_webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
AppSettings().webSocketAPIEndpoint, pingInterval: Duration(seconds: 15));
|
||||||
_socketSubscription = _socket.stream.listen(
|
_socketSubscription = _socket.stream.listen(
|
||||||
(message) {
|
(message) {
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
@ -157,9 +102,9 @@ class ConnectionManager {
|
|||||||
} else if (data["type"] == "auth_ok") {
|
} else if (data["type"] == "auth_ok") {
|
||||||
String v = data["ha_version"];
|
String v = data["ha_version"];
|
||||||
if (v != null && v.isNotEmpty) {
|
if (v != null && v.isNotEmpty) {
|
||||||
haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
|
AppSettings().haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0;
|
||||||
}
|
}
|
||||||
Logger.d("Home assistant version: $v ($haVersion)");
|
Logger.d("Home assistant version: $v (${AppSettings().haVersion})");
|
||||||
Crashlytics.instance.setString('ha_version', v);
|
Crashlytics.instance.setString('ha_version', v);
|
||||||
Logger.d("[Connection] Subscribing to events");
|
Logger.d("[Connection] Subscribing to events");
|
||||||
sendSocketMessage(
|
sendSocketMessage(
|
||||||
@ -172,7 +117,7 @@ class ConnectionManager {
|
|||||||
).whenComplete((){
|
).whenComplete((){
|
||||||
_messageResolver["auth"]?.complete();
|
_messageResolver["auth"]?.complete();
|
||||||
_messageResolver.remove("auth");
|
_messageResolver.remove("auth");
|
||||||
if (_token != null) {
|
if (AppSettings().isAuthenticated) {
|
||||||
if (!connecting.isCompleted) connecting.complete();
|
if (!connecting.isCompleted) connecting.complete();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -266,21 +211,21 @@ class ConnectionManager {
|
|||||||
|
|
||||||
Future _authenticate() {
|
Future _authenticate() {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
if (_token != null) {
|
if (AppSettings().isAuthenticated) {
|
||||||
Logger.d( "Long-lived token exist");
|
Logger.d( "Long-lived token exist");
|
||||||
Logger.d( "[Sending] ==> auth request");
|
Logger.d( "[Sending] ==> auth request");
|
||||||
sendSocketMessage(
|
sendSocketMessage(
|
||||||
type: "auth",
|
type: "auth",
|
||||||
additionalData: {"access_token": "$_token"},
|
additionalData: {"access_token": "${AppSettings().longLivedToken}"},
|
||||||
auth: true
|
auth: true
|
||||||
).then((_) {
|
).then((_) {
|
||||||
completer.complete();
|
completer.complete();
|
||||||
}).catchError((e) => completer.completeError(e));
|
}).catchError((e) => completer.completeError(e));
|
||||||
} else if (_tempToken != null) {
|
} else if (AppSettings().isTempAuthenticated != null) {
|
||||||
Logger.d("We have temp token. Loging in...");
|
Logger.d("We have temp token. Loging in...");
|
||||||
sendSocketMessage(
|
sendSocketMessage(
|
||||||
type: "auth",
|
type: "auth",
|
||||||
additionalData: {"access_token": "$_tempToken"},
|
additionalData: {"access_token": "${AppSettings().tempToken}"},
|
||||||
auth: true
|
auth: true
|
||||||
).then((_) {
|
).then((_) {
|
||||||
Logger.d("Requesting long-lived token...");
|
Logger.d("Requesting long-lived token...");
|
||||||
@ -298,35 +243,17 @@ class ConnectionManager {
|
|||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future logout() {
|
Future logout() async {
|
||||||
Logger.d("Logging out");
|
Logger.d("Logging out");
|
||||||
Completer completer = Completer();
|
await _disconnect();
|
||||||
_disconnect().whenComplete(() {
|
await AppSettings().clearTokens();
|
||||||
_token = null;
|
|
||||||
_tempToken = null;
|
|
||||||
final storage = new FlutterSecureStorage();
|
|
||||||
storage.delete(key: "hacl_llt").whenComplete((){
|
|
||||||
completer.complete();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getLongLivedToken() {
|
Future _getLongLivedToken() {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
|
sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app ${DateTime.now().millisecondsSinceEpoch}", "lifespan": 365}).then((data) {
|
||||||
Logger.d("Got long-lived token.");
|
Logger.d("Got long-lived token.");
|
||||||
_token = data;
|
AppSettings().saveLongLivedToken(data);
|
||||||
_tempToken = null;
|
|
||||||
final storage = new FlutterSecureStorage();
|
|
||||||
storage.write(key: "hacl_llt", value: "$_token").then((_) {
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
prefs.setBool("oauth-used", true);
|
|
||||||
completer.complete();
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
completer.completeError(HACException("Authentication error: $e", actions: [HAErrorAction.reload(title: "Retry"), HAErrorAction.loginAgain(title: "Relogin")]));
|
||||||
});
|
});
|
||||||
@ -398,11 +325,11 @@ class ConnectionManager {
|
|||||||
DateTime now = DateTime.now();
|
DateTime now = DateTime.now();
|
||||||
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
//String endTime = formatDate(now, [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||||
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
String startTime = formatDate(now.subtract(Duration(hours: 24)), [yyyy, '-', mm, '-', dd, 'T', HH, ':', nn, ':', ss, z]);
|
||||||
String url = "$httpWebHost/api/history/period/$startTime?&filter_entity_id=$entityId";
|
String url = "${AppSettings().httpWebHost}/api/history/period/$startTime?&filter_entity_id=$entityId";
|
||||||
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
|
Logger.d("[Sending] ==> HTTP /api/history/period/$startTime?&filter_entity_id=$entityId");
|
||||||
http.Response historyResponse;
|
http.Response historyResponse;
|
||||||
historyResponse = await http.get(url, headers: {
|
historyResponse = await http.get(url, headers: {
|
||||||
"authorization": "Bearer $_token",
|
"authorization": "Bearer ${AppSettings().longLivedToken}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
});
|
});
|
||||||
var history = json.decode(historyResponse.body);
|
var history = json.decode(historyResponse.body);
|
||||||
@ -416,14 +343,14 @@ class ConnectionManager {
|
|||||||
|
|
||||||
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
|
Future sendHTTPPost({String endPoint, String data, String contentType: "application/json", bool includeAuthHeader: true}) async {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
String url = "$httpWebHost$endPoint";
|
String url = "${AppSettings().httpWebHost}$endPoint";
|
||||||
Logger.d("[Sending] ==> HTTP $endPoint");
|
Logger.d("[Sending] ==> HTTP $endPoint");
|
||||||
Map<String, String> headers = {};
|
Map<String, String> headers = {};
|
||||||
if (contentType != null) {
|
if (contentType != null) {
|
||||||
headers["Content-Type"] = contentType;
|
headers["Content-Type"] = contentType;
|
||||||
}
|
}
|
||||||
if (includeAuthHeader) {
|
if (includeAuthHeader) {
|
||||||
headers["authorization"] = "Bearer $_token";
|
headers["authorization"] = "Bearer ${AppSettings().longLivedToken}";
|
||||||
}
|
}
|
||||||
http.post(
|
http.post(
|
||||||
url,
|
url,
|
||||||
|
@ -13,68 +13,53 @@ class LocationManager {
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final int defaultUpdateIntervalMinutes = 20;
|
|
||||||
final String backgroundTaskId = "haclocationtask0";
|
final String backgroundTaskId = "haclocationtask0";
|
||||||
final String backgroundTaskTag = "haclocation";
|
final String backgroundTaskTag = "haclocation";
|
||||||
Duration _updateInterval;
|
|
||||||
bool _isRunning;
|
|
||||||
|
|
||||||
void init() async {
|
void init() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
if (AppSettings().locationTrackingEnabled) {
|
||||||
await prefs.reload();
|
|
||||||
_updateInterval = Duration(minutes: prefs.getInt("location-interval") ??
|
|
||||||
defaultUpdateIntervalMinutes);
|
|
||||||
_isRunning = prefs.getBool("location-enabled") ?? false;
|
|
||||||
if (_isRunning) {
|
|
||||||
await _startLocationService();
|
await _startLocationService();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSettings(bool enabled, int interval) async {
|
setSettings(bool enabled, int interval) async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
if (interval != AppSettings().locationUpdateInterval.inMinutes) {
|
||||||
if (interval != _updateInterval.inMinutes) {
|
await _stopLocationService();
|
||||||
prefs.setInt("location-interval", interval);
|
|
||||||
_updateInterval = Duration(minutes: interval);
|
|
||||||
if (_isRunning) {
|
|
||||||
Logger.d("Stopping location tracking...");
|
|
||||||
_isRunning = false;
|
|
||||||
await _stopLocationService();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (enabled && !_isRunning) {
|
AppSettings().save({
|
||||||
|
'location-interval': interval,
|
||||||
|
'location-enabled': enabled
|
||||||
|
});
|
||||||
|
AppSettings().locationUpdateInterval = Duration(minutes: interval);
|
||||||
|
AppSettings().locationTrackingEnabled = enabled;
|
||||||
|
if (enabled && !AppSettings().locationTrackingEnabled) {
|
||||||
Logger.d("Starting location tracking");
|
Logger.d("Starting location tracking");
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
prefs.setBool("location-enabled", enabled);
|
|
||||||
_isRunning = true;
|
|
||||||
await _startLocationService();
|
await _startLocationService();
|
||||||
} else if (!enabled && _isRunning) {
|
} else if (!enabled && AppSettings().locationTrackingEnabled) {
|
||||||
Logger.d("Stopping location tracking...");
|
Logger.d("Stopping location tracking...");
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
prefs.setBool("location-enabled", enabled);
|
|
||||||
_isRunning = false;
|
|
||||||
await _stopLocationService();
|
await _stopLocationService();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_startLocationService() async {
|
_startLocationService() async {
|
||||||
String webhookId = ConnectionManager().webhookId;
|
String webhookId = AppSettings().webhookId;
|
||||||
String httpWebHost = ConnectionManager().httpWebHost;
|
String httpWebHost = AppSettings().httpWebHost;
|
||||||
if (webhookId != null && webhookId.isNotEmpty) {
|
if (webhookId != null && webhookId.isNotEmpty) {
|
||||||
Duration interval;
|
Duration interval;
|
||||||
int delayFactor;
|
int delayFactor;
|
||||||
int taskCount;
|
int taskCount;
|
||||||
Logger.d("Starting location update for every ${_updateInterval
|
Logger.d("Starting location update for every ${AppSettings().locationUpdateInterval
|
||||||
.inMinutes} minutes...");
|
.inMinutes} minutes...");
|
||||||
if (_updateInterval.inMinutes == 10) {
|
if (AppSettings().locationUpdateInterval.inMinutes == 10) {
|
||||||
interval = Duration(minutes: 20);
|
interval = Duration(minutes: 20);
|
||||||
taskCount = 2;
|
taskCount = 2;
|
||||||
delayFactor = 10;
|
delayFactor = 10;
|
||||||
} else if (_updateInterval.inMinutes == 5) {
|
} else if (AppSettings().locationUpdateInterval.inMinutes == 5) {
|
||||||
interval = Duration(minutes: 15);
|
interval = Duration(minutes: 15);
|
||||||
taskCount = 3;
|
taskCount = 3;
|
||||||
delayFactor = 5;
|
delayFactor = 5;
|
||||||
} else {
|
} else {
|
||||||
interval = _updateInterval;
|
interval = AppSettings().locationUpdateInterval;
|
||||||
taskCount = 1;
|
taskCount = 1;
|
||||||
delayFactor = 0;
|
delayFactor = 0;
|
||||||
}
|
}
|
||||||
@ -112,8 +97,8 @@ class LocationManager {
|
|||||||
Logger.d("[Foreground location] Started");
|
Logger.d("[Foreground location] Started");
|
||||||
Geolocator geolocator = Geolocator();
|
Geolocator geolocator = Geolocator();
|
||||||
var battery = Battery();
|
var battery = Battery();
|
||||||
String webhookId = ConnectionManager().webhookId;
|
String webhookId = AppSettings().webhookId;
|
||||||
String httpWebHost = ConnectionManager().httpWebHost;
|
String httpWebHost = AppSettings().httpWebHost;
|
||||||
if (webhookId != null && webhookId.isNotEmpty) {
|
if (webhookId != null && webhookId.isNotEmpty) {
|
||||||
Logger.d("[Foreground location] Getting battery level...");
|
Logger.d("[Foreground location] Getting battery level...");
|
||||||
int batteryLevel = await battery.batteryLevel;
|
int batteryLevel = await battery.batteryLevel;
|
||||||
|
@ -12,7 +12,7 @@ class MobileAppIntegrationManager {
|
|||||||
"os_version": DeviceInfoManager().osVersion,
|
"os_version": DeviceInfoManager().osVersion,
|
||||||
"app_data": {
|
"app_data": {
|
||||||
"push_token": "",
|
"push_token": "",
|
||||||
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/sendPushNotification"
|
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV2"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,9 +25,9 @@ class MobileAppIntegrationManager {
|
|||||||
|
|
||||||
static Future checkAppRegistration() {
|
static Future checkAppRegistration() {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
_appRegistrationData["device_name"] = ConnectionManager().mobileAppDeviceName ?? getDefaultDeviceName();
|
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName();
|
||||||
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
|
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
|
||||||
if (ConnectionManager().webhookId == null) {
|
if (AppSettings().webhookId == null) {
|
||||||
Logger.d("Mobile app was not registered yet. Registering...");
|
Logger.d("Mobile app was not registered yet. Registering...");
|
||||||
var registrationData = Map.from(_appRegistrationData);
|
var registrationData = Map.from(_appRegistrationData);
|
||||||
registrationData.addAll({
|
registrationData.addAll({
|
||||||
@ -36,7 +36,7 @@ class MobileAppIntegrationManager {
|
|||||||
"os_name": DeviceInfoManager().osName,
|
"os_name": DeviceInfoManager().osName,
|
||||||
"supports_encryption": false,
|
"supports_encryption": false,
|
||||||
});
|
});
|
||||||
if (ConnectionManager().haVersion >= 104) {
|
if (AppSettings().haVersion >= 104) {
|
||||||
registrationData.addAll({
|
registrationData.addAll({
|
||||||
"device_id": "${DeviceInfoManager().unicDeviceId}"
|
"device_id": "${DeviceInfoManager().unicDeviceId}"
|
||||||
});
|
});
|
||||||
@ -48,12 +48,12 @@ class MobileAppIntegrationManager {
|
|||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("Processing registration responce...");
|
Logger.d("Processing registration responce...");
|
||||||
var responseObject = json.decode(response);
|
var responseObject = json.decode(response);
|
||||||
ConnectionManager().webhookId = responseObject["webhook_id"];
|
AppSettings().webhookId = responseObject["webhook_id"];
|
||||||
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
|
AppSettings().appIntegrationVersion = INTEGRATION_VERSION;
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
AppSettings().save({
|
||||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
'app-webhook-id': responseObject["webhook_id"],
|
||||||
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
|
'app-integration-version': INTEGRATION_VERSION
|
||||||
|
}).then((prefs) {
|
||||||
completer.complete();
|
completer.complete();
|
||||||
eventBus.fire(ShowPopupEvent(
|
eventBus.fire(ShowPopupEvent(
|
||||||
popup: Popup(
|
popup: Popup(
|
||||||
@ -84,7 +84,7 @@ class MobileAppIntegrationManager {
|
|||||||
"data": _appRegistrationData
|
"data": _appRegistrationData
|
||||||
};
|
};
|
||||||
ConnectionManager().sendHTTPPost(
|
ConnectionManager().sendHTTPPost(
|
||||||
endPoint: "/api/webhook/${ConnectionManager().webhookId}",
|
endPoint: "/api/webhook/${AppSettings().webhookId}",
|
||||||
includeAuthHeader: false,
|
includeAuthHeader: false,
|
||||||
data: json.encode(updateData)
|
data: json.encode(updateData)
|
||||||
).then((response) {
|
).then((response) {
|
||||||
@ -98,7 +98,7 @@ class MobileAppIntegrationManager {
|
|||||||
Logger.w("No registration data in response. MobileApp integration was removed or broken");
|
Logger.w("No registration data in response. MobileApp integration was removed or broken");
|
||||||
_askToRegisterApp();
|
_askToRegisterApp();
|
||||||
} else {
|
} else {
|
||||||
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
|
if (INTEGRATION_VERSION > AppSettings().appIntegrationVersion) {
|
||||||
Logger.d('App registration needs to be updated');
|
Logger.d('App registration needs to be updated');
|
||||||
_askToRemoveAndRegisterApp();
|
_askToRemoveAndRegisterApp();
|
||||||
} else {
|
} else {
|
||||||
@ -128,14 +128,11 @@ class MobileAppIntegrationManager {
|
|||||||
popup: Popup(
|
popup: Popup(
|
||||||
title: "App integration is not working properly",
|
title: "App integration is not working properly",
|
||||||
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue. You can try to remove Mobile App integration from Home Assistant and restart server to fix this issue.",
|
body: "Something wrong with HA Client integration on your Home Assistant server. Please report this issue. You can try to remove Mobile App integration from Home Assistant and restart server to fix this issue.",
|
||||||
positiveText: "Report to GitHub",
|
positiveText: "Report issue",
|
||||||
negativeText: "Report to Discord",
|
negativeText: "Close",
|
||||||
onPositive: () {
|
onPositive: () {
|
||||||
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
|
||||||
},
|
}
|
||||||
onNegative: () {
|
|
||||||
Launcher.launchURLInBrowser("https://discord.gg/u9vq7QE");
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -11,19 +11,27 @@ class StartupUserMessagesManager {
|
|||||||
|
|
||||||
StartupUserMessagesManager._internal();
|
StartupUserMessagesManager._internal();
|
||||||
|
|
||||||
bool _supportAppDevelopmentMessageShown;
|
bool _needToshowDonateMessage;
|
||||||
bool _whatsNewMessageShown;
|
bool _whatsNewMessageShown;
|
||||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
static final _donateMsgTimerKey = "user-msg-donate-timer";
|
||||||
|
static final _donateMsgShownKey = "user-msg-donate-shpown";
|
||||||
static final _whatsNewMessageKey = "user-msg-whats-new-url";
|
static final _whatsNewMessageKey = "user-msg-whats-new-url";
|
||||||
|
|
||||||
void checkMessagesToShow() async {
|
void checkMessagesToShow() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.reload();
|
await prefs.reload();
|
||||||
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
var tInt = prefs.getInt(_donateMsgTimerKey);
|
||||||
|
if (tInt == null) {
|
||||||
|
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
|
||||||
|
_needToshowDonateMessage = false;
|
||||||
|
} else {
|
||||||
|
bool wasShown = prefs.getBool(_donateMsgShownKey) ?? false;
|
||||||
|
_needToshowDonateMessage = (Duration(milliseconds: DateTime.now().millisecondsSinceEpoch - tInt).inDays >= 14) && !wasShown;
|
||||||
|
}
|
||||||
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
|
_whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl;
|
||||||
if (!_whatsNewMessageShown) {
|
if (!_whatsNewMessageShown) {
|
||||||
_showWhatsNewMessage();
|
_showWhatsNewMessage();
|
||||||
} else if (!_supportAppDevelopmentMessageShown) {
|
} else if (_needToshowDonateMessage) {
|
||||||
_showSupportAppDevelopmentMessage();
|
_showSupportAppDevelopmentMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,16 +42,16 @@ class StartupUserMessagesManager {
|
|||||||
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. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
|
body: "As you may have noticed this app contains no ads. Also all app features are available for you for free. I'm not planning to change this in nearest future, but still you can support this application development materially. There is one-time payment available as well as several subscription options. Thanks.",
|
||||||
positiveText: "Show options",
|
positiveText: "Show options",
|
||||||
negativeText: "Cancel",
|
negativeText: "Later",
|
||||||
onPositive: () {
|
onPositive: () {
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
prefs.setBool(_donateMsgShownKey, true);
|
||||||
eventBus.fire(ShowPageEvent(path: "/putchase"));
|
eventBus.fire(ShowPageEvent(path: "/putchase"));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onNegative: () {
|
onNegative: () {
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
prefs.setBool(_supportAppDevelopmentMessageKey, true);
|
prefs.setInt(_donateMsgTimerKey, DateTime.now().millisecondsSinceEpoch);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -99,10 +99,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_subscribe().then((_) {
|
_subscribe().then((_) {
|
||||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
HomeAssistant().lovelaceDashboardUrl = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
|
||||||
_fetchData(useCache: true);
|
_fetchData(useCache: true);
|
||||||
LocationManager();
|
LocationManager();
|
||||||
StartupUserMessagesManager().checkMessagesToShow();
|
StartupUserMessagesManager().checkMessagesToShow();
|
||||||
|
MobileAppIntegrationManager.checkAppRegistration();
|
||||||
});
|
});
|
||||||
}, onError: (e) {
|
}, onError: (e) {
|
||||||
if (e is HACNotSetUpException) {
|
if (e is HACNotSetUpException) {
|
||||||
@ -246,7 +247,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
void _showOAuth() {
|
void _showOAuth() {
|
||||||
_preventAppRefresh = true;
|
_preventAppRefresh = true;
|
||||||
Navigator.of(context).pushNamed("/auth", arguments: {"url": ConnectionManager().oauthUrl});
|
Navigator.of(context).pushNamed("/auth", arguments: {"url": AppSettings().oauthUrl});
|
||||||
}
|
}
|
||||||
|
|
||||||
_setErrorState(HACException e) {
|
_setErrorState(HACException e) {
|
||||||
@ -449,8 +450,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
Widget _buildScaffoldBody(bool empty) {
|
Widget _buildScaffoldBody(bool empty) {
|
||||||
List<PopupMenuItem<String>> serviceMenuItems = [];
|
List<Entity> activePlayers = [];
|
||||||
List<PopupMenuItem<String>> mediaMenuItems = [];
|
List<Entity> activeLights = [];
|
||||||
|
Color mediaMenuIconColor;
|
||||||
|
Color lightMenuIconColor;
|
||||||
|
|
||||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||||
if (_previousViewCount != currentViewCount) {
|
if (_previousViewCount != currentViewCount) {
|
||||||
@ -459,72 +462,25 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_previousViewCount = currentViewCount;
|
_previousViewCount = currentViewCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceMenuItems.add(PopupMenuItem<String>(
|
if (AppSettings().isAuthenticated) {
|
||||||
child: new Text("Reload"),
|
|
||||||
value: "reload",
|
|
||||||
));
|
|
||||||
if (ConnectionManager().isAuthenticated) {
|
|
||||||
_showLoginButton = false;
|
_showLoginButton = false;
|
||||||
serviceMenuItems.add(
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
child: new Text("Logout"),
|
|
||||||
value: "logout",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Widget mediaMenuIcon;
|
|
||||||
int playersCount = 0;
|
|
||||||
if (!empty && !HomeAssistant().entities.isEmpty) {
|
if (!empty && !HomeAssistant().entities.isEmpty) {
|
||||||
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
activePlayers = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
|
||||||
playersCount = activePlayers.length;
|
activeLights = HomeAssistant().entities.getByDomains(includeDomains: ["light"], stateFiler: [EntityState.on]);
|
||||||
mediaMenuItems.addAll(
|
|
||||||
activePlayers.map((entity) => PopupMenuItem<String>(
|
|
||||||
child: Text(
|
|
||||||
"${entity.displayName}",
|
|
||||||
style: Theme.of(context).textTheme.body1.copyWith(
|
|
||||||
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
value: "${entity.entityId}",
|
|
||||||
)).toList()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
mediaMenuItems.addAll([
|
|
||||||
PopupMenuItem<String>(
|
if (activePlayers.isNotEmpty) {
|
||||||
child: new Text("Play media..."),
|
mediaMenuIconColor = Theme.of(context).accentColor;
|
||||||
value: "play_media",
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
if (playersCount > 0) {
|
|
||||||
mediaMenuIcon = Stack(
|
|
||||||
overflow: Overflow.visible,
|
|
||||||
children: <Widget>[
|
|
||||||
Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
"mdi:television"), color: Colors.white,),
|
|
||||||
Positioned(
|
|
||||||
bottom: -4,
|
|
||||||
right: -4,
|
|
||||||
child: Container(
|
|
||||||
height: 16,
|
|
||||||
width: 16,
|
|
||||||
decoration: new BoxDecoration(
|
|
||||||
color: Colors.orange,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
"$playersCount",
|
|
||||||
style: Theme.of(context).textTheme.caption.copyWith(
|
|
||||||
color: Colors.white
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
|
mediaMenuIconColor = Theme.of(context).primaryIconTheme.color;
|
||||||
"mdi:television"), color: Colors.white,);
|
}
|
||||||
|
if (activeLights.isNotEmpty) {
|
||||||
|
lightMenuIconColor = Theme.of(context).accentColor;
|
||||||
|
} else {
|
||||||
|
lightMenuIconColor = Theme.of(context).primaryIconTheme.color;
|
||||||
}
|
}
|
||||||
Widget mainScrollBody;
|
Widget mainScrollBody;
|
||||||
if (empty) {
|
if (empty) {
|
||||||
@ -575,46 +531,34 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
|
snap: false,
|
||||||
primary: true,
|
primary: true,
|
||||||
title: Text(HomeAssistant().locationName ?? ""),
|
title: Text(HomeAssistant().locationName ?? ""),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
IconButton(
|
PopupMenuButton(
|
||||||
icon: mediaMenuIcon,
|
child: Padding(
|
||||||
onPressed: () {
|
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||||
showMenu(
|
child: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 50, 0),
|
"mdi:dots-vertical"), color: Theme.of(context).primaryIconTheme.color)
|
||||||
context: context,
|
),
|
||||||
items: mediaMenuItems
|
itemBuilder: (BuildContext context) {
|
||||||
).then((String val) {
|
List<PopupMenuEntry<String>> result = [
|
||||||
if (val == "play_media") {
|
PopupMenuItem<String>(
|
||||||
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
|
child: new Text("Reload"),
|
||||||
} else if (val != null) {
|
value: "reload",
|
||||||
_showEntityPage(val);
|
)
|
||||||
}
|
];
|
||||||
});
|
if (AppSettings().isAuthenticated) {
|
||||||
|
result.addAll([
|
||||||
|
PopupMenuDivider(),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: new Text("Logout"),
|
||||||
|
value: "logout",
|
||||||
|
)]);
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
|
||||||
"mdi:dots-vertical"), color: Colors.white,),
|
|
||||||
onPressed: () {
|
|
||||||
showMenu(
|
|
||||||
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + Sizes.iconSize + 20, 0.0, 0.0),
|
|
||||||
context: context,
|
|
||||||
items: serviceMenuItems
|
|
||||||
).then((String val) {
|
|
||||||
HomeAssistant().lovelaceDashboardUrl = HomeAssistant.DEFAULT_DASHBOARD;
|
|
||||||
if (val == "reload") {
|
|
||||||
|
|
||||||
_quickLoad();
|
|
||||||
} else if (val == "logout") {
|
|
||||||
HomeAssistant().logout().then((_) {
|
|
||||||
_quickLoad();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.menu),
|
icon: Icon(Icons.menu),
|
||||||
@ -622,6 +566,99 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_scaffoldKey.currentState.openDrawer();
|
_scaffoldKey.currentState.openDrawer();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
expandedHeight: 130,
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
title: Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 15),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||||
|
child: Icon(
|
||||||
|
MaterialDesignIcons.getIconDataFromIconName("mdi:television"),
|
||||||
|
color: mediaMenuIconColor,
|
||||||
|
size: 20,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onSelected: (String val) {
|
||||||
|
if (val == "play_media") {
|
||||||
|
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
|
||||||
|
} else if (val != null) {
|
||||||
|
_showEntityPage(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
List<PopupMenuEntry<String>> result = [
|
||||||
|
PopupMenuDivider(),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: new Text("Play media..."),
|
||||||
|
value: "play_media",
|
||||||
|
)
|
||||||
|
];
|
||||||
|
if (activePlayers.isNotEmpty) {
|
||||||
|
result.insertAll(0,
|
||||||
|
activePlayers.map((entity) => PopupMenuItem<String>(
|
||||||
|
child: Text(
|
||||||
|
"${entity.displayName}",
|
||||||
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
value: "${entity.entityId}",
|
||||||
|
)).toList()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result.insert(0, PopupMenuItem<String>(
|
||||||
|
child: new Text("No active players"),
|
||||||
|
value: "_",
|
||||||
|
enabled: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||||
|
child: Icon(
|
||||||
|
MaterialDesignIcons.getIconDataFromIconName("mdi:lightbulb-outline"),
|
||||||
|
color: lightMenuIconColor,
|
||||||
|
size: 20
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onSelected: (String val) {
|
||||||
|
if (val == 'turn_off_all') {
|
||||||
|
ConnectionManager().callService(
|
||||||
|
service: 'turn_off',
|
||||||
|
domain: 'light',
|
||||||
|
entityId: 'all'
|
||||||
|
);
|
||||||
|
} else if (val == 'turn_on_all') {
|
||||||
|
ConnectionManager().callService(
|
||||||
|
service: 'turn_on',
|
||||||
|
domain: 'light',
|
||||||
|
entityId: 'all'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: new Text("Turn on all lights"),
|
||||||
|
value: "turn_on_all",
|
||||||
|
),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
child: new Text("Turn off all ligts"),
|
||||||
|
value: "turn_off_all",
|
||||||
|
enabled: activeLights.isNotEmpty,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
) ),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
bottom: empty ? null : TabBar(
|
bottom: empty ? null : TabBar(
|
||||||
controller: _viewsTabController,
|
controller: _viewsTabController,
|
||||||
tabs: buildUIViewTabs(),
|
tabs: buildUIViewTabs(),
|
||||||
|
@ -39,7 +39,7 @@ class _AppSettingsPageState extends State<AppSettingsPage> {
|
|||||||
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
|
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
|
||||||
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
|
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
|
||||||
];
|
];
|
||||||
if (ConnectionManager().webhookId != null) {
|
if (AppSettings().webhookId != null) {
|
||||||
items.insert(1, _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings));
|
items.insert(1, _buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings));
|
||||||
}
|
}
|
||||||
return ListView(
|
return ListView(
|
||||||
|
@ -31,7 +31,7 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_loadSettings() async {
|
_loadSettings() async {
|
||||||
_includeDeviceName = widget.quickStart || ConnectionManager().webhookId == null;
|
_includeDeviceName = widget.quickStart || AppSettings().webhookId == null;
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
String domain = prefs.getString('hassio-domain')?? '';
|
String domain = prefs.getString('hassio-domain')?? '';
|
||||||
String port = prefs.getString('hassio-port') ?? '';
|
String port = prefs.getString('hassio-port') ?? '';
|
||||||
|
@ -11,7 +11,7 @@ class IntegrationSettingsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||||
|
|
||||||
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
|
int _locationInterval = AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||||
bool _locationTrackingEnabled = false;
|
bool _locationTrackingEnabled = false;
|
||||||
bool _wait = false;
|
bool _wait = false;
|
||||||
|
|
||||||
@ -28,7 +28,8 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||||
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
|
_locationInterval = prefs.getInt("location-interval") ??
|
||||||
|
AppSettings().defaultLocationUpdateIntervalMinutes;
|
||||||
if (_locationInterval % 5 != 0) {
|
if (_locationInterval % 5 != 0) {
|
||||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
_locationInterval = 5 * (_locationInterval ~/ 5);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ class LookAndFeelSettingsPage extends StatefulWidget {
|
|||||||
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||||
|
|
||||||
AppTheme _currentTheme;
|
AppTheme _currentTheme;
|
||||||
bool _changed = false;
|
bool _scrollBadges = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -26,13 +26,15 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
|||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index];
|
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index];
|
||||||
|
_scrollBadges = prefs.getBool('scroll-badges') ?? true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveSettings(AppTheme theme) {
|
_saveTheme(AppTheme theme) {
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
prefs.setInt('app-theme', theme.index);
|
prefs.setInt('app-theme', theme.index);
|
||||||
|
prefs.setBool('scroll-badges', _scrollBadges);
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentTheme = theme;
|
_currentTheme = theme;
|
||||||
eventBus.fire(ChangeThemeEvent(_currentTheme));
|
eventBus.fire(ChangeThemeEvent(_currentTheme));
|
||||||
@ -40,6 +42,12 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _saveOther() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
AppSettings().scrollBadges = _scrollBadges;
|
||||||
|
await prefs.setBool('scroll-badges', _scrollBadges);
|
||||||
|
}
|
||||||
|
|
||||||
Map appThemeName = {
|
Map appThemeName = {
|
||||||
AppTheme.defaultTheme: 'Default',
|
AppTheme.defaultTheme: 'Default',
|
||||||
AppTheme.haTheme: 'Home Assistant theme',
|
AppTheme.haTheme: 'Home Assistant theme',
|
||||||
@ -59,15 +67,35 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
|||||||
iconSize: 30.0,
|
iconSize: 30.0,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
style: Theme.of(context).textTheme.title,
|
style: Theme.of(context).textTheme.title,
|
||||||
//hint: Text("Select ${caption.toLowerCase()}"),
|
|
||||||
items: AppTheme.values.map((value) {
|
items: AppTheme.values.map((value) {
|
||||||
return new DropdownMenuItem<AppTheme>(
|
return new DropdownMenuItem<AppTheme>(
|
||||||
value: value,
|
value: value,
|
||||||
child: Text('${appThemeName[value]}'),
|
child: Text('${appThemeName[value]}'),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (theme) => _saveSettings(theme),
|
onChanged: (theme) => _saveTheme(theme),
|
||||||
)
|
),
|
||||||
|
Container(height: Sizes.doubleRowPadding),
|
||||||
|
Text("Badges display:", style: Theme.of(context).textTheme.body2),
|
||||||
|
Container(height: Sizes.rowPadding),
|
||||||
|
DropdownButton<bool>(
|
||||||
|
value: _scrollBadges,
|
||||||
|
iconSize: 30.0,
|
||||||
|
isExpanded: true,
|
||||||
|
style: Theme.of(context).textTheme.title,
|
||||||
|
items: [true, false].map((value) {
|
||||||
|
return new DropdownMenuItem<bool>(
|
||||||
|
value: value,
|
||||||
|
child: Text('${value ? 'Horizontal scroll' : 'In rows'}'),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() {
|
||||||
|
_scrollBadges = val;
|
||||||
|
});
|
||||||
|
_saveOther();
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
LinkToWebConfig(name: "Home Assistant configuration", url: ConnectionManager().httpWebHost+"/config"),
|
LinkToWebConfig(name: "Home Assistant configuration", url: AppSettings().httpWebHost+"/config"),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -31,14 +31,15 @@ class Panel {
|
|||||||
if (componentName.startsWith('haclient')) {
|
if (componentName.startsWith('haclient')) {
|
||||||
Navigator.of(context).pushNamed(urlPath);
|
Navigator.of(context).pushNamed(urlPath);
|
||||||
} else if (componentName == 'lovelace') {
|
} else if (componentName == 'lovelace') {
|
||||||
HomeAssistant().lovelaceDashboardUrl = this.urlPath;
|
HomeAssistant().currentDashboardPath = this.urlPath;
|
||||||
|
Logger.d('Switching to dashboard: ${this.urlPath}');
|
||||||
HomeAssistant().autoUi = false;
|
HomeAssistant().autoUi = false;
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
prefs.setString('lovelace_dashboard_url', this.urlPath);
|
prefs.setString('lovelace_dashboard_url', this.urlPath);
|
||||||
eventBus.fire(ReloadUIEvent());
|
eventBus.fire(ReloadUIEvent());
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Launcher.launchAuthenticatedWebView(context: context, url: "${ConnectionManager().httpWebHost}/$urlPath", title: "${this.title}");
|
Launcher.launchAuthenticatedWebView(context: context, url: "${AppSettings().httpWebHost}/$urlPath", title: "${this.title}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,14 +153,14 @@ class RegisterAppPopup extends Popup {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(20),
|
padding: EdgeInsets.all(20),
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
initialValue: ConnectionManager().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(),
|
initialValue: AppSettings().mobileAppDeviceName ?? MobileAppIntegrationManager.getDefaultDeviceName(),
|
||||||
onSaved: (newValue) {
|
onSaved: (newValue) {
|
||||||
String deviceName = newValue?.trim();
|
String deviceName = newValue?.trim();
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
prefs.remove("app-webhook-id");
|
prefs.remove("app-webhook-id");
|
||||||
prefs.setString('app-integration-device-name', deviceName);
|
prefs.setString('app-integration-device-name', deviceName);
|
||||||
ConnectionManager().webhookId = null;
|
AppSettings().webhookId = null;
|
||||||
ConnectionManager().mobileAppDeviceName = deviceName;
|
AppSettings().mobileAppDeviceName = deviceName;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
MobileAppIntegrationManager.checkAppRegistration();
|
MobileAppIntegrationManager.checkAppRegistration();
|
||||||
});
|
});
|
||||||
|
49
lib/ui.dart
49
lib/ui.dart
@ -16,7 +16,7 @@ class HomeAssistantUI {
|
|||||||
int viewCounter = 0;
|
int viewCounter = 0;
|
||||||
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
|
Logger.d("--Views count: ${rawLovelaceConfig['views'].length}");
|
||||||
rawLovelaceConfig["views"].forEach((rawView){
|
rawLovelaceConfig["views"].forEach((rawView){
|
||||||
Logger.d("----view id: ${rawView['id']}");
|
Logger.d("----view: ${rawView['path'] ?? viewCounter}");
|
||||||
HAView view = HAView(
|
HAView view = HAView(
|
||||||
count: viewCounter,
|
count: viewCounter,
|
||||||
rawData: rawView
|
rawData: rawView
|
||||||
@ -30,8 +30,42 @@ class HomeAssistantUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map _generateLovelaceConfig() {
|
Map _generateLovelaceConfig() {
|
||||||
Map result = {};
|
Map result = {
|
||||||
result['title'] = 'Home';
|
'title': 'Home'
|
||||||
|
};
|
||||||
|
List<Entity> left = HomeAssistant().entities.getByDomains(
|
||||||
|
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
|
||||||
|
);
|
||||||
|
List<Map> cards = [];
|
||||||
|
Map<String, Map> cardsByDomains = {};
|
||||||
|
left.forEach((Entity entity) {
|
||||||
|
if (entity is GroupEntity) {
|
||||||
|
cards.add({
|
||||||
|
'type': CardType.ENTITIES,
|
||||||
|
'title': entity.displayName,
|
||||||
|
'entities': entity.childEntities.map((e) => e.entityId)
|
||||||
|
});
|
||||||
|
} else if (entity is MediaPlayerEntity) {
|
||||||
|
cards.add({
|
||||||
|
'type': CardType.MEDIA_CONTROL,
|
||||||
|
'entity': entity.entityId
|
||||||
|
});
|
||||||
|
} else if (entity is AlarmControlPanelEntity) {
|
||||||
|
cards.add({
|
||||||
|
'type': CardType.ALARM_PANEL,
|
||||||
|
'entity': entity.entityId
|
||||||
|
});
|
||||||
|
} else if (cardsByDomains.containsKey(entity.domain)) {
|
||||||
|
cardsByDomains[entity.domain]['entities'].add(entity.entityId);
|
||||||
|
} else {
|
||||||
|
cardsByDomains[entity.domain] = {
|
||||||
|
'type': 'entities',
|
||||||
|
'entities': [entity.entityId],
|
||||||
|
'title': entity.domain
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cards.addAll(cardsByDomains.values);
|
||||||
result['views'] = [
|
result['views'] = [
|
||||||
{
|
{
|
||||||
'icon': 'mdi:home',
|
'icon': 'mdi:home',
|
||||||
@ -40,14 +74,7 @@ class HomeAssistantUI {
|
|||||||
).map(
|
).map(
|
||||||
(en) => en.entityId
|
(en) => en.entityId
|
||||||
).toList(),
|
).toList(),
|
||||||
'cards': [{
|
'cards': cards
|
||||||
'type': 'entities',
|
|
||||||
'entities': HomeAssistant().entities.getByDomains(
|
|
||||||
excludeDomains: ['sensor','binary_sensor', 'device_tracker', 'person', 'sun']
|
|
||||||
).map(
|
|
||||||
(en) => en.entityId
|
|
||||||
).toList()
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
return result;
|
return result;
|
||||||
|
@ -45,7 +45,7 @@ class Launcher {
|
|||||||
if (viewState.type == standaloneWebview.WebViewState.startLoad) {
|
if (viewState.type == standaloneWebview.WebViewState.startLoad) {
|
||||||
Logger.d("[WebView] Injecting external auth JS");
|
Logger.d("[WebView] Injecting external auth JS");
|
||||||
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
rootBundle.loadString('assets/js/externalAuth.js').then((js){
|
||||||
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", ConnectionManager()._token));
|
flutterWebViewPlugin.evalJavascript(js.replaceFirst("[token]", AppSettings().longLivedToken));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,10 +2,11 @@ part of 'main.dart';
|
|||||||
|
|
||||||
class HAView {
|
class HAView {
|
||||||
List<CardData> cards = [];
|
List<CardData> cards = [];
|
||||||
List<Entity> badges = [];
|
CardData badges;
|
||||||
Entity linkedEntity;
|
Entity linkedEntity;
|
||||||
String name;
|
String name;
|
||||||
String id;
|
String id;
|
||||||
|
String path;
|
||||||
String iconName;
|
String iconName;
|
||||||
final int count;
|
final int count;
|
||||||
bool isPanel;
|
bool isPanel;
|
||||||
@ -15,29 +16,18 @@ class HAView {
|
|||||||
name = rawData['title'];
|
name = rawData['title'];
|
||||||
iconName = rawData['icon'];
|
iconName = rawData['icon'];
|
||||||
isPanel = rawData['panel'] ?? false;
|
isPanel = rawData['panel'] ?? false;
|
||||||
|
path = rawData['path'] ?? '$count';
|
||||||
|
|
||||||
if (rawData['badges'] != null && rawData['badges'] is List) {
|
if (rawData['badges'] != null && !isPanel) {
|
||||||
rawData['badges'].forEach((entity) {
|
badges = CardData.parse({
|
||||||
if (entity is String) {
|
'type': CardType.BADGES,
|
||||||
if (HomeAssistant().entities.isExist(entity)) {
|
'badges': rawData['badges']
|
||||||
Entity e = HomeAssistant().entities.get(entity);
|
|
||||||
badges.add(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String eId = '${entity['entity']}';
|
|
||||||
if (HomeAssistant().entities.isExist(eId)) {
|
|
||||||
Entity e = HomeAssistant().entities.get(eId);
|
|
||||||
badges.add(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
(rawData["cards"] ?? []).forEach((rawCardData) {
|
(rawData['cards'] ?? []).forEach((rawCardData) {
|
||||||
cards.add(CardData.parse(rawCardData));
|
cards.add(CardData.parse(rawCardData));
|
||||||
});
|
});
|
||||||
|
|
||||||
//cards.addAll(_createLovelaceCards(rawData["cards"] ?? [], 1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTab() {
|
Widget buildTab() {
|
||||||
|
@ -10,14 +10,17 @@ class ViewWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Widget cardsContainer;
|
||||||
|
Widget badgesContainer;
|
||||||
if (this.view.isPanel) {
|
if (this.view.isPanel) {
|
||||||
return FractionallySizedBox(
|
cardsContainer = _buildPanelChild(context);
|
||||||
widthFactor: 1,
|
badgesContainer = Container(width: 0, height: 0);
|
||||||
heightFactor: 1,
|
|
||||||
child: _buildPanelChild(context),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
Widget cardsContainer;
|
if (this.view.badges != null && this.view.badges is BadgesData) {
|
||||||
|
badgesContainer = this.view.badges.buildCardWidget();
|
||||||
|
} else {
|
||||||
|
badgesContainer = Container(width: 0, height: 0);
|
||||||
|
}
|
||||||
if (this.view.cards.isNotEmpty) {
|
if (this.view.cards.isNotEmpty) {
|
||||||
cardsContainer = DynamicMultiColumnLayout(
|
cardsContainer = DynamicMultiColumnLayout(
|
||||||
minColumnWidth: Sizes.minViewColumnWidth,
|
minColumnWidth: Sizes.minViewColumnWidth,
|
||||||
@ -44,15 +47,17 @@ class ViewWidget extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
cardsContainer = Container();
|
cardsContainer = Container();
|
||||||
}
|
}
|
||||||
return ListView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
children: <Widget>[
|
|
||||||
_buildBadges(context),
|
|
||||||
cardsContainer
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
badgesContainer,
|
||||||
|
cardsContainer
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPanelChild(BuildContext context) {
|
Widget _buildPanelChild(BuildContext context) {
|
||||||
@ -63,18 +68,4 @@ class ViewWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBadges(BuildContext context) {
|
|
||||||
if (this.view.badges.isNotEmpty) {
|
|
||||||
return Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 10.0,
|
|
||||||
runSpacing: 1.0,
|
|
||||||
children: this.view.badges.map((badge) =>
|
|
||||||
badge.buildBadgeWidget(context)).toList(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Container(width: 0, height: 0,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
10
pubspec.yaml
10
pubspec.yaml
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 1.0.1+1013
|
version: 0.0.0+1144
|
||||||
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@ -14,7 +14,7 @@ dependencies:
|
|||||||
shared_preferences: ^0.5.6+1
|
shared_preferences: ^0.5.6+1
|
||||||
path_provider: ^1.6.5
|
path_provider: ^1.6.5
|
||||||
event_bus: ^1.1.1
|
event_bus: ^1.1.1
|
||||||
cached_network_image: ^2.0.0
|
cached_network_image: ^2.3.0-beta
|
||||||
url_launcher: ^5.4.1
|
url_launcher: ^5.4.1
|
||||||
date_format: ^1.0.8
|
date_format: ^1.0.8
|
||||||
charts_flutter: ^0.8.1
|
charts_flutter: ^0.8.1
|
||||||
@ -23,7 +23,7 @@ dependencies:
|
|||||||
flutter_custom_tabs: ^0.6.0
|
flutter_custom_tabs: ^0.6.0
|
||||||
flutter_webview_plugin: ^0.3.10+1
|
flutter_webview_plugin: ^0.3.10+1
|
||||||
webview_flutter: ^0.3.19+7
|
webview_flutter: ^0.3.19+7
|
||||||
firebase_messaging: ^6.0.13
|
firebase_messaging: ^6.0.15
|
||||||
flutter_secure_storage: ^3.3.3
|
flutter_secure_storage: ^3.3.3
|
||||||
device_info: ^0.4.1+4
|
device_info: ^0.4.1+4
|
||||||
flutter_local_notifications: ^1.1.6
|
flutter_local_notifications: ^1.1.6
|
||||||
@ -31,8 +31,8 @@ dependencies:
|
|||||||
workmanager: ^0.2.2
|
workmanager: ^0.2.2
|
||||||
battery: ^1.0.0
|
battery: ^1.0.0
|
||||||
firebase_crashlytics: ^0.1.3+3
|
firebase_crashlytics: ^0.1.3+3
|
||||||
syncfusion_flutter_core: ^18.1.43
|
syncfusion_flutter_core: ^18.1.52
|
||||||
syncfusion_flutter_gauges: ^18.1.43
|
syncfusion_flutter_gauges: ^18.1.52
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Reference in New Issue
Block a user