Compare commits
32 Commits
beta/0.8.4
...
beta/0.8.6
Author | SHA1 | Date | |
---|---|---|---|
29ee360ec4 | |||
c0faaafd04 | |||
bc045344a5 | |||
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
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
|
android:name="io.flutter.app.FlutterApplication"
|
||||||
android:label="HA Client"
|
android:label="HA Client"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
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,92 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class HACard {
|
class CardData {
|
||||||
List<EntityWrapper> entities = [];
|
|
||||||
List<HACard> childCards = [];
|
|
||||||
EntityWrapper linkedEntityWrapper;
|
|
||||||
String name;
|
|
||||||
String id;
|
|
||||||
String type;
|
|
||||||
bool showName;
|
|
||||||
bool showState;
|
|
||||||
bool showEmpty;
|
|
||||||
bool showHeaderToggle;
|
|
||||||
int columnsCount;
|
|
||||||
List stateFilter;
|
|
||||||
List states;
|
|
||||||
List conditions;
|
|
||||||
String content;
|
|
||||||
String unit;
|
|
||||||
int min;
|
|
||||||
int max;
|
|
||||||
Map severity;
|
|
||||||
|
|
||||||
HACard({
|
String type;
|
||||||
this.name,
|
List<EntityWrapper> entities = [];
|
||||||
this.id,
|
List conditions;
|
||||||
this.linkedEntityWrapper,
|
bool showEmpty;
|
||||||
this.columnsCount: 4,
|
List stateFilter;
|
||||||
this.showName: true,
|
bool stateColor = true;
|
||||||
this.showHeaderToggle: true,
|
|
||||||
this.showState: true,
|
EntityWrapper get entity => entities.isNotEmpty ? entities[0] : null;
|
||||||
this.stateFilter: const [],
|
|
||||||
this.showEmpty: true,
|
factory CardData.parse(Map<String, dynamic> rawData) {
|
||||||
this.content,
|
try {
|
||||||
this.states,
|
switch (rawData['type']) {
|
||||||
this.conditions: const [],
|
case CardType.ENTITIES:
|
||||||
this.unit,
|
return EntitiesCardData(rawData);
|
||||||
this.min,
|
break;
|
||||||
this.max,
|
case CardType.ALARM_PANEL:
|
||||||
this.severity,
|
return AlarmPanelCardData(rawData);
|
||||||
@required this.type
|
break;
|
||||||
}) {
|
case CardType.BUTTON:
|
||||||
if (this.columnsCount <= 0) {
|
return ButtonCardData(rawData);
|
||||||
this.columnsCount = 4;
|
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);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.e('Error parsing card: $error');
|
||||||
|
return ErrorCardData(rawData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CardData(Map<String, dynamic> rawData) {
|
||||||
|
if (rawData != null) {
|
||||||
|
type = rawData['type'] ?? CardType.ENTITIES;
|
||||||
|
conditions = rawData['conditions'] ?? [];
|
||||||
|
showEmpty = rawData['show_empty'] ?? true;
|
||||||
|
stateFilter = rawData['state_filter'] ?? [];
|
||||||
|
} else {
|
||||||
|
type = CardType.UNKNOWN;
|
||||||
|
conditions = [];
|
||||||
|
showEmpty = true;
|
||||||
|
stateFilter = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return UnsupportedCard(card: this);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<EntityWrapper> getEntitiesToShow() {
|
List<EntityWrapper> getEntitiesToShow() {
|
||||||
@ -114,10 +158,380 @@ class HACard {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
}
|
||||||
return CardWidget(
|
|
||||||
card: this,
|
class EntitiesCardData extends CardData {
|
||||||
|
|
||||||
|
String title;
|
||||||
|
String icon;
|
||||||
|
bool showHeaderToggle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return EntitiesCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntitiesCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
title = rawData["title"];
|
||||||
|
icon = rawData['icon'];
|
||||||
|
stateColor = rawData['state_color'] ?? false;
|
||||||
|
showHeaderToggle = rawData['show_header_toggle'] ?? false;
|
||||||
|
//Parsing entities
|
||||||
|
var rawEntities = rawData["entities"] ?? [];
|
||||||
|
rawEntities.forEach((rawEntity) {
|
||||||
|
if (rawEntity is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||||
|
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rawEntity["type"] == "divider") {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.divider()));
|
||||||
|
} else if (rawEntity["type"] == "section") {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
||||||
|
} else if (rawEntity["type"] == "call-service") {
|
||||||
|
Map uiActionData = {
|
||||||
|
"tap_action": {
|
||||||
|
"action": EntityUIAction.callService,
|
||||||
|
"service": rawEntity["service"],
|
||||||
|
"service_data": rawEntity["service_data"]
|
||||||
|
},
|
||||||
|
"hold_action": EntityUIAction.none
|
||||||
|
};
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: Entity.callService(
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
name: rawEntity["name"],
|
||||||
|
service: rawEntity["service"],
|
||||||
|
actionName: rawEntity["action_name"]
|
||||||
|
),
|
||||||
|
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||||
|
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
} else if (rawEntity["type"] == "weblink") {
|
||||||
|
Map uiActionData = {
|
||||||
|
"tap_action": {
|
||||||
|
"action": EntityUIAction.navigate,
|
||||||
|
"service": rawEntity["url"]
|
||||||
|
},
|
||||||
|
"hold_action": EntityUIAction.none
|
||||||
|
};
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: Entity.weblink(
|
||||||
|
icon: rawEntity["icon"],
|
||||||
|
name: rawEntity["name"],
|
||||||
|
url: rawEntity["url"]
|
||||||
|
),
|
||||||
|
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||||
|
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||||
|
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
stateColor: rawEntity["state_color"] ?? stateColor,
|
||||||
|
overrideName: rawEntity["name"],
|
||||||
|
overrideIcon: rawEntity["icon"],
|
||||||
|
stateFilter: rawEntity['state_filter'] ?? [],
|
||||||
|
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlarmPanelCardData extends CardData {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
List<dynamic> states;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return AlarmPanelCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlarmPanelCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
name = rawData['name'];
|
||||||
|
states = rawData['states'];
|
||||||
|
//Parsing entity
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
stateColor: true,
|
||||||
|
overrideName: name
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ButtonCardData extends CardData {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
String icon;
|
||||||
|
bool showName;
|
||||||
|
bool showIcon;
|
||||||
|
double iconHeightPx = 0;
|
||||||
|
double iconHeightRem = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return EntityButtonCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
name = rawData['name'];
|
||||||
|
icon = rawData['icon'];
|
||||||
|
showName = rawData['show_name'] ?? true;
|
||||||
|
showIcon = rawData['show_icon'] ?? true;
|
||||||
|
stateColor = rawData['state_color'] ?? true;
|
||||||
|
var rawHeight = rawData['icon_height'];
|
||||||
|
if (rawHeight != null && rawHeight is String) {
|
||||||
|
if (rawHeight.contains('px')) {
|
||||||
|
iconHeightPx = double.tryParse(rawHeight.replaceFirst('px', '')) ?? 0;
|
||||||
|
} else if (rawHeight.contains('rem')) {
|
||||||
|
iconHeightRem = double.tryParse(rawHeight.replaceFirst('rem', '')) ?? 0;
|
||||||
|
} else if (rawHeight.contains('em')) {
|
||||||
|
iconHeightRem = double.tryParse(rawHeight.replaceFirst('em', '')) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Parsing entity
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
overrideName: name,
|
||||||
|
overrideIcon: icon,
|
||||||
|
stateColor: stateColor,
|
||||||
|
uiAction: EntityUIAction(
|
||||||
|
rawEntityData: rawData
|
||||||
|
)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
} else if (entitiId == null) {
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: Entity.ghost(
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
),
|
||||||
|
stateColor: stateColor,
|
||||||
|
uiAction: EntityUIAction(
|
||||||
|
rawEntityData: rawData
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GaugeCardData extends CardData {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
String unit;
|
||||||
|
int min;
|
||||||
|
int max;
|
||||||
|
Map severity;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return GaugeCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
GaugeCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
name = rawData['name'];
|
||||||
|
unit = rawData['unit'];
|
||||||
|
min = rawData['min'] ?? 0;
|
||||||
|
max = rawData['max'] ?? 100;
|
||||||
|
severity = rawData['severity'];
|
||||||
|
//Parsing entity
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
overrideName: name
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlanceCardData extends CardData {
|
||||||
|
|
||||||
|
String title;
|
||||||
|
bool showName;
|
||||||
|
bool showIcon;
|
||||||
|
bool showState;
|
||||||
|
bool stateColor;
|
||||||
|
int columnsCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return GlanceCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
GlanceCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
title = rawData["title"];
|
||||||
|
showName = rawData['show_name'] ?? true;
|
||||||
|
showIcon = rawData['show_icon'] ?? true;
|
||||||
|
showState = rawData['show_state'] ?? true;
|
||||||
|
stateColor = rawData['state_color'] ?? true;
|
||||||
|
columnsCount = rawData['columns'] ?? 4;
|
||||||
|
//Parsing entities
|
||||||
|
var rawEntities = rawData["entities"] ?? [];
|
||||||
|
rawEntities.forEach((rawEntity) {
|
||||||
|
if (rawEntity is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(rawEntity)) {
|
||||||
|
entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
||||||
|
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
||||||
|
entities.add(
|
||||||
|
EntityWrapper(
|
||||||
|
entity: e,
|
||||||
|
stateColor: stateColor,
|
||||||
|
overrideName: rawEntity["name"],
|
||||||
|
overrideIcon: rawEntity["icon"],
|
||||||
|
stateFilter: rawEntity['state_filter'] ?? [],
|
||||||
|
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class HorizontalStackCardData extends CardData {
|
||||||
|
|
||||||
|
List<CardData> childCards;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return HorizontalStackCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
if (rawData.containsKey('cards')) {
|
||||||
|
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||||
|
return CardData.parse(childCard);
|
||||||
|
}).toList();
|
||||||
|
} else {
|
||||||
|
childCards = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerticalStackCardData extends CardData {
|
||||||
|
|
||||||
|
List<CardData> childCards;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return VerticalStackCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalStackCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
if (rawData.containsKey('cards')) {
|
||||||
|
childCards = rawData['cards'].map<CardData>((childCard) {
|
||||||
|
return CardData.parse(childCard);
|
||||||
|
}).toList();
|
||||||
|
} else {
|
||||||
|
childCards = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarkdownCardData extends CardData {
|
||||||
|
|
||||||
|
String title;
|
||||||
|
String content;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return MarkdownCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
//Parsing card data
|
||||||
|
title = rawData['title'];
|
||||||
|
content = rawData['content'];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaControlCardData extends CardData {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return MediaControlsCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaControlCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
var entitiId = rawData["entity"];
|
||||||
|
if (entitiId != null && entitiId is String) {
|
||||||
|
if (HomeAssistant().entities.isExist(entitiId)) {
|
||||||
|
entities.add(EntityWrapper(
|
||||||
|
entity: HomeAssistant().entities.get(entitiId),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
entities.add(EntityWrapper(entity: Entity.missed(entitiId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorCardData extends CardData {
|
||||||
|
|
||||||
|
String cardConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildCardWidget() {
|
||||||
|
return ErrorCard(card: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorCardData(Map<String, dynamic> rawData) : super(rawData) {
|
||||||
|
cardConfig = '$rawData';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
38
lib/cards/error_card.dart
Normal file
38
lib/cards/error_card.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class ErrorCard extends StatelessWidget {
|
||||||
|
final ErrorCardData card;
|
||||||
|
|
||||||
|
const ErrorCard({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: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(new ClipboardData(text: card.cardConfig));
|
||||||
|
},
|
||||||
|
child: Text('Copy card config'),
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
|
||||||
|
},
|
||||||
|
child: Text('Report issue'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
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 String name;
|
||||||
final Widget trailing;
|
final Widget trailing;
|
||||||
|
final Widget leading;
|
||||||
final Widget subtitle;
|
final Widget subtitle;
|
||||||
|
final double emptyPadding;
|
||||||
|
|
||||||
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
const CardHeader({Key key, this.name, this.leading, this.emptyPadding: 0, this.trailing, this.subtitle}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -14,6 +16,7 @@ class CardHeader extends StatelessWidget {
|
|||||||
if ((name != null) && (name.trim().length > 0)) {
|
if ((name != null) && (name.trim().length > 0)) {
|
||||||
result = new ListTile(
|
result = new ListTile(
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
|
leading: leading,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
title: Text("$name",
|
title: Text("$name",
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
@ -21,7 +24,7 @@ class CardHeader extends StatelessWidget {
|
|||||||
style: Theme.of(context).textTheme.headline),
|
style: Theme.of(context).textTheme.headline),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = new Container(width: 0.0, height: 0.0);
|
result = new Container(width: 0.0, height: emptyPadding);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
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,8 @@ class CardType {
|
|||||||
static const ALARM_PANEL = "alarm-panel";
|
static const ALARM_PANEL = "alarm-panel";
|
||||||
static const MARKDOWN = "markdown";
|
static const MARKDOWN = "markdown";
|
||||||
static const LIGHT = "light";
|
static const LIGHT = "light";
|
||||||
|
static const ENTITY_FILTER = "entity-filter";
|
||||||
|
static const UNKNOWN = "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
class Sizes {
|
class Sizes {
|
||||||
|
@ -36,7 +36,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
.of(context)
|
.of(context)
|
||||||
.entityWrapper
|
.entityWrapper
|
||||||
.entity;
|
.entity;
|
||||||
if (_entity.supportStream) {
|
if (_entity.supportStream && HomeAssistant().isComponentEnabled('stream')) {
|
||||||
HomeAssistant().getCameraStream(_entity.entityId)
|
HomeAssistant().getCameraStream(_entity.entityId)
|
||||||
.then((data) {
|
.then((data) {
|
||||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
|
@ -11,13 +11,13 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityModel entityModel = EntityModel.of(context);
|
final EntityModel entityModel = EntityModel.of(context);
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.MISSED) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.missed) {
|
||||||
return MissedEntityWidget();
|
return MissedEntityWidget();
|
||||||
}
|
}
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.DIVIDER) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.divider) {
|
||||||
return Divider();
|
return Divider();
|
||||||
}
|
}
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.SECTION) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.section) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class StatelessEntityType {
|
enum StatelessEntityType {none, missed, ghost, divider, section, callService, webLink}
|
||||||
static const NONE = 0;
|
|
||||||
static const MISSED = 1;
|
|
||||||
static const DIVIDER = 2;
|
|
||||||
static const SECTION = 3;
|
|
||||||
static const CALL_SERVICE = 4;
|
|
||||||
static const WEBLINK = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Entity {
|
class Entity {
|
||||||
|
|
||||||
@ -76,8 +69,8 @@ class Entity {
|
|||||||
String entityPicture;
|
String entityPicture;
|
||||||
String state;
|
String state;
|
||||||
String displayState;
|
String displayState;
|
||||||
DateTime _lastUpdated;
|
DateTime lastUpdatedTimestamp;
|
||||||
int statelessType = 0;
|
StatelessEntityType statelessType = StatelessEntityType.none;
|
||||||
|
|
||||||
List<Entity> childEntities = [];
|
List<Entity> childEntities = [];
|
||||||
String deviceClass;
|
String deviceClass;
|
||||||
@ -120,31 +113,36 @@ class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Entity.missed(String entityId) {
|
Entity.missed(String entityId) {
|
||||||
statelessType = StatelessEntityType.MISSED;
|
statelessType = StatelessEntityType.missed;
|
||||||
attributes = {"hidden": false};
|
attributes = {"hidden": false};
|
||||||
this.entityId = entityId;
|
this.entityId = entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.divider() {
|
Entity.divider() {
|
||||||
statelessType = StatelessEntityType.DIVIDER;
|
statelessType = StatelessEntityType.divider;
|
||||||
attributes = {"hidden": false};
|
attributes = {"hidden": false};
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.section(String label) {
|
Entity.section(String label) {
|
||||||
statelessType = StatelessEntityType.SECTION;
|
statelessType = StatelessEntityType.section;
|
||||||
attributes = {"hidden": false, "friendly_name": "$label"};
|
attributes = {"hidden": false, "friendly_name": "$label"};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Entity.ghost(String name, String icon) {
|
||||||
|
statelessType = StatelessEntityType.ghost;
|
||||||
|
attributes = {"icon": icon, "hidden": false, "friendly_name": name};
|
||||||
|
}
|
||||||
|
|
||||||
Entity.callService({String icon, String name, String service, String actionName}) {
|
Entity.callService({String icon, String name, String service, String actionName}) {
|
||||||
statelessType = StatelessEntityType.CALL_SERVICE;
|
statelessType = StatelessEntityType.callService;
|
||||||
entityId = service;
|
entityId = service;
|
||||||
displayState = actionName?.toUpperCase() ?? "RUN";
|
displayState = actionName?.toUpperCase() ?? "RUN";
|
||||||
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
attributes = {"hidden": false, "friendly_name": "$name", "icon": "$icon"};
|
||||||
}
|
}
|
||||||
|
|
||||||
Entity.weblink({String url, String name, String icon}) {
|
Entity.weblink({String url, String name, String icon}) {
|
||||||
statelessType = StatelessEntityType.WEBLINK;
|
statelessType = StatelessEntityType.webLink;
|
||||||
entityId = "custom.custom"; //TODO wtf??
|
entityId = "custom.custom";
|
||||||
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
attributes = {"hidden": false, "friendly_name": "${name ?? url}", "icon": "${icon ?? 'mdi:link'}"};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +153,7 @@ class Entity {
|
|||||||
deviceClass = attributes["device_class"];
|
deviceClass = attributes["device_class"];
|
||||||
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
|
state = rawData["state"] is bool ? (rawData["state"] ? EntityState.on : EntityState.off) : rawData["state"];
|
||||||
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
displayState = Entity.StateByDeviceClass["$deviceClass.$state"] ?? (state.toLowerCase() == 'unknown' ? '-' : state);
|
||||||
_lastUpdated = DateTime.tryParse(rawData["last_updated"]);
|
lastUpdatedTimestamp = DateTime.tryParse(rawData["last_updated"]);
|
||||||
entityPicture = _getEntityPictureUrl(webHost);
|
entityPicture = _getEntityPictureUrl(webHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,11 +225,11 @@ class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _getLastUpdatedFormatted() {
|
String _getLastUpdatedFormatted() {
|
||||||
if (_lastUpdated == null) {
|
if (lastUpdatedTimestamp == null) {
|
||||||
return "-";
|
return "-";
|
||||||
} else {
|
} else {
|
||||||
DateTime now = DateTime.now();
|
DateTime now = DateTime.now();
|
||||||
Duration d = now.difference(_lastUpdated);
|
Duration d = now.difference(lastUpdatedTimestamp);
|
||||||
String text;
|
String text;
|
||||||
int v;
|
int v;
|
||||||
if (d.inDays == 0) {
|
if (d.inDays == 0) {
|
||||||
|
@ -50,24 +50,29 @@ class EntityIcon extends StatelessWidget {
|
|||||||
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
iconCode = getDefaultIconByEntityId(data.entity.entityId,
|
||||||
data.entity.deviceClass, data.entity.state); //
|
data.entity.deviceClass, data.entity.state); //
|
||||||
}
|
}
|
||||||
return Padding(
|
return Icon(
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 6.0),
|
|
||||||
child: Icon(
|
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
size: size,
|
size: size,
|
||||||
color: color,
|
color: color,
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
|
Color iconColor;
|
||||||
|
if (color != null) {
|
||||||
|
iconColor = color;
|
||||||
|
} else if (entityWrapper.stateColor) {
|
||||||
|
iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context);
|
||||||
|
} else {
|
||||||
|
iconColor = HAClientTheme().getOffStateColor(context);
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: buildIcon(
|
child: buildIcon(
|
||||||
entityWrapper,
|
entityWrapper,
|
||||||
color ?? HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context)
|
iconColor
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,11 @@ class EntityModel extends InheritedWidget {
|
|||||||
final bool handleTap;
|
final bool handleTap;
|
||||||
|
|
||||||
static EntityModel of(BuildContext context) {
|
static EntityModel of(BuildContext context) {
|
||||||
return context.inheritFromWidgetOfExactType(EntityModel);
|
return context.dependOnInheritedWidgetOfExactType<EntityModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
bool updateShouldNotify(EntityModel oldWidget) {
|
||||||
return true;
|
return entityWrapper.entity.lastUpdatedTimestamp != oldWidget.entityWrapper.entity.lastUpdatedTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ class EntityName extends StatelessWidget {
|
|||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
TextStyle tStyle;
|
TextStyle tStyle;
|
||||||
if (textStyle == null) {
|
if (textStyle == null) {
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) {
|
||||||
tStyle = HAClientTheme().getLinkTextStyle(context);
|
tStyle = HAClientTheme().getLinkTextStyle(context);
|
||||||
} else {
|
} else {
|
||||||
tStyle = Theme.of(context).textTheme.body1;
|
tStyle = Theme.of(context).textTheme.body1;
|
||||||
|
@ -4,6 +4,7 @@ class EntityWrapper {
|
|||||||
|
|
||||||
String overrideName;
|
String overrideName;
|
||||||
final String overrideIcon;
|
final String overrideIcon;
|
||||||
|
final bool stateColor;
|
||||||
EntityUIAction uiAction;
|
EntityUIAction uiAction;
|
||||||
Entity entity;
|
Entity entity;
|
||||||
String unitOfMeasurementOverride;
|
String unitOfMeasurementOverride;
|
||||||
@ -18,10 +19,11 @@ class EntityWrapper {
|
|||||||
this.entity,
|
this.entity,
|
||||||
this.overrideIcon,
|
this.overrideIcon,
|
||||||
this.overrideName,
|
this.overrideName,
|
||||||
|
this.stateColor: true,
|
||||||
this.uiAction,
|
this.uiAction,
|
||||||
this.stateFilter
|
this.stateFilter
|
||||||
}) {
|
}) {
|
||||||
if (entity.statelessType == StatelessEntityType.NONE || entity.statelessType == StatelessEntityType.CALL_SERVICE || entity.statelessType == StatelessEntityType.WEBLINK) {
|
if (entity.statelessType == StatelessEntityType.ghost || entity.statelessType == StatelessEntityType.none || entity.statelessType == StatelessEntityType.callService || entity.statelessType == StatelessEntityType.webLink) {
|
||||||
if (uiAction == null) {
|
if (uiAction == null) {
|
||||||
uiAction = EntityUIAction();
|
uiAction = EntityUIAction();
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
TextStyle tStyle;
|
TextStyle tStyle;
|
||||||
if (textStyle != null) {
|
if (textStyle != null) {
|
||||||
tStyle = textStyle;
|
tStyle = textStyle;
|
||||||
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
|
||||||
tStyle = Theme.of(context).textTheme.subhead.copyWith(
|
tStyle = Theme.of(context).textTheme.subhead.copyWith(
|
||||||
color: Colors.blue
|
color: Colors.blue
|
||||||
);
|
);
|
||||||
|
@ -27,7 +27,7 @@ class _TimerStateState extends State<TimerState> {
|
|||||||
try {
|
try {
|
||||||
int passed = DateTime
|
int passed = DateTime
|
||||||
.now()
|
.now()
|
||||||
.difference(entity._lastUpdated)
|
.difference(entity.lastUpdatedTimestamp)
|
||||||
.inSeconds;
|
.inSeconds;
|
||||||
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
remaining = Duration(seconds: entity.duration.inSeconds - passed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
part of '../main.dart';
|
part of '../main.dart';
|
||||||
|
|
||||||
class UniversalSlider extends StatelessWidget {
|
class UniversalSlider extends StatefulWidget {
|
||||||
|
|
||||||
final onChanged;
|
final onChanged;
|
||||||
final onChangeEnd;
|
final onChangeEnd;
|
||||||
@ -14,33 +14,69 @@ class UniversalSlider extends StatelessWidget {
|
|||||||
|
|
||||||
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
const UniversalSlider({Key key, this.onChanged, this.onChangeEnd, this.leading, this.closing, this.title, this.min, this.max, this.value, this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0)}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return UniversalSliderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class UniversalSliderState extends State<UniversalSlider> {
|
||||||
|
|
||||||
|
double _value;
|
||||||
|
bool _changeStarted = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_value = widget.value;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List <Widget> row = [];
|
List <Widget> row = [];
|
||||||
if (leading != null) {
|
if (widget.leading != null) {
|
||||||
row.add(leading);
|
row.add(widget.leading);
|
||||||
}
|
}
|
||||||
row.add(
|
row.add(
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
value: value,
|
value: _value,
|
||||||
min: min,
|
min: widget.min,
|
||||||
max: max,
|
max: widget.max,
|
||||||
onChanged: (value) => onChanged(value),
|
onChangeStart: (_) {
|
||||||
onChangeEnd: (value) => onChangeEnd(value),
|
_changeStarted = true;
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_value = value;
|
||||||
|
});
|
||||||
|
widget.onChanged(value);
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_changeStarted = false;
|
||||||
|
Timer(Duration(milliseconds: 500), () {
|
||||||
|
if (!_changeStarted) {
|
||||||
|
setState(() {
|
||||||
|
_value = value;
|
||||||
|
});
|
||||||
|
widget.onChangeEnd(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (closing != null) {
|
if (widget.closing != null) {
|
||||||
row.add(closing);
|
row.add(widget.closing);
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: widget.padding,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text("$title"),
|
Text('${widget.title}'),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -51,5 +87,4 @@ class UniversalSlider extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -46,7 +46,6 @@ class HomeAssistant {
|
|||||||
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
String get userAvatarText => userName.length > 0 ? userName[0] : "";
|
||||||
bool get isNoEntities => entities == null || entities.isEmpty;
|
bool get isNoEntities => entities == null || entities.isEmpty;
|
||||||
bool get isNoViews => ui == null || ui.isEmpty;
|
bool get isNoViews => ui == null || ui.isEmpty;
|
||||||
bool get isMobileAppEnabled => _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("mobile_app");
|
|
||||||
|
|
||||||
HomeAssistant._internal() {
|
HomeAssistant._internal() {
|
||||||
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
ConnectionManager().onStateChangeCallback = _handleEntityStateChange;
|
||||||
@ -75,7 +74,7 @@ class HomeAssistant {
|
|||||||
futures.add(_getLovelace(null));
|
futures.add(_getLovelace(null));
|
||||||
}
|
}
|
||||||
Future.wait(futures).then((_) {
|
Future.wait(futures).then((_) {
|
||||||
if (isMobileAppEnabled) {
|
if (isComponentEnabled('mobile_app')) {
|
||||||
_createUI();
|
_createUI();
|
||||||
_fetchCompleter.complete();
|
_fetchCompleter.complete();
|
||||||
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
if (!uiOnly) MobileAppIntegrationManager.checkAppRegistration();
|
||||||
@ -103,7 +102,7 @@ class HomeAssistant {
|
|||||||
_getUserInfo(prefs);
|
_getUserInfo(prefs);
|
||||||
_getPanels(prefs);
|
_getPanels(prefs);
|
||||||
_getServices(prefs);
|
_getServices(prefs);
|
||||||
if (isMobileAppEnabled) {
|
if (isComponentEnabled('mobile_app')) {
|
||||||
_createUI();
|
_createUI();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -156,6 +155,7 @@ class HomeAssistant {
|
|||||||
|
|
||||||
void _parseConfig(data) {
|
void _parseConfig(data) {
|
||||||
_instanceConfig = Map.from(data);
|
_instanceConfig = Map.from(data);
|
||||||
|
Logger.d('stream: ${_instanceConfig['components'].contains('stream')}');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _getStates(SharedPreferences sharedPrefs) async {
|
Future _getStates(SharedPreferences sharedPrefs) async {
|
||||||
@ -305,6 +305,10 @@ class HomeAssistant {
|
|||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isComponentEnabled(String name) {
|
||||||
|
return _instanceConfig["components"] != null && (_instanceConfig["components"] as List).contains("$name");
|
||||||
|
}
|
||||||
|
|
||||||
void _handleLovelaceUpdate() {
|
void _handleLovelaceUpdate() {
|
||||||
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
if (_fetchCompleter != null && _fetchCompleter.isCompleted) {
|
||||||
eventBus.fire(new LovelaceChangedEvent());
|
eventBus.fire(new LovelaceChangedEvent());
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -13,7 +14,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:date_format/date_format.dart';
|
import 'package:date_format/date_format.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:charts_flutter/flutter.dart' as charts;
|
import 'package:charts_flutter/flutter.dart' as charts;
|
||||||
import 'package:progress_indicators/progress_indicators.dart';
|
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
@ -21,7 +21,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
import 'package:device_info/device_info.dart';
|
import 'package:device_info/device_info.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'plugins/circular_slider/single_circular_slider.dart';
|
|
||||||
import 'plugins/dynamic_multi_column_layout.dart';
|
import 'plugins/dynamic_multi_column_layout.dart';
|
||||||
import 'plugins/spoiler_card.dart';
|
import 'plugins/spoiler_card.dart';
|
||||||
import 'package:workmanager/workmanager.dart' as workManager;
|
import 'package:workmanager/workmanager.dart' as workManager;
|
||||||
@ -63,8 +62,7 @@ part 'entities/badge.widget.dart';
|
|||||||
part 'entities/entity_model.widget.dart';
|
part 'entities/entity_model.widget.dart';
|
||||||
part 'entities/default_entity_container.widget.dart';
|
part 'entities/default_entity_container.widget.dart';
|
||||||
part 'entities/missed_entity.widget.dart';
|
part 'entities/missed_entity.widget.dart';
|
||||||
part 'cards/widgets/glance_card_entity_container.dart';
|
part 'cards/entity_button_card.dart';
|
||||||
part 'cards/widgets/entity_button_card_body.widget.dart';
|
|
||||||
part 'pages/widgets/entity_attributes_list.dart';
|
part 'pages/widgets/entity_attributes_list.dart';
|
||||||
part 'entities/entity_icon.widget.dart';
|
part 'entities/entity_icon.widget.dart';
|
||||||
part 'entities/entity_name.widget.dart';
|
part 'entities/entity_name.widget.dart';
|
||||||
@ -106,6 +104,7 @@ part 'pages/settings/connection_settings.part.dart';
|
|||||||
part 'pages/purchase.page.dart';
|
part 'pages/purchase.page.dart';
|
||||||
part 'pages/widgets/product_purchase.widget.dart';
|
part 'pages/widgets/product_purchase.widget.dart';
|
||||||
part 'pages/widgets/page_loading_indicator.dart';
|
part 'pages/widgets/page_loading_indicator.dart';
|
||||||
|
part 'pages/widgets/bottom_info_bar.dart';
|
||||||
part 'pages/widgets/page_loading_error.dart';
|
part 'pages/widgets/page_loading_error.dart';
|
||||||
part 'pages/panel.page.dart';
|
part 'pages/panel.page.dart';
|
||||||
part 'pages/main/main.page.dart';
|
part 'pages/main/main.page.dart';
|
||||||
@ -130,14 +129,22 @@ part 'view.class.dart';
|
|||||||
part 'cards/card.class.dart';
|
part 'cards/card.class.dart';
|
||||||
part 'panels/panel_class.dart';
|
part 'panels/panel_class.dart';
|
||||||
part 'viewWidget.widget.dart';
|
part 'viewWidget.widget.dart';
|
||||||
part 'cards/card_widget.dart';
|
|
||||||
part 'cards/widgets/card_header.widget.dart';
|
part 'cards/widgets/card_header.widget.dart';
|
||||||
part 'panels/config_panel_widget.dart';
|
part 'panels/config_panel_widget.dart';
|
||||||
part 'panels/widgets/link_to_web_config.dart';
|
part 'panels/widgets/link_to_web_config.dart';
|
||||||
part 'types/ha_error.dart';
|
part 'types/ha_error.dart';
|
||||||
part 'types/event_bus_events.dart';
|
part 'types/event_bus_events.dart';
|
||||||
part 'cards/widgets/gauge_card_body.dart';
|
part 'cards/gauge_card.dart';
|
||||||
part 'cards/widgets/light_card_body.dart';
|
part 'cards/widgets/card_wrapper.widget.dart';
|
||||||
|
part 'cards/entities_card.dart';
|
||||||
|
part 'cards/alarm_panel_card.dart';
|
||||||
|
part 'cards/horizontal_srack_card.dart';
|
||||||
|
part 'cards/markdown_card.dart';
|
||||||
|
part 'cards/media_control_card.dart';
|
||||||
|
part 'cards/unsupported_card.dart';
|
||||||
|
part 'cards/error_card.dart';
|
||||||
|
part 'cards/vertical_stack_card.dart';
|
||||||
|
part 'cards/glance_card.dart';
|
||||||
part 'pages/play_media.page.dart';
|
part 'pages/play_media.page.dart';
|
||||||
part 'entities/entity_page_layout.widget.dart';
|
part 'entities/entity_page_layout.widget.dart';
|
||||||
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
|
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
|
||||||
@ -149,7 +156,7 @@ EventBus eventBus = new EventBus();
|
|||||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
||||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
|
||||||
const String appName = "HA Client";
|
const String appName = "HA Client";
|
||||||
const appVersionNumber = "0.8.4";
|
const appVersionNumber = "0.8.6";
|
||||||
const appVersionAdd = "";
|
const appVersionAdd = "";
|
||||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||||
|
|
||||||
@ -164,7 +171,7 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
Crashlytics.instance.enableInDevMode = false;
|
Crashlytics.instance.enableInDevMode = true;
|
||||||
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
|
SyncfusionLicense.registerLicense(secrets['syncfusion_license_key']);
|
||||||
|
|
||||||
FlutterError.onError = (FlutterErrorDetails details) {
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
|
@ -14,7 +14,7 @@ class StartupUserMessagesManager {
|
|||||||
bool _supportAppDevelopmentMessageShown;
|
bool _supportAppDevelopmentMessageShown;
|
||||||
bool _whatsNewMessageShown;
|
bool _whatsNewMessageShown;
|
||||||
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
|
||||||
static final _whatsNewMessageKey = "user-message-shown-whats-new-888";
|
static final _whatsNewMessageKey = "user-message-shown-whats-new-894";
|
||||||
|
|
||||||
void checkMessagesToShow() async {
|
void checkMessagesToShow() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
@ -21,7 +21,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
StreamSubscription _showPopupDialogSubscription;
|
StreamSubscription _showPopupDialogSubscription;
|
||||||
StreamSubscription _showPopupMessageSubscription;
|
StreamSubscription _showPopupMessageSubscription;
|
||||||
StreamSubscription _reloadUISubscription;
|
StreamSubscription _reloadUISubscription;
|
||||||
|
StreamSubscription _fullReloadSubscription;
|
||||||
StreamSubscription _showPageSubscription;
|
StreamSubscription _showPageSubscription;
|
||||||
|
BottomInfoBarController _bottomInfoBarController;
|
||||||
int _previousViewCount;
|
int _previousViewCount;
|
||||||
bool _showLoginButton = false;
|
bool _showLoginButton = false;
|
||||||
bool _preventAppRefresh = false;
|
bool _preventAppRefresh = false;
|
||||||
@ -46,6 +48,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_bottomInfoBarController = BottomInfoBarController();
|
||||||
|
|
||||||
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
|
||||||
|
|
||||||
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
||||||
@ -91,7 +95,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _fullLoad() {
|
void _fullLoad() {
|
||||||
_showInfoBottomBar(progress: true,);
|
_bottomInfoBarController.showInfoBottomBar(progress: true,);
|
||||||
_subscribe().then((_) {
|
_subscribe().then((_) {
|
||||||
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
@ -107,8 +111,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _quickLoad({bool uiOnly: false}) {
|
void _quickLoad({bool uiOnly: false}) {
|
||||||
_hideBottomBar();
|
_bottomInfoBarController.showInfoBottomBar(progress: true,);
|
||||||
_showInfoBottomBar(progress: true,);
|
|
||||||
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
ConnectionManager().init(loadSettings: false, forceReconnect: false).then((_){
|
||||||
_fetchData(useCache: false, uiOnly: uiOnly);
|
_fetchData(useCache: false, uiOnly: uiOnly);
|
||||||
}, onError: (e) {
|
}, onError: (e) {
|
||||||
@ -123,7 +126,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
await HomeAssistant().fetchData(uiOnly).then((_) {
|
await HomeAssistant().fetchData(uiOnly).then((_) {
|
||||||
_hideBottomBar();
|
setState((){
|
||||||
|
_bottomInfoBarController.hideBottomBar();
|
||||||
|
});
|
||||||
|
HomeAssistant().saveCache();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
if (e is HAError) {
|
if (e is HAError) {
|
||||||
_setErrorState(e);
|
_setErrorState(e);
|
||||||
@ -140,7 +146,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
if (state == AppLifecycleState.resumed && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||||
_quickLoad();
|
_quickLoad();
|
||||||
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
} else if (state == AppLifecycleState.paused && ConnectionManager().settingsLoaded && !_preventAppRefresh) {
|
||||||
HomeAssistant().saveCache();
|
//HomeAssistant().saveCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,6 +173,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_quickLoad(uiOnly: true);
|
_quickLoad(uiOnly: true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (_fullReloadSubscription == null) {
|
||||||
|
_fullReloadSubscription = eventBus.on<FullReloadEvent>().listen((event){
|
||||||
|
_fullLoad();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (_showPopupDialogSubscription == null) {
|
if (_showPopupDialogSubscription == null) {
|
||||||
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
|
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
|
||||||
_showPopupDialog(
|
_showPopupDialog(
|
||||||
@ -213,7 +224,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
if (_showErrorSubscription == null) {
|
if (_showErrorSubscription == null) {
|
||||||
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
_showErrorSubscription = eventBus.on<ShowErrorEvent>().listen((event){
|
||||||
_showErrorBottomBar(event.error);
|
_bottomInfoBarController.showErrorBottomBar(event.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,11 +256,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
_setErrorState(HAError e) {
|
_setErrorState(HAError e) {
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
_showErrorBottomBar(
|
_bottomInfoBarController.showErrorBottomBar(
|
||||||
HAError("Unknown error")
|
HAError("Unknown error")
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_showErrorBottomBar(e);
|
_bottomInfoBarController.showErrorBottomBar(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +302,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _notifyServiceCalled(String domain, String service, entityId) {
|
void _notifyServiceCalled(String domain, String service, entityId) {
|
||||||
_showInfoBottomBar(
|
_bottomInfoBarController.showInfoBottomBar(
|
||||||
message: "Calling $domain.$service",
|
message: "Calling $domain.$service",
|
||||||
duration: Duration(seconds: 4)
|
duration: Duration(seconds: 4)
|
||||||
);
|
);
|
||||||
@ -481,111 +492,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _hideBottomBar() {
|
|
||||||
//_scaffoldKey?.currentState?.hideCurrentSnackBar();
|
|
||||||
setState(() {
|
|
||||||
_showBottomBar = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _bottomBarAction;
|
|
||||||
bool _showBottomBar = false;
|
|
||||||
String _bottomBarText;
|
|
||||||
bool _bottomBarProgress;
|
|
||||||
bool _bottomBarErrorColor;
|
|
||||||
Timer _bottomBarTimer;
|
|
||||||
|
|
||||||
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
|
||||||
_bottomBarTimer?.cancel();
|
|
||||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
|
||||||
_bottomBarErrorColor = false;
|
|
||||||
setState(() {
|
|
||||||
_bottomBarText = message;
|
|
||||||
_bottomBarProgress = progress;
|
|
||||||
_showBottomBar = true;
|
|
||||||
});
|
|
||||||
if (duration != null) {
|
|
||||||
_bottomBarTimer = Timer(duration, () {
|
|
||||||
_hideBottomBar();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showErrorBottomBar(HAError error) {
|
|
||||||
TextStyle textStyle = Theme.of(context).textTheme.button.copyWith(
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
);
|
|
||||||
_bottomBarErrorColor = true;
|
|
||||||
List<Widget> actions = [];
|
|
||||||
error.actions.forEach((HAErrorAction action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case HAErrorActionType.FULL_RELOAD: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_fullLoad();
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.QUICK_RELOAD: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
_quickLoad();
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.RELOGIN: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
ConnectionManager().logout().then((_) => _fullLoad());
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.URL: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
Launcher.launchURLInCustomTab(context: context, url: "${action.url}");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case HAErrorActionType.OPEN_CONNECTION_SETTINGS: {
|
|
||||||
actions.add(FlatButton(
|
|
||||||
child: Text("${action.title}", style: textStyle),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pushNamed(context, '/connection-settings');
|
|
||||||
},
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (actions.isNotEmpty) {
|
|
||||||
_bottomBarAction = Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: actions,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_bottomBarProgress = false;
|
|
||||||
_bottomBarText = "${error.message}";
|
|
||||||
_showBottomBar = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
Widget _buildScaffoldBody(bool empty) {
|
Widget _buildScaffoldBody(bool empty) {
|
||||||
@ -765,60 +671,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget bottomBar;
|
|
||||||
if (_showBottomBar) {
|
|
||||||
List<Widget> bottomBarChildren = [];
|
|
||||||
if (_bottomBarText != null) {
|
|
||||||
bottomBarChildren.add(
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
Sizes.leftWidgetPadding, Sizes.rowPadding, 0.0,
|
|
||||||
Sizes.rowPadding),
|
|
||||||
child: Text(
|
|
||||||
"$_bottomBarText",
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (_bottomBarProgress) {
|
|
||||||
bottomBarChildren.add(
|
|
||||||
CollectionScaleTransition(
|
|
||||||
children: <Widget>[
|
|
||||||
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOnStateColor(context),),
|
|
||||||
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getDisabledStateColor(context),),
|
|
||||||
Icon(Icons.stop, size: 10.0, color: HAClientTheme().getOffStateColor(context),),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (bottomBarChildren.isNotEmpty) {
|
|
||||||
bottomBar = Container(
|
|
||||||
color: _bottomBarErrorColor ? Theme.of(context).errorColor : Theme.of(context).primaryColorLight,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: _bottomBarProgress ? CrossAxisAlignment.center : CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: bottomBarChildren,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_bottomBarAction
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (HomeAssistant().isNoViews) {
|
if (HomeAssistant().isNoViews) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
primary: false,
|
primary: false,
|
||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
bottomNavigationBar: bottomBar,
|
bottomNavigationBar: BottomInfoBar(
|
||||||
|
controller: _bottomInfoBarController,
|
||||||
|
),
|
||||||
body: _buildScaffoldBody(true)
|
body: _buildScaffoldBody(true)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -826,7 +686,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
primary: false,
|
primary: false,
|
||||||
bottomNavigationBar: bottomBar,
|
bottomNavigationBar: BottomInfoBar(
|
||||||
|
controller: _bottomInfoBarController,
|
||||||
|
),
|
||||||
body: _buildScaffoldBody(false)
|
body: _buildScaffoldBody(false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -848,9 +710,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_showErrorSubscription?.cancel();
|
_showErrorSubscription?.cancel();
|
||||||
_startAuthSubscription?.cancel();
|
_startAuthSubscription?.cancel();
|
||||||
_showPageSubscription?.cancel();
|
_showPageSubscription?.cancel();
|
||||||
|
_fullReloadSubscription?.cancel();
|
||||||
_reloadUISubscription?.cancel();
|
_reloadUISubscription?.cancel();
|
||||||
//TODO disconnect
|
|
||||||
//widget.homeAssistant?.disconnect();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,8 +57,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
_loaded = false;
|
_loaded = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_isMediaExtractorExist = HomeAssistant().isServiceExist("media_extractor");
|
_isMediaExtractorExist = HomeAssistant().isComponentEnabled("media_extractor");
|
||||||
//_useMediaExtractor = _isMediaExtractorExist;
|
|
||||||
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
|
_players = HomeAssistant().entities.getByDomains(includeDomains: ["media_player"]);
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_players.isNotEmpty) {
|
if (_players.isNotEmpty) {
|
||||||
|
@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> {
|
|||||||
error = "";
|
error = "";
|
||||||
});
|
});
|
||||||
http.Response response;
|
http.Response response;
|
||||||
response = await http.get("http://ha-client.app/service/whats_new_0.8.3.md");
|
response = await http.get("http://ha-client.app/service/whats_new_0.8.5.md");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
setState(() {
|
setState(() {
|
||||||
data = response.body;
|
data = response.body;
|
||||||
|
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 {
|
class ReloadUIEvent {
|
||||||
|
//TODO uiOnly bool
|
||||||
|
|
||||||
ReloadUIEvent();
|
ReloadUIEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FullReloadEvent {
|
||||||
|
FullReloadEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class ChangeThemeEvent {
|
class ChangeThemeEvent {
|
||||||
|
|
||||||
final AppTheme theme;
|
final AppTheme theme;
|
||||||
|
@ -61,13 +61,7 @@ class HomeAssistantUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildViews(BuildContext context) {
|
List<Widget> _buildViews(BuildContext context) {
|
||||||
List<Widget> result = [];
|
return views.map((view) => view.build(context)).toList();
|
||||||
views.forEach((view) {
|
|
||||||
result.add(
|
|
||||||
view.build(context)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
part of 'main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class HAView {
|
class HAView {
|
||||||
List<HACard> cards = [];
|
List<CardData> cards = [];
|
||||||
List<Entity> badges = [];
|
List<Entity> badges = [];
|
||||||
Entity linkedEntity;
|
Entity linkedEntity;
|
||||||
String name;
|
String name;
|
||||||
@ -33,137 +33,11 @@ class HAView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cards.addAll(_createLovelaceCards(rawData["cards"] ?? []));
|
(rawData["cards"] ?? []).forEach((rawCardData) {
|
||||||
}
|
cards.add(CardData.parse(rawCardData));
|
||||||
|
});
|
||||||
|
|
||||||
List<HACard> _createLovelaceCards(List rawCards) {
|
//cards.addAll(_createLovelaceCards(rawData["cards"] ?? [], 1));
|
||||||
List<HACard> result = [];
|
|
||||||
rawCards.forEach((rawCard){
|
|
||||||
try {
|
|
||||||
//bool isThereCardOptionsInside = rawCard["card"] != null;
|
|
||||||
var rawCardInfo = rawCard["card"] ?? rawCard;
|
|
||||||
HACard card = HACard(
|
|
||||||
id: "card",
|
|
||||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
|
||||||
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
|
||||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
|
||||||
showName: (rawCardInfo['show_name'] ?? rawCard['show_name']) ?? true,
|
|
||||||
showHeaderToggle: (rawCardInfo['show_header_toggle'] ?? rawCard['show_header_toggle']) ?? false,
|
|
||||||
showState: (rawCardInfo['show_state'] ?? rawCard['show_state']) ?? true,
|
|
||||||
showEmpty: (rawCardInfo['show_empty'] ?? rawCard['show_empty']) ?? true,
|
|
||||||
stateFilter: (rawCard['state_filter'] ?? rawCardInfo['state_filter']) ?? [],
|
|
||||||
states: rawCardInfo['states'],
|
|
||||||
conditions: rawCard['conditions'] ?? [],
|
|
||||||
content: rawCardInfo['content'],
|
|
||||||
min: rawCardInfo['min'] ?? 0,
|
|
||||||
max: rawCardInfo['max'] ?? 100,
|
|
||||||
unit: rawCardInfo['unit'],
|
|
||||||
severity: rawCardInfo['severity']
|
|
||||||
);
|
|
||||||
if (rawCardInfo["cards"] != null) {
|
|
||||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
|
||||||
}
|
|
||||||
var rawEntities = rawCard["entities"] ?? rawCardInfo["entities"];
|
|
||||||
rawEntities?.forEach((rawEntity) {
|
|
||||||
if (rawEntity is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(rawEntity)) {
|
|
||||||
card.entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawEntity)));
|
|
||||||
} else {
|
|
||||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rawEntity["type"] == "divider") {
|
|
||||||
card.entities.add(EntityWrapper(entity: Entity.divider()));
|
|
||||||
} else if (rawEntity["type"] == "section") {
|
|
||||||
card.entities.add(EntityWrapper(entity: Entity.section(rawEntity["label"] ?? "")));
|
|
||||||
} else if (rawEntity["type"] == "call-service") {
|
|
||||||
Map uiActionData = {
|
|
||||||
"tap_action": {
|
|
||||||
"action": EntityUIAction.callService,
|
|
||||||
"service": rawEntity["service"],
|
|
||||||
"service_data": rawEntity["service_data"]
|
|
||||||
},
|
|
||||||
"hold_action": EntityUIAction.none
|
|
||||||
};
|
|
||||||
card.entities.add(EntityWrapper(
|
|
||||||
entity: Entity.callService(
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
name: rawEntity["name"],
|
|
||||||
service: rawEntity["service"],
|
|
||||||
actionName: rawEntity["action_name"]
|
|
||||||
),
|
|
||||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (rawEntity["type"] == "weblink") {
|
|
||||||
Map uiActionData = {
|
|
||||||
"tap_action": {
|
|
||||||
"action": EntityUIAction.navigate,
|
|
||||||
"service": rawEntity["url"]
|
|
||||||
},
|
|
||||||
"hold_action": EntityUIAction.none
|
|
||||||
};
|
|
||||||
card.entities.add(EntityWrapper(
|
|
||||||
entity: Entity.weblink(
|
|
||||||
icon: rawEntity["icon"],
|
|
||||||
name: rawEntity["name"],
|
|
||||||
url: rawEntity["url"]
|
|
||||||
),
|
|
||||||
uiAction: EntityUIAction(rawEntityData: uiActionData)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (HomeAssistant().entities.isExist(rawEntity["entity"])) {
|
|
||||||
Entity e = HomeAssistant().entities.get(rawEntity["entity"]);
|
|
||||||
card.entities.add(
|
|
||||||
EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
overrideName: rawEntity["name"],
|
|
||||||
overrideIcon: rawEntity["icon"],
|
|
||||||
stateFilter: rawEntity['state_filter'] ?? [],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawEntity)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
card.entities.add(EntityWrapper(entity: Entity.missed(rawEntity["entity"])));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var rawSingleEntity = rawCard["entity"] ?? rawCardInfo["entity"];
|
|
||||||
if (rawSingleEntity != null) {
|
|
||||||
var en = rawSingleEntity;
|
|
||||||
if (en is String) {
|
|
||||||
if (HomeAssistant().entities.isExist(en)) {
|
|
||||||
Entity e = HomeAssistant().entities.get(en);
|
|
||||||
card.linkedEntityWrapper = EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
overrideIcon: rawCardInfo["icon"],
|
|
||||||
overrideName: rawCardInfo["name"],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (HomeAssistant().entities.isExist(en["entity"])) {
|
|
||||||
Entity e = HomeAssistant().entities.get(en["entity"]);
|
|
||||||
card.linkedEntityWrapper = EntityWrapper(
|
|
||||||
entity: e,
|
|
||||||
overrideIcon: en["icon"],
|
|
||||||
overrideName: en["name"],
|
|
||||||
stateFilter: en['state_filter'] ?? [],
|
|
||||||
uiAction: EntityUIAction(rawEntityData: rawCard)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
card.linkedEntityWrapper = EntityWrapper(entity: Entity.missed(en["entity"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.add(card);
|
|
||||||
} catch (e) {
|
|
||||||
Logger.e("There was an error parsing card: ${e.toString()}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTab() {
|
Widget buildTab() {
|
||||||
|
@ -21,7 +21,25 @@ class ViewWidget extends StatelessWidget {
|
|||||||
if (this.view.cards.isNotEmpty) {
|
if (this.view.cards.isNotEmpty) {
|
||||||
cardsContainer = DynamicMultiColumnLayout(
|
cardsContainer = DynamicMultiColumnLayout(
|
||||||
minColumnWidth: Sizes.minViewColumnWidth,
|
minColumnWidth: Sizes.minViewColumnWidth,
|
||||||
children: this.view.cards.map((card) => card.build(context)).toList(),
|
children: this.view.cards.map((card) {
|
||||||
|
if (card.conditions.isNotEmpty) {
|
||||||
|
bool showCardByConditions = true;
|
||||||
|
for (var condition in card.conditions) {
|
||||||
|
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||||
|
if (conditionEntity != null &&
|
||||||
|
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||||
|
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||||
|
) {
|
||||||
|
showCardByConditions = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!showCardByConditions) {
|
||||||
|
return Container(width: 0.0, height: 0.0,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return card.buildCardWidget();
|
||||||
|
}).toList(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cardsContainer = Container();
|
cardsContainer = Container();
|
||||||
@ -39,7 +57,7 @@ class ViewWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildPanelChild(BuildContext context) {
|
Widget _buildPanelChild(BuildContext context) {
|
||||||
if (this.view.cards != null && this.view.cards.isNotEmpty) {
|
if (this.view.cards != null && this.view.cards.isNotEmpty) {
|
||||||
return this.view.cards[0].build(context);
|
return this.view.cards[0].buildCardWidget();
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0, height: 0);
|
return Container(width: 0, height: 0);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.8.4+892
|
version: 0.8.6+897
|
||||||
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@ -12,7 +12,6 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
web_socket_channel: ^1.1.0
|
web_socket_channel: ^1.1.0
|
||||||
shared_preferences: ^0.5.6+1
|
shared_preferences: ^0.5.6+1
|
||||||
progress_indicators: ^0.1.4
|
|
||||||
path_provider: ^1.6.5
|
path_provider: ^1.6.5
|
||||||
event_bus: ^1.1.1
|
event_bus: ^1.1.1
|
||||||
cached_network_image: ^2.0.0
|
cached_network_image: ^2.0.0
|
||||||
@ -24,7 +23,7 @@ dependencies:
|
|||||||
flutter_custom_tabs: ^0.6.0
|
flutter_custom_tabs: ^0.6.0
|
||||||
flutter_webview_plugin: ^0.3.10+1
|
flutter_webview_plugin: ^0.3.10+1
|
||||||
webview_flutter: ^0.3.19+7
|
webview_flutter: ^0.3.19+7
|
||||||
firebase_messaging: ^6.0.9
|
firebase_messaging: ^6.0.13
|
||||||
flutter_secure_storage: ^3.3.1+1
|
flutter_secure_storage: ^3.3.1+1
|
||||||
device_info: ^0.4.1+4
|
device_info: ^0.4.1+4
|
||||||
flutter_local_notifications: ^1.1.6
|
flutter_local_notifications: ^1.1.6
|
||||||
|
Reference in New Issue
Block a user