Compare commits
77 Commits
beta/0.8.0
...
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 | |||
6afbd37d71 | |||
0e8869878f | |||
7c2cfe3215 | |||
c376c0e952 | |||
da5f663396 | |||
0e92418a33 | |||
2eef7cfe5e | |||
de4e0bfb3a | |||
8bf2d31e72 | |||
2125c46143 | |||
5402eb84df | |||
ad5aa0898f | |||
040d40b614 | |||
8e58f22c56 | |||
c91695fbe5 | |||
c43741da49 | |||
f2563a0397 | |||
fba4017819 | |||
5f23e108a1 | |||
68d14bd13d | |||
022622522f | |||
89513ca4e5 | |||
a934ee2335 | |||
49aeea634f | |||
e18b9ebe14 | |||
08ee3f3d80 | |||
62d07bf8b9 | |||
ab398cbdc3 | |||
007d12719c | |||
524d195800 | |||
405de64249 | |||
f53554702e | |||
379e1a4a7e | |||
d6f7096055 | |||
37c721e4f6 | |||
d94235ef6d | |||
eb4184713f | |||
a0a0cb4612 | |||
f448a20784 | |||
36eff26862 | |||
5b2a1163b9 | |||
e627a8b963 | |||
4432124e8c | |||
b8ba3c59e9 | |||
c40a496b6b |
2
.gitignore
vendored
2
.gitignore
vendored
@ -18,5 +18,5 @@ flutter_export_environment.sh
|
|||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
|
|
||||||
key.properties
|
key.properties
|
||||||
premium_features_manager.class.dart
|
.secrets.dart
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
|
@ -3,7 +3,7 @@ image:
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- before: |
|
- before: |
|
||||||
export PATH=$FLUTTER_HOME/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
export PATH=$FLUTTER_HOME/bin:$FLUTTER_HOME/bin/cache/dart-sdk/bin:$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||||
mkdir -p /home/gitpod/.android
|
mkdir -p /home/gitpod/.android
|
||||||
touch /home/gitpod/.android/repositories.cfg
|
touch /home/gitpod/.android/repositories.cfg
|
||||||
init: |
|
init: |
|
||||||
|
@ -6,7 +6,7 @@ Visit [ha-client.app](http://ha-client.app/) for more info.
|
|||||||
|
|
||||||
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
Download the app from [Google Play](https://play.google.com/apps/testing/com.keyboardcrumbs.haclient)
|
||||||
|
|
||||||
Discuss it on [Spectrum.chat](https://spectrum.chat/ha-client) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
Discuss it on [Discord](https://discord.gg/nd6FZQ) or at [Home Assistant community](https://community.home-assistant.io/c/mobile-apps/ha-client-android)
|
||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
[](https://gitpod.io/#https://github.com/estevez-dev/ha_client)
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
61
assets/html/cameraLiveView.html
Normal file
61
assets/html/cameraLiveView.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
widows: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
var messageChannel = '{{message_channel}}';
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video id="screen" width="100%" controls></video>
|
||||||
|
<script>
|
||||||
|
if (Hls.isSupported()) {
|
||||||
|
var video = document.getElementById('screen');
|
||||||
|
var hls = new Hls();
|
||||||
|
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||||
|
if (data.fatal) {
|
||||||
|
switch(data.type) {
|
||||||
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
// try to recover network error
|
||||||
|
console.log("fatal network error encountered, try to recover");
|
||||||
|
hls.startLoad();
|
||||||
|
break;
|
||||||
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
console.log("fatal media error encountered, try to recover");
|
||||||
|
hls.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// cannot recover
|
||||||
|
hls.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// bind them together
|
||||||
|
hls.attachMedia(video);
|
||||||
|
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
|
||||||
|
console.log("video and hls.js are now bound together !");
|
||||||
|
hls.loadSource("{{stream_url}}");
|
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
|
||||||
|
console.log("manifest loaded, found " + data.levels.length + " quality level");
|
||||||
|
video.play();
|
||||||
|
video.onloadedmetadata = function() {
|
||||||
|
window[messageChannel].postMessage(document.body.clientWidth / video.offsetHeight);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
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,393 +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.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 = [];
|
|
||||||
card.childCards.forEach((card) {
|
|
||||||
if (card.getEntitiesToShow().isNotEmpty || card.showEmpty) {
|
|
||||||
children.add(
|
|
||||||
Flexible(
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: card.build(context),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Container(height: 0.0, width: 0.0,);
|
|
||||||
}
|
|
||||||
|
|
||||||
case CardType.VERTICAL_STACK: {
|
|
||||||
if (card.childCards.isNotEmpty) {
|
|
||||||
List<Widget> children = [];
|
|
||||||
card.childCards.forEach((card) {
|
|
||||||
children.add(
|
|
||||||
card.build(context)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
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}",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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,14 +16,15 @@ 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,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
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(
|
|
||||||
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,
|
|
||||||
fontSize: Sizes.nameFontSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Container(width: 0, height: 0);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class GaugeCardBody extends StatefulWidget {
|
|
||||||
|
|
||||||
final int min;
|
|
||||||
final int max;
|
|
||||||
final Map severity;
|
|
||||||
|
|
||||||
GaugeCardBody({Key key, this.min, this.max, this.severity}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_GaugeCardBodyState createState() => _GaugeCardBodyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GaugeCardBodyState extends State<GaugeCardBody> {
|
|
||||||
|
|
||||||
List<charts.Series> seriesList;
|
|
||||||
|
|
||||||
List<charts.Series<GaugeSegment, String>> _createData(double value) {
|
|
||||||
double fixedValue;
|
|
||||||
if (value > widget.max) {
|
|
||||||
fixedValue = widget.max.toDouble();
|
|
||||||
} else if (value < widget.min) {
|
|
||||||
fixedValue = widget.min.toDouble();
|
|
||||||
} else {
|
|
||||||
fixedValue = value;
|
|
||||||
}
|
|
||||||
double toShow = ((fixedValue - widget.min) / (widget.max - widget.min)) * 100;
|
|
||||||
Color mainColor;
|
|
||||||
if (widget.severity != null) {
|
|
||||||
if (widget.severity["red"] is int && fixedValue >= widget.severity["red"]) {
|
|
||||||
mainColor = Colors.red;
|
|
||||||
} else if (widget.severity["yellow"] is int && fixedValue >= widget.severity["yellow"]) {
|
|
||||||
mainColor = Colors.amber;
|
|
||||||
} else {
|
|
||||||
mainColor = Colors.green;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mainColor = Colors.green;
|
|
||||||
}
|
|
||||||
final data = [
|
|
||||||
GaugeSegment('Main', toShow, mainColor),
|
|
||||||
GaugeSegment('Rest', 100 - toShow, Colors.black45),
|
|
||||||
];
|
|
||||||
|
|
||||||
return [
|
|
||||||
charts.Series<GaugeSegment, String>(
|
|
||||||
id: 'Segments',
|
|
||||||
domainFn: (GaugeSegment segment, _) => segment.segment,
|
|
||||||
measureFn: (GaugeSegment segment, _) => segment.value,
|
|
||||||
colorFn: (GaugeSegment segment, _) => segment.color,
|
|
||||||
// Set a label accessor to control the text of the arc label.
|
|
||||||
labelAccessorFn: (GaugeSegment segment, _) =>
|
|
||||||
segment.segment == 'Main' ? '${segment.value}' : null,
|
|
||||||
data: data,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1.5,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
overflow: Overflow.clip,
|
|
||||||
children: [
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
double verticalOffset;
|
|
||||||
if(constraints.maxWidth > 150.0) {
|
|
||||||
verticalOffset = 0.2;
|
|
||||||
} else if (constraints.maxWidth > 100.0) {
|
|
||||||
verticalOffset = 0.3;
|
|
||||||
} else {
|
|
||||||
verticalOffset = 0.3;
|
|
||||||
}
|
|
||||||
return FractionallySizedBox(
|
|
||||||
heightFactor: 2,
|
|
||||||
widthFactor: 1,
|
|
||||||
alignment: FractionalOffset(0,verticalOffset),
|
|
||||||
child: charts.PieChart(
|
|
||||||
_createData(entityWrapper.entity.doubleState),
|
|
||||||
animate: false,
|
|
||||||
defaultRenderer: charts.ArcRendererConfig(
|
|
||||||
arcRatio: 0.4,
|
|
||||||
startAngle: pi,
|
|
||||||
arcLength: pi,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
double fontSize = constraints.maxHeight / 7;
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
|
||||||
child: SimpleEntityState(
|
|
||||||
//textAlign: TextAlign.center,
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
bold: true,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
fontSize: fontSize,
|
|
||||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
double fontSize = constraints.maxHeight / 7;
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: fontSize),
|
|
||||||
child: EntityName(
|
|
||||||
fontSize: fontSize,
|
|
||||||
maxLines: 1,
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GaugeSegment {
|
|
||||||
final String segment;
|
|
||||||
final double value;
|
|
||||||
final charts.Color color;
|
|
||||||
|
|
||||||
GaugeSegment(this.segment, this.value, Color color)
|
|
||||||
: this.color = charts.Color(
|
|
||||||
r: color.red, g: color.green, b: color.blue, a: color.alpha);
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
part of '../../main.dart';
|
|
||||||
|
|
||||||
class GlanceCardEntityContainer extends StatelessWidget {
|
|
||||||
|
|
||||||
final bool showName;
|
|
||||||
final bool showState;
|
|
||||||
final bool nameInTheBottom;
|
|
||||||
final double iconSize;
|
|
||||||
final double nameFontSize;
|
|
||||||
final bool wordsWrapInName;
|
|
||||||
|
|
||||||
GlanceCardEntityContainer({
|
|
||||||
Key key,
|
|
||||||
@required this.showName,
|
|
||||||
@required this.showState,
|
|
||||||
this.nameInTheBottom: false,
|
|
||||||
this.iconSize: Sizes.iconSize,
|
|
||||||
this.nameFontSize: Sizes.smallFontSize,
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
} 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Center(
|
|
||||||
child: InkResponse(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: result,
|
|
||||||
),
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildName() {
|
|
||||||
return EntityName(
|
|
||||||
padding: EdgeInsets.only(bottom: Sizes.rowPadding),
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
wordsWrap: wordsWrapInName,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
fontSize: nameFontSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildState() {
|
|
||||||
return SimpleEntityState(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
padding: EdgeInsets.only(top: Sizes.rowPadding),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +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],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => entityWrapper.handleTap(),
|
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
|
||||||
onDoubleTap: () => entityWrapper.handleDoubleTap(),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1.5,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
overflow: Overflow.clip,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
double fontSize = constraints.maxHeight / 7;
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: 2*fontSize),
|
|
||||||
child: SimpleEntityState(
|
|
||||||
//textAlign: TextAlign.center,
|
|
||||||
expanded: false,
|
|
||||||
maxLines: 1,
|
|
||||||
bold: true,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
fontSize: fontSize,
|
|
||||||
//padding: EdgeInsets.only(top: Sizes.rowPadding),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
double fontSize = constraints.maxHeight / 7;
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: fontSize),
|
|
||||||
child: EntityName(
|
|
||||||
fontSize: fontSize,
|
|
||||||
maxLines: 1,
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
textOverflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -113,10 +113,13 @@ class CardType {
|
|||||||
static const IFRAME = "iframe";
|
static const IFRAME = "iframe";
|
||||||
static const GAUGE = "gauge";
|
static const GAUGE = "gauge";
|
||||||
static const ENTITY_BUTTON = "entity-button";
|
static const ENTITY_BUTTON = "entity-button";
|
||||||
|
static const BUTTON = "button";
|
||||||
static const CONDITIONAL = "conditional";
|
static const CONDITIONAL = "conditional";
|
||||||
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 {
|
||||||
@ -126,10 +129,10 @@ class Sizes {
|
|||||||
static const extendedWidgetHeight = 50.0;
|
static const extendedWidgetHeight = 50.0;
|
||||||
static const iconSize = 28.0;
|
static const iconSize = 28.0;
|
||||||
static const largeIconSize = 46.0;
|
static const largeIconSize = 46.0;
|
||||||
static const stateFontSize = 15.0;
|
//static const stateFontSize = 15.0;
|
||||||
static const nameFontSize = 15.0;
|
//static const nameFontSize = 15.0;
|
||||||
static const smallFontSize = 14.0;
|
//static const smallFontSize = 14.0;
|
||||||
static const largeFontSize = 24.0;
|
//static const largeFontSize = 24.0;
|
||||||
static const inputWidth = 160.0;
|
static const inputWidth = 160.0;
|
||||||
static const rowPadding = 10.0;
|
static const rowPadding = 10.0;
|
||||||
static const doubleRowPadding = rowPadding*2;
|
static const doubleRowPadding = rowPadding*2;
|
||||||
|
@ -248,7 +248,9 @@ class _AlarmControlPanelControlsWidgetWidgetState extends State<AlarmControlPane
|
|||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
"TRIGGER",
|
"TRIGGER",
|
||||||
style: TextStyle(color: Colors.redAccent)
|
style: Theme.of(context).textTheme.subhead.copyWith(
|
||||||
|
color: Theme.of(context).errorColor
|
||||||
|
)
|
||||||
),
|
),
|
||||||
onPressed: () => _askToTrigger(entity),
|
onPressed: () => _askToTrigger(entity),
|
||||||
)
|
)
|
||||||
|
@ -7,8 +7,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
double iconSize = 26.0;
|
double iconSize = 26.0;
|
||||||
Widget badgeIcon;
|
Widget badgeIcon;
|
||||||
String onBadgeTextValue;
|
String onBadgeTextValue;
|
||||||
Color iconColor = EntityColor.badgeColors[entityModel.entityWrapper.entity.domain] ??
|
Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain);
|
||||||
EntityColor.badgeColors["default"];
|
|
||||||
switch (entityModel.entityWrapper.entity.domain) {
|
switch (entityModel.entityWrapper.entity.domain) {
|
||||||
case "sun":
|
case "sun":
|
||||||
{
|
{
|
||||||
@ -30,7 +29,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Colors.black
|
color: Theme.of(context).textTheme.body1.color
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -40,7 +39,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
badgeIcon = EntityIcon(
|
badgeIcon = EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
color: Colors.black
|
color: Theme.of(context).textTheme.body1.color
|
||||||
);
|
);
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
onBadgeTextValue = entityModel.entityWrapper.entity.displayState;
|
||||||
break;
|
break;
|
||||||
@ -64,7 +63,9 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: stateFontSize),
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
fontSize: stateFontSize
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -77,7 +78,9 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
onBadgeText = Container(
|
onBadgeText = Container(
|
||||||
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0),
|
||||||
child: Text("$onBadgeTextValue",
|
child: Text("$onBadgeTextValue",
|
||||||
style: TextStyle(fontSize: 12.0, color: Colors.white),
|
style: Theme.of(context).textTheme.overline.copyWith(
|
||||||
|
color: HAClientTheme().getOnBadgeTextColor()
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade),
|
overflow: TextOverflow.fade),
|
||||||
@ -98,7 +101,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
decoration: new BoxDecoration(
|
decoration: new BoxDecoration(
|
||||||
// Circle shape
|
// Circle shape
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: Colors.white,
|
color: Theme.of(context).cardColor,
|
||||||
// The border you want
|
// The border you want
|
||||||
border: new Border.all(
|
border: new Border.all(
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
@ -131,7 +134,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.displayName}",
|
"${entityModel.entityWrapper.displayName}",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 12.0),
|
style: Theme.of(context).textTheme.caption,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
@ -14,8 +14,6 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
|
|
||||||
CameraEntity _entity;
|
CameraEntity _entity;
|
||||||
String _streamUrl = "";
|
String _streamUrl = "";
|
||||||
VideoPlayerController _videoPlayerController;
|
|
||||||
Timer _monitorTimer;
|
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
double _aspectRatio = 1.33;
|
double _aspectRatio = 1.33;
|
||||||
String _webViewHtml;
|
String _webViewHtml;
|
||||||
@ -38,67 +36,48 @@ 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) {
|
||||||
if (_videoPlayerController != null) {
|
|
||||||
_videoPlayerController.dispose().then((_) => createPlayer(data));
|
|
||||||
} else {
|
|
||||||
createPlayer(data);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catchError((e) {
|
|
||||||
_loading.completeError(e);
|
|
||||||
Logger.e("[Camera Player] $e");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
|
||||||
.entityId}?token=${_entity.attributes['access_token']}';
|
|
||||||
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
rootBundle.loadString('assets/html/cameraView.html').then((file) {
|
rootBundle.loadString('assets/html/cameraLiveView.html').then((file) {
|
||||||
_webViewHtml = Uri.dataFromString(
|
_webViewHtml = Uri.dataFromString(
|
||||||
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
file.replaceFirst('{{stream_url}}', '${ConnectionManager().httpWebHost}${data["url"]}').replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||||
mimeType: 'text/html',
|
mimeType: 'text/html',
|
||||||
encoding: Encoding.getByName('utf-8')
|
encoding: Encoding.getByName('utf-8')
|
||||||
).toString();
|
).toString();
|
||||||
_loading.complete();
|
_loading.complete();
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catchError((e) {
|
||||||
|
if (e == 'start_stream_failed') {
|
||||||
|
Logger.e("[Camera Player] Home Assistant failed starting stream. Forcing MJPEG: $e");
|
||||||
|
_loadMJPEG().then((_) {
|
||||||
|
_loading.complete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_loading.completeError(e);
|
||||||
|
Logger.e("[Camera Player] Error loading stream: $e");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_loadMJPEG().then((_) {
|
||||||
|
_loading.complete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return _loading.future;
|
return _loading.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
void createPlayer(data) {
|
Future _loadMJPEG() async {
|
||||||
_videoPlayerController = VideoPlayerController.network("${ConnectionManager().httpWebHost}${data["url"]}");
|
_streamUrl = '${ConnectionManager().httpWebHost}/api/camera_proxy_stream/${_entity
|
||||||
_videoPlayerController.initialize().then((_) {
|
.entityId}?token=${_entity.attributes['access_token']}';
|
||||||
setState((){
|
_jsMessageChannelName = 'HA_${_entity.entityId.replaceAll('.', '_')}';
|
||||||
_aspectRatio = _videoPlayerController.value.aspectRatio;
|
var file = await rootBundle.loadString('assets/html/cameraView.html');
|
||||||
});
|
_webViewHtml = Uri.dataFromString(
|
||||||
_loading.complete();
|
file.replaceFirst('{{stream_url}}', _streamUrl).replaceFirst('{{message_channel}}', _jsMessageChannelName),
|
||||||
autoPlay();
|
mimeType: 'text/html',
|
||||||
startMonitor();
|
encoding: Encoding.getByName('utf-8')
|
||||||
}).catchError((e) {
|
).toString();
|
||||||
_loading.completeError(e);
|
|
||||||
Logger.e("[Camera Player] Error player init. Retrying");
|
|
||||||
_loadResources();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void autoPlay() {
|
|
||||||
if (!_videoPlayerController.value.isPlaying) {
|
|
||||||
_videoPlayerController.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void startMonitor() {
|
|
||||||
_monitorTimer?.cancel();
|
|
||||||
_monitorTimer = Timer.periodic(Duration(milliseconds: 500), (timer) {
|
|
||||||
if (_videoPlayerController.value.hasError) {
|
|
||||||
timer.cancel();
|
|
||||||
setState(() {
|
|
||||||
_isLoaded = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScreen() {
|
Widget _buildScreen() {
|
||||||
@ -109,16 +88,6 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else if (_entity.supportStream) {
|
|
||||||
if (_videoPlayerController.value.initialized) {
|
|
||||||
screenWidget = VideoPlayer(_videoPlayerController);
|
|
||||||
} else {
|
|
||||||
screenWidget = Center(
|
|
||||||
child: EntityPicture(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
screenWidget = WebView(
|
screenWidget = WebView(
|
||||||
initialUrl: _webViewHtml,
|
initialUrl: _webViewHtml,
|
||||||
@ -130,6 +99,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
JavascriptChannel(
|
JavascriptChannel(
|
||||||
name: _jsMessageChannelName,
|
name: _jsMessageChannelName,
|
||||||
onMessageReceived: ((message) {
|
onMessageReceived: ((message) {
|
||||||
|
Logger.d('[Camera Player] Message from page: $message');
|
||||||
setState((){
|
setState((){
|
||||||
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
_aspectRatio = double.tryParse(message.message) ?? 1.33;
|
||||||
});
|
});
|
||||||
@ -145,28 +115,6 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildControls() {
|
Widget _buildControls() {
|
||||||
Widget playControl;
|
|
||||||
if (_entity.supportStream) {
|
|
||||||
playControl = Center(
|
|
||||||
child: IconButton(
|
|
||||||
icon: Icon((_videoPlayerController != null && _videoPlayerController.value.isPlaying) ? Icons.pause_circle_outline : Icons.play_circle_outline),
|
|
||||||
iconSize: 60,
|
|
||||||
color: Colors.amberAccent,
|
|
||||||
onPressed: (_videoPlayerController == null || _videoPlayerController.value.hasError || !_isLoaded) ? null :
|
|
||||||
() {
|
|
||||||
setState(() {
|
|
||||||
if (_videoPlayerController != null && _videoPlayerController.value.isPlaying) {
|
|
||||||
_videoPlayerController.pause();
|
|
||||||
} else {
|
|
||||||
_videoPlayerController.play();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
playControl = Container();
|
|
||||||
}
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@ -175,7 +123,7 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.refresh),
|
icon: Icon(Icons.refresh),
|
||||||
iconSize: 40,
|
iconSize: 40,
|
||||||
color: Colors.amberAccent,
|
color: Theme.of(context).accentColor,
|
||||||
onPressed: _isLoaded ? () {
|
onPressed: _isLoaded ? () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoaded = false;
|
_isLoaded = false;
|
||||||
@ -183,14 +131,13 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
} : null,
|
} : null,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: playControl,
|
child: Container(),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.fullscreen),
|
icon: Icon(Icons.fullscreen),
|
||||||
iconSize: 40,
|
iconSize: 40,
|
||||||
color: Colors.amberAccent,
|
color: Theme.of(context).accentColor,
|
||||||
onPressed: _isLoaded ? () {
|
onPressed: _isLoaded ? () {
|
||||||
_videoPlayerController?.pause();
|
|
||||||
eventBus.fire(ShowEntityPageEvent());
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@ -239,8 +186,6 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_monitorTimer?.cancel();
|
|
||||||
_videoPlayerController?.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -204,20 +204,20 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
//_buildOnOffControl(entity),
|
//_buildOnOffControl(entity),
|
||||||
_buildTemperatureControls(entity),
|
_buildTemperatureControls(entity, context),
|
||||||
_buildTargetTemperatureControls(entity),
|
_buildTargetTemperatureControls(entity, context),
|
||||||
_buildHumidityControls(entity),
|
_buildHumidityControls(entity, context),
|
||||||
_buildOperationControl(entity),
|
_buildOperationControl(entity, context),
|
||||||
_buildFanControl(entity),
|
_buildFanControl(entity, context),
|
||||||
_buildSwingControl(entity),
|
_buildSwingControl(entity, context),
|
||||||
_buildPresetModeControl(entity),
|
_buildPresetModeControl(entity, context),
|
||||||
_buildAuxHeatControl(entity)
|
_buildAuxHeatControl(entity, context)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPresetModeControl(ClimateEntity entity) {
|
Widget _buildPresetModeControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportPresetMode) {
|
if (entity.supportPresetMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.presetModes,
|
options: entity.presetModes,
|
||||||
@ -242,7 +242,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
Widget _buildAuxHeatControl(ClimateEntity entity) {
|
Widget _buildAuxHeatControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportAuxHeat ) {
|
if (entity.supportAuxHeat ) {
|
||||||
return ModeSwitchWidget(
|
return ModeSwitchWidget(
|
||||||
caption: "Aux heat",
|
caption: "Aux heat",
|
||||||
@ -254,7 +254,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOperationControl(ClimateEntity entity) {
|
Widget _buildOperationControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.hvacModes != null) {
|
if (entity.hvacModes != null) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setHVACMode(entity, mode),
|
onChange: (mode) => _setHVACMode(entity, mode),
|
||||||
@ -267,7 +267,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFanControl(ClimateEntity entity) {
|
Widget _buildFanControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportFanMode) {
|
if (entity.supportFanMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
options: entity.fanModes,
|
options: entity.fanModes,
|
||||||
@ -280,7 +280,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSwingControl(ClimateEntity entity) {
|
Widget _buildSwingControl(ClimateEntity entity, BuildContext context) {
|
||||||
if (entity.supportSwingMode) {
|
if (entity.supportSwingMode) {
|
||||||
return ModeSelectorWidget(
|
return ModeSelectorWidget(
|
||||||
onChange: (mode) => _setSwingMode(entity, mode),
|
onChange: (mode) => _setSwingMode(entity, mode),
|
||||||
@ -293,17 +293,15 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTemperatureControls(ClimateEntity entity) {
|
Widget _buildTemperatureControls(ClimateEntity entity, BuildContext context) {
|
||||||
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
if ((entity.supportTargetTemperature) && (entity.temperature != null)) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Target temperature", style: TextStyle(
|
Text("Target temperature", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTemperature,
|
value: _tmpTemperature,
|
||||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _temperatureDown(entity),
|
onDec: () => _temperatureDown(entity),
|
||||||
onInc: () => _temperatureUp(entity),
|
onInc: () => _temperatureUp(entity),
|
||||||
)
|
)
|
||||||
@ -314,13 +312,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTargetTemperatureControls(ClimateEntity entity) {
|
Widget _buildTargetTemperatureControls(ClimateEntity entity, BuildContext context) {
|
||||||
List<Widget> controls = [];
|
List<Widget> controls = [];
|
||||||
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
if ((entity.supportTargetTemperatureRange) && (entity.targetLow != null)) {
|
||||||
controls.addAll(<Widget>[
|
controls.addAll(<Widget>[
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetLow,
|
value: _tmpTargetLow,
|
||||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _targetLowDown(entity),
|
onDec: () => _targetLowDown(entity),
|
||||||
onInc: () => _targetLowUp(entity),
|
onInc: () => _targetLowUp(entity),
|
||||||
),
|
),
|
||||||
@ -333,7 +331,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
controls.add(
|
controls.add(
|
||||||
TemperatureControlWidget(
|
TemperatureControlWidget(
|
||||||
value: _tmpTargetHigh,
|
value: _tmpTargetHigh,
|
||||||
fontColor: _temperaturePending ? Colors.red : Colors.black,
|
active: _temperaturePending,
|
||||||
onDec: () => _targetHighDown(entity),
|
onDec: () => _targetHighDown(entity),
|
||||||
onInc: () => _targetHighUp(entity),
|
onInc: () => _targetHighUp(entity),
|
||||||
)
|
)
|
||||||
@ -343,9 +341,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Target temperature range", style: TextStyle(
|
Text("Target temperature range", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
Row(
|
Row(
|
||||||
children: controls,
|
children: controls,
|
||||||
)
|
)
|
||||||
@ -356,13 +352,13 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHumidityControls(ClimateEntity entity) {
|
Widget _buildHumidityControls(ClimateEntity entity, BuildContext context) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (entity.supportTargetHumidity) {
|
if (entity.supportTargetHumidity) {
|
||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$_tmpTargetHumidity%",
|
"$_tmpTargetHumidity%",
|
||||||
style: TextStyle(fontSize: Sizes.largeFontSize),
|
style: Theme.of(context).textTheme.display1,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
@ -387,9 +383,7 @@ class _ClimateControlWidgetState extends State<ClimateControlWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Target humidity", style: TextStyle(
|
child: Text("Target humidity", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
@ -33,23 +33,16 @@ class ClimateStateWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("$displayState",
|
Text("$displayState",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.body2),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
)),
|
|
||||||
Text(" $targetTemp",
|
Text(" $targetTemp",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.body1)
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
entity.currentTemperature != null ?
|
entity.currentTemperature != null ?
|
||||||
Text("Currently: ${entity.currentTemperature}",
|
Text("Currently: ${entity.currentTemperature}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: new TextStyle(
|
style: Theme.of(context).textTheme.subtitle
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
color: Colors.black45)
|
|
||||||
) :
|
) :
|
||||||
Container(height: 0.0,)
|
Container(height: 0.0,)
|
||||||
],
|
],
|
||||||
|
@ -5,8 +5,6 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
final String caption;
|
final String caption;
|
||||||
final List options;
|
final List options;
|
||||||
final String value;
|
final String value;
|
||||||
final double captionFontSize;
|
|
||||||
final double valueFontSize;
|
|
||||||
final onChange;
|
final onChange;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
|
|
||||||
@ -16,8 +14,6 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
@required this.options,
|
@required this.options,
|
||||||
this.value,
|
this.value,
|
||||||
@required this.onChange,
|
@required this.onChange,
|
||||||
this.captionFontSize,
|
|
||||||
this.valueFontSize,
|
|
||||||
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0),
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -28,9 +24,7 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("$caption", style: TextStyle(
|
Text("$caption", style: Theme.of(context).textTheme.body1),
|
||||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -40,10 +34,7 @@ class ModeSelectorWidget extends StatelessWidget {
|
|||||||
value: value,
|
value: value,
|
||||||
iconSize: 30.0,
|
iconSize: 30.0,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.title,
|
||||||
fontSize: valueFontSize ?? Sizes.largeFontSize,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
hint: Text("Select ${caption.toLowerCase()}"),
|
hint: Text("Select ${caption.toLowerCase()}"),
|
||||||
items: options.map((value) {
|
items: options.map((value) {
|
||||||
return new DropdownMenuItem<String>(
|
return new DropdownMenuItem<String>(
|
||||||
|
@ -4,7 +4,6 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
final onChange;
|
final onChange;
|
||||||
final double captionFontSize;
|
|
||||||
final bool value;
|
final bool value;
|
||||||
final bool expanded;
|
final bool expanded;
|
||||||
final EdgeInsets padding;
|
final EdgeInsets padding;
|
||||||
@ -13,7 +12,6 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
@required this.caption,
|
@required this.caption,
|
||||||
@required this.onChange,
|
@required this.onChange,
|
||||||
this.captionFontSize,
|
|
||||||
this.value,
|
this.value,
|
||||||
this.expanded: true,
|
this.expanded: true,
|
||||||
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
this.padding: const EdgeInsets.only(left: Sizes.leftWidgetPadding, right: Sizes.rightWidgetPadding)
|
||||||
@ -25,7 +23,7 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
padding: this.padding,
|
padding: this.padding,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildCaption(),
|
_buildCaption(context),
|
||||||
Switch(
|
Switch(
|
||||||
onChanged: (value) => onChange(value),
|
onChanged: (value) => onChange(value),
|
||||||
value: value ?? false,
|
value: value ?? false,
|
||||||
@ -35,12 +33,10 @@ class ModeSwitchWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCaption() {
|
Widget _buildCaption(BuildContext context) {
|
||||||
Widget captionWidget = Text(
|
Widget captionWidget = Text(
|
||||||
"$caption",
|
"$caption",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1,
|
||||||
fontSize: captionFontSize ?? Sizes.stateFontSize
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
|
@ -2,8 +2,7 @@ part of '../../../main.dart';
|
|||||||
|
|
||||||
class TemperatureControlWidget extends StatelessWidget {
|
class TemperatureControlWidget extends StatelessWidget {
|
||||||
final double value;
|
final double value;
|
||||||
final double fontSize;
|
final bool active;
|
||||||
final Color fontColor;
|
|
||||||
final onInc;
|
final onInc;
|
||||||
final onDec;
|
final onDec;
|
||||||
|
|
||||||
@ -12,8 +11,9 @@ class TemperatureControlWidget extends StatelessWidget {
|
|||||||
@required this.value,
|
@required this.value,
|
||||||
@required this.onInc,
|
@required this.onInc,
|
||||||
@required this.onDec,
|
@required this.onDec,
|
||||||
this.fontSize,
|
//this.fontSize,
|
||||||
this.fontColor})
|
this.active: false
|
||||||
|
})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -23,10 +23,7 @@ class TemperatureControlWidget extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$value",
|
"$value",
|
||||||
style: TextStyle(
|
style: active ? Theme.of(context).textTheme.display2 : Theme.of(context).textTheme.display1,
|
||||||
fontSize: fontSize ?? 24.0,
|
|
||||||
color: fontColor ?? Colors.black
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
@ -64,9 +64,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Position", style: TextStyle(
|
child: Text("Position"),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
Slider(
|
Slider(
|
||||||
value: _tmpPosition,
|
value: _tmpPosition,
|
||||||
@ -118,9 +116,7 @@ class _CoverControlWidgetState extends State<CoverControlWidget> {
|
|||||||
controls.insert(0, Padding(
|
controls.insert(0, Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
0.0, Sizes.rowPadding, 0.0, Sizes.rowPadding),
|
||||||
child: Text("Tilt position", style: TextStyle(
|
child: Text("Tilt position"),
|
||||||
fontSize: Sizes.stateFontSize
|
|
||||||
)),
|
|
||||||
));
|
));
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
@ -9,10 +9,8 @@ class DateTimeStateWidget extends StatelessWidget {
|
|||||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
padding: EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Text("${entity.formattedState}",
|
child: Text("${entity.formattedState}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right
|
||||||
style: new TextStyle(
|
),
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
)),
|
|
||||||
onTap: () => _handleStateTap(context, entity),
|
onTap: () => _handleStateTap(context, entity),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -11,25 +11,23 @@ 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();
|
||||||
color: Colors.black45,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
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,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Divider(
|
Divider(),
|
||||||
color: Colors.black45,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
"${entityModel.entityWrapper.entity.displayName}",
|
"${entityModel.entityWrapper.entity.displayName}",
|
||||||
style: TextStyle(color: Colors.blue),
|
style: HAClientTheme().getLinkTextStyle(context).copyWith(
|
||||||
|
decoration: TextDecoration.none
|
||||||
|
)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -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) {
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class EntityColor {
|
|
||||||
|
|
||||||
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
|
||||||
|
|
||||||
static const badgeColors = {
|
|
||||||
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
|
||||||
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
static const _stateColors = {
|
|
||||||
EntityState.on: Colors.amber,
|
|
||||||
"auto": Colors.amber,
|
|
||||||
EntityState.active: Colors.amber,
|
|
||||||
EntityState.playing: Colors.amber,
|
|
||||||
EntityState.paused: Colors.amber,
|
|
||||||
"above_horizon": Colors.amber,
|
|
||||||
EntityState.home: Colors.amber,
|
|
||||||
EntityState.open: Colors.amber,
|
|
||||||
EntityState.cleaning: Colors.amber,
|
|
||||||
EntityState.returning: Colors.amber,
|
|
||||||
EntityState.off: defaultStateColor,
|
|
||||||
EntityState.closed: defaultStateColor,
|
|
||||||
"below_horizon": defaultStateColor,
|
|
||||||
"default": defaultStateColor,
|
|
||||||
EntityState.idle: defaultStateColor,
|
|
||||||
"heat": Colors.redAccent,
|
|
||||||
"cool": Colors.lightBlue,
|
|
||||||
EntityState.unavailable: Colors.black26,
|
|
||||||
EntityState.unknown: Colors.black26,
|
|
||||||
EntityState.alarm_disarmed: Colors.green,
|
|
||||||
EntityState.alarm_armed_away: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_custom_bypass: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_home: Colors.redAccent,
|
|
||||||
EntityState.alarm_armed_night: Colors.redAccent,
|
|
||||||
EntityState.alarm_triggered: Colors.redAccent,
|
|
||||||
EntityState.alarm_arming: Colors.amber,
|
|
||||||
EntityState.alarm_disarming: Colors.amber,
|
|
||||||
EntityState.alarm_pending: Colors.amber,
|
|
||||||
};
|
|
||||||
|
|
||||||
static Color stateColor(String state) {
|
|
||||||
return _stateColors[state] ?? _stateColors["default"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static charts.Color chartHistoryStateColor(String state, int id) {
|
|
||||||
Color c = _stateColors[state];
|
|
||||||
if (c != null) {
|
|
||||||
return charts.Color(
|
|
||||||
r: c.red,
|
|
||||||
g: c.green,
|
|
||||||
b: c.blue,
|
|
||||||
a: c.alpha
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
double r = id.toDouble() % 10;
|
|
||||||
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Color historyStateColor(String state, int id) {
|
|
||||||
Color c = _stateColors[state];
|
|
||||||
if (c != null) {
|
|
||||||
return c;
|
|
||||||
} else {
|
|
||||||
if (id > -1) {
|
|
||||||
double r = id.toDouble() % 10;
|
|
||||||
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
|
||||||
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
|
||||||
} else {
|
|
||||||
return _stateColors[EntityState.on];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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 ?? EntityColor.stateColor(entityWrapper.entity.state)
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,18 +5,24 @@ class EntityName extends StatelessWidget {
|
|||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final TextOverflow textOverflow;
|
final TextOverflow textOverflow;
|
||||||
final bool wordsWrap;
|
final bool wordsWrap;
|
||||||
final double fontSize;
|
|
||||||
final TextAlign textAlign;
|
final TextAlign textAlign;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
final TextStyle textStyle;
|
||||||
|
|
||||||
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.wordsWrap: true, this.fontSize: Sizes.nameFontSize, this.textAlign: TextAlign.left}) : super(key: key);
|
const EntityName({Key key, this.maxLines, this.padding: const EdgeInsets.only(right: 10.0), this.textOverflow: TextOverflow.ellipsis, this.textStyle, this.wordsWrap: true, this.textAlign: TextAlign.left}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper;
|
||||||
TextStyle textStyle = TextStyle(fontSize: fontSize);
|
TextStyle tStyle;
|
||||||
if (entityWrapper.entity.statelessType == StatelessEntityType.WEBLINK) {
|
if (textStyle == null) {
|
||||||
textStyle = textStyle.apply(color: Colors.blue, decoration: TextDecoration.underline);
|
if (entityWrapper.entity.statelessType == StatelessEntityType.webLink) {
|
||||||
|
tStyle = HAClientTheme().getLinkTextStyle(context);
|
||||||
|
} else {
|
||||||
|
tStyle = Theme.of(context).textTheme.body1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tStyle = textStyle;
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
@ -25,7 +31,7 @@ class EntityName extends StatelessWidget {
|
|||||||
overflow: textOverflow,
|
overflow: textOverflow,
|
||||||
softWrap: wordsWrap,
|
softWrap: wordsWrap,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
style: textStyle,
|
style: tStyle,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -16,7 +16,7 @@ class EntityPageLayout extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
showClose ?
|
showClose ?
|
||||||
Container(
|
Container(
|
||||||
color: Colors.blue[300],
|
color: Theme.of(context).primaryColor,
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -25,18 +25,14 @@ class EntityPageLayout extends StatelessWidget {
|
|||||||
padding: EdgeInsets.only(left: 8),
|
padding: EdgeInsets.only(left: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
entity.displayName,
|
entity.displayName,
|
||||||
style: TextStyle(
|
style: Theme.of(context).primaryTextTheme.headline
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 22
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
icon: Icon(Icons.close),
|
icon: Icon(Icons.close),
|
||||||
color: Colors.white,
|
color: Theme.of(context).primaryTextTheme.headline.color,
|
||||||
iconSize: 36.0,
|
iconSize: 36.0,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
eventBus.fire(ShowEntityPageEvent());
|
eventBus.fire(ShowEntityPageEvent());
|
||||||
|
@ -22,7 +22,7 @@ class EntityPicture extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildIcon(EntityWrapper data) {
|
Widget buildIcon(EntityWrapper data, BuildContext context) {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ class EntityPicture extends StatelessWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
IconData(iconCode, fontFamily: 'Material Design Icons'),
|
||||||
size: Sizes.largeIconSize,
|
size: Sizes.largeIconSize,
|
||||||
color: EntityColor.defaultStateColor,
|
color: HAClientTheme().getOffStateColor(context),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -63,7 +63,8 @@ class EntityPicture extends StatelessWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: buildIcon(
|
child: buildIcon(
|
||||||
entityWrapper
|
entityWrapper,
|
||||||
|
context
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
final String serviceName;
|
final String serviceName;
|
||||||
final String entityId;
|
final String entityId;
|
||||||
final String text;
|
final String text;
|
||||||
final double fontSize;
|
|
||||||
|
|
||||||
FlatServiceButton({
|
FlatServiceButton({
|
||||||
Key key,
|
Key key,
|
||||||
@ -14,7 +13,6 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
@required this.serviceName,
|
@required this.serviceName,
|
||||||
@required this.entityId,
|
@required this.entityId,
|
||||||
@required this.text,
|
@required this.text,
|
||||||
this.fontSize: Sizes.stateFontSize
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
void _setNewState() {
|
void _setNewState() {
|
||||||
@ -24,7 +22,7 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: fontSize*2.5,
|
height: Theme.of(context).textTheme.subhead.fontSize*2.5,
|
||||||
child: FlatButton(
|
child: FlatButton(
|
||||||
onPressed: (() {
|
onPressed: (() {
|
||||||
_setNewState();
|
_setNewState();
|
||||||
@ -32,8 +30,7 @@ class FlatServiceButton extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: fontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -183,7 +183,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
}
|
}
|
||||||
return UniversalSlider(
|
return UniversalSlider(
|
||||||
title: "Color temperature",
|
title: "Color temperature",
|
||||||
leading: Text("Cold", style: TextStyle(color: Colors.lightBlue),),
|
leading: Text("Cold", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.lightBlue)),
|
||||||
value: val,
|
value: val,
|
||||||
onChangeEnd: (value) => _setColorTemp(entity, value),
|
onChangeEnd: (value) => _setColorTemp(entity, value),
|
||||||
max: entity.maxMireds,
|
max: entity.maxMireds,
|
||||||
@ -194,7 +194,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
_tmpColorTemp = value.round();
|
_tmpColorTemp = value.round();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
closing: Text("Warm", style: TextStyle(color: Colors.amberAccent),),
|
closing: Text("Warm", style: Theme.of(context).textTheme.body1.copyWith(color: Colors.amberAccent),),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(width: 0.0, height: 0.0);
|
return Container(width: 0.0, height: 0.0);
|
||||||
@ -224,7 +224,7 @@ class _LightControlsWidgetState extends State<LightControlsWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
color: savedColor?.toColor() ?? Colors.transparent,
|
color: savedColor?.toColor() ?? Theme.of(context).backgroundColor,
|
||||||
child: Text('Paste color'),
|
child: Text('Paste color'),
|
||||||
onPressed: savedColor == null ? null : () {
|
onPressed: savedColor == null ? null : () {
|
||||||
_setColor(entity, savedColor);
|
_setColor(entity, savedColor);
|
||||||
|
@ -28,8 +28,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
onPressed: () => _unlock(entity),
|
onPressed: () => _unlock(entity),
|
||||||
child: Text("UNLOCK",
|
child: Text("UNLOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context)
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -39,8 +38,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
onPressed: () => _lock(entity),
|
onPressed: () => _lock(entity),
|
||||||
child: Text("LOCK",
|
child: Text("LOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -56,8 +54,7 @@ class LockStateWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
entity.isLocked ? "UNLOCK" : "LOCK",
|
entity.isLocked ? "UNLOCK" : "LOCK",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style:
|
style: HAClientTheme().getActionTextStyle(context),
|
||||||
new TextStyle(fontSize: Sizes.stateFontSize, color: Colors.blue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -33,7 +33,7 @@ class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
|
|||||||
return LinearProgressIndicator(
|
return LinearProgressIndicator(
|
||||||
value: progress,
|
value: progress,
|
||||||
backgroundColor: Colors.black45,
|
backgroundColor: Colors.black45,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
|
valueColor: AlwaysStoppedAnimation<Color>(HAClientTheme().getOnStateColor(context)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,12 +13,6 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
double _currentPosition = 0;
|
double _currentPosition = 0;
|
||||||
int _savedPosition = 0;
|
int _savedPosition = 0;
|
||||||
|
|
||||||
final TextStyle _seekTextStyle = TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.bold
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -53,8 +47,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
buttons.add(
|
buttons.add(
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
|
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
|
||||||
color: Colors.orange,
|
color: Theme.of(context).accentColor,
|
||||||
focusColor: Colors.white,
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ConnectionManager().callService(
|
ConnectionManager().callService(
|
||||||
domain: "media_player",
|
domain: "media_player",
|
||||||
@ -79,7 +72,13 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("00:00"),
|
Text("00:00"),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
|
child: Text(
|
||||||
|
"${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.title.copyWith(
|
||||||
|
color: Colors.blue
|
||||||
|
)
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
|
||||||
],
|
],
|
||||||
@ -87,8 +86,7 @@ class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
|
|||||||
Container(height: 10,),
|
Container(height: 10,),
|
||||||
Slider(
|
Slider(
|
||||||
min: 0,
|
min: 0,
|
||||||
activeColor: Colors.amber,
|
activeColor: Theme.of(context).accentColor,
|
||||||
inactiveColor: Colors.black26,
|
|
||||||
max: entity.durationSeconds.toDouble(),
|
max: entity.durationSeconds.toDouble(),
|
||||||
value: _currentPosition,
|
value: _currentPosition,
|
||||||
onChangeStart: (val) {
|
onChangeStart: (val) {
|
||||||
|
@ -12,14 +12,14 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
Stack(
|
Stack(
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildImage(entity),
|
_buildImage(entity, context),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
right: 0.0,
|
right: 0.0,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black45,
|
color: Colors.black45,
|
||||||
child: _buildState(entity),
|
child: _buildState(entity, context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -35,12 +35,9 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildState(MediaPlayerEntity entity) {
|
Widget _buildState(MediaPlayerEntity entity, BuildContext context) {
|
||||||
TextStyle style = TextStyle(
|
TextStyle style = Theme.of(context).textTheme.body1.copyWith(
|
||||||
fontSize: 14.0,
|
color: Colors.white
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
height: 1.2
|
|
||||||
);
|
);
|
||||||
List<Widget> states = [];
|
List<Widget> states = [];
|
||||||
states.add(Text("${entity.displayName}", style: style));
|
states.add(Text("${entity.displayName}", style: style));
|
||||||
@ -71,7 +68,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImage(MediaPlayerEntity entity) {
|
Widget _buildImage(MediaPlayerEntity entity, BuildContext context) {
|
||||||
String state = entity.state;
|
String state = entity.state;
|
||||||
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
if (entity.entityPicture != null && state != EntityState.off && state != EntityState.unavailable && state != EntityState.idle) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -97,7 +94,7 @@ class MediaPlayerWidget extends StatelessWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
MaterialDesignIcons.getIconDataFromIconName("mdi:movie"),
|
||||||
size: 150.0,
|
size: 150.0,
|
||||||
color: EntityColor.stateColor("$state"),
|
color: HAClientTheme().getColorByEntityState("$state", context),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -7,10 +7,10 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
final String customValue;
|
final String customValue;
|
||||||
final double fontSize;
|
final TextStyle textStyle;
|
||||||
final bool bold;
|
//final bool bold;
|
||||||
|
|
||||||
const SimpleEntityState({Key key,this.bold: false, this.maxLines: 10, this.fontSize: Sizes.stateFontSize, this.expanded: true, this.textAlign: TextAlign.right, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
const SimpleEntityState({Key key,/*this.bold: false,*/ this.maxLines: 10, this.expanded: true, this.textAlign: TextAlign.right, this.textStyle, this.padding: const EdgeInsets.fromLTRB(0.0, 0.0, Sizes.rightWidgetPadding, 0.0), this.customValue}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -22,16 +22,19 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
state = customValue;
|
state = customValue;
|
||||||
}
|
}
|
||||||
TextStyle textStyle = TextStyle(
|
TextStyle tStyle;
|
||||||
fontSize: this.fontSize,
|
if (textStyle != null) {
|
||||||
fontWeight: FontWeight.normal
|
tStyle = textStyle;
|
||||||
|
} else if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.callService) {
|
||||||
|
tStyle = Theme.of(context).textTheme.subhead.copyWith(
|
||||||
|
color: Colors.blue
|
||||||
);
|
);
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
} else {
|
||||||
textStyle = textStyle.apply(color: Colors.blue);
|
tStyle = Theme.of(context).textTheme.body1;
|
||||||
}
|
}
|
||||||
if (this.bold) {
|
/*if (this.bold) {
|
||||||
textStyle = textStyle.apply(fontWeightDelta: 100);
|
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||||
}
|
}*/
|
||||||
while (state.contains(" ")){
|
while (state.contains(" ")){
|
||||||
state = state.replaceAll(" ", " ");
|
state = state.replaceAll(" ", " ");
|
||||||
}
|
}
|
||||||
@ -43,7 +46,7 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
style: textStyle
|
style: tStyle
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
|
@ -62,8 +62,7 @@ class _SliderControlsWidgetState extends State<SliderControlsWidget> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"$_newValue",
|
"$_newValue",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.display1.copyWith(
|
||||||
fontSize: Sizes.largeFontSize,
|
|
||||||
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,36 +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(
|
Text('${widget.title}'),
|
||||||
"$title",
|
|
||||||
style: TextStyle(fontSize: Sizes.stateFontSize),
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -54,5 +87,4 @@ class UniversalSlider extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -10,7 +10,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildStatusAndBattery(entity),
|
_buildStatusAndBattery(entity, context),
|
||||||
_buildCommands(entity),
|
_buildCommands(entity),
|
||||||
_buildFanSpeed(entity),
|
_buildFanSpeed(entity),
|
||||||
_buildAdditionalInfo(entity)
|
_buildAdditionalInfo(entity)
|
||||||
@ -19,12 +19,12 @@ class VacuumControls extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatusAndBattery(VacuumEntity entity) {
|
Widget _buildStatusAndBattery(VacuumEntity entity, BuildContext context) {
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (entity.supportStatus) {
|
if (entity.supportStatus) {
|
||||||
result.addAll(
|
result.addAll(
|
||||||
<Widget>[
|
<Widget>[
|
||||||
Text("Status:", style: TextStyle(fontSize: Sizes.stateFontSize),),
|
Text("Status:"),
|
||||||
Container(width: 6,),
|
Container(width: 6,),
|
||||||
Expanded(
|
Expanded(
|
||||||
//flex: 1,
|
//flex: 1,
|
||||||
@ -33,10 +33,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body2,
|
||||||
fontSize: Sizes.stateFontSize,
|
|
||||||
fontWeight: FontWeight.bold
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -48,7 +45,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
||||||
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
|
Icon(MaterialDesignIcons.getIconDataFromIconName(iconName)),
|
||||||
Container(width: 6,),
|
Container(width: 6,),
|
||||||
Text("$batteryLevel %", style: TextStyle(fontSize: Sizes.stateFontSize))
|
Text("$batteryLevel %")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -172,7 +169,7 @@ class VacuumControls extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("Vacuum cleaner commands:", style: TextStyle(fontSize: Sizes.stateFontSize)),
|
Text("Vacuum cleaner commands:"),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
@ -27,10 +27,7 @@ class VacuumStateButton extends StatelessWidget {
|
|||||||
text: "RETURN TO DOCK"
|
text: "RETURN TO DOCK"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = Text(entity.state.toUpperCase(), style: TextStyle(
|
result = Text(entity.state.toUpperCase(), style: Theme.of(context).textTheme.subhead);
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.grey
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(right: 15),
|
padding: EdgeInsets.only(right: 15),
|
||||||
|
@ -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 {
|
||||||
@ -191,11 +191,15 @@ class HomeAssistant {
|
|||||||
return Future.value();
|
return Future.value();
|
||||||
} else {
|
} else {
|
||||||
Completer completer = Completer();
|
Completer completer = Completer();
|
||||||
|
var additionalData;
|
||||||
|
if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) {
|
||||||
|
additionalData = {
|
||||||
|
'url_path': _lovelaceDashbordUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
ConnectionManager().sendSocketMessage(
|
ConnectionManager().sendSocketMessage(
|
||||||
type: 'lovelace/config',
|
type: 'lovelace/config',
|
||||||
additionalData: {
|
additionalData: additionalData
|
||||||
'url_path': _lovelaceDashbordUrl == HomeAssistant.DEFAULT_DASHBOARD ? null : _lovelaceDashbordUrl
|
|
||||||
}
|
|
||||||
).then((data) {
|
).then((data) {
|
||||||
_rawLovelaceData = data;
|
_rawLovelaceData = data;
|
||||||
completer.complete();
|
completer.complete();
|
||||||
@ -301,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());
|
||||||
@ -327,7 +335,7 @@ class HomeAssistant {
|
|||||||
void _createUI() {
|
void _createUI() {
|
||||||
Logger.d("Creating Lovelace UI");
|
Logger.d("Creating Lovelace UI");
|
||||||
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
ui = HomeAssistantUI(rawLovelaceConfig: _rawLovelaceData);
|
||||||
if (isServiceExist('zha_map')) {
|
/*if (isServiceExist('zha_map')) {
|
||||||
panels.add(
|
panels.add(
|
||||||
Panel(
|
Panel(
|
||||||
id: 'haclient_zha',
|
id: 'haclient_zha',
|
||||||
@ -337,7 +345,7 @@ class HomeAssistant {
|
|||||||
icon: 'mdi:zigbee'
|
icon: 'mdi:zigbee'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math' as math;
|
||||||
import 'dart:io';
|
|
||||||
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';
|
||||||
@ -10,13 +9,11 @@ import 'package:web_socket_channel/io.dart';
|
|||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
import 'package:url_launcher/url_launcher.dart' as urlLauncher;
|
||||||
import 'package:flutter/services.dart';
|
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';
|
||||||
@ -24,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;
|
||||||
@ -33,9 +29,11 @@ import 'package:battery/battery.dart';
|
|||||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview;
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:syncfusion_flutter_core/core.dart';
|
||||||
|
import 'package:syncfusion_flutter_gauges/gauges.dart';
|
||||||
|
|
||||||
import 'utils/logger.dart';
|
import 'utils/logger.dart';
|
||||||
|
import '.secrets.dart';
|
||||||
|
|
||||||
part 'const.dart';
|
part 'const.dart';
|
||||||
part 'utils/launcher.dart';
|
part 'utils/launcher.dart';
|
||||||
@ -64,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';
|
||||||
@ -76,7 +73,6 @@ part 'entities/universal_slider.widget.dart';
|
|||||||
part 'entities/flat_service_button.widget.dart';
|
part 'entities/flat_service_button.widget.dart';
|
||||||
part 'entities/light/widgets/light_color_picker.dart';
|
part 'entities/light/widgets/light_color_picker.dart';
|
||||||
part 'entities/camera/widgets/camera_stream_view.dart';
|
part 'entities/camera/widgets/camera_stream_view.dart';
|
||||||
part 'entities/entity_colors.class.dart';
|
|
||||||
part 'plugins/history_chart/entity_history.dart';
|
part 'plugins/history_chart/entity_history.dart';
|
||||||
part 'plugins/history_chart/simple_state_history_chart.dart';
|
part 'plugins/history_chart/simple_state_history_chart.dart';
|
||||||
part 'plugins/history_chart/numeric_state_history_chart.dart';
|
part 'plugins/history_chart/numeric_state_history_chart.dart';
|
||||||
@ -104,14 +100,17 @@ part 'entities/alarm_control_panel/widgets/alarm_control_panel_controls.widget.d
|
|||||||
part 'entities/vacuum/vacuum_entity.class.dart';
|
part 'entities/vacuum/vacuum_entity.class.dart';
|
||||||
part 'entities/vacuum/widgets/vacuum_controls.dart';
|
part 'entities/vacuum/widgets/vacuum_controls.dart';
|
||||||
part 'entities/vacuum/widgets/vacuum_state_button.dart';
|
part 'entities/vacuum/widgets/vacuum_state_button.dart';
|
||||||
part 'pages/settings.page.dart';
|
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';
|
||||||
part 'pages/integration_settings.page.dart';
|
part 'pages/settings/integration_settings.part.dart';
|
||||||
|
part 'pages/settings/app_settings.page.dart';
|
||||||
|
part 'pages/settings/lookandfeel_settings.part.dart';
|
||||||
part 'pages/zha_page.dart';
|
part 'pages/zha_page.dart';
|
||||||
part 'home_assistant.class.dart';
|
part 'home_assistant.class.dart';
|
||||||
part 'pages/log.page.dart';
|
part 'pages/log.page.dart';
|
||||||
@ -124,19 +123,28 @@ part 'managers/mobile_app_integration_manager.class.dart';
|
|||||||
part 'managers/connection_manager.class.dart';
|
part 'managers/connection_manager.class.dart';
|
||||||
part 'managers/device_info_manager.class.dart';
|
part 'managers/device_info_manager.class.dart';
|
||||||
part 'managers/startup_user_messages_manager.class.dart';
|
part 'managers/startup_user_messages_manager.class.dart';
|
||||||
|
part 'managers/theme_manager.dart';
|
||||||
part 'ui.dart';
|
part 'ui.dart';
|
||||||
part 'view.class.dart';
|
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';
|
||||||
@ -148,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.0";
|
const appVersionNumber = "0.8.6";
|
||||||
const appVersionAdd = "";
|
const appVersionAdd = "";
|
||||||
const appVersion = "$appVersionNumber$appVersionAdd";
|
const appVersion = "$appVersionNumber$appVersionAdd";
|
||||||
|
|
||||||
@ -163,18 +171,25 @@ 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']);
|
||||||
|
|
||||||
FlutterError.onError = (FlutterErrorDetails details) {
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
Logger.e(" Caut Flutter runtime error: ${details.exception}");
|
Logger.e("Caut Flutter runtime error: ${details.exception}");
|
||||||
if (Logger.isInDebugMode) {
|
if (Logger.isInDebugMode) {
|
||||||
FlutterError.dumpErrorToConsole(details);
|
FlutterError.dumpErrorToConsole(details);
|
||||||
}
|
}
|
||||||
Crashlytics.instance.recordFlutterError(details);
|
Crashlytics.instance.recordFlutterError(details);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
AppTheme theme = AppTheme.values[prefs.getInt('app-theme') ?? AppTheme.defaultTheme.index];
|
||||||
|
|
||||||
runZoned(() {
|
runZoned(() {
|
||||||
runApp(new HAClientApp());
|
runApp(new HAClientApp(
|
||||||
|
theme: theme,
|
||||||
|
));
|
||||||
}, onError: (error, stack) {
|
}, onError: (error, stack) {
|
||||||
_reportError(error, stack);
|
_reportError(error, stack);
|
||||||
});
|
});
|
||||||
@ -182,22 +197,34 @@ void main() async {
|
|||||||
|
|
||||||
class HAClientApp extends StatefulWidget {
|
class HAClientApp extends StatefulWidget {
|
||||||
|
|
||||||
|
final AppTheme theme;
|
||||||
|
|
||||||
|
const HAClientApp({Key key, this.theme: AppTheme.defaultTheme}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_HAClientAppState createState() => new _HAClientAppState();
|
_HAClientAppState createState() => new _HAClientAppState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HAClientAppState extends State<HAClientApp> {
|
class _HAClientAppState extends State<HAClientApp> {
|
||||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription;
|
||||||
|
StreamSubscription _themeChangeSubscription;
|
||||||
|
AppTheme _currentTheme = AppTheme.defaultTheme;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
InAppPurchaseConnection.enablePendingPurchases();
|
InAppPurchaseConnection.enablePendingPurchases();
|
||||||
final Stream purchaseUpdates =
|
final Stream purchaseUpdates =
|
||||||
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||||
_subscription = purchaseUpdates.listen((purchases) {
|
_purchaseUpdateSubscription = purchaseUpdates.listen((purchases) {
|
||||||
_handlePurchaseUpdates(purchases);
|
_handlePurchaseUpdates(purchases);
|
||||||
});
|
});
|
||||||
|
_currentTheme = widget.theme;
|
||||||
|
_themeChangeSubscription = eventBus.on<ChangeThemeEvent>().listen((event){
|
||||||
|
setState(() {
|
||||||
|
_currentTheme = event.theme;
|
||||||
|
});
|
||||||
|
});
|
||||||
workManager.Workmanager.initialize(
|
workManager.Workmanager.initialize(
|
||||||
updateDeviceLocationIsolate,
|
updateDeviceLocationIsolate,
|
||||||
isInDebugMode: false
|
isInDebugMode: false
|
||||||
@ -226,14 +253,15 @@ class _HAClientAppState extends State<HAClientApp> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new MaterialApp(
|
return new MaterialApp(
|
||||||
title: appName,
|
title: appName,
|
||||||
theme: new ThemeData(
|
theme: HAClientTheme().getThemeData(_currentTheme),
|
||||||
primarySwatch: Colors.blue,
|
darkTheme: HAClientTheme().darkTheme,
|
||||||
),
|
debugShowCheckedModeBanner: false,
|
||||||
initialRoute: "/",
|
initialRoute: "/",
|
||||||
routes: {
|
routes: {
|
||||||
"/": (context) => MainPage(title: 'HA Client'),
|
"/": (context) => MainPage(title: 'HA Client'),
|
||||||
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
|
"/app-settings": (context) => AppSettingsPage(),
|
||||||
"/integration-settings": (context) => IntegrationSettingsPage(title: "Integration settings"),
|
"/connection-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.connectionSettings),
|
||||||
|
"/integration-settings": (context) => AppSettingsPage(showSection: AppSettingsSection.integrationSettings),
|
||||||
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
"/putchase": (context) => PurchasePage(title: "Support app development"),
|
||||||
"/play-media": (context) => PlayMediaPage(
|
"/play-media": (context) => PlayMediaPage(
|
||||||
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
|
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
|
||||||
@ -262,7 +290,9 @@ class _HAClientAppState extends State<HAClientApp> {
|
|||||||
title: new Text("Login with HA"),
|
title: new Text("Login with HA"),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text("Manual", style: TextStyle(color: Colors.white)),
|
child: Text("Manual", style: Theme.of(context).textTheme.button.copyWith(
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
|
eventBus.fire(ShowPageEvent(path: "/connection-settings", goBackFirst: true));
|
||||||
},
|
},
|
||||||
@ -276,7 +306,8 @@ class _HAClientAppState extends State<HAClientApp> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subscription.cancel();
|
_purchaseUpdateSubscription.cancel();
|
||||||
|
_themeChangeSubscription.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ class ConnectionManager {
|
|||||||
String oauthUrl;
|
String oauthUrl;
|
||||||
String webhookId;
|
String webhookId;
|
||||||
bool settingsLoaded = false;
|
bool settingsLoaded = false;
|
||||||
|
int appIntegrationVersion;
|
||||||
bool get isAuthenticated => _token != null;
|
bool get isAuthenticated => _token != null;
|
||||||
StreamSubscription _socketSubscription;
|
StreamSubscription _socketSubscription;
|
||||||
Duration connectTimeout = Duration(seconds: 15);
|
Duration connectTimeout = Duration(seconds: 15);
|
||||||
@ -43,6 +44,7 @@ class ConnectionManager {
|
|||||||
_domain = prefs.getString('hassio-domain');
|
_domain = prefs.getString('hassio-domain');
|
||||||
_port = prefs.getString('hassio-port');
|
_port = prefs.getString('hassio-port');
|
||||||
webhookId = prefs.getString('app-webhook-id');
|
webhookId = prefs.getString('app-webhook-id');
|
||||||
|
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
|
||||||
displayHostname = "$_domain:$_port";
|
displayHostname = "$_domain:$_port";
|
||||||
_webSocketAPIEndpoint =
|
_webSocketAPIEndpoint =
|
||||||
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
"${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket";
|
||||||
@ -223,16 +225,6 @@ class ConnectionManager {
|
|||||||
onLovelaceUpdatedCallback();
|
onLovelaceUpdatedCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
|
|
||||||
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
|
|
||||||
onStateChangeCallback(data["event"]["data"]);
|
|
||||||
} else if (data["event"] != null) {
|
|
||||||
Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
|
|
||||||
} else {
|
|
||||||
Logger.e("Event is null: $data");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Logger.d("[Received unhandled] <== ${data.toString()}");
|
Logger.d("[Received unhandled] <== ${data.toString()}");
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,11 @@ part of '../main.dart';
|
|||||||
|
|
||||||
class MobileAppIntegrationManager {
|
class MobileAppIntegrationManager {
|
||||||
|
|
||||||
|
static const INTEGRATION_VERSION = 3;
|
||||||
|
|
||||||
static final _appRegistrationData = {
|
static final _appRegistrationData = {
|
||||||
"app_version": "$appVersion",
|
|
||||||
"device_name": "",
|
"device_name": "",
|
||||||
|
"app_version": "$appVersion",
|
||||||
"manufacturer": DeviceInfoManager().manufacturer,
|
"manufacturer": DeviceInfoManager().manufacturer,
|
||||||
"model": DeviceInfoManager().model,
|
"model": DeviceInfoManager().model,
|
||||||
"os_version": DeviceInfoManager().osVersion,
|
"os_version": DeviceInfoManager().osVersion,
|
||||||
@ -22,6 +24,7 @@ class MobileAppIntegrationManager {
|
|||||||
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
Logger.d("Mobile app was not registered yet or need to be reseted. Registering...");
|
||||||
var registrationData = Map.from(_appRegistrationData);
|
var registrationData = Map.from(_appRegistrationData);
|
||||||
registrationData.addAll({
|
registrationData.addAll({
|
||||||
|
"device_id": "${DeviceInfoManager().unicDeviceId}",
|
||||||
"app_id": "ha_client",
|
"app_id": "ha_client",
|
||||||
"app_name": "$appName",
|
"app_name": "$appName",
|
||||||
"os_name": DeviceInfoManager().osName,
|
"os_name": DeviceInfoManager().osName,
|
||||||
@ -34,9 +37,11 @@ class MobileAppIntegrationManager {
|
|||||||
).then((response) {
|
).then((response) {
|
||||||
Logger.d("Processing registration responce...");
|
Logger.d("Processing registration responce...");
|
||||||
var responseObject = json.decode(response);
|
var responseObject = json.decode(response);
|
||||||
|
ConnectionManager().webhookId = responseObject["webhook_id"];
|
||||||
|
ConnectionManager().appIntegrationVersion = INTEGRATION_VERSION;
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||||
ConnectionManager().webhookId = responseObject["webhook_id"];
|
prefs.setInt('app-integration-version', INTEGRATION_VERSION);
|
||||||
|
|
||||||
completer.complete();
|
completer.complete();
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
@ -65,11 +70,21 @@ class MobileAppIntegrationManager {
|
|||||||
includeAuthHeader: false,
|
includeAuthHeader: false,
|
||||||
data: json.encode(updateData)
|
data: json.encode(updateData)
|
||||||
).then((response) {
|
).then((response) {
|
||||||
if (response == null || response.isEmpty) {
|
var registrationData;
|
||||||
Logger.d("No registration data in response. MobileApp integration was removed");
|
try {
|
||||||
|
registrationData = json.decode(response);
|
||||||
|
} catch (e) {
|
||||||
|
registrationData = null;
|
||||||
|
}
|
||||||
|
if (registrationData == null || registrationData.isEmpty) {
|
||||||
|
Logger.d("No registration data in response. MobileApp integration was removed or broken");
|
||||||
_askToRegisterApp();
|
_askToRegisterApp();
|
||||||
} else {
|
} else {
|
||||||
Logger.d("App registration works fine");
|
if (INTEGRATION_VERSION > ConnectionManager().appIntegrationVersion) {
|
||||||
|
Logger.d('App registration needs to be updated');
|
||||||
|
_askToRemoveAndRegisterApp();
|
||||||
|
} else {
|
||||||
|
Logger.d('App registration works fine');
|
||||||
if (showOkDialog) {
|
if (showOkDialog) {
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
title: "All good",
|
title: "All good",
|
||||||
@ -79,6 +94,7 @@ class MobileAppIntegrationManager {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
completer.complete();
|
completer.complete();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
if (e is http.Response && e.statusCode == 410) {
|
if (e is http.Response && e.statusCode == 410) {
|
||||||
@ -105,10 +121,22 @@ class MobileAppIntegrationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void _askToRemoveAndRegisterApp() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Mobile app integration needs to be updated",
|
||||||
|
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
|
||||||
|
positiveText: "Ok",
|
||||||
|
negativeText: "Report an issue",
|
||||||
|
onNegative: () {
|
||||||
|
Launcher.launchURL("https://github.com/estevez-dev/ha_client/issues/new");
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
static void _askToRegisterApp() {
|
static void _askToRegisterApp() {
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
title: "App integration was removed",
|
title: "App integration is broken",
|
||||||
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
body: "Looks like app integration was removed from your Home Assistant or it needs to be updated. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant before registering and restart Home Assistant. Then go back here.",
|
||||||
positiveText: "Register now",
|
positiveText: "Register now",
|
||||||
negativeText: "Cancel",
|
negativeText: "Cancel",
|
||||||
onPositive: () {
|
onPositive: () {
|
||||||
|
@ -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-884";
|
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();
|
||||||
|
272
lib/managers/theme_manager.dart
Normal file
272
lib/managers/theme_manager.dart
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
enum AppTheme {darkTheme, defaultTheme, haTheme}
|
||||||
|
|
||||||
|
class HAClientTheme {
|
||||||
|
|
||||||
|
static const TextTheme textTheme = TextTheme(
|
||||||
|
display1: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
|
||||||
|
display2: TextStyle(fontSize: 34, fontWeight: FontWeight.normal),
|
||||||
|
headline: TextStyle(fontSize: 24, fontWeight: FontWeight.normal),
|
||||||
|
title: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
|
||||||
|
subhead: TextStyle(fontSize: 16, fontWeight: FontWeight.normal),
|
||||||
|
body1: TextStyle(fontSize: 15, fontWeight: FontWeight.normal),
|
||||||
|
body2: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||||
|
subtitle: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||||
|
caption: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
|
||||||
|
overline: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
button: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
|
);
|
||||||
|
|
||||||
|
static const offEntityStates = [
|
||||||
|
EntityState.off,
|
||||||
|
EntityState.closed,
|
||||||
|
"below_horizon",
|
||||||
|
"default",
|
||||||
|
EntityState.idle,
|
||||||
|
EntityState.alarm_disarmed,
|
||||||
|
];
|
||||||
|
|
||||||
|
static const onEntityStates = [
|
||||||
|
EntityState.on,
|
||||||
|
"auto",
|
||||||
|
EntityState.active,
|
||||||
|
EntityState.playing,
|
||||||
|
EntityState.paused,
|
||||||
|
"above_horizon",
|
||||||
|
EntityState.home,
|
||||||
|
EntityState.open,
|
||||||
|
EntityState.cleaning,
|
||||||
|
EntityState.returning,
|
||||||
|
"cool",
|
||||||
|
EntityState.alarm_arming,
|
||||||
|
EntityState.alarm_disarming,
|
||||||
|
EntityState.alarm_pending,
|
||||||
|
];
|
||||||
|
|
||||||
|
static const disabledEntityStates = [
|
||||||
|
EntityState.unavailable,
|
||||||
|
EntityState.unknown,
|
||||||
|
];
|
||||||
|
|
||||||
|
static const alarmEntityStates = [
|
||||||
|
EntityState.alarm_armed_away,
|
||||||
|
EntityState.alarm_armed_custom_bypass,
|
||||||
|
EntityState.alarm_armed_home,
|
||||||
|
EntityState.alarm_armed_night,
|
||||||
|
EntityState.alarm_triggered,
|
||||||
|
"heat",
|
||||||
|
];
|
||||||
|
|
||||||
|
static const defaultStateColor = Color.fromRGBO(68, 115, 158, 1.0);
|
||||||
|
|
||||||
|
static const badgeColors = {
|
||||||
|
"default": Color.fromRGBO(223, 76, 30, 1.0),
|
||||||
|
"binary_sensor": Color.fromRGBO(3, 155, 229, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
static final HAClientTheme _instance = HAClientTheme
|
||||||
|
._internal();
|
||||||
|
|
||||||
|
factory HAClientTheme() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
HAClientTheme._internal();
|
||||||
|
|
||||||
|
ThemeData getThemeData(AppTheme theme) {
|
||||||
|
switch (theme) {
|
||||||
|
case AppTheme.darkTheme:
|
||||||
|
return darkTheme;
|
||||||
|
break;
|
||||||
|
case AppTheme.defaultTheme:
|
||||||
|
return defaultTheme;
|
||||||
|
break;
|
||||||
|
case AppTheme.haTheme:
|
||||||
|
return homeAssistantTheme;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return defaultTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final ThemeData defaultTheme = ThemeData.from(
|
||||||
|
colorScheme: ColorScheme(
|
||||||
|
primary: Color.fromRGBO(112, 154, 193, 1),
|
||||||
|
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
||||||
|
secondary: Color.fromRGBO(253, 216, 53, 1),
|
||||||
|
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
||||||
|
background: Color.fromRGBO(250, 250, 250, 1),
|
||||||
|
surface: Colors.white,
|
||||||
|
error: Colors.red,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSecondary: Colors.black87,
|
||||||
|
onBackground: Colors.black87,
|
||||||
|
onSurface: Colors.black87,
|
||||||
|
onError: Colors.white,
|
||||||
|
brightness: Brightness.light
|
||||||
|
),
|
||||||
|
textTheme: ThemeData.light().textTheme.copyWith(
|
||||||
|
display1: textTheme.display1.copyWith(color: Colors.black54),
|
||||||
|
display2: textTheme.display2.copyWith(color: Colors.redAccent),
|
||||||
|
headline: textTheme.headline.copyWith(color: Colors.black87),
|
||||||
|
title: textTheme.title.copyWith(color: Colors.black87),
|
||||||
|
subhead: textTheme.subhead.copyWith(color: Colors.black54),
|
||||||
|
body1: textTheme.body1.copyWith(color: Colors.black87),
|
||||||
|
body2: textTheme.body2.copyWith(color: Colors.black87),
|
||||||
|
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
|
||||||
|
caption: textTheme.caption.copyWith(color: Colors.black45),
|
||||||
|
overline: textTheme.overline.copyWith(color: Colors.black26),
|
||||||
|
button: textTheme.button.copyWith(color: Colors.white),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
final ThemeData homeAssistantTheme = ThemeData.from(
|
||||||
|
colorScheme: ColorScheme(
|
||||||
|
primary: Color.fromRGBO(2, 165, 238, 1),
|
||||||
|
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
||||||
|
secondary: Color.fromRGBO(253, 216, 53, 1),
|
||||||
|
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
||||||
|
background: Color.fromRGBO(250, 250, 250, 1),
|
||||||
|
surface: Colors.white,
|
||||||
|
error: Colors.red,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSecondary: Colors.black87,
|
||||||
|
onBackground: Colors.black87,
|
||||||
|
onSurface: Colors.black87,
|
||||||
|
onError: Colors.white,
|
||||||
|
brightness: Brightness.light
|
||||||
|
),
|
||||||
|
textTheme: ThemeData.light().textTheme.copyWith(
|
||||||
|
display1: textTheme.display1.copyWith(color: Colors.black54),
|
||||||
|
display2: textTheme.display2.copyWith(color: Colors.redAccent),
|
||||||
|
headline: textTheme.headline.copyWith(color: Colors.black87),
|
||||||
|
title: textTheme.title.copyWith(color: Colors.black87),
|
||||||
|
subhead: textTheme.subhead.copyWith(color: Colors.black54),
|
||||||
|
body1: textTheme.body1.copyWith(color: Colors.black87),
|
||||||
|
body2: textTheme.body2.copyWith(color: Colors.black87),
|
||||||
|
subtitle: textTheme.subtitle.copyWith(color: Colors.black45),
|
||||||
|
caption: textTheme.caption.copyWith(color: Colors.black45),
|
||||||
|
overline: textTheme.overline.copyWith(color: Colors.black26),
|
||||||
|
button: textTheme.button.copyWith(color: Colors.white),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
final ThemeData darkTheme = ThemeData.from(
|
||||||
|
colorScheme: ColorScheme(
|
||||||
|
primary: Color.fromRGBO(112, 154, 193, 1),
|
||||||
|
primaryVariant: Color.fromRGBO(68, 115, 158, 1),
|
||||||
|
secondary: Color.fromRGBO(253, 216, 53, 1),
|
||||||
|
secondaryVariant: Color.fromRGBO(222, 181, 2, 1),
|
||||||
|
background: Color.fromRGBO(47, 49, 54, 1),
|
||||||
|
surface: Color.fromRGBO(54, 57, 63, 1),
|
||||||
|
error: Color.fromRGBO(183, 109, 109, 1),
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSecondary: Colors.black87,
|
||||||
|
onBackground: Color.fromRGBO(220, 221, 222, 1),
|
||||||
|
onSurface: Colors.white,
|
||||||
|
onError: Colors.white,
|
||||||
|
brightness: Brightness.dark
|
||||||
|
),
|
||||||
|
textTheme: textTheme
|
||||||
|
);
|
||||||
|
|
||||||
|
Color getOnStateColor(BuildContext context) {
|
||||||
|
return Theme.of(context).colorScheme.secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getOffStateColor(BuildContext context) {
|
||||||
|
return Theme.of(context).colorScheme.primaryVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getDisabledStateColor(BuildContext context) {
|
||||||
|
return Theme.of(context).disabledColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getAlertStateColor(BuildContext context) {
|
||||||
|
return Theme.of(context).colorScheme.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getColorByEntityState(String state, BuildContext context) {
|
||||||
|
if (onEntityStates.contains(state)) {
|
||||||
|
return getOnStateColor(context);
|
||||||
|
} else if (disabledEntityStates.contains(state)) {
|
||||||
|
return getDisabledStateColor(context);
|
||||||
|
} else if (alarmEntityStates.contains(state)) {
|
||||||
|
return getAlertStateColor(context);
|
||||||
|
} else {
|
||||||
|
return getOffStateColor(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getGreenGaugeColor() {
|
||||||
|
return Colors.green;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getYellowGaugeColor() {
|
||||||
|
return Colors.yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getRedGaugeColor() {
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle getLinkTextStyle(BuildContext context) {
|
||||||
|
ThemeData theme = Theme.of(context);
|
||||||
|
return theme.textTheme.body1.copyWith(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle getActionTextStyle(BuildContext context) {
|
||||||
|
ThemeData theme = Theme.of(context);
|
||||||
|
return theme.textTheme.subhead.copyWith(
|
||||||
|
color: Colors.blue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getBadgeColor(String entityDomain) {
|
||||||
|
return badgeColors[entityDomain] ??
|
||||||
|
badgeColors["default"];
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getOnBadgeTextColor() {
|
||||||
|
return Colors.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
charts.Color chartHistoryStateColor(String state, int id, BuildContext context) {
|
||||||
|
Color c = getColorByEntityState(state, context);
|
||||||
|
if (c != null) {
|
||||||
|
return charts.Color(
|
||||||
|
r: c.red,
|
||||||
|
g: c.green,
|
||||||
|
b: c.blue,
|
||||||
|
a: c.alpha
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
double r = id.toDouble() % 10;
|
||||||
|
return charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color historyStateColor(String state, int id, BuildContext context) {
|
||||||
|
Color c = getColorByEntityState(state, context);
|
||||||
|
if (c != null) {
|
||||||
|
return c;
|
||||||
|
} else {
|
||||||
|
if (id > -1) {
|
||||||
|
double r = id.toDouble() % 10;
|
||||||
|
charts.Color c1 = charts.MaterialPalette.getOrderedPalettes(10)[r.round()].shadeDefault;
|
||||||
|
return Color.fromARGB(c1.a, c1.r, c1.g, c1.b);
|
||||||
|
} else {
|
||||||
|
return getOnStateColor(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,61 +12,52 @@ class EntityViewPage extends StatefulWidget {
|
|||||||
class _EntityViewPageState extends State<EntityViewPage> {
|
class _EntityViewPageState extends State<EntityViewPage> {
|
||||||
StreamSubscription _refreshDataSubscription;
|
StreamSubscription _refreshDataSubscription;
|
||||||
StreamSubscription _stateSubscription;
|
StreamSubscription _stateSubscription;
|
||||||
Entity entity;
|
Entity _entity;
|
||||||
Entity forwardToMainPage;
|
|
||||||
bool _popScheduled = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
if (event.entityId == widget.entityId) {
|
if (event.entityId == widget.entityId) {
|
||||||
entity = HomeAssistant().entities.get(widget.entityId);
|
|
||||||
Logger.d("[Entity page] State change event handled: ${event.entityId}");
|
Logger.d("[Entity page] State change event handled: ${event.entityId}");
|
||||||
setState(() {});
|
setState(() {
|
||||||
|
_getEntity();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||||
entity = HomeAssistant().entities.get(widget.entityId);
|
Logger.d("[Entity page] Refresh data event handled");
|
||||||
setState(() {});
|
setState(() {
|
||||||
|
_getEntity();
|
||||||
});
|
});
|
||||||
entity = HomeAssistant().entities.get(widget.entityId);
|
});
|
||||||
|
_getEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
_getEntity() {
|
||||||
|
_entity = HomeAssistant().entities.get(widget.entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget body;
|
String entityNameToDisplay = '${(_entity?.displayName ?? widget.entityId) ?? ''}';
|
||||||
if (MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
|
|
||||||
if (!_popScheduled) {
|
|
||||||
_popScheduled = true;
|
|
||||||
_popAfterBuild();
|
|
||||||
}
|
|
||||||
body = PageLoadingIndicator();
|
|
||||||
} else {
|
|
||||||
body = EntityPageLayout(entity: entity);
|
|
||||||
}
|
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}),
|
}),
|
||||||
title: new Text("${entity.displayName}"),
|
title: new Text(entityNameToDisplay),
|
||||||
),
|
),
|
||||||
body: body,
|
body: _entity == null ? PageLoadingError(
|
||||||
|
errorText: 'Entity is not available $entityNameToDisplay',
|
||||||
|
) : EntityPageLayout(entity: _entity),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_popAfterBuild() async {
|
|
||||||
forwardToMainPage = entity;
|
|
||||||
await Future.delayed(Duration(milliseconds: 300));
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose(){
|
void dispose(){
|
||||||
if (_stateSubscription != null) _stateSubscription.cancel();
|
if (_stateSubscription != null) _stateSubscription.cancel();
|
||||||
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
if (_refreshDataSubscription != null) _refreshDataSubscription.cancel();
|
||||||
eventBus.fire(ShowEntityPageEvent(entity: forwardToMainPage));
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,200 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class IntegrationSettingsPage extends StatefulWidget {
|
|
||||||
IntegrationSettingsPage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_IntegrationSettingsPageState createState() => new _IntegrationSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
|
||||||
|
|
||||||
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
|
|
||||||
bool _locationTrackingEnabled = false;
|
|
||||||
bool _wait = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadSettings();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSettings() async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.reload();
|
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
setState(() {
|
|
||||||
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
|
||||||
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
|
|
||||||
if (_locationInterval % 5 != 0) {
|
|
||||||
_locationInterval = 5 * (_locationInterval ~/ 5);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void incLocationInterval() {
|
|
||||||
if (_locationInterval < 720) {
|
|
||||||
setState(() {
|
|
||||||
_locationInterval = _locationInterval + 5;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void decLocationInterval() {
|
|
||||||
if (_locationInterval > 5) {
|
|
||||||
setState(() {
|
|
||||||
_locationInterval = _locationInterval - 5;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
restart() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Are you sure you want to restart Home Assistant?",
|
|
||||||
body: "This will restart your Home Assistant server.",
|
|
||||||
positiveText: "Sure. Make it so",
|
|
||||||
negativeText: "What?? No!",
|
|
||||||
onPositive: () {
|
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Are you sure you want to STOP Home Assistant?",
|
|
||||||
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
|
||||||
positiveText: "Sure. Make it so",
|
|
||||||
negativeText: "What?? No!",
|
|
||||||
onPositive: () {
|
|
||||||
ConnectionManager().callService(domain: "homeassistant", service: "stop");
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRegistration() {
|
|
||||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetRegistration() {
|
|
||||||
eventBus.fire(ShowPopupDialogEvent(
|
|
||||||
title: "Waaaait",
|
|
||||||
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
|
||||||
positiveText: "Done it already",
|
|
||||||
negativeText: "Ok, I will",
|
|
||||||
onPositive: () {
|
|
||||||
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
_switchLocationTrackingState(bool state) async {
|
|
||||||
if (state) {
|
|
||||||
await LocationManager().updateDeviceLocation();
|
|
||||||
}
|
|
||||||
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
|
||||||
setState(() {
|
|
||||||
_wait = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return new Scaffold(
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
Navigator.pop(context);
|
|
||||||
}),
|
|
||||||
title: new Text(widget.title),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
children: <Widget>[
|
|
||||||
Text("Location tracking", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
InkWell(
|
|
||||||
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
|
||||||
child: Text(
|
|
||||||
"Please read documentation!",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: 16,
|
|
||||||
decoration: TextDecoration.underline
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Text("Enable device location tracking"),
|
|
||||||
Switch(
|
|
||||||
value: _locationTrackingEnabled,
|
|
||||||
onChanged: _wait ? null : (value) {
|
|
||||||
setState(() {
|
|
||||||
_locationTrackingEnabled = value;
|
|
||||||
_wait = true;
|
|
||||||
});
|
|
||||||
_switchLocationTrackingState(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text("Location update interval in minutes:"),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
//Expanded(child: Container(),),
|
|
||||||
FlatButton(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
child: Text("-", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
|
||||||
onPressed: () => decLocationInterval(),
|
|
||||||
),
|
|
||||||
Text("$_locationInterval", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
|
||||||
FlatButton(
|
|
||||||
padding: EdgeInsets.all(0.0),
|
|
||||||
child: Text("+", style: TextStyle(fontSize: Sizes.largeFontSize)),
|
|
||||||
onPressed: () => incLocationInterval(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Divider(),
|
|
||||||
Text("Integration status", style: TextStyle(fontSize: Sizes.largeFontSize-2)),
|
|
||||||
Container(height: Sizes.rowPadding,),
|
|
||||||
Text("${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}"),
|
|
||||||
Container(height: 6.0,),
|
|
||||||
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
|
||||||
//Divider(),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
RaisedButton(
|
|
||||||
color: Colors.blue,
|
|
||||||
onPressed: () => updateRegistration(),
|
|
||||||
child: Text("Check integration", style: TextStyle(color: Colors.white))
|
|
||||||
),
|
|
||||||
Container(width: 10.0,),
|
|
||||||
RaisedButton(
|
|
||||||
color: Colors.redAccent,
|
|
||||||
onPressed: () => resetRegistration(),
|
|
||||||
child: Text("Reset integration", style: TextStyle(color: Colors.white))
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,11 +21,12 @@ 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;
|
||||||
Entity _entityToShow;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -47,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
|
||||||
@ -92,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) {
|
||||||
@ -108,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) {
|
||||||
@ -124,10 +126,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
await HomeAssistant().fetchData(uiOnly).then((_) {
|
await HomeAssistant().fetchData(uiOnly).then((_) {
|
||||||
_hideBottomBar();
|
setState((){
|
||||||
if (_entityToShow != null) {
|
_bottomInfoBarController.hideBottomBar();
|
||||||
_entityToShow = HomeAssistant().entities.get(_entityToShow.entityId);
|
});
|
||||||
}
|
HomeAssistant().saveCache();
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
if (e is HAError) {
|
if (e is HAError) {
|
||||||
_setErrorState(e);
|
_setErrorState(e);
|
||||||
@ -144,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,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(
|
||||||
@ -217,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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,19 +302,19 @@ 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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showEntityPage(String entityId) {
|
void _showEntityPage(String entityId) {
|
||||||
setState(() {
|
Navigator.push(
|
||||||
_entityToShow = HomeAssistant().entities?.get(entityId);
|
context,
|
||||||
if (_entityToShow != null) {
|
MaterialPageRoute(
|
||||||
_mainScrollController?.jumpTo(0);
|
builder: (context) => EntityViewPage(entityId: entityId),
|
||||||
}
|
)
|
||||||
});
|
);
|
||||||
/*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
|
/*if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@ -347,11 +354,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
accountName: Text(HomeAssistant().userName),
|
accountName: Text(HomeAssistant().userName),
|
||||||
accountEmail: Text(HomeAssistant().locationName ?? ""),
|
accountEmail: Text(HomeAssistant().locationName ?? ""),
|
||||||
currentAccountPicture: CircleAvatar(
|
currentAccountPicture: CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context).backgroundColor,
|
||||||
child: Text(
|
child: Text(
|
||||||
HomeAssistant().userAvatarText,
|
HomeAssistant().userAvatarText,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.display1
|
||||||
fontSize: 32.0
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -360,41 +366,19 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
HomeAssistant().panels.forEach((Panel panel) {
|
HomeAssistant().panels.forEach((Panel panel) {
|
||||||
if (!panel.isHidden) {
|
if (!panel.isHidden) {
|
||||||
menuItems.add(
|
menuItems.add(
|
||||||
new ListTile(
|
panel.getMenuItemWidget(context)
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName(panel.icon)),
|
|
||||||
title: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text("${panel.title}"),
|
|
||||||
Container(width: 4.0,),
|
|
||||||
panel.isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
panel.handleOpen(context);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
menuItems.addAll([
|
menuItems.addAll([
|
||||||
Divider(),
|
Divider(),
|
||||||
ListTile(
|
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:server-network")),
|
|
||||||
title: Text("Connection settings"),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.of(context).pushNamed('/connection-settings');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:cellphone-settings-variant")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:cellphone-settings-variant")),
|
||||||
title: Text("Integration settings"),
|
title: Text("App settings"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pushNamed('/integration-settings');
|
Navigator.of(context).pushNamed('/app-settings');
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
@ -435,11 +419,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:forum")),
|
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:discord")),
|
||||||
title: Text("Contacts/Discussion"),
|
title: Text("Contacts/Discussion"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Launcher.launchURL("https://spectrum.chat/ha-client");
|
Launcher.launchURL("https://discord.gg/nd6FZQ");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
@ -458,9 +442,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"ha-client.app",
|
"ha-client.app",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -474,9 +458,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"Terms and Conditions",
|
"Terms and Conditions",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -490,9 +474,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"Privacy Policy",
|
"Privacy Policy",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -508,114 +492,7 @@ 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;
|
|
||||||
Color _bottomBarColor;
|
|
||||||
Timer _bottomBarTimer;
|
|
||||||
|
|
||||||
void _showInfoBottomBar({String message, bool progress: false, Duration duration}) {
|
|
||||||
_bottomBarTimer?.cancel();
|
|
||||||
_bottomBarAction = Container(height: 0.0, width: 0.0,);
|
|
||||||
_bottomBarColor = Colors.grey.shade50;
|
|
||||||
setState(() {
|
|
||||||
_bottomBarText = message;
|
|
||||||
_bottomBarProgress = progress;
|
|
||||||
_showBottomBar = true;
|
|
||||||
});
|
|
||||||
if (duration != null) {
|
|
||||||
_bottomBarTimer = Timer(duration, () {
|
|
||||||
_hideBottomBar();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showErrorBottomBar(HAError error) {
|
|
||||||
TextStyle textStyle = TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: Sizes.nameFontSize
|
|
||||||
);
|
|
||||||
_bottomBarColor = Colors.red.shade100;
|
|
||||||
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>();
|
||||||
final ScrollController _mainScrollController = ScrollController();
|
|
||||||
|
|
||||||
Widget _buildScaffoldBody(bool empty) {
|
Widget _buildScaffoldBody(bool empty) {
|
||||||
List<PopupMenuItem<String>> serviceMenuItems = [];
|
List<PopupMenuItem<String>> serviceMenuItems = [];
|
||||||
@ -649,9 +526,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
activePlayers.map((entity) => PopupMenuItem<String>(
|
activePlayers.map((entity) => PopupMenuItem<String>(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${entity.displayName}",
|
"${entity.displayName}",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: EntityColor.stateColor(entity.state)
|
color: HAClientTheme().getColorByEntityState(entity.state, context)
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
value: "${entity.entityId}",
|
value: "${entity.entityId}",
|
||||||
)).toList()
|
)).toList()
|
||||||
@ -680,7 +557,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
|
child: Text(
|
||||||
|
"$playersCount",
|
||||||
|
style: Theme.of(context).textTheme.caption.copyWith(
|
||||||
|
color: Colors.white
|
||||||
|
)
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -698,7 +580,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text("Login with Home Assistant", style: TextStyle(fontSize: 16.0, color: Colors.white)),
|
child: Text("Login with Home Assistant", style: Theme.of(context).textTheme.button),
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: () => _fullLoad(),
|
onPressed: () => _fullLoad(),
|
||||||
)
|
)
|
||||||
@ -715,30 +597,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
|
|
||||||
mainScrollBody = Flex(
|
|
||||||
direction: Axis.horizontal,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: HomeAssistant().ui.build(context, _viewsTabController),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: Sizes.mainPageScreenSeparatorWidth,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
|
|
||||||
child: EntityPageLayout(entity: _entityToShow, showClose: true,),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else if (_entityToShow != null) {
|
|
||||||
mainScrollBody = EntityPageLayout(entity: _entityToShow, showClose: true,);
|
|
||||||
} else {
|
} else {
|
||||||
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
|
mainScrollBody = HomeAssistant().ui.build(context, _viewsTabController);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return NestedScrollView(
|
return NestedScrollView(
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
@ -793,7 +654,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_scaffoldKey.currentState.openDrawer();
|
_scaffoldKey.currentState.openDrawer();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
bottom: (empty || _entityToShow != null) ? null : TabBar(
|
bottom: empty ? null : TabBar(
|
||||||
controller: _viewsTabController,
|
controller: _viewsTabController,
|
||||||
tabs: buildUIViewTabs(),
|
tabs: buildUIViewTabs(),
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
@ -803,7 +664,6 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
body: mainScrollBody,
|
body: mainScrollBody,
|
||||||
controller: _mainScrollController,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -811,79 +671,25 @@ 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: EntityColor.stateColor(EntityState.on),),
|
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.unavailable),),
|
|
||||||
Icon(Icons.stop, size: 10.0, color: EntityColor.stateColor(EntityState.off),),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (bottomBarChildren.isNotEmpty) {
|
|
||||||
bottomBar = Container(
|
|
||||||
color: _bottomBarColor,
|
|
||||||
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 {
|
||||||
return WillPopScope(
|
return Scaffold(
|
||||||
child: Scaffold(
|
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
drawer: _buildAppDrawer(),
|
drawer: _buildAppDrawer(),
|
||||||
primary: false,
|
primary: false,
|
||||||
bottomNavigationBar: bottomBar,
|
bottomNavigationBar: BottomInfoBar(
|
||||||
body: _buildScaffoldBody(false)
|
controller: _bottomInfoBarController,
|
||||||
),
|
),
|
||||||
onWillPop: () {
|
body: _buildScaffoldBody(false)
|
||||||
if (_entityToShow != null) {
|
|
||||||
eventBus.fire(ShowEntityPageEvent());
|
|
||||||
return Future.value(false);
|
|
||||||
} else {
|
|
||||||
return Future.value(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -904,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) {
|
||||||
@ -135,7 +134,9 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
if (_validationMessage.isNotEmpty) {
|
if (_validationMessage.isNotEmpty) {
|
||||||
children.add(Text(
|
children.add(Text(
|
||||||
"$_validationMessage",
|
"$_validationMessage",
|
||||||
style: TextStyle(color: Colors.red)
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
color: Theme.of(context).errorColor
|
||||||
|
)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
children.addAll(<Widget>[
|
children.addAll(<Widget>[
|
||||||
@ -193,7 +194,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
|
|||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"How?",
|
"How?",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
decoration: TextDecoration.underline
|
decoration: TextDecoration.underline
|
||||||
),
|
),
|
||||||
|
@ -62,7 +62,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProducts() {
|
List<Widget> _buildProducts() {
|
||||||
List<Widget> productWidgets = [];
|
List<Widget> productWidgets = [];
|
||||||
for (ProductDetails product in _products) {
|
for (ProductDetails product in _products) {
|
||||||
productWidgets.add(
|
productWidgets.add(
|
||||||
@ -72,10 +72,7 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView(
|
return productWidgets;
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
children: productWidgets
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _buyProduct(ProductDetails product) {
|
void _buyProduct(ProductDetails product) {
|
||||||
@ -87,12 +84,28 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget body;
|
List<Widget> body;
|
||||||
if (!_loaded) {
|
if (!_loaded) {
|
||||||
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
body = [_error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error)];
|
||||||
} else {
|
} else {
|
||||||
body = _buildProducts();
|
body = _buildProducts();
|
||||||
}
|
}
|
||||||
|
body.add(
|
||||||
|
Card(
|
||||||
|
child: Container(
|
||||||
|
height: 80,
|
||||||
|
child: InkWell(
|
||||||
|
child: Image.network('https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif'),
|
||||||
|
onTap: () {
|
||||||
|
Launcher.launchURLInCustomTab(
|
||||||
|
context: context,
|
||||||
|
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARWGETZD2D83Q&source=url'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
@ -100,7 +113,10 @@ class _PurchasePageState extends State<PurchasePage> {
|
|||||||
}),
|
}),
|
||||||
title: new Text(widget.title),
|
title: new Text(widget.title),
|
||||||
),
|
),
|
||||||
body: body,
|
body: ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
children: body
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,234 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class ConnectionSettingsPage extends StatefulWidget {
|
|
||||||
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
|
||||||
String _hassioDomain = "";
|
|
||||||
String _newHassioDomain = "";
|
|
||||||
String _hassioPort = "";
|
|
||||||
String _newHassioPort = "";
|
|
||||||
String _socketProtocol = "wss";
|
|
||||||
String _newSocketProtocol = "wss";
|
|
||||||
String _longLivedToken = "";
|
|
||||||
String _newLongLivedToken = "";
|
|
||||||
bool _useLovelace = true;
|
|
||||||
bool _newUseLovelace = true;
|
|
||||||
|
|
||||||
String oauthUrl;
|
|
||||||
bool useOAuth = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadSettings();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSettings() async {
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
final storage = new FlutterSecureStorage();
|
|
||||||
|
|
||||||
try {
|
|
||||||
useOAuth = prefs.getBool("oauth-used") ?? true;
|
|
||||||
} catch (e) {
|
|
||||||
useOAuth = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!useOAuth) {
|
|
||||||
try {
|
|
||||||
_longLivedToken = _newLongLivedToken =
|
|
||||||
await storage.read(key: "hacl_llt");
|
|
||||||
} catch (e) {
|
|
||||||
_longLivedToken = _newLongLivedToken = "";
|
|
||||||
await storage.delete(key: "hacl_llt");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
|
||||||
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
|
||||||
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
|
||||||
try {
|
|
||||||
_useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true;
|
|
||||||
} catch (e) {
|
|
||||||
_useLovelace = _newUseLovelace = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _checkConfigChanged() {
|
|
||||||
return (
|
|
||||||
(_newHassioPort != _hassioPort) ||
|
|
||||||
(_newHassioDomain != _hassioDomain) ||
|
|
||||||
(_newSocketProtocol != _socketProtocol) ||
|
|
||||||
(_newUseLovelace != _useLovelace) ||
|
|
||||||
(_newLongLivedToken != _longLivedToken));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveSettings() async {
|
|
||||||
_newHassioDomain = _newHassioDomain.trim();
|
|
||||||
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
|
|
||||||
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
|
|
||||||
_newHassioDomain = _newHassioDomain.split("//")[1];
|
|
||||||
}
|
|
||||||
_newHassioDomain = _newHassioDomain.split("/")[0];
|
|
||||||
if (_newHassioDomain.contains(":")) {
|
|
||||||
List<String> domainAndPort = _newHassioDomain.split(":");
|
|
||||||
_newHassioDomain = domainAndPort[0];
|
|
||||||
_newHassioPort = domainAndPort[1];
|
|
||||||
}
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
||||||
final storage = new FlutterSecureStorage();
|
|
||||||
if (_newLongLivedToken.isNotEmpty) {
|
|
||||||
_newLongLivedToken = _newLongLivedToken.trim();
|
|
||||||
prefs.setBool("oauth-used", false);
|
|
||||||
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
|
||||||
} else if (!useOAuth) {
|
|
||||||
await storage.delete(key: "hacl_llt");
|
|
||||||
}
|
|
||||||
prefs.setString("hassio-domain", _newHassioDomain);
|
|
||||||
if (_newHassioPort == null || _newHassioPort.isEmpty) {
|
|
||||||
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
|
|
||||||
} else {
|
|
||||||
_newHassioPort = _newHassioPort.trim();
|
|
||||||
}
|
|
||||||
prefs.setString("hassio-port", _newHassioPort);
|
|
||||||
prefs.setString("hassio-protocol", _newSocketProtocol);
|
|
||||||
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
|
||||||
prefs.setBool("use-lovelace", _newUseLovelace);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return new Scaffold(
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
|
||||||
Navigator.pop(context);
|
|
||||||
}),
|
|
||||||
title: new Text(widget.title),
|
|
||||||
actions: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.check),
|
|
||||||
onPressed: (){
|
|
||||||
if (_checkConfigChanged()) {
|
|
||||||
Logger.d("Settings changed. Saving...");
|
|
||||||
_saveSettings().then((r) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
eventBus.fire(SettingsChangedEvent(true));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Logger.d("Settings was not changed");
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
"Connection settings",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.black45,
|
|
||||||
fontSize: 20.0
|
|
||||||
),
|
|
||||||
),
|
|
||||||
new Row(
|
|
||||||
children: [
|
|
||||||
Text("Use ssl (HTTPS)"),
|
|
||||||
Switch(
|
|
||||||
value: (_newSocketProtocol == "wss"),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_newSocketProtocol = value ? "wss" : "ws";
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Home Assistant domain or ip address"
|
|
||||||
),
|
|
||||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
|
|
||||||
onChanged: (value) {
|
|
||||||
_newHassioDomain = value;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Home Assistant port (default is 8123)"
|
|
||||||
),
|
|
||||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
|
|
||||||
onChanged: (value) {
|
|
||||||
_newHassioPort = value;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
new Text(
|
|
||||||
"Try ports 80 and 443 if default is not working and you don't know why.",
|
|
||||||
style: TextStyle(color: Colors.grey),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(top: 20.0),
|
|
||||||
child: Text(
|
|
||||||
"UI",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.black45,
|
|
||||||
fontSize: 20.0
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
new Row(
|
|
||||||
children: [
|
|
||||||
Text("Use Lovelace UI"),
|
|
||||||
Switch(
|
|
||||||
value: _newUseLovelace,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_newUseLovelace = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Authentication settings",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.black45,
|
|
||||||
fontSize: 20.0
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(height: 10.0,),
|
|
||||||
Text(
|
|
||||||
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
|
||||||
style: TextStyle(color: Colors.redAccent),
|
|
||||||
),
|
|
||||||
new TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Long-lived token"
|
|
||||||
),
|
|
||||||
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
|
|
||||||
onChanged: (value) {
|
|
||||||
_newLongLivedToken = value;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
104
lib/pages/settings/app_settings.page.dart
Normal file
104
lib/pages/settings/app_settings.page.dart
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
enum AppSettingsSection {menu, connectionSettings, integrationSettings, lookAndFeel}
|
||||||
|
|
||||||
|
class AppSettingsPage extends StatefulWidget {
|
||||||
|
final AppSettingsSection showSection;
|
||||||
|
|
||||||
|
AppSettingsPage({Key key, this.showSection: AppSettingsSection.menu}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AppSettingsPageState createState() => new _AppSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppSettingsPageState extends State<AppSettingsPage> {
|
||||||
|
|
||||||
|
var _currentSection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentSection = widget.showSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenuItem(BuildContext context, IconData icon,String title, AppSettingsSection section) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(title, style: Theme.of(context).textTheme.subhead),
|
||||||
|
leading: Icon(icon),
|
||||||
|
trailing: Icon(Icons.keyboard_arrow_right),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_currentSection = section;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenu(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:network'), 'Connection settings', AppSettingsSection.connectionSettings),
|
||||||
|
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:cellphone-android'), 'Integration settings', AppSettingsSection.integrationSettings),
|
||||||
|
_buildMenuItem(context, MaterialDesignIcons.getIconDataFromIconName('mdi:brush'), 'Look and feel', AppSettingsSection.lookAndFeel),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget section;
|
||||||
|
String title;
|
||||||
|
switch (_currentSection) {
|
||||||
|
case AppSettingsSection.menu: {
|
||||||
|
section = _buildMenu(context);
|
||||||
|
title = 'App settings';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AppSettingsSection.connectionSettings: {
|
||||||
|
section = ConnectionSettingsPage();
|
||||||
|
title = 'App settings - Connection';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AppSettingsSection.integrationSettings: {
|
||||||
|
section = IntegrationSettingsPage();
|
||||||
|
title = 'App settings - Integration';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AppSettingsSection.lookAndFeel: {
|
||||||
|
section = LookAndFeelSettingsPage();
|
||||||
|
title = 'App settings - Look&Feel';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
title = ':(';
|
||||||
|
section = PageLoadingIndicator();
|
||||||
|
}
|
||||||
|
return WillPopScope(
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
if (_currentSection == AppSettingsSection.menu) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_currentSection = AppSettingsSection.menu;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
title: Text(title),
|
||||||
|
),
|
||||||
|
body: section
|
||||||
|
),
|
||||||
|
onWillPop: () {
|
||||||
|
if (_currentSection == AppSettingsSection.menu) {
|
||||||
|
return Future.value(true);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_currentSection = AppSettingsSection.menu;
|
||||||
|
});
|
||||||
|
return Future.value(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
192
lib/pages/settings/connection_settings.part.dart
Normal file
192
lib/pages/settings/connection_settings.part.dart
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class ConnectionSettingsPage extends StatefulWidget {
|
||||||
|
ConnectionSettingsPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ConnectionSettingsPageState createState() => new _ConnectionSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
|
||||||
|
String _hassioDomain = "";
|
||||||
|
String _newHassioDomain = "";
|
||||||
|
String _hassioPort = "";
|
||||||
|
String _newHassioPort = "";
|
||||||
|
String _socketProtocol = "wss";
|
||||||
|
String _newSocketProtocol = "wss";
|
||||||
|
String _longLivedToken = "";
|
||||||
|
String _newLongLivedToken = "";
|
||||||
|
|
||||||
|
String oauthUrl;
|
||||||
|
bool useOAuth = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSettings() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
useOAuth = prefs.getBool("oauth-used") ?? true;
|
||||||
|
} catch (e) {
|
||||||
|
useOAuth = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useOAuth) {
|
||||||
|
try {
|
||||||
|
_longLivedToken = _newLongLivedToken =
|
||||||
|
await storage.read(key: "hacl_llt");
|
||||||
|
} catch (e) {
|
||||||
|
_longLivedToken = _newLongLivedToken = "";
|
||||||
|
await storage.delete(key: "hacl_llt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? "";
|
||||||
|
_hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? "";
|
||||||
|
_socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _checkConfigChanged() {
|
||||||
|
return (
|
||||||
|
(_newHassioPort != _hassioPort) ||
|
||||||
|
(_newHassioDomain != _hassioDomain) ||
|
||||||
|
(_newSocketProtocol != _socketProtocol) ||
|
||||||
|
(_newLongLivedToken != _longLivedToken));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveSettings() async {
|
||||||
|
_newHassioDomain = _newHassioDomain.trim();
|
||||||
|
if (_newHassioDomain.startsWith("http") && _newHassioDomain.indexOf("//") > 0) {
|
||||||
|
_newHassioDomain.startsWith("https") ? _newSocketProtocol = "wss" : _newSocketProtocol = "ws";
|
||||||
|
_newHassioDomain = _newHassioDomain.split("//")[1];
|
||||||
|
}
|
||||||
|
_newHassioDomain = _newHassioDomain.split("/")[0];
|
||||||
|
if (_newHassioDomain.contains(":")) {
|
||||||
|
List<String> domainAndPort = _newHassioDomain.split(":");
|
||||||
|
_newHassioDomain = domainAndPort[0];
|
||||||
|
_newHassioPort = domainAndPort[1];
|
||||||
|
}
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
final storage = new FlutterSecureStorage();
|
||||||
|
if (_newLongLivedToken.isNotEmpty) {
|
||||||
|
_newLongLivedToken = _newLongLivedToken.trim();
|
||||||
|
prefs.setBool("oauth-used", false);
|
||||||
|
await storage.write(key: "hacl_llt", value: _newLongLivedToken);
|
||||||
|
} else if (!useOAuth) {
|
||||||
|
await storage.delete(key: "hacl_llt");
|
||||||
|
}
|
||||||
|
prefs.setString("hassio-domain", _newHassioDomain);
|
||||||
|
if (_newHassioPort == null || _newHassioPort.isEmpty) {
|
||||||
|
_newHassioPort = _newSocketProtocol == "wss" ? "443" : "80";
|
||||||
|
} else {
|
||||||
|
_newHassioPort = _newHassioPort.trim();
|
||||||
|
}
|
||||||
|
prefs.setString("hassio-port", _newHassioPort);
|
||||||
|
prefs.setString("hassio-protocol", _newSocketProtocol);
|
||||||
|
prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
"Connection settings",
|
||||||
|
style: Theme.of(context).textTheme.headline,
|
||||||
|
),
|
||||||
|
new Row(
|
||||||
|
children: [
|
||||||
|
Text("Use ssl (HTTPS)"),
|
||||||
|
Switch(
|
||||||
|
value: (_newSocketProtocol == "wss"),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_newSocketProtocol = value ? "wss" : "ws";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Home Assistant domain or ip address"
|
||||||
|
),
|
||||||
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioDomain)),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioDomain = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Home Assistant port (default is 8123)"
|
||||||
|
),
|
||||||
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newHassioPort)),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newHassioPort = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
new Text(
|
||||||
|
"Try ports 80 and 443 if default is not working and you don't know why.",
|
||||||
|
style: Theme.of(context).textTheme.caption,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Authentication settings",
|
||||||
|
style: Theme.of(context).textTheme.headline,
|
||||||
|
),
|
||||||
|
Container(height: 10.0,),
|
||||||
|
Text(
|
||||||
|
"You can leave this field blank to make app generate new long-lived token automatically by asking you to login to your Home Assistant. Use this field only if you still want to use manually generated long-lived token. Leave it blank if you don't understand what we are talking about.",
|
||||||
|
style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
color: Colors.redAccent
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Long-lived token"
|
||||||
|
),
|
||||||
|
controller: TextEditingController.fromValue(TextEditingValue(text: _newLongLivedToken)),
|
||||||
|
onChanged: (value) {
|
||||||
|
_newLongLivedToken = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: Sizes.rowPadding,
|
||||||
|
),
|
||||||
|
RaisedButton(
|
||||||
|
child: Text('Apply', style: Theme.of(context).textTheme.button),
|
||||||
|
color: Theme.of(context).primaryColorDark,
|
||||||
|
onPressed: () {
|
||||||
|
if (_checkConfigChanged()) {
|
||||||
|
Logger.d("Settings changed. Saving...");
|
||||||
|
_saveSettings().then((r) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
eventBus.fire(SettingsChangedEvent(true));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Logger.d("Settings was not changed");
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
194
lib/pages/settings/integration_settings.part.dart
Normal file
194
lib/pages/settings/integration_settings.part.dart
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class IntegrationSettingsPage extends StatefulWidget {
|
||||||
|
IntegrationSettingsPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_IntegrationSettingsPageState createState() => new _IntegrationSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> {
|
||||||
|
|
||||||
|
int _locationInterval = LocationManager().defaultUpdateIntervalMinutes;
|
||||||
|
bool _locationTrackingEnabled = false;
|
||||||
|
bool _wait = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSettings() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.reload();
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
setState(() {
|
||||||
|
_locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
|
||||||
|
_locationInterval = prefs.getInt("location-interval") ?? LocationManager().defaultUpdateIntervalMinutes;
|
||||||
|
if (_locationInterval % 5 != 0) {
|
||||||
|
_locationInterval = 5 * (_locationInterval ~/ 5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void incLocationInterval() {
|
||||||
|
if (_locationInterval < 720) {
|
||||||
|
setState(() {
|
||||||
|
_locationInterval = _locationInterval + 5;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void decLocationInterval() {
|
||||||
|
if (_locationInterval > 5) {
|
||||||
|
setState(() {
|
||||||
|
_locationInterval = _locationInterval - 5;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Are you sure you want to restart Home Assistant?",
|
||||||
|
body: "This will restart your Home Assistant server.",
|
||||||
|
positiveText: "Sure. Make it so",
|
||||||
|
negativeText: "What?? No!",
|
||||||
|
onPositive: () {
|
||||||
|
ConnectionManager().callService(domain: "homeassistant", service: "restart");
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Are you sure you want to STOP Home Assistant?",
|
||||||
|
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
||||||
|
positiveText: "Sure. Make it so",
|
||||||
|
negativeText: "What?? No!",
|
||||||
|
onPositive: () {
|
||||||
|
ConnectionManager().callService(domain: "homeassistant", service: "stop");
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRegistration() {
|
||||||
|
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetRegistration() {
|
||||||
|
eventBus.fire(ShowPopupDialogEvent(
|
||||||
|
title: "Waaaait",
|
||||||
|
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
||||||
|
positiveText: "Done it already",
|
||||||
|
negativeText: "Ok, I will",
|
||||||
|
onPositive: () {
|
||||||
|
MobileAppIntegrationManager.checkAppRegistration(showOkDialog: true, forceRegister: true);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
_switchLocationTrackingState(bool state) async {
|
||||||
|
if (state) {
|
||||||
|
await LocationManager().updateDeviceLocation();
|
||||||
|
}
|
||||||
|
await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||||
|
setState(() {
|
||||||
|
_wait = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Location tracking", style: Theme.of(context).textTheme.title),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => Launcher.launchURLInCustomTab(context: context, url: "http://ha-client.app/docs#location-tracking"),
|
||||||
|
child: Text(
|
||||||
|
"Please read documentation!",
|
||||||
|
style: Theme.of(context).textTheme.subhead.copyWith(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Enable device location tracking"),
|
||||||
|
Switch(
|
||||||
|
value: _locationTrackingEnabled,
|
||||||
|
onChanged: _wait ? null : (value) {
|
||||||
|
setState(() {
|
||||||
|
_locationTrackingEnabled = value;
|
||||||
|
_wait = true;
|
||||||
|
});
|
||||||
|
_switchLocationTrackingState(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text("Location update interval in minutes:"),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
//Expanded(child: Container(),),
|
||||||
|
FlatButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
child: Text("-", style: Theme.of(context).textTheme.title),
|
||||||
|
onPressed: () => decLocationInterval(),
|
||||||
|
),
|
||||||
|
Text("$_locationInterval", style: Theme.of(context).textTheme.title),
|
||||||
|
FlatButton(
|
||||||
|
padding: EdgeInsets.all(0.0),
|
||||||
|
child: Text("+", style: Theme.of(context).textTheme.title),
|
||||||
|
onPressed: () => incLocationInterval(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Divider(),
|
||||||
|
Text("Integration status", style: Theme.of(context).textTheme.title),
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
Text(
|
||||||
|
"${HomeAssistant().userName}'s ${DeviceInfoManager().model}, ${DeviceInfoManager().osName} ${DeviceInfoManager().osVersion}",
|
||||||
|
style: Theme.of(context).textTheme.subtitle,
|
||||||
|
),
|
||||||
|
Container(height: 6.0,),
|
||||||
|
Text("Here you can manually check if HA Client integration with your Home Assistant works fine. As mobileApp integration in Home Assistant is still in development, this is not 100% correct check."),
|
||||||
|
//Divider(),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
RaisedButton(
|
||||||
|
color: Colors.blue,
|
||||||
|
onPressed: () => updateRegistration(),
|
||||||
|
child: Text("Check integration", style: Theme.of(context).textTheme.button)
|
||||||
|
),
|
||||||
|
Container(width: 10.0,),
|
||||||
|
RaisedButton(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
onPressed: () => resetRegistration(),
|
||||||
|
child: Text("Reset integration", style: Theme.of(context).textTheme.button)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
LocationManager().setSettings(_locationTrackingEnabled, _locationInterval);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
79
lib/pages/settings/lookandfeel_settings.part.dart
Normal file
79
lib/pages/settings/lookandfeel_settings.part.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
part of '../../main.dart';
|
||||||
|
|
||||||
|
class LookAndFeelSettingsPage extends StatefulWidget {
|
||||||
|
LookAndFeelSettingsPage({Key key, this.title}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LookAndFeelSettingsPageState createState() => new _LookAndFeelSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> {
|
||||||
|
|
||||||
|
AppTheme _currentTheme;
|
||||||
|
bool _changed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSettings() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.reload();
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
setState(() {
|
||||||
|
_currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveSettings(AppTheme theme) {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setInt('app-theme', theme.index);
|
||||||
|
setState(() {
|
||||||
|
_currentTheme = theme;
|
||||||
|
eventBus.fire(ChangeThemeEvent(_currentTheme));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Map appThemeName = {
|
||||||
|
AppTheme.defaultTheme: 'Default',
|
||||||
|
AppTheme.haTheme: 'Home Assistant theme',
|
||||||
|
AppTheme.darkTheme: 'Dark theme'
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
children: <Widget>[
|
||||||
|
Text("Application theme:", style: Theme.of(context).textTheme.body2),
|
||||||
|
Container(height: Sizes.rowPadding),
|
||||||
|
DropdownButton<AppTheme>(
|
||||||
|
value: _currentTheme,
|
||||||
|
iconSize: 30.0,
|
||||||
|
isExpanded: true,
|
||||||
|
style: Theme.of(context).textTheme.title,
|
||||||
|
//hint: Text("Select ${caption.toLowerCase()}"),
|
||||||
|
items: AppTheme.values.map((value) {
|
||||||
|
return new DropdownMenuItem<AppTheme>(
|
||||||
|
value: value,
|
||||||
|
child: Text('${appThemeName[value]}'),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (theme) => _saveSettings(theme),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -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.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,8 +10,7 @@ class LastUpdatedWidget extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'${entityModel.entityWrapper.entity.lastUpdated}',
|
'${entityModel.entityWrapper.entity.lastUpdated}',
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.caption
|
||||||
fontSize: Sizes.smallFontSize, color: Colors.black26),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ class PageLoadingError extends StatelessWidget {
|
|||||||
size: 48.0
|
size: 48.0
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
Text(this.errorText, style: TextStyle(color: Colors.black45))
|
Text(this.errorText, style: Theme.of(context).textTheme.subtitle)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -14,7 +14,7 @@ class PageLoadingIndicator extends StatelessWidget {
|
|||||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||||
child: CircularProgressIndicator()
|
child: CircularProgressIndicator()
|
||||||
),
|
),
|
||||||
Text("Loading...", style: TextStyle(color: Colors.black45))
|
Text("Loading...", style: Theme.of(context).textTheme.subtitle)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -40,10 +40,7 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"${product.title}",
|
"${product.title}",
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.body2,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16.0
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text(
|
Text(
|
||||||
@ -53,7 +50,9 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
),
|
||||||
Container(height: Sizes.rowPadding,),
|
Container(height: Sizes.rowPadding,),
|
||||||
Text("${product.price} $period", style: TextStyle(color: priceColor)),
|
Text("${product.price} $period", style: Theme.of(context).textTheme.body1.copyWith(
|
||||||
|
color: priceColor
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -61,7 +60,7 @@ class ProductPurchase extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: RaisedButton(
|
child: RaisedButton(
|
||||||
child: Text(this.purchased ? buttonTextInactive : buttonText, style: TextStyle(color: Colors.white)),
|
child: Text(this.purchased ? buttonTextInactive : buttonText, style: Theme.of(context).textTheme.button),
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
||||||
),
|
),
|
||||||
|
@ -56,7 +56,7 @@ class Panel {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text("${this.title}"),
|
Text("${this.title}"),
|
||||||
Container(width: 4.0,),
|
Container(width: 4.0,),
|
||||||
isWebView ? Text("webview", style: TextStyle(fontSize: 8.0, color: Colors.black45),) : Container(width: 1.0,)
|
isWebView ? Text("webview", style: Theme.of(context).textTheme.overline) : Container(width: 1.0,)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -16,7 +16,7 @@ class LinkToWebConfig extends StatelessWidget {
|
|||||||
title: Text("${this.name}",
|
title: Text("${this.name}",
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
style: Theme.of(context).textTheme.headline),
|
||||||
subtitle: Text("Tap to open web version"),
|
subtitle: Text("Tap to open web version"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
|
Launcher.launchAuthenticatedWebView(context: context, url: this.url, title: this.name);
|
||||||
|
@ -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);
|
|
||||||
}
|
|
@ -156,7 +156,8 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
result.add(
|
result.add(
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: "value",
|
id: "value",
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor("_", historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor("_", historyMoment.colorId, context),
|
||||||
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) {
|
||||||
if (historyMoment.hiddenDot) {
|
if (historyMoment.hiddenDot) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@ -179,7 +180,8 @@ class _CombinedHistoryChartWidgetState extends State<CombinedHistoryChartWidget>
|
|||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'state',
|
id: 'state',
|
||||||
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 4.0,
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainLowerBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
domainUpperBoundFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
||||||
|
@ -28,7 +28,7 @@ class HistoryControlWidget extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(right: 10.0),
|
padding: EdgeInsets.only(right: 10.0),
|
||||||
child: _buildStates(),
|
child: _buildStates(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildTime(),
|
_buildTime(),
|
||||||
@ -46,18 +46,16 @@ class HistoryControlWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStates() {
|
Widget _buildStates(BuildContext context) {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
for (int i = 0; i < selectedStates.length; i++) {
|
for (int i = 0; i < selectedStates.length; i++) {
|
||||||
children.add(
|
children.add(
|
||||||
Text(
|
Text(
|
||||||
"${selectedStates[i] ?? '-'}",
|
"${selectedStates[i] ?? '-'}",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.title.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
color: HAClientTheme().historyStateColor(selectedStates[i], colorIndexes[i], context)
|
||||||
color: EntityColor.historyStateColor(selectedStates[i], colorIndexes[i]),
|
)
|
||||||
fontSize: 22.0
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,8 @@ class _NumericStateHistoryChartWidgetState extends State<NumericStateHistoryChar
|
|||||||
return [
|
return [
|
||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'State',
|
id: 'State',
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(EntityState.on, -1),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(EntityState.on, -1, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
|
measureFn: (EntityHistoryMoment historyMoment, _) => historyMoment.value ?? historyMoment.previousValue,
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -107,7 +107,8 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'State',
|
id: 'State',
|
||||||
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
|
strokeWidthPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 6.0 : 3.0,
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
data: data,
|
data: data,
|
||||||
@ -115,7 +116,8 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'State',
|
id: 'State',
|
||||||
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.startTime,
|
||||||
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
data: data,
|
data: data,
|
||||||
@ -123,7 +125,8 @@ class _SimpleStateHistoryChartWidgetState extends State<SimpleStateHistoryChartW
|
|||||||
new charts.Series<EntityHistoryMoment, DateTime>(
|
new charts.Series<EntityHistoryMoment, DateTime>(
|
||||||
id: 'State',
|
id: 'State',
|
||||||
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
radiusPxFn: (EntityHistoryMoment historyMoment, __) => (historyMoment.id == _selectedId) ? 5.0 : 3.0,
|
||||||
colorFn: (EntityHistoryMoment historyMoment, __) => EntityColor.chartHistoryStateColor(historyMoment.state, historyMoment.colorId),
|
colorFn: (EntityHistoryMoment historyMoment, __) =>
|
||||||
|
HAClientTheme().chartHistoryStateColor(historyMoment.state, historyMoment.colorId, context),
|
||||||
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
domainFn: (EntityHistoryMoment historyMoment, _) => historyMoment.endTime ?? DateTime.now(),
|
||||||
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
measureFn: (EntityHistoryMoment historyMoment, _) => 10,
|
||||||
data: data,
|
data: data,
|
||||||
|
@ -25,9 +25,22 @@ class RefreshDataFinishedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ReloadUIEvent {
|
class ReloadUIEvent {
|
||||||
|
//TODO uiOnly bool
|
||||||
|
|
||||||
ReloadUIEvent();
|
ReloadUIEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FullReloadEvent {
|
||||||
|
FullReloadEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangeThemeEvent {
|
||||||
|
|
||||||
|
final AppTheme theme;
|
||||||
|
|
||||||
|
ChangeThemeEvent(this.theme);
|
||||||
|
}
|
||||||
|
|
||||||
class StartAuthEvent {
|
class StartAuthEvent {
|
||||||
String oauthUrl;
|
String oauthUrl;
|
||||||
bool showButton;
|
bool showButton;
|
||||||
|
@ -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() {
|
||||||
|
@ -7,9 +7,9 @@ class MaterialDesignIcons {
|
|||||||
"binary_sensor": "mdi:checkbox-blank-circle-outline",
|
"binary_sensor": "mdi:checkbox-blank-circle-outline",
|
||||||
"group": "mdi:google-circles-communities",
|
"group": "mdi:google-circles-communities",
|
||||||
"sensor": "mdi:eye",
|
"sensor": "mdi:eye",
|
||||||
"automation": "mdi:playlist-play",
|
"automation": "mdi:robot",
|
||||||
"script": "mdi:file-document",
|
"script": "mdi:script-text",
|
||||||
"input_boolean": "mdi:drawing",
|
"input_boolean": "mdi:toggle-switch-outline",
|
||||||
"input_datetime": "mdi:clock",
|
"input_datetime": "mdi:clock",
|
||||||
"input_number": "mdi:ray-vertex",
|
"input_number": "mdi:ray-vertex",
|
||||||
"input_select": "mdi:format-list-bulleted",
|
"input_select": "mdi:format-list-bulleted",
|
||||||
|
@ -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']) ?? true,
|
|
||||||
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.0+885
|
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
|
||||||
@ -32,7 +31,8 @@ dependencies:
|
|||||||
workmanager: ^0.2.2
|
workmanager: ^0.2.2
|
||||||
battery: ^0.3.1+7
|
battery: ^0.3.1+7
|
||||||
firebase_crashlytics: ^0.1.3+3
|
firebase_crashlytics: ^0.1.3+3
|
||||||
video_player: ^0.10.7
|
syncfusion_flutter_core: ^18.1.43
|
||||||
|
syncfusion_flutter_gauges: ^18.1.43
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
@ -53,6 +53,7 @@ flutter:
|
|||||||
- images/hassio-192x192.png
|
- images/hassio-192x192.png
|
||||||
- assets/js/externalAuth.js
|
- assets/js/externalAuth.js
|
||||||
- assets/html/cameraView.html
|
- assets/html/cameraView.html
|
||||||
|
- assets/html/cameraLiveView.html
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: "Material Design Icons"
|
- family: "Material Design Icons"
|
||||||
|
11
tool/secrets.dart
Normal file
11
tool/secrets.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
final config = {
|
||||||
|
'syncfusion_license_key': Platform.environment['SYNCFUSION_LICENSE_KEY'],
|
||||||
|
};
|
||||||
|
|
||||||
|
final filename = 'lib/.secrets.dart';
|
||||||
|
File(filename).writeAsString('final secrets = ${json.encode(config)};');
|
||||||
|
}
|
Reference in New Issue
Block a user