Compare commits

...

29 Commits

Author SHA1 Message Date
7d746fd546 896 2020-04-29 07:11:09 +00:00
3ff55f181e Fix icon_height parsing issue 2020-04-29 10:00:28 +03:00
187e12dd79 895 2020-04-29 01:19:43 +03:00
10daf2d952 0.8.5 2020-04-28 21:47:41 +00:00
31c6509d13 0.8.5 2020-04-28 21:43:48 +00:00
cb74108814 893 2020-04-28 21:12:18 +00:00
9efded2139 Possible workaround for firebase messaging crash 2020-04-28 21:09:45 +00:00
96b3e7c739 Add state_color support 2020-04-28 21:03:00 +00:00
b029146bf3 Gauge scale fixes 2020-04-28 20:36:11 +00:00
d715aaf5e5 Gauge card elements scale fix 2020-04-27 20:56:33 +00:00
0dc12963f0 Cards parsing improvements 2020-04-26 22:46:37 +00:00
4da3b40d55 WIP: Cards parsing improvements 2020-04-26 18:44:21 +00:00
f7d05a57ad Use '***' in markdawn card to add empty space 2020-04-25 21:45:58 +00:00
df01599fe0 Resloves #541 Prevent double service call when slider is moved 2020-04-25 21:24:15 +00:00
2c3335ebf3 Resolves #539 Fix button card without entity 2020-04-25 20:53:08 +00:00
05c1427aa8 Add icon support for entities card title 2020-04-25 18:23:26 +00:00
02bfaf7db6 WIP: Cards build optimization 2020-04-25 17:38:21 +00:00
f488c0810b WIP: Cards build optimization 2020-04-25 15:59:07 +00:00
8dbfb91234 Add Lovelase card widget 2020-04-25 14:53:33 +00:00
aee99e3925 Entities card build optimization 2020-04-25 14:39:15 +00:00
50d3280803 Gauge font size fix 2020-04-25 14:33:54 +00:00
a90eb5c4db Fix glance card title 2020-04-25 14:20:46 +00:00
16c06a2d48 Widget rendering improvements 2020-04-25 14:15:19 +00:00
513bf85cae Fix dashboard switching issues 2020-04-21 09:01:21 +00:00
82d7aeba02 Some cleanup 2020-04-15 18:23:29 +00:00
12f7cb86de Linear progress indicator 2020-04-15 18:21:16 +00:00
b65c885467 Bottom info bar as standalone component 2020-04-15 18:13:03 +00:00
2a828a1289 WIP: bottom info bar as separate component 2020-04-15 15:40:21 +00:00
291f12ba97 Component detection 2020-04-15 15:40:21 +00:00
48 changed files with 1535 additions and 2059 deletions

View File

@ -17,6 +17,7 @@
additional functionality it is fine to subclass or reimplement additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. --> FlutterApplication and put your custom class here. -->
<application <application
android:name="io.flutter.app.FlutterApplication"
android:label="HA Client" android:label="HA Client"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">

View File

@ -0,0 +1,62 @@
part of '../main.dart';
class AlarmPanelCard extends StatelessWidget {
final AlarmPanelCardData card;
const AlarmPanelCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
}
List<Widget> body = [];
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.entity.entity.displayState}",
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
EntityIcon(
size: 50.0,
),
Container(
width: 26.0,
child: IconButton(
padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.entity.entity))
)
)
]
),
));
body.add(
AlarmControlPanelControlsWidget(
extended: true,
states: card.states,
)
);
return CardWrapper(
child: EntityModel(
entityWrapper: card.entity,
handleTap: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: body
)
)
);
}
}

View File

@ -1,48 +1,80 @@
part of '../main.dart'; part of '../main.dart';
class HACard { class CardData {
List<EntityWrapper> entities = [];
List<HACard> childCards = [];
EntityWrapper linkedEntityWrapper;
String name;
String id;
String type;
bool showName;
bool showState;
bool showEmpty;
bool showHeaderToggle;
int columnsCount;
List stateFilter;
List states;
List conditions;
String content;
String unit;
int min;
int max;
Map severity;
HACard({ String type;
this.name, List<EntityWrapper> entities = [];
this.id, List conditions;
this.linkedEntityWrapper, bool showEmpty;
this.columnsCount: 4, List stateFilter;
this.showName: true, bool stateColor = true;
this.showHeaderToggle: true,
this.showState: true, EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
this.stateFilter: const [],
this.showEmpty: true, factory CardData.parse(Map<String, dynamic> rawData) {
this.content, switch (rawData['type']) {
this.states, case CardType.ENTITIES:
this.conditions: const [], return EntitiesCardData(rawData);
this.unit, break;
this.min, case CardType.ALARM_PANEL:
this.max, return AlarmPanelCardData(rawData);
this.severity, break;
@required this.type case CardType.BUTTON:
}) { return ButtonCardData(rawData);
if (this.columnsCount <= 0) { break;
this.columnsCount = 4; case CardType.ENTITY_BUTTON:
return ButtonCardData(rawData);
break;
case CardType.CONDITIONAL:
return CardData.parse(rawData['card']);
break;
case CardType.ENTITY_FILTER:
Map<String, dynamic> cardData = Map.from(rawData);
cardData.remove('type');
if (rawData.containsKey('card')) {
cardData.addAll(rawData['card']);
} }
cardData['type'] ??= CardType.ENTITIES;
return CardData.parse(cardData);
break;
case CardType.GAUGE:
return GaugeCardData(rawData);
break;
case CardType.GLANCE:
return GlanceCardData(rawData);
break;
case CardType.HORIZONTAL_STACK:
return HorizontalStackCardData(rawData);
break;
case CardType.VERTICAL_STACK:
return VerticalStackCardData(rawData);
break;
case CardType.MARKDOWN:
return MarkdownCardData(rawData);
break;
case CardType.MEDIA_CONTROL:
return MediaControlCardData(rawData);
break;
default:
if (rawData.containsKey('entities')) {
return EntitiesCardData(rawData);
} else if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
return EntitiesCardData(rawData);
}
return CardData(rawData);
}
}
CardData(Map<String, dynamic> rawData) {
type = rawData['type'] ?? CardType.ENTITIES;
conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true;
stateFilter = rawData['state_filter'] ?? [];
}
Widget buildCardWidget() {
return UnsupportedCard(card: this);
} }
List<EntityWrapper> getEntitiesToShow() { List<EntityWrapper> getEntitiesToShow() {
@ -114,10 +146,365 @@ class HACard {
}).toList(); }).toList();
} }
Widget build(BuildContext context) { }
return CardWidget(
card: this, class EntitiesCardData extends CardData {
String title;
String icon;
bool showHeaderToggle;
@override
Widget buildCardWidget() {
return EntitiesCard(card: this);
}
EntitiesCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
title = rawData["title"];
icon = rawData['icon'];
stateColor = rawData['state_color'] ?? false;
showHeaderToggle = rawData['show_header_toggle'] ?? false;
//Parsing entities
var rawEntities = rawData["entities"] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (rawEntity["type"] == "divider") {
entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
entities.add(
EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
stateColor: rawEntity["state_color"] ?? stateColor,
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
); );
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
stateColor: rawEntity["state_color"] ?? stateColor,
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
entities.add(
EntityWrapper(
entity: e,
stateColor: rawEntity["state_color"] ?? stateColor,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
}
}
class AlarmPanelCardData extends CardData {
String name;
List<dynamic> states;
@override
Widget buildCardWidget() {
return AlarmPanelCard(card: this);
}
AlarmPanelCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
states = rawData['states'];
//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),
stateColor: true,
overrideName: name
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
}
}
}
class ButtonCardData extends CardData {
String name;
String icon;
bool showName;
bool showIcon;
double iconHeightPx = 0;
double iconHeightRem = 0;
@override
Widget buildCardWidget() {
return EntityButtonCard(card: this);
}
ButtonCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
icon = rawData['icon'];
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
stateColor = rawData['state_color'] ?? true;
var rawHeight = rawData['icon_height'];
if (rawHeight != null && rawHeight is String) {
if (rawHeight.contains('px')) {
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
} else if (rawHeight.contains('rem')) {
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
} else if (rawHeight.contains('em')) {
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
}
}
//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,
stateColor: stateColor,
uiAction: EntityUIAction(
rawEntityData: rawData
)
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
} else if (entitiId == null) {
entities.add(
EntityWrapper(
entity: Entity.ghost(
name,
icon,
),
stateColor: stateColor,
uiAction: EntityUIAction(
rawEntityData: rawData
)
)
);
}
}
}
class GaugeCardData extends CardData {
String name;
String unit;
int min;
int max;
Map severity;
@override
Widget buildCardWidget() {
return GaugeCard(card: this);
}
GaugeCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
name = rawData['name'];
unit = rawData['unit'];
min = rawData['min'] ?? 0;
max = rawData['max'] ?? 100;
severity = rawData['severity'];
//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
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
}
}
class GlanceCardData extends CardData {
String title;
bool showName;
bool showIcon;
bool showState;
bool stateColor;
int columnsCount;
@override
Widget buildCardWidget() {
return GlanceCard(card: this);
}
GlanceCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
title = rawData["title"];
showName = rawData['show_name'] ?? true;
showIcon = rawData['show_icon'] ?? true;
showState = rawData['show_state'] ?? true;
stateColor = rawData['state_color'] ?? true;
columnsCount = rawData['columns'] ?? 4;
//Parsing entities
var rawEntities = rawData["entities"] ?? [];
rawEntities.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
entities.add(
EntityWrapper(
entity: e,
stateColor: stateColor,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
}
}
class HorizontalStackCardData extends CardData {
List<CardData> childCards;
@override
Widget buildCardWidget() {
return HorizontalStackCard(card: this);
}
HorizontalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
if (rawData.containsKey('cards')) {
childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard);
}).toList();
} else {
childCards = [];
}
}
}
class VerticalStackCardData extends CardData {
List<CardData> childCards;
@override
Widget buildCardWidget() {
return VerticalStackCard(card: this);
}
VerticalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
if (rawData.containsKey('cards')) {
childCards = rawData['cards'].map<CardData>((childCard) {
return CardData.parse(childCard);
}).toList();
} else {
childCards = [];
}
}
}
class MarkdownCardData extends CardData {
String title;
String content;
@override
Widget buildCardWidget() {
return MarkdownCard(card: this);
}
MarkdownCardData(Map<String, dynamic> rawData) : super(rawData) {
//Parsing card data
title = rawData['title'];
content = rawData['content'];
}
}
class MediaControlCardData extends CardData {
@override
Widget buildCardWidget() {
return MediaControlsCard(card: this);
}
MediaControlCardData(Map<String, dynamic> rawData) : super(rawData) {
var entitiId = rawData["entity"];
if (entitiId != null && entitiId is String) {
if (HomeAssistant().entities.isExist(entitiId)) {
entities.add(EntityWrapper(
entity: HomeAssistant().entities.get(entitiId),
));
} else {
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
}
}
} }
} }

View File

@ -1,384 +0,0 @@
part of '../main.dart';
class CardWidget extends StatelessWidget {
final HACard card;
const CardWidget({
Key key,
this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.linkedEntityWrapper!= null) {
if (card.linkedEntityWrapper.entity.isHidden) {
return Container(width: 0.0, height: 0.0,);
}
if (card.linkedEntityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: MissedEntityWidget(),
handleTap: false,
);
}
}
if (card.conditions.isNotEmpty) {
bool showCardByConditions = true;
for (var condition in card.conditions) {
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
if (conditionEntity != null &&
((condition['state'] != null && conditionEntity.state != condition['state']) ||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
) {
showCardByConditions = false;
break;
}
}
if (!showCardByConditions) {
return Container(width: 0.0, height: 0.0,);
}
}
switch (card.type) {
case CardType.ENTITIES: {
return _buildEntitiesCard(context);
}
case CardType.GLANCE: {
return _buildGlanceCard(context);
}
case CardType.MEDIA_CONTROL: {
return _buildMediaControlsCard(context);
}
case CardType.ENTITY_BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.BUTTON: {
return _buildEntityButtonCard(context);
}
case CardType.GAUGE: {
return _buildGaugeCard(context);
}
/* case CardType.LIGHT: {
return _buildLightCard(context);
}*/
case CardType.MARKDOWN: {
return _buildMarkdownCard(context);
}
case CardType.ALARM_PANEL: {
return _buildAlarmPanelCard(context);
}
case CardType.HORIZONTAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
children = card.childCards.map((childCard) => Flexible(
fit: FlexFit.tight,
child: childCard.build(context),
)
).toList();
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
case CardType.VERTICAL_STACK: {
if (card.childCards.isNotEmpty) {
List<Widget> children = card.childCards.map((childCard) => childCard.build(context)).toList();
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: children,
);
}
return Container(height: 0.0, width: 0.0,);
}
default: {
if ((card.linkedEntityWrapper == null) && (card.entities.isNotEmpty)) {
return _buildEntitiesCard(context);
} else {
return _buildUnsupportedCard(context);
}
}
}
}
Widget _buildEntitiesCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
Widget headerSwitch;
if (card.showHeaderToggle) {
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
}).map((EntityWrapper en) {
return en.entity.entityId;
}).toList();
headerSwitch = Switch(
value: headerToggleVal,
onChanged: (val) {
if (entitiesToToggle.isNotEmpty) {
ConnectionManager().callService(
domain: "homeassistant",
service: val ? "turn_on" : "turn_off",
entityId: entitiesToToggle
);
}
},
);
}
body.add(
CardHeader(
name: card.name,
trailing: headerSwitch
)
);
entitiesToShow.forEach((EntityWrapper entity) {
body.add(
Padding(
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
));
});
return Card(
child: Padding(
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding, left: Sizes.leftWidgetPadding),
child: Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildMarkdownCard(BuildContext context) {
if (card.content == null) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
body.add(CardHeader(name: card.name));
body.add(MarkdownBody(data: card.content));
return Card(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: new Column(mainAxisSize: MainAxisSize.min, children: body),
)
);
}
Widget _buildAlarmPanelCard(BuildContext context) {
List<Widget> body = [];
body.add(CardHeader(
name: card.name ?? "",
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
EntityIcon(
size: 50.0,
),
Container(
width: 26.0,
child: IconButton(
padding: EdgeInsets.all(0.0),
alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
)
)
]
),
));
body.add(
AlarmControlPanelControlsWidget(
extended: true,
states: card.states,
)
);
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: body
)
)
);
}
Widget _buildGlanceCard(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> rows = [];
rows.add(CardHeader(name: card.name));
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
rows.add(
Padding(
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
child: FractionallySizedBox(
widthFactor: 1,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
List<Widget> buttons = [];
double buttonWidth = constraints.maxWidth / columnsCount;
entitiesToShow.forEach((EntityWrapper entity) {
buttons.add(
SizedBox(
width: buttonWidth,
child: EntityModel(
entityWrapper: entity,
child: GlanceCardEntityContainer(
showName: card.showName,
showState: card.showState,
),
handleTap: true
),
)
);
});
return Wrap(
//spacing: 5.0,
//alignment: WrapAlignment.spaceEvenly,
runSpacing: Sizes.doubleRowPadding,
children: buttons,
);
}
),
),
)
);
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: rows
)
);
}
Widget _buildMediaControlsCard(BuildContext context) {
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
Widget _buildEntityButtonCard(BuildContext context) {
card.linkedEntityWrapper.overrideName = card.name?.toUpperCase() ??
card.linkedEntityWrapper.displayName.toUpperCase();
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: EntityButtonCardBody(
showName: card.showName,
),
handleTap: true
)
);
}
Widget _buildGaugeCard(BuildContext context) {
card.linkedEntityWrapper.overrideName = card.name ??
card.linkedEntityWrapper.displayName;
card.linkedEntityWrapper.unitOfMeasurementOverride = card.unit ??
card.linkedEntityWrapper.unitOfMeasurement;
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: GaugeCardBody(
min: card.min,
max: card.max,
severity: card.severity,
),
handleTap: true
)
);
}
Widget _buildLightCard(BuildContext context) {
card.linkedEntityWrapper.overrideName = card.name ??
card.linkedEntityWrapper.displayName;
return Card(
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
child: LightCardBody(
min: card.min,
max: card.max,
severity: card.severity,
),
handleTap: true
)
);
}
Widget _buildUnsupportedCard(BuildContext context) {
List<Widget> body = [];
body.add(
CardHeader(
name: card.name ?? ""
)
);
List<Widget> result = [];
if (card.linkedEntityWrapper != null) {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
child: EntityModel(
entityWrapper: card.linkedEntityWrapper,
handleTap: true,
child: card.linkedEntityWrapper.entity.buildDefaultWidget(context)
),
)
]);
} else {
result.addAll(<Widget>[
Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
),
]);
}
body.addAll(result);
return Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: body
)
);
}
}

View File

@ -0,0 +1,79 @@
part of '../main.dart';
class EntitiesCard extends StatelessWidget {
final EntitiesCardData card;
const EntitiesCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
List<Widget> body = [];
Widget headerSwitch;
if (card.showHeaderToggle) {
bool headerToggleVal = entitiesToShow.any((EntityWrapper en){ return en.entity.state == EntityState.on; });
List<String> entitiesToToggle = entitiesToShow.where((EntityWrapper enw) {
return <String>["switch", "light", "automation", "input_boolean"].contains(enw.entity.domain);
}).map((EntityWrapper en) {
return en.entity.entityId;
}).toList();
headerSwitch = Switch(
value: headerToggleVal,
onChanged: (val) {
if (entitiesToToggle.isNotEmpty) {
ConnectionManager().callService(
domain: "homeassistant",
service: val ? "turn_on" : "turn_off",
entityId: entitiesToToggle
);
}
},
);
}
body.add(
CardHeader(
name: card.title,
trailing: headerSwitch,
emptyPadding: Sizes.rowPadding,
leading: card.icon != null ? Icon(
MaterialDesignIcons.getIconDataFromIconName(card.icon),
size: Sizes.iconSize,
color: Theme.of(context).textTheme.headline.color
) : null,
)
);
body.addAll(
entitiesToShow.map((EntityWrapper entity) {
return Padding(
padding: EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0),
child: EntityModel(
entityWrapper: entity,
handleTap: true,
child: entity.entity.buildDefaultWidget(context)
),
);
})
);
return CardWrapper(
child: Padding(
padding: EdgeInsets.only(
right: Sizes.rightWidgetPadding,
left: Sizes.leftWidgetPadding,
bottom: Sizes.rowPadding,
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: body
)
),
)
);
}
}

View File

@ -0,0 +1,93 @@
part of '../main.dart';
class EntityButtonCard extends StatelessWidget {
final ButtonCardData card;
EntityButtonCard({
Key key, this.card
}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = card.entity;
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
} else if (entityWrapper.entity.statelessType != StatelessEntityType.ghost && entityWrapper.entity.statelessType != StatelessEntityType.none) {
return Container(width: 0.0, height: 0.0,);
}
double iconSize = math.max(card.iconHeightPx, card.iconHeightRem * Theme.of(context).textTheme.body1.fontSize);
Widget buttonIcon;
if (!card.showIcon) {
buttonIcon = Container(height: Sizes.rowPadding, width: 10);
} else if (iconSize > 0) {
buttonIcon = SizedBox(
height: iconSize,
child: FractionallySizedBox(
widthFactor: 0.5,
child: FittedBox(
fit: BoxFit.contain,
child: EntityIcon(
//padding: EdgeInsets.only(top: 6),
),
)
),
);
} else {
buttonIcon = AspectRatio(
aspectRatio: 2,
child: FractionallySizedBox(
widthFactor: 0.5,
child: FittedBox(
fit: BoxFit.fitWidth,
child: EntityIcon(
//padding: EdgeInsets.only(top: 6),
),
)
),
);
}
return CardWrapper(
child: EntityModel(
entityWrapper: card.entity,
child: InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buttonIcon,
_buildName(context)
],
)
),
),
handleTap: true
)
);
}
Widget _buildName(BuildContext context) {
if (card.showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
textStyle: Theme.of(context).textTheme.subhead,
wordsWrap: true,
textAlign: TextAlign.center
);
}
return Container(width: 0, height: 0);
}
}

198
lib/cards/gauge_card.dart Normal file
View File

@ -0,0 +1,198 @@
part of '../main.dart';
class GaugeCard extends StatelessWidget {
final GaugeCardData card;
GaugeCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = card.entity;
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
}
entityWrapper.overrideName = card.name ??
entityWrapper.displayName;
entityWrapper.unitOfMeasurementOverride = card.unit ??
entityWrapper.unitOfMeasurement;
double fixedValue;
double value = entityWrapper.entity.doubleState;
if (value > card.max) {
fixedValue = card.max.toDouble();
} else if (value < card.min) {
fixedValue = card.min.toDouble();
} else {
fixedValue = value;
}
List<GaugeRange> ranges;
Color currentColor;
if (card.severity != null && card.severity["green"] is int && card.severity["red"] is int && card.severity["yellow"] is int) {
List<RangeContainer> rangesList = <RangeContainer>[
RangeContainer(card.severity["green"], HAClientTheme().getGreenGaugeColor()),
RangeContainer(card.severity["red"], HAClientTheme().getRedGaugeColor()),
RangeContainer(card.severity["yellow"], HAClientTheme().getYellowGaugeColor())
];
rangesList.sort((current, next) {
if (current.startFrom > next.startFrom) {
return 1;
}
if (current.startFrom < next.startFrom) {
return -1;
}
return 0;
});
if (fixedValue < rangesList[1].startFrom) {
currentColor = rangesList[0].color;
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
currentColor = rangesList[1].color;
} else {
currentColor = rangesList[2].color;
}
ranges = [
GaugeRange(
startValue: rangesList[0].startFrom.toDouble(),
endValue: rangesList[1].startFrom.toDouble(),
color: rangesList[0].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[1].startFrom.toDouble(),
endValue: rangesList[2].startFrom.toDouble(),
color: rangesList[1].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[2].startFrom.toDouble(),
endValue: card.max.toDouble(),
color: rangesList[2].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
)
];
}
if (ranges == null) {
currentColor = Theme.of(context).primaryColorDark;
ranges = <GaugeRange>[
GaugeRange(
startValue: card.min.toDouble(),
endValue: card.max.toDouble(),
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3,
)
];
}
return CardWrapper(
padding: EdgeInsets.all(4),
child: EntityModel(
entityWrapper: entityWrapper,
child: InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: AspectRatio(
aspectRatio: 1.8,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
maximum: card.max.toDouble(),
minimum: card.min.toDouble(),
showLabels: false,
useRangeColorForAxis: true,
showTicks: false,
canScaleToFit: true,
ranges: ranges,
axisLineStyle: AxisLineStyle(
thickness: 0.3,
thicknessUnit: GaugeSizeUnit.factor,
color: Colors.transparent
),
startAngle: 180,
endAngle: 0,
pointers: <GaugePointer>[
RangePointer(
value: fixedValue,
sizeUnit: GaugeSizeUnit.factor,
width: 0.3,
color: currentColor,
enableAnimation: true,
animationType: AnimationType.bounceOut,
)
]
)
],
),
Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
flex: 8,
fit: FlexFit.tight,
child: Container()
),
Flexible(
flex: 6,
fit: FlexFit.tight,
child: FractionallySizedBox(
widthFactor: 0.4,
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.bottomCenter,
child: SimpleEntityState(
padding: EdgeInsets.all(0),
expanded: false,
maxLines: 1,
textAlign: TextAlign.center
),
)
)
),
Flexible(
flex: 3,
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.contain,
child: EntityName(
padding: EdgeInsets.all(0),
textStyle: Theme.of(context).textTheme.subhead
),
)
),
],
)
],
)
),
),
handleTap: true
)
);
}
}
class RangeContainer {
final int startFrom;
Color color;
RangeContainer(this.startFrom, this.color);
}

120
lib/cards/glance_card.dart Normal file
View File

@ -0,0 +1,120 @@
part of '../main.dart';
class GlanceCard extends StatelessWidget {
final GlanceCardData card;
const GlanceCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
List<EntityWrapper> entitiesToShow = card.getEntitiesToShow();
if (entitiesToShow.isEmpty && !card.showEmpty) {
return Container(height: 0.0, width: 0.0,);
}
int length = entitiesToShow.length;
int rowsCount;
int columnsCount;
if (length == 0) {
columnsCount = 0;
rowsCount = 0;
} else {
columnsCount = length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
rowsCount = (length / columnsCount).round();
}
List<TableRow> rows = [];
for (int i = 0; i < rowsCount; i++) {
int start = i*columnsCount;
int end = start + math.min(columnsCount, length - start);
List<Widget> rowChildren = [];
rowChildren.addAll(entitiesToShow.sublist(
start, end
).map(
(EntityWrapper entity){
return EntityModel(
entityWrapper: entity,
child: _buildEntityContainer(context, entity),
handleTap: true
);
}
).toList()
);
while (rowChildren.length < columnsCount) {
rowChildren.add(
Container()
);
}
rows.add(
TableRow(
children: rowChildren
)
);
}
return CardWrapper(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardHeader(name: card.title),
Padding(
padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding),
child: Table(
children: rows
)
)
],
)
);
}
Widget _buildEntityContainer(BuildContext context, EntityWrapper entityWrapper) {
if (entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return MissedEntityWidget();
} else if (entityWrapper.entity.statelessType != StatelessEntityType.none) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = [];
if (card.showName) {
result.add(_buildName(context));
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: Sizes.iconSize,
)
);
if (card.showState) {
result.add(_buildState());
}
return Center(
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
),
);
}
Widget _buildName(BuildContext context) {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: false,
textAlign: TextAlign.center,
textStyle: Theme.of(context).textTheme.body1,
);
}
Widget _buildState() {
return SimpleEntityState(
textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
padding: EdgeInsets.only(top: Sizes.rowPadding),
);
}
}

View File

@ -0,0 +1,30 @@
part of '../main.dart';
class HorizontalStackCard extends StatelessWidget {
final HorizontalStackCardData card;
const HorizontalStackCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.childCards.isNotEmpty) {
List<Widget> children = [];
children = card.childCards.map((childCard) => Flexible(
fit: FlexFit.tight,
child: childCard.buildCardWidget()
)
).toList();
return IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
);
}
return Container(height: 0.0, width: 0.0,);
}
}

View File

@ -0,0 +1,33 @@
part of '../main.dart';
class MarkdownCard extends StatelessWidget {
final MarkdownCardData card;
const MarkdownCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.content == null) {
return Container(height: 0.0, width: 0.0,);
} else if (card.content == '***') {
return Container(height: Sizes.rowPadding, width: 0.0,);
}
return CardWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
CardHeader(name: card.title),
MarkdownBody(
data: card.content,
)
],
),
)
);
}
}

View File

@ -0,0 +1,27 @@
part of '../main.dart';
class MediaControlsCard extends StatelessWidget {
final MediaControlCardData card;
const MediaControlsCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.entity.entity.statelessType == StatelessEntityType.missed) {
return EntityModel(
entityWrapper: card.entity,
child: MissedEntityWidget(),
handleTap: false,
);
}
return CardWrapper(
child: EntityModel(
entityWrapper: card.entity,
handleTap: null,
child: MediaPlayerWidget()
)
);
}
}

View File

@ -0,0 +1,17 @@
part of '../main.dart';
class UnsupportedCard extends StatelessWidget {
final CardData card;
const UnsupportedCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
return CardWrapper(
child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Text("'${card.type}' card is not supported yet"),
)
);
}
}

View File

@ -0,0 +1,23 @@
part of '../main.dart';
class VerticalStackCard extends StatelessWidget {
final VerticalStackCardData card;
const VerticalStackCard({Key key, this.card}) : super(key: key);
@override
Widget build(BuildContext context) {
if (card.childCards.isNotEmpty) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: card.childCards.map<Widget>(
(childCard) => childCard.buildCardWidget()
).toList(),
);
}
return Container(height: 0.0, width: 0.0,);
}
}

View File

@ -4,9 +4,11 @@ class CardHeader extends StatelessWidget {
final String name; final String name;
final Widget trailing; final Widget trailing;
final Widget leading;
final Widget subtitle; final Widget subtitle;
final double emptyPadding;
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key); const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -14,6 +16,7 @@ class CardHeader extends StatelessWidget {
if ((name != null) && (name.trim().length > 0)) { if ((name != null) && (name.trim().length > 0)) {
result = new ListTile( result = new ListTile(
trailing: trailing, trailing: trailing,
leading: leading,
subtitle: subtitle, subtitle: subtitle,
title: Text("$name", title: Text("$name",
textAlign: TextAlign.left, textAlign: TextAlign.left,
@ -21,7 +24,7 @@ class CardHeader extends StatelessWidget {
style: Theme.of(context).textTheme.headline), style: Theme.of(context).textTheme.headline),
); );
} else { } else {
result = new Container(width: 0.0, height: 0.0); result = new Container(width: 0.0, height: emptyPadding);
} }
return result; return result;
} }

View File

@ -0,0 +1,21 @@
part of '../../main.dart';
class CardWrapper extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: padding,
child: child
),
);
}
}

View File

@ -1,57 +0,0 @@
part of '../../main.dart';
class EntityButtonCardBody extends StatelessWidget {
final bool showName;
EntityButtonCardBody({
Key key, this.showName: true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: FractionallySizedBox(
widthFactor: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return EntityIcon(
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
size: constraints.maxWidth / 2.5,
);
}
),
_buildName()
],
),
),
);
}
Widget _buildName() {
if (showName) {
return EntityName(
padding: EdgeInsets.fromLTRB(Sizes.buttonPadding, 0.0, Sizes.buttonPadding, Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
maxLines: 3,
wordsWrap: true,
textAlign: TextAlign.center
);
}
return Container(width: 0, height: 0);
}
}

View File

@ -1,177 +0,0 @@
part of '../../main.dart';
class GaugeCardBody extends StatelessWidget {
final int min;
final int max;
final Map severity;
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
double fixedValue;
double value = entityWrapper.entity.doubleState;
if (value > max) {
fixedValue = max.toDouble();
} else if (value < min) {
fixedValue = min.toDouble();
} else {
fixedValue = value;
}
List<GaugeRange> ranges;
Color currentColor;
if (severity != null && severity["green"] is int && severity["red"] is int && severity["yellow"] is int) {
List<RangeContainer> rangesList = <RangeContainer>[
RangeContainer(severity["green"], HAClientTheme().getGreenGaugeColor()),
RangeContainer(severity["red"], HAClientTheme().getRedGaugeColor()),
RangeContainer(severity["yellow"], HAClientTheme().getYellowGaugeColor())
];
rangesList.sort((current, next) {
if (current.startFrom > next.startFrom) {
return 1;
}
if (current.startFrom < next.startFrom) {
return -1;
}
return 0;
});
if (fixedValue < rangesList[1].startFrom) {
currentColor = rangesList[0].color;
} else if (fixedValue < rangesList[2].startFrom && fixedValue >= rangesList[1].startFrom) {
currentColor = rangesList[1].color;
} else {
currentColor = rangesList[2].color;
}
ranges = [
GaugeRange(
startValue: rangesList[0].startFrom.toDouble(),
endValue: rangesList[1].startFrom.toDouble(),
color: rangesList[0].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[1].startFrom.toDouble(),
endValue: rangesList[2].startFrom.toDouble(),
color: rangesList[1].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
),
GaugeRange(
startValue: rangesList[2].startFrom.toDouble(),
endValue: max.toDouble(),
color: rangesList[2].color.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3
)
];
}
if (ranges == null) {
currentColor = Theme.of(context).primaryColorDark;
ranges = <GaugeRange>[
GaugeRange(
startValue: min.toDouble(),
endValue: max.toDouble(),
color: Theme.of(context).primaryColorDark.withOpacity(0.1),
sizeUnit: GaugeSizeUnit.factor,
endWidth: 0.3,
startWidth: 0.3,
)
];
}
return InkWell(
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
child: AspectRatio(
aspectRatio: 2,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double fontSizeFactor;
if (constraints.maxWidth > 300.0) {
fontSizeFactor = 1.6;
} else if (constraints.maxWidth > 150.0) {
fontSizeFactor = 1;
} else if (constraints.maxWidth > 100.0) {
fontSizeFactor = 0.6;
} else {
fontSizeFactor = 0.4;
}
return SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
maximum: max.toDouble(),
minimum: min.toDouble(),
showLabels: false,
useRangeColorForAxis: true,
showTicks: false,
canScaleToFit: true,
ranges: ranges,
axisLineStyle: AxisLineStyle(
thickness: 0.3,
thicknessUnit: GaugeSizeUnit.factor,
color: Colors.transparent
),
annotations: <GaugeAnnotation>[
GaugeAnnotation(
angle: -90,
positionFactor: 1.3,
//verticalAlignment: GaugeAlignment.far,
widget: EntityName(
textStyle: Theme.of(context).textTheme.body1.copyWith(
fontSize: Theme.of(context).textTheme.body1.fontSize * fontSizeFactor
),
),
),
GaugeAnnotation(
angle: 180,
positionFactor: 0,
verticalAlignment: GaugeAlignment.center,
widget: SimpleEntityState(
expanded: false,
maxLines: 1,
textAlign: TextAlign.center,
textStyle: Theme.of(context).textTheme.title.copyWith(
fontSize: Theme.of(context).textTheme.title.fontSize * fontSizeFactor,
),
),
)
],
startAngle: 180,
endAngle: 0,
pointers: <GaugePointer>[
RangePointer(
value: fixedValue,
sizeUnit: GaugeSizeUnit.factor,
width: 0.3,
color: currentColor,
enableAnimation: true,
animationType: AnimationType.bounceOut,
)
]
)
],
);
},
),
),
);
}
}
class RangeContainer {
final int startFrom;
Color color;
RangeContainer(this.startFrom, this.color);
}

View File

@ -1,84 +0,0 @@
part of '../../main.dart';
class GlanceCardEntityContainer extends StatelessWidget {
final bool showName;
final bool showState;
final bool nameInTheBottom;
final double iconSize;
final bool wordsWrapInName;
GlanceCardEntityContainer({
Key key,
@required this.showName,
@required this.showState,
this.nameInTheBottom: false,
this.iconSize: Sizes.iconSize,
this.wordsWrapInName: false
}) : super(key: key);
@override
Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
if (entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
return MissedEntityWidget();
}
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
return Container(width: 0.0, height: 0.0,);
}
List<Widget> result = [];
if (!nameInTheBottom) {
if (showName) {
result.add(_buildName(context));
}
} else {
if (showState) {
result.add(_buildState());
}
}
result.add(
EntityIcon(
padding: EdgeInsets.all(0.0),
size: iconSize,
)
);
if (!nameInTheBottom) {
if (showState) {
result.add(_buildState());
}
} else {
result.add(_buildName(context));
}
return Center(
child: InkResponse(
child: Column(
mainAxisSize: MainAxisSize.min,
children: result,
),
onTap: () => entityWrapper.handleTap(),
onLongPress: () => entityWrapper.handleHold(),
onDoubleTap: () => entityWrapper.handleDoubleTap(),
),
);
}
Widget _buildName(BuildContext context) {
return EntityName(
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
textOverflow: TextOverflow.ellipsis,
wordsWrap: wordsWrapInName,
textAlign: TextAlign.center,
textStyle: Theme.of(context).textTheme.body1,
);
}
Widget _buildState() {
return SimpleEntityState(
textAlign: TextAlign.center,
expanded: false,
maxLines: 1,
padding: EdgeInsets.only(top: Sizes.rowPadding),
);
}
}

View File

@ -1,38 +0,0 @@
part of '../../main.dart';
class LightCardBody extends StatefulWidget {
final int min;
final int max;
final Map severity;
LightCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
@override
_LightCardBodyState createState() => _LightCardBodyState();
}
class _LightCardBodyState extends State<LightCardBody> {
@override
Widget build(BuildContext context) {
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
LightEntity entity = entityWrapper.entity;
Logger.d("Light brightness: ${entity.brightness}");
return FractionallySizedBox(
widthFactor: 0.5,
child: Container(
//color: Colors.redAccent,
child: SingleCircularSlider(
255,
entity.brightness ?? 0,
baseColor: Colors.white,
handlerColor: Colors.blue[200],
selectionColor: Colors.blue[100],
),
),
);
}
}

View File

@ -118,6 +118,7 @@ class CardType {
static const ALARM_PANEL = "alarm-panel"; static const ALARM_PANEL = "alarm-panel";
static const MARKDOWN = "markdown"; static const MARKDOWN = "markdown";
static const LIGHT = "light"; static const LIGHT = "light";
static const ENTITY_FILTER = "entity-filter";
} }
class Sizes { class Sizes {

View File

@ -36,7 +36,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
.of(context) .of(context)
.entityWrapper .entityWrapper
.entity; .entity;
if (_entity.supportStream) { if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
HomeAssistant().getCameraStream(_entity.entityId) HomeAssistant().getCameraStream(_entity.entityId)
.then((data) { .then((data) {
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}'; _jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';

View File

@ -11,13 +11,13 @@ class DefaultEntityContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context); final EntityModel entityModel = EntityModel.of(context);
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) { if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) {
return MissedEntityWidget(); return MissedEntityWidget();
} }
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) { if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) {
return Divider(); return Divider();
} }
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) { if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -1,13 +1,6 @@
part of '../main.dart'; part of '../main.dart';
class StatelessEntityType { enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
static const NONE = 0;
static const MISSED = 1;
static const DIVIDER = 2;
static const SECTION = 3;
static const CALL_SERVICE = 4;
static const WEBLINK = 5;
}
class Entity { class Entity {
@ -76,8 +69,8 @@ class Entity {
String entityPicture; String entityPicture;
String state; String state;
String displayState; String displayState;
DateTime _lastUpdated; DateTime lastUpdatedTimestamp;
int statelessType = 0; StatelessEntityType statelessType = StatelessEntityType.none;
List<Entity> childEntities = []; List<Entity> childEntities = [];
String deviceClass; String deviceClass;
@ -120,31 +113,36 @@ class Entity {
} }
Entity.missed(String entityId) { Entity.missed(String entityId) {
statelessType = StatelessEntityType.MISSED; statelessType = StatelessEntityType.missed;
attributes = {"hidden": false}; attributes = {"hidden": false};
this.entityId = entityId; this.entityId = entityId;
} }
Entity.divider() { Entity.divider() {
statelessType = StatelessEntityType.DIVIDER; statelessType = StatelessEntityType.divider;
attributes = {"hidden": false}; attributes = {"hidden": false};
} }
Entity.section(String label) { Entity.section(String label) {
statelessType = StatelessEntityType.SECTION; statelessType = StatelessEntityType.section;
attributes = {"hidden": false, "friendly_name": "$label"}; attributes = {"hidden": false, "friendly_name": "$label"};
} }
Entity.ghost(String name, String icon) {
statelessType = StatelessEntityType.ghost;
attributes = {"icon": icon, "hidden": false, "friendly_name": name};
}
Entity.callService({String icon, String name, String service, String actionName}) { Entity.callService({String icon, String name, String service, String actionName}) {
statelessType = StatelessEntityType.CALL_SERVICE; statelessType = StatelessEntityType.callService;
entityId = service; entityId = service;
displayState = actionName?.toUpperCase() ?? "RUN"; displayState = actionName?.toUpperCase() ?? "RUN";
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"}; attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
} }
Entity.weblink({String url, String name, String icon}) { Entity.weblink({String url, String name, String icon}) {
statelessType = StatelessEntityType.WEBLINK; statelessType = StatelessEntityType.webLink;
entityId = "custom.custom"; //TODO wtf?? entityId = "custom.custom";
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"}; attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
} }
@ -155,7 +153,7 @@ class Entity {
deviceClass = attributes["device_class"]; deviceClass = attributes["device_class"];
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"]; state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state); displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
_lastUpdated = DateTime.tryParse(rawData["last_updated"]); lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]);
entityPicture = _getEntityPictureUrl(webHost); entityPicture = _getEntityPictureUrl(webHost);
} }
@ -227,11 +225,11 @@ class Entity {
} }
String _getLastUpdatedFormatted() { String _getLastUpdatedFormatted() {
if (_lastUpdated == null) { if (lastUpdatedTimestamp == null) {
return "-"; return "-";
} else { } else {
DateTime now = DateTime.now(); DateTime now = DateTime.now();
Duration d = now.difference(_lastUpdated); Duration d = now.difference(lastUpdatedTimestamp);
String text; String text;
int v; int v;
if (d.inDays == 0) { if (d.inDays == 0) {

View File

@ -50,24 +50,29 @@ class EntityIcon extends StatelessWidget {
iconCode = getDefaultIconByEntityId(data.entity.entityId, iconCode = getDefaultIconByEntityId(data.entity.entityId,
data.entity.deviceClass, data.entity.state); // data.entity.deviceClass, data.entity.state); //
} }
return Padding( return Icon(
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
child: Icon(
IconData(iconCode, fontFamily: 'Material Design Icons'), IconData(iconCode, fontFamily: 'Material Design Icons'),
size: size, size: size,
color: color, color: color,
)
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
Color iconColor;
if (color != null) {
iconColor = color;
} else if (entityWrapper.stateColor) {
iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context);
} else {
iconColor = HAClientTheme().getOffStateColor(context);
}
return Padding( return Padding(
padding: padding, padding: padding,
child: buildIcon( child: buildIcon(
entityWrapper, entityWrapper,
color ?? HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context) iconColor
), ),
); );
} }

View File

@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
final bool handleTap; final bool handleTap;
static EntityModel of(BuildContext context) { static EntityModel of(BuildContext context) {
return context.inheritFromWidgetOfExactType(EntityModel); return context.dependOnInheritedWidgetOfExactType<EntityModel>();
} }
@override @override
bool updateShouldNotify(InheritedWidget oldWidget) { bool updateShouldNotify(EntityModel oldWidget) {
return true; return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
} }
} }

View File

@ -16,7 +16,7 @@ class EntityName extends StatelessWidget {
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
TextStyle tStyle; TextStyle tStyle;
if (textStyle == null) { if (textStyle == null) {
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) { if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) {
tStyle = HAClientTheme().getLinkTextStyle(context); tStyle = HAClientTheme().getLinkTextStyle(context);
} else { } else {
tStyle = Theme.of(context).textTheme.body1; tStyle = Theme.of(context).textTheme.body1;

View File

@ -4,6 +4,7 @@ class EntityWrapper {
String overrideName; String overrideName;
final String overrideIcon; final String overrideIcon;
final bool stateColor;
EntityUIAction uiAction; EntityUIAction uiAction;
Entity entity; Entity entity;
String unitOfMeasurementOverride; String unitOfMeasurementOverride;
@ -18,10 +19,11 @@ class EntityWrapper {
this.entity, this.entity,
this.overrideIcon, this.overrideIcon,
this.overrideName, this.overrideName,
this.stateColor: true,
this.uiAction, this.uiAction,
this.stateFilter this.stateFilter
}) { }) {
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) { if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || entity.statelessType == StatelessEntityType.webLink) {
if (uiAction == null) { if (uiAction == null) {
uiAction = EntityUIAction(); uiAction = EntityUIAction();
} }

View File

@ -25,7 +25,7 @@ class SimpleEntityState extends StatelessWidget {
TextStyle tStyle; TextStyle tStyle;
if (textStyle != null) { if (textStyle != null) {
tStyle = textStyle; tStyle = textStyle;
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) { } else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
tStyle = Theme.of(context).textTheme.subhead.copyWith( tStyle = Theme.of(context).textTheme.subhead.copyWith(
color: Colors.blue color: Colors.blue
); );

View File

@ -27,7 +27,7 @@ class _TimerStateState extends State<TimerState> {
try { try {
int passed = DateTime int passed = DateTime
.now() .now()
.difference(entity._lastUpdated) .difference(entity.lastUpdatedTimestamp)
.inSeconds; .inSeconds;
remaining = Duration(seconds: entity.duration.inSeconds - passed); remaining = Duration(seconds: entity.duration.inSeconds - passed);
} catch (e) { } catch (e) {

View File

@ -1,6 +1,6 @@
part of '../main.dart'; part of '../main.dart';
class UniversalSlider extends StatelessWidget { class UniversalSlider extends StatefulWidget {
final onChanged; final onChanged;
final onChangeEnd; final onChangeEnd;
@ -14,33 +14,69 @@ class UniversalSlider extends StatelessWidget {
const UniversalSlider({Key key, this.onChanged, 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.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
State<StatefulWidget> createState() {
return UniversalSliderState();
}
}
class UniversalSliderState extends State<UniversalSlider> {
double _value;
bool _changeStarted = false;
@override
void initState() {
_value = widget.value;
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List <Widget> row = []; List <Widget> row = [];
if (leading != null) { if (widget.leading != null) {
row.add(leading); row.add(widget.leading);
} }
row.add( row.add(
Flexible( Flexible(
child: Slider( child: Slider(
value: value, value: _value,
min: min, min: widget.min,
max: max, max: widget.max,
onChanged: (value) => onChanged(value), onChangeStart: (_) {
onChangeEnd: (value) => onChangeEnd(value), _changeStarted = true;
},
onChanged: (value) {
setState(() {
_value = value;
});
widget.onChanged(value);
},
onChangeEnd: (value) {
_changeStarted = false;
Timer(Duration(milliseconds: 500), () {
if (!_changeStarted) {
setState(() {
_value = value;
});
widget.onChangeEnd(value);
}
});
}
), ),
) )
); );
if (closing != null) { if (widget.closing != null) {
row.add(closing); row.add(widget.closing);
} }
return Padding( return Padding(
padding: padding, padding: widget.padding,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Container(height: Sizes.rowPadding,), Container(height: Sizes.rowPadding,),
Text("$title"), Text('${widget.title}'),
Container(height: Sizes.rowPadding,), Container(height: Sizes.rowPadding,),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -51,5 +87,4 @@ class UniversalSlider extends StatelessWidget {
), ),
); );
} }
} }

View File

@ -46,7 +46,6 @@ class HomeAssistant {
String get userAvatarText => userName.length > 0 ? userName[0] : ""; String get userAvatarText => userName.length > 0 ? userName[0] : "";
bool get isNoEntities => entities == null || entities.isEmpty; bool get isNoEntities => entities == null || entities.isEmpty;
bool get isNoViews => ui == null || ui.isEmpty; bool get isNoViews => ui == null || ui.isEmpty;
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
HomeAssistant._internal() { HomeAssistant._internal() {
ConnectionManager().onStateChangeCallback = _handleEntityStateChange; ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
@ -75,7 +74,7 @@ class HomeAssistant {
futures.add(_getLovelace(null)); futures.add(_getLovelace(null));
} }
Future.wait(futures).then((_) { Future.wait(futures).then((_) {
if (isMobileAppEnabled) { if (isComponentEnabled('mobile_app')) {
_createUI(); _createUI();
_fetchCompleter.complete(); _fetchCompleter.complete();
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration(); if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
@ -103,7 +102,7 @@ class HomeAssistant {
_getUserInfo(prefs); _getUserInfo(prefs);
_getPanels(prefs); _getPanels(prefs);
_getServices(prefs); _getServices(prefs);
if (isMobileAppEnabled) { if (isComponentEnabled('mobile_app')) {
_createUI(); _createUI();
} }
} catch (e) { } catch (e) {
@ -156,6 +155,7 @@ class HomeAssistant {
void _parseConfig(data) { void _parseConfig(data) {
_instanceConfig = Map.from(data); _instanceConfig = Map.from(data);
Logger.d('stream: ${_instanceConfig['components'].contains('stream')}');
} }
Future _getStates(SharedPreferences sharedPrefs) async { Future _getStates(SharedPreferences sharedPrefs) async {
@ -305,6 +305,10 @@ class HomeAssistant {
return completer.future; return completer.future;
} }
bool isComponentEnabled(String name) {
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name");
}
void _handleLovelaceUpdate() { void _handleLovelaceUpdate() {
if (_fetchCompleter != null && _fetchCompleter.isCompleted) { if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
eventBus.fire(new LovelaceChangedEvent()); eventBus.fire(new LovelaceChangedEvent());

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -13,7 +14,6 @@ import 'package:flutter/services.dart';
import 'package:date_format/date_format.dart'; import 'package:date_format/date_format.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts; import 'package:charts_flutter/flutter.dart' as charts;
import 'package:progress_indicators/progress_indicators.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
@ -21,7 +21,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:device_info/device_info.dart'; import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/circular_slider/single_circular_slider.dart';
import 'plugins/dynamic_multi_column_layout.dart'; import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart'; import 'plugins/spoiler_card.dart';
import 'package:workmanager/workmanager.dart' as workManager; import 'package:workmanager/workmanager.dart' as workManager;
@ -63,8 +62,7 @@ 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';
part 'cards/widgets/glance_card_entity_container.dart'; part 'cards/entity_button_card.dart';
part 'cards/widgets/entity_button_card_body.widget.dart';
part 'pages/widgets/entity_attributes_list.dart'; part 'pages/widgets/entity_attributes_list.dart';
part 'entities/entity_icon.widget.dart'; part 'entities/entity_icon.widget.dart';
part 'entities/entity_name.widget.dart'; part 'entities/entity_name.widget.dart';
@ -106,6 +104,7 @@ part 'pages/settings/connection_settings.part.dart';
part 'pages/purchase.page.dart'; part 'pages/purchase.page.dart';
part 'pages/widgets/product_purchase.widget.dart'; part 'pages/widgets/product_purchase.widget.dart';
part 'pages/widgets/page_loading_indicator.dart'; part 'pages/widgets/page_loading_indicator.dart';
part 'pages/widgets/bottom_info_bar.dart';
part 'pages/widgets/page_loading_error.dart'; part 'pages/widgets/page_loading_error.dart';
part 'pages/panel.page.dart'; part 'pages/panel.page.dart';
part 'pages/main/main.page.dart'; part 'pages/main/main.page.dart';
@ -130,14 +129,21 @@ part 'view.class.dart';
part 'cards/card.class.dart'; part 'cards/card.class.dart';
part 'panels/panel_class.dart'; part 'panels/panel_class.dart';
part 'viewWidget.widget.dart'; part 'viewWidget.widget.dart';
part 'cards/card_widget.dart';
part 'cards/widgets/card_header.widget.dart'; part 'cards/widgets/card_header.widget.dart';
part 'panels/config_panel_widget.dart'; part 'panels/config_panel_widget.dart';
part 'panels/widgets/link_to_web_config.dart'; part 'panels/widgets/link_to_web_config.dart';
part 'types/ha_error.dart'; part 'types/ha_error.dart';
part 'types/event_bus_events.dart'; part 'types/event_bus_events.dart';
part 'cards/widgets/gauge_card_body.dart'; part 'cards/gauge_card.dart';
part 'cards/widgets/light_card_body.dart'; part 'cards/widgets/card_wrapper.widget.dart';
part 'cards/entities_card.dart';
part 'cards/alarm_panel_card.dart';
part 'cards/horizontal_srack_card.dart';
part 'cards/markdown_card.dart';
part 'cards/media_control_card.dart';
part 'cards/unsupported_card.dart';
part 'cards/vertical_stack_card.dart';
part 'cards/glance_card.dart';
part 'pages/play_media.page.dart'; part 'pages/play_media.page.dart';
part 'entities/entity_page_layout.widget.dart'; part 'entities/entity_page_layout.widget.dart';
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart'; part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
@ -149,7 +155,7 @@ 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 = "0.8.4"; const appVersionNumber = "0.8.5";
const appVersionAdd = ""; const appVersionAdd = "";
const appVersion = "$appVersionNumber$appVersionAdd"; const appVersion = "$appVersionNumber$appVersionAdd";
@ -164,7 +170,7 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
} }
void main() async { void main() async {
Crashlytics.instance.enableInDevMode = false; Crashlytics.instance.enableInDevMode = true;
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']); SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {

View File

@ -14,7 +14,7 @@ class StartupUserMessagesManager {
bool _supportAppDevelopmentMessageShown; bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown; bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3"; static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-888"; static final _whatsNewMessageKey = "user-message-shown-whats-new-894";
void checkMessagesToShow() async { void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();

View File

@ -21,7 +21,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
StreamSubscription _showPopupDialogSubscription; StreamSubscription _showPopupDialogSubscription;
StreamSubscription _showPopupMessageSubscription; StreamSubscription _showPopupMessageSubscription;
StreamSubscription _reloadUISubscription; StreamSubscription _reloadUISubscription;
StreamSubscription _fullReloadSubscription;
StreamSubscription _showPageSubscription; StreamSubscription _showPageSubscription;
BottomInfoBarController _bottomInfoBarController;
int _previousViewCount; int _previousViewCount;
bool _showLoginButton = false; bool _showLoginButton = false;
bool _preventAppRefresh = false; bool _preventAppRefresh = false;
@ -46,6 +48,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
); );
_bottomInfoBarController = BottomInfoBarController();
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true)); _firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
@ -91,7 +95,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
void _fullLoad() { void _fullLoad() {
_showInfoBottomBar(progress: true,); _bottomInfoBarController.showInfoBottomBar(progress: true,);
_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) {
@ -107,8 +111,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
void _quickLoad({bool uiOnly: false}) { void _quickLoad({bool uiOnly: false}) {
_hideBottomBar(); _bottomInfoBarController.showInfoBottomBar(progress: true,);
_showInfoBottomBar(progress: true,);
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){ ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
_fetchData(useCache: false, uiOnly: uiOnly); _fetchData(useCache: false, uiOnly: uiOnly);
}, onError: (e) { }, onError: (e) {
@ -123,7 +126,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}); });
} }
await HomeAssistant().fetchData(uiOnly).then((_) { await HomeAssistant().fetchData(uiOnly).then((_) {
_hideBottomBar(); setState((){
_bottomInfoBarController.hideBottomBar();
});
HomeAssistant().saveCache();
}).catchError((e) { }).catchError((e) {
if (e is HAError) { if (e is HAError) {
_setErrorState(e); _setErrorState(e);
@ -140,7 +146,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) { if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
_quickLoad(); _quickLoad();
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) { } else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
HomeAssistant().saveCache(); //HomeAssistant().saveCache();
} }
} }
@ -167,6 +173,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_quickLoad(uiOnly: true); _quickLoad(uiOnly: true);
}); });
} }
if (_fullReloadSubscription == null) {
_fullReloadSubscription = eventBus.on<FullReloadEvent>().listen((event){
_fullLoad();
});
}
if (_showPopupDialogSubscription == null) { if (_showPopupDialogSubscription == null) {
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){ _showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
_showPopupDialog( _showPopupDialog(
@ -213,7 +224,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (_showErrorSubscription == null) { if (_showErrorSubscription == null) {
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){ _showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
_showErrorBottomBar(event.error); _bottomInfoBarController.showErrorBottomBar(event.error);
}); });
} }
@ -245,11 +256,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_setErrorState(HAError e) { _setErrorState(HAError e) {
if (e == null) { if (e == null) {
_showErrorBottomBar( _bottomInfoBarController.showErrorBottomBar(
HAError("Unknown error") HAError("Unknown error")
); );
} else { } else {
_showErrorBottomBar(e); _bottomInfoBarController.showErrorBottomBar(e);
} }
} }
@ -291,7 +302,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
} }
void _notifyServiceCalled(String domain, String service, entityId) { void _notifyServiceCalled(String domain, String service, entityId) {
_showInfoBottomBar( _bottomInfoBarController.showInfoBottomBar(
message: "Calling $domain.$service", message: "Calling $domain.$service",
duration: Duration(seconds: 4) duration: Duration(seconds: 4)
); );
@ -481,111 +492,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
); );
} }
void _hideBottomBar() {
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
setState(() {
_showBottomBar = false;
});
}
Widget _bottomBarAction;
bool _showBottomBar = false;
String _bottomBarText;
bool _bottomBarProgress;
bool _bottomBarErrorColor;
Timer _bottomBarTimer;
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
_bottomBarAction = Container(height: 0.0, width: 0.0,);
_bottomBarErrorColor = false;
setState(() {
_bottomBarText = message;
_bottomBarProgress = progress;
_showBottomBar = true;
});
if (duration != null) {
_bottomBarTimer = Timer(duration, () {
_hideBottomBar();
});
}
}
void _showErrorBottomBar(HAError error) {
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
);
_bottomBarErrorColor = true;
List<Widget> actions = [];
error.actions.forEach((HAErrorAction action) {
switch (action.type) {
case HAErrorActionType.FULL_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
_fullLoad();
},
));
break;
}
case HAErrorActionType.QUICK_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
_quickLoad();
},
));
break;
}
case HAErrorActionType.RELOGIN: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
ConnectionManager().logout().then((_) => _fullLoad());
},
));
break;
}
case HAErrorActionType.URL: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
},
));
break;
}
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Navigator.pushNamed(context, '/connection-settings');
},
));
break;
}
}
});
if (actions.isNotEmpty) {
_bottomBarAction = Row(
mainAxisSize: MainAxisSize.min,
children: actions,
mainAxisAlignment: MainAxisAlignment.end,
);
} else {
_bottomBarAction = Container(height: 0.0, width: 0.0,);
}
setState(() {
_bottomBarProgress = false;
_bottomBarText = "${error.message}";
_showBottomBar = true;
});
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) { Widget _buildScaffoldBody(bool empty) {
@ -765,60 +671,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget bottomBar;
if (_showBottomBar) {
List<Widget> bottomBarChildren = [];
if (_bottomBarText != null) {
bottomBarChildren.add(
Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
Sizes.rowPadding),
child: Text(
"$_bottomBarText",
textAlign: TextAlign.left,
softWrap: true,
),
)
);
}
if (_bottomBarProgress) {
bottomBarChildren.add(
CollectionScaleTransition(
children: <Widget>[
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOnStateColor(context),),
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getDisabledStateColor(context),),
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOffStateColor(context),),
],
),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: _bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: bottomBarChildren,
),
),
_bottomBarAction
],
),
);
}
}
if (HomeAssistant().isNoViews) { if (HomeAssistant().isNoViews) {
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
primary: false, primary: false,
drawer: _buildAppDrawer(), drawer: _buildAppDrawer(),
bottomNavigationBar: bottomBar, bottomNavigationBar: BottomInfoBar(
controller: _bottomInfoBarController,
),
body: _buildScaffoldBody(true) body: _buildScaffoldBody(true)
); );
} else { } else {
@ -826,7 +686,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
key: _scaffoldKey, key: _scaffoldKey,
drawer: _buildAppDrawer(), drawer: _buildAppDrawer(),
primary: false, primary: false,
bottomNavigationBar: bottomBar, bottomNavigationBar: BottomInfoBar(
controller: _bottomInfoBarController,
),
body: _buildScaffoldBody(false) body: _buildScaffoldBody(false)
); );
} }
@ -848,9 +710,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_showErrorSubscription?.cancel(); _showErrorSubscription?.cancel();
_startAuthSubscription?.cancel(); _startAuthSubscription?.cancel();
_showPageSubscription?.cancel(); _showPageSubscription?.cancel();
_fullReloadSubscription?.cancel();
_reloadUISubscription?.cancel(); _reloadUISubscription?.cancel();
//TODO disconnect
//widget.homeAssistant?.disconnect();
super.dispose(); super.dispose();
} }
} }

View File

@ -57,8 +57,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
_loaded = false; _loaded = false;
}); });
} else { } else {
_isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor"); _isMediaExtractorExist = HomeAssistant().isComponentEnabled("media_extractor");
//_useMediaExtractor = _isMediaExtractorExist;
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]); _players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
setState(() { setState(() {
if (_players.isNotEmpty) { if (_players.isNotEmpty) {

View File

@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
error = ""; error = "";
}); });
http.Response response; http.Response response;
response = await http.get("http://ha-client.app/service/whats_new_0.8.3.md"); response = await http.get("http://ha-client.app/service/whats_new_0.8.5.md");
if (response.statusCode == 200) { if (response.statusCode == 200) {
setState(() { setState(() {
data = response.body; data = response.body;

View File

@ -0,0 +1,210 @@
part of '../../main.dart';
class BottomInfoBarController {
Function show;
Function hide;
String bottomBarText;
bool bottomBarProgress;
bool bottomBarErrorColor;
Timer _bottomBarTimer;
bool initialState = false;
List<HAErrorAction> actions = [];
void hideBottomBar() {
_bottomBarTimer?.cancel();
if (hide == null) {
initialState = false;
} else {
hide();
}
}
void showInfoBottomBar({String message, bool progress: false, Duration duration}) {
_bottomBarTimer?.cancel();
actions.clear();
bottomBarErrorColor = false;
bottomBarText = message;
bottomBarProgress = progress;
if (show == null) {
initialState = true;
} else {
show();
}
if (duration != null) {
_bottomBarTimer = Timer(duration, () {
hideBottomBar();
});
}
}
void showErrorBottomBar(HAError error) {
actions.clear();
actions.addAll(error.actions);
bottomBarErrorColor = true;
bottomBarProgress = false;
bottomBarText = "${error.message}";
if (show == null) {
initialState = true;
} else {
show();
}
}
}
class BottomInfoBar extends StatefulWidget {
final BottomInfoBarController controller;
const BottomInfoBar({Key key, this.controller}) : super(key: key);
@override
State<StatefulWidget> createState() {
return new _BottomInfoBarState();
}
}
class _BottomInfoBarState extends State<BottomInfoBar> {
bool _show;
@override
void initState() {
_show = widget.controller.initialState;
widget.controller.show = () {
setState(() {
_show = true;
});
};
widget.controller.hide = () {
setState(() {
_show = false;
});
};
super.initState();
}
@override
Widget build(BuildContext context) {
if (!_show) {
return Container(width: 0, height: 0,);
} else {
Widget bottomBar;
List<Widget> bottomBarChildren = [];
Widget actionsWidget;
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
decoration: TextDecoration.underline
);
List<Widget> actions = [];
widget.controller.actions.forEach((HAErrorAction action) {
switch (action.type) {
case HAErrorActionType.FULL_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
eventBus.fire(FullReloadEvent());
},
));
break;
}
case HAErrorActionType.QUICK_RELOAD: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
eventBus.fire(ReloadUIEvent());
},
));
break;
}
case HAErrorActionType.RELOGIN: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
ConnectionManager().logout().then((_) => eventBus.fire(FullReloadEvent()));
},
));
break;
}
case HAErrorActionType.URL: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
},
));
break;
}
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
actions.add(FlatButton(
child: Text("${action.title}", style: textStyle),
onPressed: () {
Navigator.pushNamed(context, '/connection-settings');
},
));
break;
}
}
});
if (actions.isNotEmpty) {
actionsWidget = Row(
mainAxisSize: MainAxisSize.min,
children: actions,
mainAxisAlignment: MainAxisAlignment.end,
);
} else {
actionsWidget = Container(height: 0.0, width: 0.0,);
}
if (widget.controller.bottomBarText != null) {
bottomBarChildren.add(
Padding(
padding: EdgeInsets.fromLTRB(
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
Sizes.rowPadding),
child: Text(
"${widget.controller.bottomBarText}",
textAlign: TextAlign.left,
softWrap: true,
),
)
);
}
if (widget.controller.bottomBarProgress) {
bottomBarChildren.add(
LinearProgressIndicator(),
);
}
if (bottomBarChildren.isNotEmpty) {
bottomBar = Container(
color: widget.controller.bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: widget.controller.bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: bottomBarChildren,
),
),
actionsWidget
],
),
);
} else {
bottomBar = Container(height: 0,);
}
return bottomBar;
}
}
}

View File

@ -1,77 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'utils.dart';
class BasePainter extends CustomPainter {
Color baseColor;
Color selectionColor;
int primarySectors;
int secondarySectors;
double sliderStrokeWidth;
Offset center;
double radius;
BasePainter({
@required this.baseColor,
@required this.selectionColor,
@required this.primarySectors,
@required this.secondarySectors,
@required this.sliderStrokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
Paint base = _getPaint(color: baseColor);
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
// we need this in the parent to calculate if the user clicks on the circumference
assert(radius > 0);
canvas.drawCircle(center, radius, base);
if (primarySectors > 0) {
_paintSectors(primarySectors, 8.0, selectionColor, canvas);
}
if (secondarySectors > 0) {
_paintSectors(secondarySectors, 6.0, baseColor, canvas);
}
}
void _paintSectors(
int sectors, double radiusPadding, Color color, Canvas canvas) {
Paint section = _getPaint(color: color, width: 2.0);
var endSectors =
getSectionsCoordinatesInCircle(center, radius + radiusPadding, sectors);
var initSectors =
getSectionsCoordinatesInCircle(center, radius - radiusPadding, sectors);
_paintLines(canvas, initSectors, endSectors, section);
}
void _paintLines(
Canvas canvas, List<Offset> inits, List<Offset> ends, Paint section) {
assert(inits.length == ends.length && inits.length > 0);
for (var i = 0; i < inits.length; i++) {
canvas.drawLine(inits[i], ends[i], section);
}
}
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
Paint()
..color = color
..strokeCap = StrokeCap.round
..style = style ?? PaintingStyle.stroke
..strokeWidth = width ?? sliderStrokeWidth;
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

View File

@ -1,366 +0,0 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'base_painter.dart';
import 'slider_painter.dart';
import 'utils.dart';
enum CircularSliderMode { singleHandler, doubleHandler }
enum SlidingState { none, endIsBiggerThanStart, endIsSmallerThanStart }
typedef SelectionChanged<T> = void Function(T a, T b, T c);
class CircularSliderPaint extends StatefulWidget {
final CircularSliderMode mode;
final int init;
final int end;
final int divisions;
final int primarySectors;
final int secondarySectors;
final SelectionChanged<int> onSelectionChange;
final SelectionChanged<int> onSelectionEnd;
final Color baseColor;
final Color selectionColor;
final Color handlerColor;
final double handlerOutterRadius;
final Widget child;
final bool showRoundedCapInSelection;
final bool showHandlerOutter;
final double sliderStrokeWidth;
final bool shouldCountLaps;
CircularSliderPaint({
@required this.mode,
@required this.divisions,
@required this.init,
@required this.end,
this.child,
@required this.primarySectors,
@required this.secondarySectors,
@required this.onSelectionChange,
@required this.onSelectionEnd,
@required this.baseColor,
@required this.selectionColor,
@required this.handlerColor,
@required this.handlerOutterRadius,
@required this.showRoundedCapInSelection,
@required this.showHandlerOutter,
@required this.sliderStrokeWidth,
@required this.shouldCountLaps,
});
@override
_CircularSliderState createState() => _CircularSliderState();
}
class _CircularSliderState extends State<CircularSliderPaint> {
bool _isInitHandlerSelected = false;
bool _isEndHandlerSelected = false;
SliderPainter _painter;
/// start angle in radians where we need to locate the init handler
double _startAngle;
/// end angle in radians where we need to locate the end handler
double _endAngle;
/// the absolute angle in radians representing the selection
double _sweepAngle;
/// in case we have a double slider and we want to move the whole selection by clicking in the slider
/// this will capture the position in the selection relative to the initial handler
/// that way we will be able to keep the selection constant when moving
int _differenceFromInitPoint;
/// will store the number of full laps (2pi radians) as part of the selection
int _laps = 0;
/// will be used to calculate in the next movement if we need to increase or decrease _laps
SlidingState _slidingState = SlidingState.none;
bool get isDoubleHandler => widget.mode == CircularSliderMode.doubleHandler;
bool get isSingleHandler => widget.mode == CircularSliderMode.singleHandler;
bool get isBothHandlersSelected =>
_isEndHandlerSelected && _isInitHandlerSelected;
bool get isNoHandlersSelected =>
!_isEndHandlerSelected && !_isInitHandlerSelected;
@override
void initState() {
super.initState();
_calculatePaintData();
}
// we need to update this widget both with gesture detector but
// also when the parent widget rebuilds itself
@override
void didUpdateWidget(CircularSliderPaint oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.init != widget.init || oldWidget.end != widget.end) {
_calculatePaintData();
}
}
@override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
CustomPanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
() => CustomPanGestureRecognizer(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
),
(CustomPanGestureRecognizer instance) {},
),
},
child: CustomPaint(
painter: BasePainter(
baseColor: widget.baseColor,
selectionColor: widget.selectionColor,
primarySectors: widget.primarySectors,
secondarySectors: widget.secondarySectors,
sliderStrokeWidth: widget.sliderStrokeWidth,
),
foregroundPainter: _painter,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: widget.child,
),
),
);
}
void _calculatePaintData() {
var initPercent = isDoubleHandler
? valueToPercentage(widget.init, widget.divisions)
: 0.0;
var endPercent = valueToPercentage(widget.end, widget.divisions);
var sweep = getSweepAngle(initPercent, endPercent);
var previousStartAngle = _startAngle;
var previousEndAngle = _endAngle;
_startAngle = isDoubleHandler ? percentageToRadians(initPercent) : 0.0;
_endAngle = percentageToRadians(endPercent);
_sweepAngle = percentageToRadians(sweep.abs());
// update full laps if need be
if (widget.shouldCountLaps) {
var newSlidingState = _calculateSlidingState(_startAngle, _endAngle);
if (isSingleHandler) {
_laps = _calculateLapsForsSingleHandler(
_endAngle, previousEndAngle, _slidingState, _laps);
_slidingState = newSlidingState;
} else {
// is double handler
if (newSlidingState != _slidingState) {
_laps = _calculateLapsForDoubleHandler(
_startAngle,
_endAngle,
previousStartAngle,
previousEndAngle,
_slidingState,
newSlidingState,
_laps);
_slidingState = newSlidingState;
}
}
}
_painter = SliderPainter(
mode: widget.mode,
startAngle: _startAngle,
endAngle: _endAngle,
sweepAngle: _sweepAngle,
selectionColor: widget.selectionColor,
handlerColor: widget.handlerColor,
handlerOutterRadius: widget.handlerOutterRadius,
showRoundedCapInSelection: widget.showRoundedCapInSelection,
showHandlerOutter: widget.showHandlerOutter,
sliderStrokeWidth: widget.sliderStrokeWidth,
);
}
int _calculateLapsForsSingleHandler(
double end, double prevEnd, SlidingState slidingState, int laps) {
if (slidingState != SlidingState.none) {
if (radiansWasModuloed(end, prevEnd)) {
var lapIncrement = end < prevEnd ? 1 : -1;
var newLaps = laps + lapIncrement;
return newLaps < 0 ? 0 : newLaps;
}
}
return laps;
}
int _calculateLapsForDoubleHandler(
double start,
double end,
double prevStart,
double prevEnd,
SlidingState slidingState,
SlidingState newSlidingState,
int laps) {
if (slidingState != SlidingState.none) {
if (!radiansWasModuloed(start, prevStart) &&
!radiansWasModuloed(end, prevEnd)) {
var lapIncrement =
newSlidingState == SlidingState.endIsBiggerThanStart ? 1 : -1;
var newLaps = laps + lapIncrement;
return newLaps < 0 ? 0 : newLaps;
}
}
return laps;
}
SlidingState _calculateSlidingState(double start, double end) {
return end > start
? SlidingState.endIsBiggerThanStart
: SlidingState.endIsSmallerThanStart;
}
void _onPanUpdate(Offset details) {
if (!_isInitHandlerSelected && !_isEndHandlerSelected) {
return;
}
if (_painter.center == null) {
return;
}
_handlePan(details, false);
}
void _onPanEnd(Offset details) {
_handlePan(details, true);
_isInitHandlerSelected = false;
_isEndHandlerSelected = false;
}
void _handlePan(Offset details, bool isPanEnd) {
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details);
var angle = coordinatesToRadians(_painter.center, position);
var percentage = radiansToPercentage(angle);
var newValue = percentageToValue(percentage, widget.divisions);
if (isBothHandlersSelected) {
var newValueInit =
(newValue - _differenceFromInitPoint) % widget.divisions;
if (newValueInit != widget.init) {
var newValueEnd =
(widget.end + (newValueInit - widget.init)) % widget.divisions;
widget.onSelectionChange(newValueInit, newValueEnd, _laps);
if (isPanEnd) {
widget.onSelectionEnd(newValueInit, newValueEnd, _laps);
}
}
return;
}
// isDoubleHandler but one handler was selected
if (_isInitHandlerSelected) {
widget.onSelectionChange(newValue, widget.end, _laps);
if (isPanEnd) {
widget.onSelectionEnd(newValue, widget.end, _laps);
}
} else {
widget.onSelectionChange(widget.init, newValue, _laps);
if (isPanEnd) {
widget.onSelectionEnd(widget.init, newValue, _laps);
}
}
}
bool _onPanDown(Offset details) {
if (_painter == null) {
return false;
}
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details);
if (position == null) {
return false;
}
if (isSingleHandler) {
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
_isEndHandlerSelected = true;
_onPanUpdate(details);
}
} else {
_isInitHandlerSelected = isPointInsideCircle(
position, _painter.initHandler, widget.handlerOutterRadius);
if (!_isInitHandlerSelected) {
_isEndHandlerSelected = isPointInsideCircle(
position, _painter.endHandler, widget.handlerOutterRadius);
}
if (isNoHandlersSelected) {
// we check if the user pressed in the selection in a double handler slider
// that means the user wants to move the selection as a whole
if (isPointAlongCircle(position, _painter.center, _painter.radius)) {
var angle = coordinatesToRadians(_painter.center, position);
if (isAngleInsideRadiansSelection(angle, _startAngle, _sweepAngle)) {
_isEndHandlerSelected = true;
_isInitHandlerSelected = true;
var positionPercentage = radiansToPercentage(angle);
// no need to account for negative values, that will be sorted out in the onPanUpdate
_differenceFromInitPoint =
percentageToValue(positionPercentage, widget.divisions) -
widget.init;
}
}
}
}
return _isInitHandlerSelected || _isEndHandlerSelected;
}
}
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onPanDown;
final Function onPanUpdate;
final Function onPanEnd;
CustomPanGestureRecognizer({
@required this.onPanDown,
@required this.onPanUpdate,
@required this.onPanEnd,
});
@override
void addPointer(PointerEvent event) {
if (onPanDown(event.position)) {
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
} else {
stopTrackingPointer(event.pointer);
}
}
@override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
onPanUpdate(event.position);
}
if (event is PointerUpEvent) {
onPanEnd(event.position);
stopTrackingPointer(event.pointer);
}
}
@override
String get debugDescription => 'customPan';
@override
void didStopTrackingLastPointer(int pointer) {}
}

View File

@ -1,148 +0,0 @@
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart';
/// Returns a widget which displays a circle to be used as a slider.
///
/// Required arguments are init and end to set the initial selection.
/// onSelectionChange is a callback function which returns new values as the user
/// changes the interval.
/// The rest of the params are used to change the look and feel.
///
/// DoubleCircularSlider(5, 10, onSelectionChange: () => {});
class DoubleCircularSlider extends StatefulWidget {
/// the selection will be values between 0..divisions; max value is 300
final int divisions;
/// the initial value in the selection
final int init;
/// the end value in the selection
final int end;
/// the number of primary sectors to be painted
/// will be painted using selectionColor
final int primarySectors;
/// the number of secondary sectors to be painted
/// will be painted using baseColor
final int secondarySectors;
/// an optional widget that would be mounted inside the circle
final Widget child;
/// height of the canvas, default at 220
final double height;
/// width of the canvas, default at 220
final double width;
/// color of the base circle and sections
final Color baseColor;
/// color of the selection
final Color selectionColor;
/// color of the handlers
final Color handlerColor;
/// callback function when init and end change
/// (int init, int end) => void
final SelectionChanged<int> onSelectionChange;
/// callback function when init and end finish
/// (int init, int end) => void
final SelectionChanged<int> onSelectionEnd;
/// outter radius for the handlers
final double handlerOutterRadius;
/// if true an extra handler ring will be displayed in the handler
final bool showHandlerOutter;
/// stroke width for the slider, defaults at 12.0
final double sliderStrokeWidth;
/// if true, the onSelectionChange will also return the number of laps in the slider
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
final bool shouldCountLaps;
DoubleCircularSlider(
this.divisions,
this.init,
this.end, {
this.height,
this.width,
this.child,
this.primarySectors,
this.secondarySectors,
this.baseColor,
this.selectionColor,
this.handlerColor,
this.onSelectionChange,
this.onSelectionEnd,
this.handlerOutterRadius,
this.showHandlerOutter,
this.sliderStrokeWidth,
this.shouldCountLaps,
}) : assert(init >= 0 && init <= divisions,
'init has to be > 0 and < divisions value'),
assert(end >= 0 && end <= divisions,
'end has to be > 0 and < divisions value'),
assert(divisions >= 0 && divisions <= 300,
'divisions has to be > 0 and <= 300');
@override
_DoubleCircularSliderState createState() => _DoubleCircularSliderState();
}
class _DoubleCircularSliderState extends State<DoubleCircularSlider> {
int _init;
int _end;
@override
void initState() {
super.initState();
_init = widget.init;
_end = widget.end;
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.height ?? 220,
width: widget.width ?? 220,
child: CircularSliderPaint(
mode: CircularSliderMode.doubleHandler,
init: _init,
end: _end,
divisions: widget.divisions,
primarySectors: widget.primarySectors ?? 0,
secondarySectors: widget.secondarySectors ?? 0,
child: widget.child,
onSelectionChange: (newInit, newEnd, laps) {
if (widget.onSelectionChange != null) {
widget.onSelectionChange(newInit, newEnd, laps);
}
setState(() {
_init = newInit;
_end = newEnd;
});
},
onSelectionEnd: (newInit, newEnd, laps) {
if (widget.onSelectionEnd != null) {
widget.onSelectionEnd(newInit, newEnd, laps);
}
},
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
selectionColor:
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
handlerColor: widget.handlerColor ?? Colors.white,
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
showRoundedCapInSelection: false,
showHandlerOutter: widget.showHandlerOutter ?? true,
shouldCountLaps: widget.shouldCountLaps ?? false,
));
}
}

View File

@ -1,147 +0,0 @@
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart';
import '../../utils/logger.dart';
/// Returns a widget which displays a circle to be used as a slider.
///
/// Required arguments are position and divisions to set the initial selection.
/// onSelectionChange is a callback function which returns new values as the user
/// changes the interval.
/// The rest of the params are used to change the look and feel.
///
/// SingleCircularSlider(5, 10, onSelectionChange: () => {});
class SingleCircularSlider extends StatefulWidget {
/// the selection will be values between 0..divisions; max value is 300
final int divisions;
/// the initial value in the selection
int position;
/// the number of primary sectors to be painted
/// will be painted using selectionColor
final int primarySectors;
/// the number of secondary sectors to be painted
/// will be painted using baseColor
final int secondarySectors;
/// an optional widget that would be mounted inside the circle
final Widget child;
/// height of the canvas, default at 220
final double height;
/// width of the canvas, default at 220
final double width;
/// color of the base circle and sections
final Color baseColor;
/// color of the selection
final Color selectionColor;
/// color of the handlers
final Color handlerColor;
/// callback function when init and end change
/// (int init, int end) => void
final SelectionChanged<int> onSelectionChange;
/// callback function when init and end finish
/// (int init, int end) => void
final SelectionChanged<int> onSelectionEnd;
/// outter radius for the handlers
final double handlerOutterRadius;
/// if true will paint a rounded cap in the selection slider start
final bool showRoundedCapInSelection;
/// if true an extra handler ring will be displayed in the handler
final bool showHandlerOutter;
/// stroke width for the slider, defaults at 12.0
final double sliderStrokeWidth;
/// if true, the onSelectionChange will also return the number of laps in the slider
/// otherwise, everytime the user completes a full lap, the selection restarts from 0
final bool shouldCountLaps;
SingleCircularSlider(
this.divisions,
this.position, {
this.height,
this.width,
this.child,
this.primarySectors,
this.secondarySectors,
this.baseColor,
this.selectionColor,
this.handlerColor,
this.onSelectionChange,
this.onSelectionEnd,
this.handlerOutterRadius,
this.showRoundedCapInSelection,
this.showHandlerOutter,
this.sliderStrokeWidth,
this.shouldCountLaps,
}) : assert(position >= 0 && position <= divisions,
'init has to be > 0 and < divisions value'),
assert(divisions >= 0 && divisions <= 300,
'divisions has to be > 0 and <= 300');
@override
_SingleCircularSliderState createState() => _SingleCircularSliderState();
}
class _SingleCircularSliderState extends State<SingleCircularSlider> {
int _end;
@override
void initState() {
super.initState();
_end = widget.position;
Logger.d('Init: _end=$_end');
}
@override
Widget build(BuildContext context) {
Logger.d('Build: _end=$_end');
return Container(
height: widget.height ?? 220,
width: widget.width ?? 220,
child: CircularSliderPaint(
mode: CircularSliderMode.singleHandler,
init: 0,
end: _end,
divisions: widget.divisions,
primarySectors: widget.primarySectors ?? 0,
secondarySectors: widget.secondarySectors ?? 0,
child: widget.child,
onSelectionChange: (newInit, newEnd, laps) {
if (widget.onSelectionChange != null) {
widget.onSelectionChange(newInit, newEnd, laps);
}
setState(() {
_end = newEnd;
});
},
onSelectionEnd: (newInit, newEnd, laps) {
if (widget.onSelectionEnd != null) {
widget.onSelectionEnd(newInit, newEnd, laps);
}
},
sliderStrokeWidth: widget.sliderStrokeWidth ?? 12.0,
baseColor: widget.baseColor ?? Color.fromRGBO(255, 255, 255, 0.1),
selectionColor:
widget.selectionColor ?? Color.fromRGBO(255, 255, 255, 0.3),
handlerColor: widget.handlerColor ?? Colors.white,
handlerOutterRadius: widget.handlerOutterRadius ?? 12.0,
showRoundedCapInSelection: widget.showRoundedCapInSelection ?? false,
showHandlerOutter: widget.showHandlerOutter ?? true,
shouldCountLaps: widget.shouldCountLaps ?? false,
));
}
}

View File

@ -1,77 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'circular_slider_paint.dart' show CircularSliderMode;
import 'utils.dart';
class SliderPainter extends CustomPainter {
CircularSliderMode mode;
double startAngle;
double endAngle;
double sweepAngle;
Color selectionColor;
Color handlerColor;
double handlerOutterRadius;
bool showRoundedCapInSelection;
bool showHandlerOutter;
double sliderStrokeWidth;
Offset initHandler;
Offset endHandler;
Offset center;
double radius;
SliderPainter({
@required this.mode,
@required this.startAngle,
@required this.endAngle,
@required this.sweepAngle,
@required this.selectionColor,
@required this.handlerColor,
@required this.handlerOutterRadius,
@required this.showRoundedCapInSelection,
@required this.showHandlerOutter,
@required this.sliderStrokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
Paint progress = _getPaint(color: selectionColor);
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2) - sliderStrokeWidth;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
-pi / 2 + startAngle, sweepAngle, false, progress);
Paint handler = _getPaint(color: handlerColor, style: PaintingStyle.fill);
Paint handlerOutter = _getPaint(color: handlerColor, width: 2.0);
// draw handlers
if (mode == CircularSliderMode.doubleHandler) {
initHandler = radiansToCoordinates(center, -pi / 2 + startAngle, radius);
canvas.drawCircle(initHandler, 8.0, handler);
canvas.drawCircle(initHandler, handlerOutterRadius, handlerOutter);
}
endHandler = radiansToCoordinates(center, -pi / 2 + endAngle, radius);
canvas.drawCircle(endHandler, 8.0, handler);
if (showHandlerOutter) {
canvas.drawCircle(endHandler, handlerOutterRadius, handlerOutter);
}
}
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
Paint()
..color = color
..strokeCap =
showRoundedCapInSelection ? StrokeCap.round : StrokeCap.butt
..style = style ?? PaintingStyle.stroke
..strokeWidth = width ?? sliderStrokeWidth;
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

View File

@ -1,75 +0,0 @@
import 'dart:math';
import 'dart:ui';
double percentageToRadians(double percentage) => ((2 * pi * percentage) / 100);
double radiansToPercentage(double radians) {
var normalized = radians < 0 ? -radians : 2 * pi - radians;
var percentage = ((100 * normalized) / (2 * pi));
// TODO we have an inconsistency of pi/2 in terms of percentage and radians
return (percentage + 25) % 100;
}
double coordinatesToRadians(Offset center, Offset coords) {
var a = coords.dx - center.dx;
var b = center.dy - coords.dy;
return atan2(b, a);
}
Offset radiansToCoordinates(Offset center, double radians, double radius) {
var dx = center.dx + radius * cos(radians);
var dy = center.dy + radius * sin(radians);
return Offset(dx, dy);
}
double valueToPercentage(int time, int intervals) => (time / intervals) * 100;
int percentageToValue(double percentage, int intervals) =>
((percentage * intervals) / 100).round();
bool isPointInsideCircle(Offset point, Offset center, double rradius) {
var radius = rradius * 1.2;
return point.dx < (center.dx + radius) &&
point.dx > (center.dx - radius) &&
point.dy < (center.dy + radius) &&
point.dy > (center.dy - radius);
}
bool isPointAlongCircle(Offset point, Offset center, double radius) {
// distance is root(sqr(x2 - x1) + sqr(y2 - y1))
// i.e., (7,8) and (3,2) -> 7.21
var d1 = pow(point.dx - center.dx, 2);
var d2 = pow(point.dy - center.dy, 2);
var distance = sqrt(d1 + d2);
return (distance - radius).abs() < 10;
}
double getSweepAngle(double init, double end) {
if (end > init) {
return end - init;
}
return (100 - init + end).abs();
}
List<Offset> getSectionsCoordinatesInCircle(
Offset center, double radius, int sections) {
var intervalAngle = (pi * 2) / sections;
return List<int>.generate(sections, (int index) => index).map((i) {
var radians = (pi / 2) + (intervalAngle * i);
return radiansToCoordinates(center, radians, radius);
}).toList();
}
bool isAngleInsideRadiansSelection(double angle, double start, double sweep) {
var normalized = angle > pi / 2 ? 5 * pi / 2 - angle : pi / 2 - angle;
var end = (start + sweep) % (2 * pi);
return end > start
? normalized > start && normalized < end
: normalized > start || normalized < end;
}
// this is not 100% accurate but it works
// we just want to see if a value changed drastically its value
bool radiansWasModuloed(double current, double previous) {
return (previous - current).abs() > (3 * pi / 2);
}

View File

@ -25,9 +25,15 @@ class RefreshDataFinishedEvent {
} }
class ReloadUIEvent { class ReloadUIEvent {
//TODO uiOnly bool
ReloadUIEvent(); ReloadUIEvent();
} }
class FullReloadEvent {
FullReloadEvent();
}
class ChangeThemeEvent { class ChangeThemeEvent {
final AppTheme theme; final AppTheme theme;

View File

@ -61,13 +61,7 @@ class HomeAssistantUI {
} }
List<Widget> _buildViews(BuildContext context) { List<Widget> _buildViews(BuildContext context) {
List<Widget> result = []; return views.map((view) => view.build(context)).toList();
views.forEach((view) {
result.add(
view.build(context)
);
});
return result;
} }
void clear() { void clear() {

View File

@ -1,7 +1,7 @@
part of 'main.dart'; part of 'main.dart';
class HAView { class HAView {
List<HACard> cards = []; List<CardData> cards = [];
List<Entity> badges = []; List<Entity> badges = [];
Entity linkedEntity; Entity linkedEntity;
String name; String name;
@ -33,137 +33,11 @@ class HAView {
}); });
} }
cards.addAll(_createLovelaceCards(rawData["cards"] ?? [])); (rawData["cards"] ?? []).forEach((rawCardData) {
} cards.add(CardData.parse(rawCardData));
});
List<HACard> _createLovelaceCards(List rawCards) { //cards.addAll(_createLovelaceCards(rawData["cards"] ?? [], 1));
List<HACard> result = [];
rawCards.forEach((rawCard){
try {
//bool isThereCardOptionsInside = rawCard["card"] != null;
var rawCardInfo = rawCard["card"] ?? rawCard;
HACard card = HACard(
id: "card",
name: rawCardInfo["title"] ?? rawCardInfo["name"],
type: rawCardInfo['type'] ?? CardType.ENTITIES,
columnsCount: rawCardInfo['columns'] ?? 4,
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
showHeaderToggle: (rawCardInfo['show_header_toggle'] ?? rawCard['show_header_toggle']) ?? false,
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
states: rawCardInfo['states'],
conditions: rawCard['conditions'] ?? [],
content: rawCardInfo['content'],
min: rawCardInfo['min'] ?? 0,
max: rawCardInfo['max'] ?? 100,
unit: rawCardInfo['unit'],
severity: rawCardInfo['severity']
);
if (rawCardInfo["cards"] != null) {
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
}
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
rawEntities?.forEach((rawEntity) {
if (rawEntity is String) {
if (HomeAssistant().entities.isExist(rawEntity)) {
card.entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
}
} else {
if (rawEntity["type"] == "divider") {
card.entities.add(EntityWrapper(entity: Entity.divider()));
} else if (rawEntity["type"] == "section") {
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
} else if (rawEntity["type"] == "call-service") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.callService,
"service": rawEntity["service"],
"service_data": rawEntity["service_data"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.callService(
icon: rawEntity["icon"],
name: rawEntity["name"],
service: rawEntity["service"],
actionName: rawEntity["action_name"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (rawEntity["type"] == "weblink") {
Map uiActionData = {
"tap_action": {
"action": EntityUIAction.navigate,
"service": rawEntity["url"]
},
"hold_action": EntityUIAction.none
};
card.entities.add(EntityWrapper(
entity: Entity.weblink(
icon: rawEntity["icon"],
name: rawEntity["name"],
url: rawEntity["url"]
),
uiAction: EntityUIAction(rawEntityData: uiActionData)
)
);
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
card.entities.add(
EntityWrapper(
entity: e,
overrideName: rawEntity["name"],
overrideIcon: rawEntity["icon"],
stateFilter: rawEntity['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawEntity)
)
);
} else {
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
}
}
});
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
if (rawSingleEntity != null) {
var en = rawSingleEntity;
if (en is String) {
if (HomeAssistant().entities.isExist(en)) {
Entity e = HomeAssistant().entities.get(en);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
overrideIcon: rawCardInfo["icon"],
overrideName: rawCardInfo["name"],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
}
} else {
if (HomeAssistant().entities.isExist(en["entity"])) {
Entity e = HomeAssistant().entities.get(en["entity"]);
card.linkedEntityWrapper = EntityWrapper(
entity: e,
overrideIcon: en["icon"],
overrideName: en["name"],
stateFilter: en['state_filter'] ?? [],
uiAction: EntityUIAction(rawEntityData: rawCard)
);
} else {
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
}
}
}
result.add(card);
} catch (e) {
Logger.e("There was an error parsing card: ${e.toString()}");
}
});
return result;
} }
Widget buildTab() { Widget buildTab() {

View File

@ -21,7 +21,25 @@ class ViewWidget extends StatelessWidget {
if (this.view.cards.isNotEmpty) { if (this.view.cards.isNotEmpty) {
cardsContainer = DynamicMultiColumnLayout( cardsContainer = DynamicMultiColumnLayout(
minColumnWidth: Sizes.minViewColumnWidth, minColumnWidth: Sizes.minViewColumnWidth,
children: this.view.cards.map((card) => card.build(context)).toList(), children: this.view.cards.map((card) {
if (card.conditions.isNotEmpty) {
bool showCardByConditions = true;
for (var condition in card.conditions) {
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
if (conditionEntity != null &&
((condition['state'] != null && conditionEntity.state != condition['state']) ||
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
) {
showCardByConditions = false;
break;
}
}
if (!showCardByConditions) {
return Container(width: 0.0, height: 0.0,);
}
}
return card.buildCardWidget();
}).toList(),
); );
} else { } else {
cardsContainer = Container(); cardsContainer = Container();
@ -39,7 +57,7 @@ class ViewWidget extends StatelessWidget {
Widget _buildPanelChild(BuildContext context) { Widget _buildPanelChild(BuildContext context) {
if (this.view.cards != null && this.view.cards.isNotEmpty) { if (this.view.cards != null && this.view.cards.isNotEmpty) {
return this.view.cards[0].build(context); return this.view.cards[0].buildCardWidget();
} else { } else {
return Container(width: 0, height: 0); return Container(width: 0, height: 0);
} }

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.8.4+892 version: 0.8.5+896
environment: environment:
@ -12,7 +12,6 @@ dependencies:
sdk: flutter sdk: flutter
web_socket_channel: ^1.1.0 web_socket_channel: ^1.1.0
shared_preferences: ^0.5.6+1 shared_preferences: ^0.5.6+1
progress_indicators: ^0.1.4
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.0.0
@ -24,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.9 firebase_messaging: ^6.0.13
flutter_secure_storage: ^3.3.1+1 flutter_secure_storage: ^3.3.1+1
device_info: ^0.4.1+4 device_info: ^0.4.1+4
flutter_local_notifications: ^1.1.6 flutter_local_notifications: ^1.1.6