Compare commits
29 Commits
beta/0.8.4
...
beta/0.8.5
Author | SHA1 | Date | |
---|---|---|---|
7d746fd546 | |||
3ff55f181e | |||
187e12dd79 | |||
10daf2d952 | |||
31c6509d13 | |||
cb74108814 | |||
9efded2139 | |||
96b3e7c739 | |||
b029146bf3 | |||
d715aaf5e5 | |||
0dc12963f0 | |||
4da3b40d55 | |||
f7d05a57ad | |||
df01599fe0 | |||
2c3335ebf3 | |||
05c1427aa8 | |||
02bfaf7db6 | |||
f488c0810b | |||
8dbfb91234 | |||
aee99e3925 | |||
50d3280803 | |||
a90eb5c4db | |||
16c06a2d48 | |||
513bf85cae | |||
82d7aeba02 | |||
12f7cb86de | |||
b65c885467 | |||
2a828a1289 | |||
291f12ba97 |
@ -17,6 +17,7 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="HA Client"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
62
lib/cards/alarm_panel_card.dart
Normal file
62
lib/cards/alarm_panel_card.dart
Normal 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
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,48 +1,80 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class HACard {
|
||||
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;
|
||||
class CardData {
|
||||
|
||||
HACard({
|
||||
this.name,
|
||||
this.id,
|
||||
this.linkedEntityWrapper,
|
||||
this.columnsCount: 4,
|
||||
this.showName: true,
|
||||
this.showHeaderToggle: true,
|
||||
this.showState: true,
|
||||
this.stateFilter: const [],
|
||||
this.showEmpty: true,
|
||||
this.content,
|
||||
this.states,
|
||||
this.conditions: const [],
|
||||
this.unit,
|
||||
this.min,
|
||||
this.max,
|
||||
this.severity,
|
||||
@required this.type
|
||||
}) {
|
||||
if (this.columnsCount <= 0) {
|
||||
this.columnsCount = 4;
|
||||
String type;
|
||||
List<EntityWrapper> entities = [];
|
||||
List conditions;
|
||||
bool showEmpty;
|
||||
List stateFilter;
|
||||
bool stateColor = true;
|
||||
|
||||
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
||||
|
||||
factory CardData.parse(Map<String, dynamic> rawData) {
|
||||
switch (rawData['type']) {
|
||||
case CardType.ENTITIES:
|
||||
return EntitiesCardData(rawData);
|
||||
break;
|
||||
case CardType.ALARM_PANEL:
|
||||
return AlarmPanelCardData(rawData);
|
||||
break;
|
||||
case CardType.BUTTON:
|
||||
return ButtonCardData(rawData);
|
||||
break;
|
||||
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() {
|
||||
@ -114,10 +146,365 @@ class HACard {
|
||||
}).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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
79
lib/cards/entities_card.dart
Normal file
79
lib/cards/entities_card.dart
Normal 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
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
93
lib/cards/entity_button_card.dart
Normal file
93
lib/cards/entity_button_card.dart
Normal 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
198
lib/cards/gauge_card.dart
Normal 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
120
lib/cards/glance_card.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
30
lib/cards/horizontal_srack_card.dart
Normal file
30
lib/cards/horizontal_srack_card.dart
Normal 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,);
|
||||
}
|
||||
|
||||
|
||||
}
|
33
lib/cards/markdown_card.dart
Normal file
33
lib/cards/markdown_card.dart
Normal 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,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
27
lib/cards/media_control_card.dart
Normal file
27
lib/cards/media_control_card.dart
Normal 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()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
17
lib/cards/unsupported_card.dart
Normal file
17
lib/cards/unsupported_card.dart
Normal 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"),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
23
lib/cards/vertical_stack_card.dart
Normal file
23
lib/cards/vertical_stack_card.dart
Normal 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,);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -4,9 +4,11 @@ class CardHeader extends StatelessWidget {
|
||||
|
||||
final String name;
|
||||
final Widget trailing;
|
||||
final Widget leading;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@ -14,6 +16,7 @@ class CardHeader extends StatelessWidget {
|
||||
if ((name != null) && (name.trim().length > 0)) {
|
||||
result = new ListTile(
|
||||
trailing: trailing,
|
||||
leading: leading,
|
||||
subtitle: subtitle,
|
||||
title: Text("$name",
|
||||
textAlign: TextAlign.left,
|
||||
@ -21,7 +24,7 @@ class CardHeader extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.headline),
|
||||
);
|
||||
} else {
|
||||
result = new Container(width: 0.0, height: 0.0);
|
||||
result = new Container(width: 0.0, height: emptyPadding);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
21
lib/cards/widgets/card_wrapper.widget.dart
Normal file
21
lib/cards/widgets/card_wrapper.widget.dart
Normal 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
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
@ -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],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
@ -118,6 +118,7 @@ class CardType {
|
||||
static const ALARM_PANEL = "alarm-panel";
|
||||
static const MARKDOWN = "markdown";
|
||||
static const LIGHT = "light";
|
||||
static const ENTITY_FILTER = "entity-filter";
|
||||
}
|
||||
|
||||
class Sizes {
|
||||
|
@ -36,7 +36,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
||||
.of(context)
|
||||
.entityWrapper
|
||||
.entity;
|
||||
if (_entity.supportStream) {
|
||||
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
|
||||
HomeAssistant().getCameraStream(_entity.entityId)
|
||||
.then((data) {
|
||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||
|
@ -11,13 +11,13 @@ class DefaultEntityContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EntityModel entityModel = EntityModel.of(context);
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||
return MissedEntityWidget();
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) {
|
||||
return Divider();
|
||||
}
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -1,13 +1,6 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class StatelessEntityType {
|
||||
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;
|
||||
}
|
||||
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
|
||||
|
||||
class Entity {
|
||||
|
||||
@ -76,8 +69,8 @@ class Entity {
|
||||
String entityPicture;
|
||||
String state;
|
||||
String displayState;
|
||||
DateTime _lastUpdated;
|
||||
int statelessType = 0;
|
||||
DateTime lastUpdatedTimestamp;
|
||||
StatelessEntityType statelessType = StatelessEntityType.none;
|
||||
|
||||
List<Entity> childEntities = [];
|
||||
String deviceClass;
|
||||
@ -120,31 +113,36 @@ class Entity {
|
||||
}
|
||||
|
||||
Entity.missed(String entityId) {
|
||||
statelessType = StatelessEntityType.MISSED;
|
||||
statelessType = StatelessEntityType.missed;
|
||||
attributes = {"hidden": false};
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
Entity.divider() {
|
||||
statelessType = StatelessEntityType.DIVIDER;
|
||||
statelessType = StatelessEntityType.divider;
|
||||
attributes = {"hidden": false};
|
||||
}
|
||||
|
||||
Entity.section(String label) {
|
||||
statelessType = StatelessEntityType.SECTION;
|
||||
statelessType = StatelessEntityType.section;
|
||||
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}) {
|
||||
statelessType = StatelessEntityType.CALL_SERVICE;
|
||||
statelessType = StatelessEntityType.callService;
|
||||
entityId = service;
|
||||
displayState = actionName?.toUpperCase() ?? "RUN";
|
||||
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
||||
}
|
||||
|
||||
Entity.weblink({String url, String name, String icon}) {
|
||||
statelessType = StatelessEntityType.WEBLINK;
|
||||
entityId = "custom.custom"; //TODO wtf??
|
||||
statelessType = StatelessEntityType.webLink;
|
||||
entityId = "custom.custom";
|
||||
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||
}
|
||||
|
||||
@ -155,7 +153,7 @@ class Entity {
|
||||
deviceClass = attributes["device_class"];
|
||||
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["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);
|
||||
}
|
||||
|
||||
@ -227,11 +225,11 @@ class Entity {
|
||||
}
|
||||
|
||||
String _getLastUpdatedFormatted() {
|
||||
if (_lastUpdated == null) {
|
||||
if (lastUpdatedTimestamp == null) {
|
||||
return "-";
|
||||
} else {
|
||||
DateTime now = DateTime.now();
|
||||
Duration d = now.difference(_lastUpdated);
|
||||
Duration d = now.difference(lastUpdatedTimestamp);
|
||||
String text;
|
||||
int v;
|
||||
if (d.inDays == 0) {
|
||||
|
@ -50,24 +50,29 @@ class EntityIcon extends StatelessWidget {
|
||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||
data.entity.deviceClass, data.entity.state); //
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
|
||||
child: Icon(
|
||||
return Icon(
|
||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||
size: size,
|
||||
color: color,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
padding: padding,
|
||||
child: buildIcon(
|
||||
entityWrapper,
|
||||
color ?? HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context)
|
||||
iconColor
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
|
||||
final bool handleTap;
|
||||
|
||||
static EntityModel of(BuildContext context) {
|
||||
return context.inheritFromWidgetOfExactType(EntityModel);
|
||||
return context.dependOnInheritedWidgetOfExactType<EntityModel>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||
return true;
|
||||
bool updateShouldNotify(EntityModel oldWidget) {
|
||||
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ class EntityName extends StatelessWidget {
|
||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||
TextStyle tStyle;
|
||||
if (textStyle == null) {
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
||||
if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) {
|
||||
tStyle = HAClientTheme().getLinkTextStyle(context);
|
||||
} else {
|
||||
tStyle = Theme.of(context).textTheme.body1;
|
||||
|
@ -4,6 +4,7 @@ class EntityWrapper {
|
||||
|
||||
String overrideName;
|
||||
final String overrideIcon;
|
||||
final bool stateColor;
|
||||
EntityUIAction uiAction;
|
||||
Entity entity;
|
||||
String unitOfMeasurementOverride;
|
||||
@ -18,10 +19,11 @@ class EntityWrapper {
|
||||
this.entity,
|
||||
this.overrideIcon,
|
||||
this.overrideName,
|
||||
this.stateColor: true,
|
||||
this.uiAction,
|
||||
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) {
|
||||
uiAction = EntityUIAction();
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class SimpleEntityState extends StatelessWidget {
|
||||
TextStyle tStyle;
|
||||
if (textStyle != null) {
|
||||
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(
|
||||
color: Colors.blue
|
||||
);
|
||||
|
@ -27,7 +27,7 @@ class _TimerStateState extends State<TimerState> {
|
||||
try {
|
||||
int passed = DateTime
|
||||
.now()
|
||||
.difference(entity._lastUpdated)
|
||||
.difference(entity.lastUpdatedTimestamp)
|
||||
.inSeconds;
|
||||
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
||||
} catch (e) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class UniversalSlider extends StatelessWidget {
|
||||
class UniversalSlider extends StatefulWidget {
|
||||
|
||||
final onChanged;
|
||||
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);
|
||||
|
||||
@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
|
||||
Widget build(BuildContext context) {
|
||||
List <Widget> row = [];
|
||||
if (leading != null) {
|
||||
row.add(leading);
|
||||
if (widget.leading != null) {
|
||||
row.add(widget.leading);
|
||||
}
|
||||
row.add(
|
||||
Flexible(
|
||||
child: Slider(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
onChanged: (value) => onChanged(value),
|
||||
onChangeEnd: (value) => onChangeEnd(value),
|
||||
value: _value,
|
||||
min: widget.min,
|
||||
max: widget.max,
|
||||
onChangeStart: (_) {
|
||||
_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) {
|
||||
row.add(closing);
|
||||
if (widget.closing != null) {
|
||||
row.add(widget.closing);
|
||||
}
|
||||
return Padding(
|
||||
padding: padding,
|
||||
padding: widget.padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("$title"),
|
||||
Text('${widget.title}'),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -51,5 +87,4 @@ class UniversalSlider extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -46,7 +46,6 @@ class HomeAssistant {
|
||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||
bool get isNoEntities => entities == null || entities.isEmpty;
|
||||
bool get isNoViews => ui == null || ui.isEmpty;
|
||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
||||
|
||||
HomeAssistant._internal() {
|
||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||
@ -75,7 +74,7 @@ class HomeAssistant {
|
||||
futures.add(_getLovelace(null));
|
||||
}
|
||||
Future.wait(futures).then((_) {
|
||||
if (isMobileAppEnabled) {
|
||||
if (isComponentEnabled('mobile_app')) {
|
||||
_createUI();
|
||||
_fetchCompleter.complete();
|
||||
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
||||
@ -103,7 +102,7 @@ class HomeAssistant {
|
||||
_getUserInfo(prefs);
|
||||
_getPanels(prefs);
|
||||
_getServices(prefs);
|
||||
if (isMobileAppEnabled) {
|
||||
if (isComponentEnabled('mobile_app')) {
|
||||
_createUI();
|
||||
}
|
||||
} catch (e) {
|
||||
@ -156,6 +155,7 @@ class HomeAssistant {
|
||||
|
||||
void _parseConfig(data) {
|
||||
_instanceConfig = Map.from(data);
|
||||
Logger.d('stream: ${_instanceConfig['components'].contains('stream')}');
|
||||
}
|
||||
|
||||
Future _getStates(SharedPreferences sharedPrefs) async {
|
||||
@ -305,6 +305,10 @@ class HomeAssistant {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
bool isComponentEnabled(String name) {
|
||||
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name");
|
||||
}
|
||||
|
||||
void _handleLovelaceUpdate() {
|
||||
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||
eventBus.fire(new LovelaceChangedEvent());
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -13,7 +14,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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_custom_tabs/flutter_custom_tabs.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:flutter_local_notifications/flutter_local_notifications.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/spoiler_card.dart';
|
||||
import 'package:workmanager/workmanager.dart' as workManager;
|
||||
@ -63,8 +62,7 @@ part 'entities/badge.widget.dart';
|
||||
part 'entities/entity_model.widget.dart';
|
||||
part 'entities/default_entity_container.widget.dart';
|
||||
part 'entities/missed_entity.widget.dart';
|
||||
part 'cards/widgets/glance_card_entity_container.dart';
|
||||
part 'cards/widgets/entity_button_card_body.widget.dart';
|
||||
part 'cards/entity_button_card.dart';
|
||||
part 'pages/widgets/entity_attributes_list.dart';
|
||||
part 'entities/entity_icon.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/widgets/product_purchase.widget.dart';
|
||||
part 'pages/widgets/page_loading_indicator.dart';
|
||||
part 'pages/widgets/bottom_info_bar.dart';
|
||||
part 'pages/widgets/page_loading_error.dart';
|
||||
part 'pages/panel.page.dart';
|
||||
part 'pages/main/main.page.dart';
|
||||
@ -130,14 +129,21 @@ part 'view.class.dart';
|
||||
part 'cards/card.class.dart';
|
||||
part 'panels/panel_class.dart';
|
||||
part 'viewWidget.widget.dart';
|
||||
part 'cards/card_widget.dart';
|
||||
part 'cards/widgets/card_header.widget.dart';
|
||||
part 'panels/config_panel_widget.dart';
|
||||
part 'panels/widgets/link_to_web_config.dart';
|
||||
part 'types/ha_error.dart';
|
||||
part 'types/event_bus_events.dart';
|
||||
part 'cards/widgets/gauge_card_body.dart';
|
||||
part 'cards/widgets/light_card_body.dart';
|
||||
part 'cards/gauge_card.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 'entities/entity_page_layout.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();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||
const String appName = "HA Client";
|
||||
const appVersionNumber = "0.8.4";
|
||||
const appVersionNumber = "0.8.5";
|
||||
const appVersionAdd = "";
|
||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||
|
||||
@ -164,7 +170,7 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Crashlytics.instance.enableInDevMode = false;
|
||||
Crashlytics.instance.enableInDevMode = true;
|
||||
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
|
||||
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
|
@ -14,7 +14,7 @@ class StartupUserMessagesManager {
|
||||
bool _supportAppDevelopmentMessageShown;
|
||||
bool _whatsNewMessageShown;
|
||||
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 {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
@ -21,7 +21,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
StreamSubscription _showPopupDialogSubscription;
|
||||
StreamSubscription _showPopupMessageSubscription;
|
||||
StreamSubscription _reloadUISubscription;
|
||||
StreamSubscription _fullReloadSubscription;
|
||||
StreamSubscription _showPageSubscription;
|
||||
BottomInfoBarController _bottomInfoBarController;
|
||||
int _previousViewCount;
|
||||
bool _showLoginButton = 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));
|
||||
|
||||
// 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() {
|
||||
_showInfoBottomBar(progress: true,);
|
||||
_bottomInfoBarController.showInfoBottomBar(progress: true,);
|
||||
_subscribe().then((_) {
|
||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
@ -107,8 +111,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
}
|
||||
|
||||
void _quickLoad({bool uiOnly: false}) {
|
||||
_hideBottomBar();
|
||||
_showInfoBottomBar(progress: true,);
|
||||
_bottomInfoBarController.showInfoBottomBar(progress: true,);
|
||||
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
||||
_fetchData(useCache: false, uiOnly: uiOnly);
|
||||
}, onError: (e) {
|
||||
@ -123,7 +126,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
});
|
||||
}
|
||||
await HomeAssistant().fetchData(uiOnly).then((_) {
|
||||
_hideBottomBar();
|
||||
setState((){
|
||||
_bottomInfoBarController.hideBottomBar();
|
||||
});
|
||||
HomeAssistant().saveCache();
|
||||
}).catchError((e) {
|
||||
if (e is HAError) {
|
||||
_setErrorState(e);
|
||||
@ -140,7 +146,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||
_quickLoad();
|
||||
} 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);
|
||||
});
|
||||
}
|
||||
if (_fullReloadSubscription == null) {
|
||||
_fullReloadSubscription = eventBus.on<FullReloadEvent>().listen((event){
|
||||
_fullLoad();
|
||||
});
|
||||
}
|
||||
if (_showPopupDialogSubscription == null) {
|
||||
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
|
||||
_showPopupDialog(
|
||||
@ -213,7 +224,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
|
||||
if (_showErrorSubscription == null) {
|
||||
_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) {
|
||||
if (e == null) {
|
||||
_showErrorBottomBar(
|
||||
_bottomInfoBarController.showErrorBottomBar(
|
||||
HAError("Unknown error")
|
||||
);
|
||||
} 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) {
|
||||
_showInfoBottomBar(
|
||||
_bottomInfoBarController.showInfoBottomBar(
|
||||
message: "Calling $domain.$service",
|
||||
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>();
|
||||
|
||||
Widget _buildScaffoldBody(bool empty) {
|
||||
@ -765,60 +671,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget bottomBar;
|
||||
if (_showBottomBar) {
|
||||
List<Widget> bottomBarChildren = [];
|
||||
if (_bottomBarText != null) {
|
||||
bottomBarChildren.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
||||
Sizes.rowPadding),
|
||||
child: Text(
|
||||
"$_bottomBarText",
|
||||
textAlign: TextAlign.left,
|
||||
softWrap: true,
|
||||
),
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
if (_bottomBarProgress) {
|
||||
bottomBarChildren.add(
|
||||
CollectionScaleTransition(
|
||||
children: <Widget>[
|
||||
Icon(Icons.stop, size: 10.0, color: 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) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
primary: false,
|
||||
drawer: _buildAppDrawer(),
|
||||
bottomNavigationBar: bottomBar,
|
||||
bottomNavigationBar: BottomInfoBar(
|
||||
controller: _bottomInfoBarController,
|
||||
),
|
||||
body: _buildScaffoldBody(true)
|
||||
);
|
||||
} else {
|
||||
@ -826,7 +686,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
key: _scaffoldKey,
|
||||
drawer: _buildAppDrawer(),
|
||||
primary: false,
|
||||
bottomNavigationBar: bottomBar,
|
||||
bottomNavigationBar: BottomInfoBar(
|
||||
controller: _bottomInfoBarController,
|
||||
),
|
||||
body: _buildScaffoldBody(false)
|
||||
);
|
||||
}
|
||||
@ -848,9 +710,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_showErrorSubscription?.cancel();
|
||||
_startAuthSubscription?.cancel();
|
||||
_showPageSubscription?.cancel();
|
||||
_fullReloadSubscription?.cancel();
|
||||
_reloadUISubscription?.cancel();
|
||||
//TODO disconnect
|
||||
//widget.homeAssistant?.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -57,8 +57,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
||||
_loaded = false;
|
||||
});
|
||||
} else {
|
||||
_isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor");
|
||||
//_useMediaExtractor = _isMediaExtractorExist;
|
||||
_isMediaExtractorExist = HomeAssistant().isComponentEnabled("media_extractor");
|
||||
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
|
||||
setState(() {
|
||||
if (_players.isNotEmpty) {
|
||||
|
@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
|
||||
error = "";
|
||||
});
|
||||
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) {
|
||||
setState(() {
|
||||
data = response.body;
|
||||
|
210
lib/pages/widgets/bottom_info_bar.dart
Normal file
210
lib/pages/widgets/bottom_info_bar.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -25,9 +25,15 @@ class RefreshDataFinishedEvent {
|
||||
}
|
||||
|
||||
class ReloadUIEvent {
|
||||
//TODO uiOnly bool
|
||||
|
||||
ReloadUIEvent();
|
||||
}
|
||||
|
||||
class FullReloadEvent {
|
||||
FullReloadEvent();
|
||||
}
|
||||
|
||||
class ChangeThemeEvent {
|
||||
|
||||
final AppTheme theme;
|
||||
|
@ -61,13 +61,7 @@ class HomeAssistantUI {
|
||||
}
|
||||
|
||||
List<Widget> _buildViews(BuildContext context) {
|
||||
List<Widget> result = [];
|
||||
views.forEach((view) {
|
||||
result.add(
|
||||
view.build(context)
|
||||
);
|
||||
});
|
||||
return result;
|
||||
return views.map((view) => view.build(context)).toList();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class HAView {
|
||||
List<HACard> cards = [];
|
||||
List<CardData> cards = [];
|
||||
List<Entity> badges = [];
|
||||
Entity linkedEntity;
|
||||
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) {
|
||||
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;
|
||||
//cards.addAll(_createLovelaceCards(rawData["cards"] ?? [], 1));
|
||||
}
|
||||
|
||||
Widget buildTab() {
|
||||
|
@ -21,7 +21,25 @@ class ViewWidget extends StatelessWidget {
|
||||
if (this.view.cards.isNotEmpty) {
|
||||
cardsContainer = DynamicMultiColumnLayout(
|
||||
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 {
|
||||
cardsContainer = Container();
|
||||
@ -39,7 +57,7 @@ class ViewWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildPanelChild(BuildContext context) {
|
||||
if (this.view.cards != null && this.view.cards.isNotEmpty) {
|
||||
return this.view.cards[0].build(context);
|
||||
return this.view.cards[0].buildCardWidget();
|
||||
} else {
|
||||
return Container(width: 0, height: 0);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: hass_client
|
||||
description: Home Assistant Android Client
|
||||
|
||||
version: 0.8.4+892
|
||||
version: 0.8.5+896
|
||||
|
||||
|
||||
environment:
|
||||
@ -12,7 +12,6 @@ dependencies:
|
||||
sdk: flutter
|
||||
web_socket_channel: ^1.1.0
|
||||
shared_preferences: ^0.5.6+1
|
||||
progress_indicators: ^0.1.4
|
||||
path_provider: ^1.6.5
|
||||
event_bus: ^1.1.1
|
||||
cached_network_image: ^2.0.0
|
||||
@ -24,7 +23,7 @@ dependencies:
|
||||
flutter_custom_tabs: ^0.6.0
|
||||
flutter_webview_plugin: ^0.3.10+1
|
||||
webview_flutter: ^0.3.19+7
|
||||
firebase_messaging: ^6.0.9
|
||||
firebase_messaging: ^6.0.13
|
||||
flutter_secure_storage: ^3.3.1+1
|
||||
device_info: ^0.4.1+4
|
||||
flutter_local_notifications: ^1.1.6
|
||||
|
Reference in New Issue
Block a user