Merge branch 'master' into release/0.6.7

This commit is contained in:
Yegor Vialov
2019-09-18 21:35:46 +03:00
committed by GitHub
56 changed files with 1056 additions and 379 deletions

41
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,41 @@
---
name: Bug report
about: Create a report to help improve HA Client
title: ''
labels: ''
assignees: ''
---
<!--
Please provide as much information as possible.
-->
**HA Client version:** <!-- Main app menu => About HA Client -->
**Home Assistant version:** <!-- 0.94.1 for example -->
**Device name:** <!-- Pixel 2 for example -->
**Android version:** <!-- 8.1 for example -->
**Connection type:** <!-- For example "Local IP" or "Remote UI" or "Own domain"-->
**Login type:** <!-- For example "HA Login" or "Manual token"-->
**Description**
<!--
Describe your issue here
-->
**Screenshots**
<!--
Please provide screenshots if it is a UI issue. Also you can attach screenshot from Home Assistant web UI as an expected result
-->
**Logs**
<!--
Right after issue reproduced go to app menu and tap "Log". Copy log with a "Copy" button in the upper-right corner and post it below
-->
```
[Replace this text with your logs]
```

View File

@ -0,0 +1,12 @@
---
name: Entity support request
about: Suggest to add support of any entity type
title: ''
labels: ENTITY, feature/improvement
assignees: ''
---
**Entity type:**
**Link to documentation:**

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for HA Client if it is not a card or entity support
title: ''
labels: feature/improvement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,12 @@
---
name: Lovelace Card support request
about: Suggest to add any Lovelace card support
title: ''
labels: CARD, feature/improvement
assignees: ''
---
**Card name:**
**Link to card repository or web page:**

11
.github/no-response.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 14
# Label requiring a response
responseRequiredLabel: more info needed
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. If the issue still relevant
feel free to reopen this issue and add more information, or report a new one.

View File

@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keyboardcrumbs.hassclient"> package="com.keyboardcrumbs.hassclient">
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
flutter needs it to communicate with the running application flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.

View File

@ -0,0 +1,10 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=/home/estevez/sdk/flutter"
export "FLUTTER_APPLICATION_PATH=/home/estevez/src/ha_client"
export "FLUTTER_TARGET=lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "SYMROOT=${SOURCE_ROOT}/../build/ios"
export "FLUTTER_FRAMEWORK_DIR=/home/estevez/sdk/flutter/bin/cache/artifacts/engine/ios"
export "FLUTTER_BUILD_NAME=0.6.6"
export "FLUTTER_BUILD_NUMBER=660"

View File

@ -192,7 +192,7 @@ class CardWidget extends StatelessWidget {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(card.linkedEntityWrapper.entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: card.linkedEntityWrapper.entity))
) )
) )
] ]

View File

@ -113,4 +113,8 @@ class Sizes {
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;
static const minViewColumnWidth = 350;
static const entityPageMaxWidth = 400.0;
static const mainPageScreenSeparatorWidth = 5.0;
static const tabletMinWidth = minViewColumnWidth + entityPageMaxWidth + 5;
} }

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../main.dart';
class BadgeWidget extends StatelessWidget { class BadgeWidget extends StatelessWidget {
@override @override
@ -140,6 +140,6 @@ class BadgeWidget extends StatelessWidget {
], ],
), ),
onTap: () => onTap: () =>
eventBus.fire(new ShowEntityPageEvent(entityModel.entityWrapper.entity))); eventBus.fire(new ShowEntityPageEvent(entity: entityModel.entityWrapper.entity)));
} }
} }

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../../../main.dart';
class CameraStreamView extends StatefulWidget { class CameraStreamView extends StatefulWidget {

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../../../main.dart';
class ModeSelectorWidget extends StatelessWidget { class ModeSelectorWidget extends StatelessWidget {

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../../../main.dart';
class ModeSwitchWidget extends StatelessWidget { class ModeSwitchWidget extends StatelessWidget {

View File

@ -211,31 +211,6 @@ class Entity {
); );
} }
Widget buildEntityPageWidget(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: this),
child: EntityPageContainer(children: <Widget>[
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: _buildStatePartForPage(context)),
),
LastUpdatedWidget(),
Divider(),
_buildAdditionalControlsForPage(context),
Divider(),
buildHistoryWidget(),
EntityAttributesList()
]),
handleTap: false,
);
}
Widget buildHistoryWidget() {
return EntityHistoryWidget(
config: historyConfig,
);
}
Widget buildBadgeWidget(BuildContext context) { Widget buildBadgeWidget(BuildContext context) {
return EntityModel( return EntityModel(
entityWrapper: EntityWrapper(entity: this), entityWrapper: EntityWrapper(entity: this),

View File

@ -0,0 +1,70 @@
part of '../main.dart';
class EntityPageLayout extends StatelessWidget {
final bool showClose;
final Entity entity;
EntityPageLayout({Key key, this.showClose: false, this.entity}) : super(key: key);
@override
Widget build(BuildContext context) {
return EntityModel(
entityWrapper: EntityWrapper(entity: entity),
child: ListView(
padding: EdgeInsets.all(0),
children: <Widget>[
showClose ?
Container(
color: Colors.blue[300],
height: 36,
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8),
child: Text(
entity.displayName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 22
),
),
),
),
IconButton(
padding: EdgeInsets.all(0),
icon: Icon(Icons.close),
color: Colors.white,
iconSize: 30.0,
onPressed: () {
eventBus.fire(ShowEntityPageEvent());
},
)
],
),
) :
Container(height: 0, width: 0,),
Padding(
padding: EdgeInsets.only(top: Sizes.rowPadding, left: Sizes.leftWidgetPadding),
child: DefaultEntityContainer(state: entity._buildStatePartForPage(context)),
),
LastUpdatedWidget(),
Divider(),
entity._buildAdditionalControlsForPage(context),
Divider(),
SpoilerCard(
title: "State history",
body: EntityHistoryWidget(),
),
SpoilerCard(
title: "Attributes",
body: EntityAttributesList(),
),
]
),
handleTap: false,
);
}
}

View File

@ -53,7 +53,7 @@ class EntityWrapper {
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entity)); new ShowEntityPageEvent(entity: entity));
break; break;
} }
@ -93,7 +93,7 @@ class EntityWrapper {
case EntityUIAction.moreInfo: { case EntityUIAction.moreInfo: {
eventBus.fire( eventBus.fire(
new ShowEntityPageEvent(entity)); new ShowEntityPageEvent(entity: entity));
break; break;
} }

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../main.dart';
class FlatServiceButton extends StatelessWidget { class FlatServiceButton extends StatelessWidget {

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../../../main.dart';
class LightColorPicker extends StatefulWidget { class LightColorPicker extends StatefulWidget {
@ -27,7 +27,6 @@ class LightColorPickerState extends State<LightColorPicker> {
List<Widget> colorRows = []; List<Widget> colorRows = [];
Border border; Border border;
bool isSomethingSelected = false; bool isSomethingSelected = false;
Logger.d("Current colotfor picker: [${widget.color.hue}, ${widget.color.saturation}]");
for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) { for (double saturation = 1.0; saturation >= (0.0 + widget.saturationStep); saturation = double.parse((saturation - widget.saturationStep).toStringAsFixed(2))) {
List<Widget> rowChildren = []; List<Widget> rowChildren = [];
//Logger.d("$saturation"); //Logger.d("$saturation");

View File

@ -74,10 +74,37 @@ class MediaPlayerEntity extends Entity {
List<String> get soundModeList => getStringListAttributeValue("sound_mode_list"); List<String> get soundModeList => getStringListAttributeValue("sound_mode_list");
List<String> get sourceList => getStringListAttributeValue("source_list"); List<String> get sourceList => getStringListAttributeValue("source_list");
DateTime get positionLastUpdated => DateTime.tryParse("${attributes["media_position_updated_at"]}")?.toLocal();
int get durationSeconds => _getIntAttributeValue("media_duration");
int get positionSeconds => _getIntAttributeValue("media_position");
@override @override
Widget _buildAdditionalControlsForPage(BuildContext context) { Widget _buildAdditionalControlsForPage(BuildContext context) {
return MediaPlayerControls(); return MediaPlayerControls();
} }
bool canCalculateActualPosition() {
return positionLastUpdated != null && durationSeconds != null && positionSeconds != null;
}
double getActualPosition() {
double result = 0;
if (canCalculateActualPosition()) {
Duration durationD;
Duration positionD;
durationD = Duration(seconds: durationSeconds);
positionD = Duration(
seconds: positionSeconds);
result = positionD.inSeconds.toDouble();
int differenceInSeconds = DateTime
.now()
.difference(positionLastUpdated)
.inSeconds;
result = ((result + differenceInSeconds) <= durationD.inSeconds) ? (result + differenceInSeconds) : durationD.inSeconds.toDouble();
}
return result;
}
} }

View File

@ -0,0 +1,46 @@
part of '../../../main.dart';
class MediaPlayerProgressBar extends StatefulWidget {
@override
_MediaPlayerProgressBarState createState() => _MediaPlayerProgressBarState();
}
class _MediaPlayerProgressBarState extends State<MediaPlayerProgressBar> {
Timer _timer;
@override
initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
setState(() {
});
});
}
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress;
int currentPosition;
if (entity.canCalculateActualPosition()) {
currentPosition = entity.getActualPosition().toInt();
progress = (currentPosition <= entity.durationSeconds) ? currentPosition / entity.durationSeconds : 100;
} else {
progress = 0;
}
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}

View File

@ -0,0 +1,135 @@
part of '../../../main.dart';
class MediaPlayerSeekBar extends StatefulWidget {
@override
_MediaPlayerSeekBarState createState() => _MediaPlayerSeekBarState();
}
class _MediaPlayerSeekBarState extends State<MediaPlayerSeekBar> {
Timer _timer;
bool _seekStarted = false;
bool _changedHere = false;
double _currentPosition = 0;
int _savedPosition = 0;
final TextStyle _seekTextStyle = TextStyle(
fontSize: 20,
color: Colors.blue,
fontWeight: FontWeight.bold
);
@override
initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
if (!_seekStarted && !_changedHere) {
setState(() {});
}
});
}
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
if (entity.canCalculateActualPosition()) {
if (HomeAssistant().sendToPlayerId == entity.entityId && HomeAssistant().savedPlayerPosition != null) {
_savedPosition = HomeAssistant().savedPlayerPosition;
HomeAssistant().savedPlayerPosition = null;
HomeAssistant().sendToPlayerId = null;
}
if (entity.state == EntityState.playing && !_seekStarted &&
!_changedHere) {
_currentPosition = entity.getActualPosition();
} else if (_changedHere) {
_changedHere = false;
}
List<Widget> buttons = [];
if (_savedPosition > 0) {
buttons.add(
RaisedButton(
child: Text("Jump to ${Duration(seconds: _savedPosition).toString().split('.')[0]}"),
color: Colors.orange,
focusColor: Colors.white,
onPressed: () {
eventBus.fire(ServiceCallEvent(
"media_player",
"media_seek",
"${entity.entityId}",
{"seek_position": _savedPosition}
));
setState(() {
_savedPosition = 0;
});
},
)
);
}
return Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, 20, Sizes.rightWidgetPadding, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text("00:00"),
Expanded(
child: Text("${Duration(seconds: _currentPosition.toInt()).toString().split(".")[0]}",textAlign: TextAlign.center, style: _seekTextStyle),
),
Text("${Duration(seconds: entity.durationSeconds).toString().split(".")[0]}")
],
),
Container(height: 10,),
Slider(
min: 0,
activeColor: Colors.amber,
inactiveColor: Colors.black26,
max: entity.durationSeconds.toDouble(),
value: _currentPosition,
onChangeStart: (val) {
_seekStarted = true;
},
onChanged: (val) {
setState(() {
_currentPosition = val;
});
},
onChangeEnd: (val) {
_seekStarted = false;
Timer(Duration(milliseconds: 500), () {
if (!_seekStarted) {
eventBus.fire(ServiceCallEvent(
"media_player",
"media_seek",
"${entity.entityId}",
{"seek_position": val}
));
setState(() {
_changedHere = true;
_currentPosition = val;
});
}
});
},
),
ButtonBar(
children: buttons,
)
],
),
);
} else {
return Container(width: 0, height: 0,);
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}

View File

@ -26,7 +26,7 @@ class MediaPlayerWidget extends StatelessWidget {
bottom: 0.0, bottom: 0.0,
left: 0.0, left: 0.0,
right: 0.0, right: 0.0,
child: MediaPlayerProgressWidget() child: MediaPlayerProgressBar()
) )
], ],
), ),
@ -229,7 +229,7 @@ class MediaPlayerPlaybackControls extends StatelessWidget {
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical")), "mdi:dots-vertical")),
onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity)) onPressed: () => eventBus.fire(new ShowEntityPageEvent(entity: entity))
) )
); );
} else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) { } else if (entity.supportStop && entity.state != EntityState.off && entity.state != EntityState.unavailable) {
@ -305,6 +305,11 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
) )
]; ];
if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) { if (entity.state != EntityState.off && entity.state != EntityState.unknown && entity.state != EntityState.unavailable) {
if (entity.supportSeek) {
children.add(MediaPlayerSeekBar());
} else {
children.add(MediaPlayerProgressBar());
}
Widget muteWidget; Widget muteWidget;
Widget volumeStepWidget; Widget volumeStepWidget;
if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) { if (entity.supportVolumeMute || entity.attributes["is_volume_muted"] != null) {
@ -398,6 +403,24 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
) )
); );
} }
children.add(
ButtonBar(
children: <Widget>[
RaisedButton(
child: Text("Duplicate to"),
color: Colors.blue,
textColor: Colors.white,
onPressed: () => _duplicateTo(entity),
),
RaisedButton(
child: Text("Switch to"),
color: Colors.blue,
textColor: Colors.white,
onPressed: () => _switchTo(entity),
)
],
)
);
} }
return Column( return Column(
@ -405,62 +428,21 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> {
); );
} }
} void _duplicateTo(entity) {
HomeAssistant().savedPlayerPosition = entity.getActualPosition().toInt();
class MediaPlayerProgressWidget extends StatefulWidget { if (MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
@override Navigator.of(context).popAndPushNamed("/play-media", arguments: {"url": entity.attributes["media_content_id"], "type": entity.attributes["media_content_type"]});
_MediaPlayerProgressWidgetState createState() => _MediaPlayerProgressWidgetState();
}
class _MediaPlayerProgressWidgetState extends State<MediaPlayerProgressWidget> {
Timer _timer;
@override
Widget build(BuildContext context) {
final EntityModel entityModel = EntityModel.of(context);
final MediaPlayerEntity entity = entityModel.entityWrapper.entity;
double progress;
try {
DateTime lastUpdated = DateTime.parse(
entity.attributes["media_position_updated_at"]).toLocal();
Duration duration = Duration(seconds: entity._getIntAttributeValue("media_duration") ?? 1);
Duration position = Duration(seconds: entity._getIntAttributeValue("media_position") ?? 0);
int currentPosition = position.inSeconds;
if (entity.state == EntityState.playing) {
_timer?.cancel();
_timer = Timer(Duration(seconds: 1), () {
setState(() {
});
});
int differenceInSeconds = DateTime
.now()
.difference(lastUpdated)
.inSeconds;
currentPosition = currentPosition + differenceInSeconds;
} else { } else {
_timer?.cancel(); Navigator.of(context).pushNamed("/play-media", arguments: {
"url": entity.attributes["media_content_id"],
"type": entity.attributes["media_content_type"]
});
} }
progress = currentPosition / duration.inSeconds;
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
valueColor: AlwaysStoppedAnimation<Color>(EntityColor.stateColor(EntityState.on)),
);
} catch (e) {
_timer?.cancel();
progress = 0.0;
}
return LinearProgressIndicator(
value: progress,
backgroundColor: Colors.black45,
);
} }
@override void _switchTo(entity) {
void dispose() { HomeAssistant().sendFromPlayerId = entity.enityId;
_timer?.cancel(); _duplicateTo(entity);
super.dispose();
} }
} }

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../main.dart';
class SimpleEntityState extends StatelessWidget { class SimpleEntityState extends StatelessWidget {

View File

@ -1,4 +1,4 @@
part of '../../main.dart'; part of '../main.dart';
class UniversalSlider extends StatelessWidget { class UniversalSlider extends StatelessWidget {

View File

@ -149,15 +149,11 @@ class EntityCollection {
return _allEntities[entityId] != null; return _allEntities[entityId] != null;
} }
List<Entity> getByDomains(List<String> domains) { List<Entity> getByDomains({List<String> domains, List<String> stateFiler}) {
List<Entity> result = []; return _allEntities.values.where((entity) {
_allEntities.forEach((id, entity) { return domains.contains(entity.domain) &&
if (domains.contains(entity.domain)) { ((stateFiler != null && stateFiler.contains(entity.state)) || stateFiler == null);
Logger.d("getByDomain: ${entity.isHidden}"); }).toList();
result.add(entity);
}
});
return result;
} }
List<Entity> filterEntitiesForDefaultView() { List<Entity> filterEntitiesForDefaultView() {

View File

@ -1,14 +0,0 @@
part of '../main.dart';
class EntityPageContainer extends StatelessWidget {
EntityPageContainer({Key key, @required this.children}) : super(key: key);
final List<Widget> children;
@override
Widget build(BuildContext context) {
return ListView(
children: children,
);
}
}

View File

@ -14,6 +14,9 @@ class HomeAssistant {
Map services; Map services;
String _userName; String _userName;
HSVColor savedColor; HSVColor savedColor;
int savedPlayerPosition;
String sendToPlayerId;
String sendFromPlayerId;
String fcmToken; String fcmToken;

View File

@ -25,6 +25,8 @@ import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/circular_slider/single_circular_slider.dart'; import 'plugins/circular_slider/single_circular_slider.dart';
import 'package:share/receive_share_state.dart'; import 'package:share/receive_share_state.dart';
import 'package:share/share.dart'; import 'package:share/share.dart';
import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
import 'utils/logger.dart'; import 'utils/logger.dart';
@ -51,35 +53,34 @@ part 'entities/fan/fan_entity.class.dart';
part 'entities/automation/automation_entity.class.dart'; part 'entities/automation/automation_entity.class.dart';
part 'entities/camera/camera_entity.class.dart'; part 'entities/camera/camera_entity.class.dart';
part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart'; part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart';
part 'entity_widgets/common/badge.dart'; part 'entities/badge.widget.dart';
part 'entity_widgets/model_widgets.dart'; part 'entities/entity_model.widget.dart';
part 'entity_widgets/default_entity_container.dart'; part 'entities/default_entity_container.widget.dart';
part 'entity_widgets/missed_entity.dart'; part 'entities/missed_entity.widget.dart';
part 'cards/widgets/glance_card_entity_container.dart'; part 'cards/widgets/glance_card_entity_container.dart';
part 'cards/widgets/entity_button_card_body.widget.dart'; part 'cards/widgets/entity_button_card_body.widget.dart';
part 'entity_widgets/common/entity_attributes_list.dart'; part 'pages/widgets/entity_attributes_list.dart';
part 'entity_widgets/entity_icon.dart'; part 'entities/entity_icon.widget.dart';
part 'entity_widgets/entity_name.dart'; part 'entities/entity_name.widget.dart';
part 'entity_widgets/common/last_updated.dart'; part 'pages/widgets/last_updated.dart';
part 'entity_widgets/common/mode_swicth.dart'; part 'entities/climate/widgets/mode_swicth.dart';
part 'entity_widgets/common/mode_selector.dart'; part 'entities/climate/widgets/mode_selector.dart';
part 'entity_widgets/common/universal_slider.dart'; part 'entities/universal_slider.widget.dart';
part 'entity_widgets/common/flat_service_button.dart'; part 'entities/flat_service_button.widget.dart';
part 'entity_widgets/common/light_color_picker.dart'; part 'entities/light/widgets/light_color_picker.dart';
part 'entity_widgets/common/camera_stream_view.dart'; part 'entities/camera/widgets/camera_stream_view.dart';
part 'entity_widgets/entity_colors.class.dart'; part 'entities/entity_colors.class.dart';
part 'entity_widgets/entity_page_container.dart'; part 'plugins/history_chart/entity_history.dart';
part 'entity_widgets/history_chart/entity_history.dart'; part 'plugins/history_chart/simple_state_history_chart.dart';
part 'entity_widgets/history_chart/simple_state_history_chart.dart'; part 'plugins/history_chart/numeric_state_history_chart.dart';
part 'entity_widgets/history_chart/numeric_state_history_chart.dart'; part 'plugins/history_chart/combined_history_chart.dart';
part 'entity_widgets/history_chart/combined_history_chart.dart'; part 'plugins/history_chart/history_control_widget.dart';
part 'entity_widgets/history_chart/history_control_widget.dart'; part 'plugins/history_chart/entity_history_moment.dart';
part 'entity_widgets/history_chart/entity_history_moment.dart';
part 'entities/switch/widget/switch_state.dart'; part 'entities/switch/widget/switch_state.dart';
part 'entities/slider/widgets/slider_controls.dart'; part 'entities/slider/widgets/slider_controls.dart';
part 'entities/text/widgets/text_input_state.dart'; part 'entities/text/widgets/text_input_state.dart';
part 'entities/select/widgets/select_state.dart'; part 'entities/select/widgets/select_state.dart';
part 'entity_widgets/common/simple_state.dart'; part 'entities/simple_state.widget.dart';
part 'entities/timer/widgets/timer_state.dart'; part 'entities/timer/widgets/timer_state.dart';
part 'entities/climate/widgets/climate_state.widget.dart'; part 'entities/climate/widgets/climate_state.widget.dart';
part 'entities/cover/widgets/cover_state.dart'; part 'entities/cover/widgets/cover_state.dart';
@ -102,7 +103,7 @@ part 'pages/main.page.dart';
part 'home_assistant.class.dart'; part 'home_assistant.class.dart';
part 'pages/log.page.dart'; part 'pages/log.page.dart';
part 'pages/entity.page.dart'; part 'pages/entity.page.dart';
part 'mdi.class.dart'; part 'utils/mdi.class.dart';
part 'entity_collection.class.dart'; part 'entity_collection.class.dart';
part 'managers/auth_manager.class.dart'; part 'managers/auth_manager.class.dart';
part 'managers/location_manager.class.dart'; part 'managers/location_manager.class.dart';
@ -114,7 +115,7 @@ 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 'view.dart'; part 'viewWidget.widget.dart';
part 'cards/card_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';
@ -124,13 +125,18 @@ part 'types/event_bus_events.dart';
part 'cards/widgets/gauge_card_body.dart'; part 'cards/widgets/gauge_card_body.dart';
part 'cards/widgets/light_card_body.dart'; part 'cards/widgets/light_card_body.dart';
part 'pages/play_media.page.dart'; part 'pages/play_media.page.dart';
part 'entities/entity_page_layout.widget.dart';
part 'entities/media_player/widgets/media_player_seek_bar.widget.dart';
part 'entities/media_player/widgets/media_player_progress_bar.widget.dart';
part 'pages/whats_new.page.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = "HA Client"; const String appName = "HA Client";
const appVersion = "0.6.7"; const appVersionNumber = "0.6.8";
const appVersionAdd = "alpha1";
const appVersion = "$appVersionNumber-$appVersionAdd";
void main() async { void main() async {
FlutterError.onError = (errorDetails) { FlutterError.onError = (errorDetails) {
@ -170,8 +176,12 @@ class HAClientApp extends StatelessWidget {
"/": (context) => MainPage(title: 'HA Client'), "/": (context) => MainPage(title: 'HA Client'),
"/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"), "/connection-settings": (context) => ConnectionSettingsPage(title: "Settings"),
"/putchase": (context) => PurchasePage(title: "Support app development"), "/putchase": (context) => PurchasePage(title: "Support app development"),
"/play-media": (context) => PlayMediaPage(mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",), "/play-media": (context) => PlayMediaPage(
mediaUrl: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['url'] : ''}",
mediaType: "${ModalRoute.of(context).settings.arguments != null ? (ModalRoute.of(context).settings.arguments as Map)['type'] ?? '' : ''}",
),
"/log-view": (context) => LogViewPage(title: "Log"), "/log-view": (context) => LogViewPage(title: "Log"),
"/whats-new": (context) => WhatsNewPage(),
"/login": (context) => WebviewScaffold( "/login": (context) => WebviewScaffold(
url: "${ConnectionManager().oauthUrl}", url: "${ConnectionManager().oauthUrl}",
appBar: new AppBar( appBar: new AppBar(

View File

@ -186,16 +186,16 @@ class ConnectionManager {
_handleMessage(data) { _handleMessage(data) {
if (data["type"] == "result") { if (data["type"] == "result") {
if (data["id"] != null && data["success"]) { if (data["id"] != null && data["success"]) {
Logger.d("[Received] <== Request id ${data['id']} was successful"); //Logger.d("[Received] <== Request id ${data['id']} was successful");
_messageResolver["${data["id"]}"]?.complete(data["result"]); _messageResolver["${data["id"]}"]?.complete(data["result"]);
} else if (data["id"] != null) { } else if (data["id"] != null) {
Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}"); //Logger.e("[Received] <== Error received on request id ${data['id']}: ${data['error']}");
_messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}"); _messageResolver["${data["id"]}"]?.completeError("${data['error']["message"]}");
} }
_messageResolver.remove("${data["id"]}"); _messageResolver.remove("${data["id"]}");
} else if (data["type"] == "event") { } else if (data["type"] == "event") {
if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) { if ((data["event"] != null) && (data["event"]["event_type"] == "state_changed")) {
Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}"); //Logger.d("[Received] <== ${data['type']}.${data["event"]["event_type"]}: ${data["event"]["data"]["entity_id"]}");
onStateChangeCallback(data["event"]["data"]); onStateChangeCallback(data["event"]["data"]);
} else if (data["event"] != null) { } else if (data["event"] != null) {
Logger.w("Unhandled event type: ${data["event"]["event_type"]}"); Logger.w("Unhandled event type: ${data["event"]["event_type"]}");
@ -349,6 +349,7 @@ class ConnectionManager {
} }
Future callService({String domain, String service, String entityId, Map additionalServiceData}) { Future callService({String domain, String service, String entityId, Map additionalServiceData}) {
Completer completer = Completer();
Map serviceData = {}; Map serviceData = {};
if (entityId != null) { if (entityId != null) {
serviceData["entity_id"] = entityId; serviceData["entity_id"] = entityId;
@ -357,9 +358,17 @@ class ConnectionManager {
serviceData.addAll(additionalServiceData); serviceData.addAll(additionalServiceData);
} }
if (serviceData.isNotEmpty) if (serviceData.isNotEmpty)
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData}); sendHTTPPost(
endPoint: "/api/services/$domain/$service",
data: json.encode(serviceData)
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service, "service_data": serviceData});
else else
return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service}); sendHTTPPost(
endPoint: "/api/services/$domain/$service"
).then((data) => completer.complete(data)).catchError((e) => completer.completeError(HAError("${e["message"]}")));;
//return sendSocketMessage(type: "call_service", additionalData: {"domain": domain, "service": service});
return completer.future;
} }
Future<List> getHistory(String entityId) async { Future<List> getHistory(String entityId) async {

View File

@ -14,7 +14,7 @@ class StartupUserMessagesManager {
bool _supportAppDevelopmentMessageShown; bool _supportAppDevelopmentMessageShown;
bool _whatsNewMessageShown; bool _whatsNewMessageShown;
static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3"; static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3";
static final _whatsNewMessageKey = "user-message-shown-whats-new-660"; static final _whatsNewMessageKey = "user-message-shown-whats-new-673";
void checkMessagesToShow() async { void checkMessagesToShow() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
@ -49,23 +49,10 @@ class StartupUserMessagesManager {
} }
void _showWhatsNewMessage() { void _showWhatsNewMessage() {
eventBus.fire(ShowPopupDialogEvent(
title: "What's new",
body: "You can now share any media url to HA Client via Android share menu. It will try to play that media on one of your media player. There is also 'tv' button available in app header if you want to send some url manually",
positiveText: "Full release notes",
negativeText: "Ok",
onPositive: () {
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_whatsNewMessageKey, true); prefs.setBool(_whatsNewMessageKey, true);
Launcher.launchURL("https://github.com/estevez-dev/ha_client/releases"); eventBus.fire(ShowPageEvent(path: "/whats-new"));
}); });
},
onNegative: () {
SharedPreferences.getInstance().then((prefs) {
prefs.setBool(_whatsNewMessageKey, true);
});
}
));
} }
} }

View File

@ -10,32 +10,40 @@ class EntityViewPage extends StatefulWidget {
} }
class _EntityViewPageState extends State<EntityViewPage> { class _EntityViewPageState extends State<EntityViewPage> {
String _title;
StreamSubscription _refreshDataSubscription; StreamSubscription _refreshDataSubscription;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
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) {
Logger.d("State change event handled by entity page: ${event.entityId}"); entity = HomeAssistant().entities.get(widget.entityId);
Logger.d("[Entity page] State change event handled: ${event.entityId}");
setState(() {}); setState(() {});
} }
}); });
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) { _refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
setState(() {}); setState(() {});
}); });
_prepareData(); entity = HomeAssistant().entities.get(widget.entityId);
} }
void _prepareData() async {
_title = HomeAssistant().entities.get(widget.entityId).displayName;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget body;
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: (){
@ -43,16 +51,23 @@ class _EntityViewPageState extends State<EntityViewPage> {
}), }),
// Here we take the value from the MyHomePage object that was created by // Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title. // the App.build method, and use it to set our appbar title.
title: new Text(_title), title: new Text("${entity.displayName}"),
), ),
body: HomeAssistant().entities.get(widget.entityId).buildEntityPageWidget(context), body: body,
); );
} }
_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();
} }
} }

View File

@ -26,6 +26,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
bool _showLoginButton = false; bool _showLoginButton = false;
bool _preventAppRefresh = false; bool _preventAppRefresh = false;
String _savedSharedText; String _savedSharedText;
String _entityToShow;
@override @override
void initState() { void initState() {
@ -227,7 +228,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
if (_showEntityPageSubscription == null) { if (_showEntityPageSubscription == null) {
_showEntityPageSubscription = _showEntityPageSubscription =
eventBus.on<ShowEntityPageEvent>().listen((event) { eventBus.on<ShowEntityPageEvent>().listen((event) {
_showEntityPage(event.entity.entityId); _showEntityPage(event.entity?.entityId);
}); });
} }
@ -317,7 +318,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
); );
} }
//TODO remove this shit //TODO remove this shit.... maybe
void _callService(String domain, String service, String entityId, Map additionalParams) { void _callService(String domain, String service, String entityId, Map additionalParams) {
_showInfoBottomBar( _showInfoBottomBar(
message: "Calling $domain.$service", message: "Calling $domain.$service",
@ -327,6 +328,10 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
} }
void _showEntityPage(String entityId) { void _showEntityPage(String entityId) {
setState(() {
_entityToShow = entityId;
});
if (_entityToShow!= null && MediaQuery.of(context).size.width < Sizes.tabletMinWidth) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -334,6 +339,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
) )
); );
} }
}
void _showPage(String path, bool goBackFirst) { void _showPage(String path, bool goBackFirst) {
if (goBackFirst) { if (goBackFirst) {
@ -637,32 +643,121 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget _buildScaffoldBody(bool empty) { Widget _buildScaffoldBody(bool empty) {
List<PopupMenuItem<String>> popupMenuItems = []; List<PopupMenuItem<String>> serviceMenuItems = [];
List<PopupMenuItem<String>> mediaMenuItems = [];
popupMenuItems.add(PopupMenuItem<String>( serviceMenuItems.add(PopupMenuItem<String>(
child: new Text("Reload"), child: new Text("Reload"),
value: "reload", value: "reload",
)); ));
List<Widget> emptyBody = [
Text("."),
];
if (ConnectionManager().isAuthenticated) { if (ConnectionManager().isAuthenticated) {
_showLoginButton = false; _showLoginButton = false;
popupMenuItems.add( serviceMenuItems.add(
PopupMenuItem<String>( PopupMenuItem<String>(
child: new Text("Logout"), child: new Text("Logout"),
value: "logout", value: "logout",
)); ));
} }
Widget mediaMenuIcon;
int playersCount = 0;
if (!empty && !HomeAssistant().entities.isEmpty) {
List<Entity> activePlayers = HomeAssistant().entities.getByDomains(domains: ["media_player"], stateFiler: [EntityState.paused, EntityState.playing, EntityState.idle]);
playersCount = activePlayers.length;
mediaMenuItems.addAll(
activePlayers.map((entity) => PopupMenuItem<String>(
child: Text(
"${entity.displayName}",
style: TextStyle(
color: EntityColor.stateColor(entity.state)
),
),
value: "${entity.entityId}",
)).toList()
);
}
mediaMenuItems.addAll([
PopupMenuItem<String>(
child: new Text("Play media..."),
value: "play_media",
)
]);
if (playersCount > 0) {
mediaMenuIcon = Stack(
overflow: Overflow.visible,
children: <Widget>[
Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,),
Positioned(
bottom: -4,
right: -4,
child: Container(
height: 16,
width: 16,
decoration: new BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: Center(
child: Text("$playersCount", style: TextStyle(fontSize: 12)),
),
),
)
],
);
} else {
mediaMenuIcon = Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:television"), color: Colors.white,);
}
Widget mainScrollBody;
if (empty) {
if (_showLoginButton) { if (_showLoginButton) {
emptyBody = [ mainScrollBody = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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: TextStyle(fontSize: 16.0, color: Colors.white)),
color: Colors.blue, color: Colors.blue,
onPressed: () => _fullLoad(), onPressed: () => _fullLoad(),
) )
]; ]
)
);
} else {
mainScrollBody = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("...")
]
),
);
} }
} else {
if (_entityToShow != null && MediaQuery.of(context).size.width >= Sizes.tabletMinWidth) {
Entity entity = HomeAssistant().entities.get(_entityToShow);
mainScrollBody = Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
child: HomeAssistant().buildViews(context, _viewsTabController),
),
Container(
width: Sizes.mainPageScreenSeparatorWidth,
color: Colors.blue,
),
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: Sizes.entityPageMaxWidth),
child: EntityPageLayout(entity: entity, showClose: true,),
)
],
);
} else {
_entityToShow = null;
mainScrollBody = HomeAssistant().buildViews(context, _viewsTabController);
}
}
return NestedScrollView( return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[ return <Widget>[
@ -673,18 +768,29 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
title: Text(HomeAssistant().locationName ?? ""), title: Text(HomeAssistant().locationName ?? ""),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: mediaMenuIcon,
"mdi:television"), color: Colors.white,), onPressed: () {
onPressed: () => Navigator.pushNamed(context, "/play-media", arguments: {"url": ""}) showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100.0, 50, 0.0),
context: context,
items: mediaMenuItems
).then((String val) {
if (val == "play_media") {
Navigator.pushNamed(context, "/play-media", arguments: {"url": ""});
} else {
_showEntityPage(val);
}
});
}
), ),
IconButton( IconButton(
icon: Icon(MaterialDesignIcons.getIconDataFromIconName( icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
"mdi:dots-vertical"), color: Colors.white,), "mdi:dots-vertical"), color: Colors.white,),
onPressed: () { onPressed: () {
showMenu( showMenu(
position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 70.0, 0.0, 0.0), position: RelativeRect.fromLTRB(MediaQuery.of(context).size.width, 100, 0.0, 0.0),
context: context, context: context,
items: popupMenuItems items: serviceMenuItems
).then((String val) { ).then((String val) {
if (val == "reload") { if (val == "reload") {
_quickLoad(); _quickLoad();
@ -712,15 +818,7 @@ class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObse
]; ];
}, },
body: empty ? body: mainScrollBody
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: emptyBody
),
)
:
HomeAssistant().buildViews(context, _viewsTabController),
); );
} }

View File

@ -3,8 +3,9 @@ part of '../main.dart';
class PlayMediaPage extends StatefulWidget { class PlayMediaPage extends StatefulWidget {
final String mediaUrl; final String mediaUrl;
final String mediaType;
PlayMediaPage({Key key, this.mediaUrl}) : super(key: key); PlayMediaPage({Key key, this.mediaUrl, this.mediaType}) : super(key: key);
@override @override
_PlayMediaPageState createState() => new _PlayMediaPageState(); _PlayMediaPageState createState() => new _PlayMediaPageState();
@ -22,13 +23,22 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
bool _isMediaExtractorExist = false; bool _isMediaExtractorExist = false;
StreamSubscription _stateSubscription; StreamSubscription _stateSubscription;
StreamSubscription _refreshDataSubscription; StreamSubscription _refreshDataSubscription;
final List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"]; List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_mediaUrl = widget.mediaUrl; _mediaUrl = widget.mediaUrl;
if (widget.mediaType.isNotEmpty) {
if (!_contentTypes.contains(widget.mediaType)) {
_contentTypes.insert(0, widget.mediaType);
_contentType = _contentTypes[0]; _contentType = _contentTypes[0];
} else {
_contentType = widget.mediaType;
}
} else {
_contentType = _contentTypes[0];
}
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) { _stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.entityId.contains("media_player")) { if (event.entityId.contains("media_player")) {
Logger.d("State change event handled by play media page: ${event.entityId}"); Logger.d("State change event handled by play media page: ${event.entityId}");
@ -49,7 +59,7 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
} else { } else {
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor"); _isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
//_useMediaExtractor = _isMediaExtractorExist; //_useMediaExtractor = _isMediaExtractorExist;
_players = HomeAssistant().entities.getByDomains(["media_player"]); _players = HomeAssistant().entities.getByDomains(domains: ["media_player"]);
setState(() { setState(() {
if (_players.isNotEmpty) { if (_players.isNotEmpty) {
_loaded = true; _loaded = true;
@ -83,7 +93,12 @@ class _PlayMediaPageState extends State<PlayMediaPage> {
"media_content_type": _contentType "media_content_type": _contentType
} }
); );
eventBus.fire(ShowEntityPageEvent(entity)); HomeAssistant().sendToPlayerId = entity.entityId;
if (HomeAssistant().sendFromPlayerId != null) {
eventBus.fire(ServiceCallEvent(HomeAssistant().sendFromPlayerId.split(".")[0], "turn_off", HomeAssistant().sendFromPlayerId, null));
HomeAssistant().sendFromPlayerId = null;
}
eventBus.fire(ShowEntityPageEvent(entity: entity));
} }
} }

View File

@ -0,0 +1,69 @@
part of '../main.dart';
class WhatsNewPage extends StatefulWidget {
WhatsNewPage({Key key}) : super(key: key);
@override
_WhatsNewPageState createState() => new _WhatsNewPageState();
}
class _WhatsNewPageState extends State<WhatsNewPage> {
String data = "";
String error = "";
@override
void initState() {
super.initState();
_loadData();
}
_loadData() async {
setState(() {
data = "";
error = "";
});
http.Response response;
response = await http.get("http://ha-client.homemade.systems/service/whats_new_$appVersionNumber.md");
if (response.statusCode == 200) {
setState(() {
data = response.body;
});
} else {
setState(() {
error = "Can't load changelog";
});
}
}
@override
Widget build(BuildContext context) {
Widget body;
if (error.isNotEmpty) {
body = PageLoadingError(errorText: error,);
} else if (data.isEmpty) {
body = PageLoadingIndicator();
} else {
body = Markdown(
data: data,
);
}
return new Scaffold(
appBar: new AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
Navigator.pop(context);
}),
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => _loadData(),
)
],
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text("What's new"),
),
body: body
);
}
}

View File

@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;
class DynamicMultiColumnLayout extends MultiChildRenderObjectWidget {
final int minColumnWidth;
DynamicMultiColumnLayout({
Key key,
this.minColumnWidth: 350,
List<Widget> children = const <Widget>[],
}) : super(key: key, children: children);
@override
RenderCustomLayoutBox createRenderObject(BuildContext context) {
return RenderCustomLayoutBox(minColumnWidth: this.minColumnWidth);
}
}
class RenderCustomLayoutBox extends RenderBox
with ContainerRenderObjectMixin<RenderBox, CustomLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, CustomLayoutParentData> {
final int minColumnWidth;
RenderCustomLayoutBox({
this.minColumnWidth,
List<RenderBox> children,
}) {
addAll(children);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! CustomLayoutParentData) {
child.parentData = CustomLayoutParentData();
}
}
double _getIntrinsicHeight(double childSize(RenderBox child)) {
double inflexibleSpace = 0.0;
RenderBox child = firstChild;
while (child != null) {
inflexibleSpace += childSize(child);
final FlexParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
return inflexibleSpace;
}
double _getIntrinsicWidth(double childSize(RenderBox child)) {
double maxSpace = 0.0;
RenderBox child = firstChild;
while (child != null) {
maxSpace = math.max(maxSpace, childSize(child));
final FlexParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
return maxSpace;
}
@override
double computeMinIntrinsicWidth(double height) {
return _getIntrinsicWidth((RenderBox child) => child.getMinIntrinsicWidth(height));
}
@override
double computeMaxIntrinsicWidth(double height) {
return _getIntrinsicWidth((RenderBox child) => child.getMaxIntrinsicWidth(height));
}
@override
double computeMinIntrinsicHeight(double width) {
return _getIntrinsicHeight((RenderBox child) => child.getMinIntrinsicHeight(width));
}
@override
double computeMaxIntrinsicHeight(double width) {
return _getIntrinsicHeight((RenderBox child) => child.getMaxIntrinsicHeight(width));
}
@override
void performLayout() {
int columnsCount;
List<double> columnXPositions = [];
List<double> columnYPositions = [];
columnsCount = (constraints.maxWidth ~/ this.minColumnWidth);
if (childCount == 0 || columnsCount == 0) {
size = constraints.biggest;
assert(size.isFinite);
return;
}
double columnWidth = constraints.maxWidth / columnsCount;
double startY = 0;
for (int i =0; i < columnsCount; i++) {
columnXPositions.add(i*columnWidth);
columnYPositions.add(startY);
}
RenderBox child = firstChild;
while (child != null) {
final CustomLayoutParentData childParentData = child.parentData;
int columnToAdd = 0;
double minYPosition = columnYPositions[0];
for (int i=0; i<columnsCount; i++) {
if (columnYPositions[i] < minYPosition) {
minYPosition = columnYPositions[i];
columnToAdd = i;
}
}
child.layout(BoxConstraints.tightFor(width: columnWidth), parentUsesSize: true);
childParentData.offset = Offset(columnXPositions[columnToAdd], columnYPositions[columnToAdd]);
final Size newSize = child.size;
columnYPositions[columnToAdd] = minYPosition + newSize.height;
child = childParentData.nextSibling;
}
double width = constraints.maxWidth;
double height = 0;
for (int i=0; i<columnsCount; i++) {
if (columnYPositions[i] > height) {
height = columnYPositions[i];
}
}
size = Size(width, height);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(HitTestResult result, { Offset position }) {
return defaultHitTestChildren(result, position: position);
}
}
class CustomLayoutParentData extends ContainerBoxParentData<RenderBox> {
}

View File

@ -17,9 +17,7 @@ class EntityHistoryConfig {
class EntityHistoryWidget extends StatefulWidget { class EntityHistoryWidget extends StatefulWidget {
final EntityHistoryConfig config; const EntityHistoryWidget({Key key}) : super(key: key);
const EntityHistoryWidget({Key key, @required this.config}) : super(key: key);
@override @override
_EntityHistoryWidgetState createState() { _EntityHistoryWidgetState createState() {
@ -33,6 +31,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
bool _needToUpdateHistory; bool _needToUpdateHistory;
DateTime _historyLastUpdated; DateTime _historyLastUpdated;
bool _disposed = false; bool _disposed = false;
Entity entity;
@override @override
void initState() { void initState() {
@ -75,10 +74,10 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
} else { } else {
_loadHistory(entity.entityId); _loadHistory(entity.entityId);
} }
return _buildChart(); return _buildChart(entity.historyConfig);
} }
Widget _buildChart() { Widget _buildChart(EntityHistoryConfig config) {
List<Widget> children = []; List<Widget> children = [];
if (_history == null) { if (_history == null) {
children.add( children.add(
@ -90,7 +89,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
); );
} else { } else {
children.add( children.add(
_selectChartWidget() _selectChartWidget(config)
); );
} }
children.add(Divider()); children.add(Divider());
@ -102,8 +101,8 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
); );
} }
Widget _selectChartWidget() { Widget _selectChartWidget(EntityHistoryConfig config) {
switch (widget.config.chartType) { switch (config.chartType) {
case EntityHistoryWidgetType.simple: { case EntityHistoryWidgetType.simple: {
return SimpleStateHistoryChartWidget( return SimpleStateHistoryChartWidget(
@ -114,14 +113,14 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> {
case EntityHistoryWidgetType.numericState: { case EntityHistoryWidgetType.numericState: {
return NumericStateHistoryChartWidget( return NumericStateHistoryChartWidget(
rawHistory: _history, rawHistory: _history,
config: widget.config, config: config,
); );
} }
case EntityHistoryWidgetType.numericAttributes: { case EntityHistoryWidgetType.numericAttributes: {
return CombinedHistoryChartWidget( return CombinedHistoryChartWidget(
rawHistory: _history, rawHistory: _history,
config: widget.config, config: config,
); );
} }

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
class SpoilerCard extends StatefulWidget {
final String title;
final Widget body;
final bool isExpanded;
SpoilerCard({Key key, @required this.title, @required this.body, this.isExpanded: false}) : super(key: key);
@override
_SpoilerCardState createState() => _SpoilerCardState();
}
class _SpoilerCardState extends State<SpoilerCard> {
bool _expanded;
@override
void initState() {
super.initState();
_expanded = widget.isExpanded;
}
@override
Widget build(BuildContext context) {
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text("${widget.title}"),
trailing: Icon(
_expanded ? Icons.arrow_drop_up : Icons.arrow_drop_down,
size: 20,
),
onTap: () => setState((){_expanded = !_expanded;}),
),
_expanded ? widget.body : Container(height: 0,)
],
),
);
}
}

View File

@ -63,9 +63,9 @@ class ShowPopupMessageEvent {
} }
class ShowEntityPageEvent { class ShowEntityPageEvent {
Entity entity; final Entity entity;
ShowEntityPageEvent(this.entity); ShowEntityPageEvent({this.entity});
} }
class ShowPageEvent { class ShowPageEvent {

View File

@ -1,4 +1,4 @@
part of 'main.dart'; part of '../main.dart';
class MaterialDesignIcons { class MaterialDesignIcons {
static final Map defaultIconsByDomains = { static final Map defaultIconsByDomains = {

View File

@ -1,99 +0,0 @@
part of 'main.dart';
class ViewWidget extends StatefulWidget {
final HAView view;
const ViewWidget({
Key key,
this.view
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return ViewWidgetState();
}
}
class ViewWidgetState extends State<ViewWidget> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.view.panel) {
return FractionallySizedBox(
widthFactor: 1,
heightFactor: 1,
child: _buildPanelChild(context),
);
} else {
return ListView(
padding: EdgeInsets.all(0.0),
//physics: const AlwaysScrollableScrollPhysics(),
children: _buildChildren(context),
);
}
}
Widget _buildPanelChild(BuildContext context) {
if (widget.view.cards != null && widget.view.cards.isNotEmpty) {
return widget.view.cards[0].build(context);
} else {
return Container(width: 0, height: 0);
}
}
List<Widget> _buildChildren(BuildContext context) {
List<Widget> result = [];
if (widget.view.badges.isNotEmpty) {
result.insert(0,
Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: _buildBadges(context),
)
);
}
List<Widget> cards = [];
widget.view.cards.forEach((HACard card){
cards.add(
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: card.build(context),
)
);
});
result.add(
Column (
children: cards,
)
);
return result;
}
List<Widget> _buildBadges(BuildContext context) {
List<Widget> result = [];
widget.view.badges.forEach((Entity entity) {
if (!entity.isHidden) {
result.add(entity.buildBadgeWidget(context));
}
});
return result;
}
@override
void dispose() {
super.dispose();
}
}

View File

@ -0,0 +1,56 @@
part of 'main.dart';
class ViewWidget extends StatelessWidget {
final HAView view;
const ViewWidget({
Key key,
this.view
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (this.view.panel) {
return FractionallySizedBox(
widthFactor: 1,
heightFactor: 1,
child: _buildPanelChild(context),
);
} else {
return ListView(
shrinkWrap: true,
padding: EdgeInsets.all(0),
children: <Widget>[
_buildBadges(context),
DynamicMultiColumnLayout(
minColumnWidth: Sizes.minViewColumnWidth,
children: this.view.cards.map((card) => card.build(context)).toList(),
)
]
);
}
}
Widget _buildPanelChild(BuildContext context) {
if (this.view.cards != null && this.view.cards.isNotEmpty) {
return this.view.cards[0].build(context);
} else {
return Container(width: 0, height: 0);
}
}
Widget _buildBadges(BuildContext context) {
if (this.view.badges.isNotEmpty) {
return Wrap(
alignment: WrapAlignment.center,
spacing: 10.0,
runSpacing: 1.0,
children: this.view.badges.map((badge) =>
badge.buildBadgeWidget(context)).toList(),
);
} else {
return Container(width: 0, height: 0,);
}
}
}

View File

@ -138,7 +138,7 @@ packages:
name: flutter_local_notifications name: flutter_local_notifications
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.8.3" version: "0.8.2+1"
flutter_markdown: flutter_markdown:
dependency: "direct main" dependency: "direct main"
description: description:
@ -152,7 +152,7 @@ packages:
name: flutter_secure_storage name: flutter_secure_storage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.3.1+1" version: "3.3.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 0.6.7+675 version: 0.6.8+680
environment: environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0" sdk: ">=2.0.0-dev.68.0 <3.0.0"
@ -21,11 +21,11 @@ dependencies:
in_app_purchase: ^0.2.1+3 in_app_purchase: ^0.2.1+3
# flutter_svg: ^0.10.3 # flutter_svg: ^0.10.3
flutter_custom_tabs: ^0.6.0 flutter_custom_tabs: ^0.6.0
firebase_messaging: ^5.1.4 firebase_messaging: ^5.1.5
flutter_webview_plugin: ^0.3.7 flutter_webview_plugin: ^0.3.8
flutter_secure_storage: ^3.2.1+1 flutter_secure_storage: ^3.3.1
device_info: ^0.4.0+2 device_info: ^0.4.0+2
flutter_local_notifications: ^0.8.2 flutter_local_notifications: ^0.8.2+1
share: share:
git: git:
url: https://github.com/d-silveira/flutter-share.git url: https://github.com/d-silveira/flutter-share.git