Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
753df3c724 | |||
dc62a08da3 | |||
56a333a852 | |||
c5922368de | |||
8c2316a51a | |||
e2e6c015de | |||
0a6ff4586d | |||
fc228d85ae | |||
61823cb43b | |||
127e0b8182 | |||
38c37fa212 | |||
dfaf2a2924 | |||
c90c40c046 | |||
d2049b726a | |||
6508f109f7 | |||
37e63637a7 | |||
6650c5c145 |
@ -3,8 +3,9 @@ package com.keyboardcrumbs.hassclient;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import io.flutter.app.FlutterActivity;
|
import io.flutter.app.FlutterActivity;
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
import io.flutter.plugins.share.FlutterShareReceiverActivity;
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
public class MainActivity extends FlutterShareReceiverActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -15,6 +15,10 @@ class HACard {
|
|||||||
List states;
|
List states;
|
||||||
List conditions;
|
List conditions;
|
||||||
String content;
|
String content;
|
||||||
|
String unit;
|
||||||
|
int min;
|
||||||
|
int max;
|
||||||
|
Map severity;
|
||||||
|
|
||||||
HACard({
|
HACard({
|
||||||
this.name,
|
this.name,
|
||||||
@ -28,6 +32,10 @@ class HACard {
|
|||||||
this.content,
|
this.content,
|
||||||
this.states,
|
this.states,
|
||||||
this.conditions: const [],
|
this.conditions: const [],
|
||||||
|
this.unit,
|
||||||
|
this.min,
|
||||||
|
this.max,
|
||||||
|
this.severity,
|
||||||
@required this.type
|
@required this.type
|
||||||
}) {
|
}) {
|
||||||
if (this.columnsCount <= 0) {
|
if (this.columnsCount <= 0) {
|
@ -25,14 +25,15 @@ class CardWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (card.conditions.isNotEmpty) {
|
if (card.conditions.isNotEmpty) {
|
||||||
bool showCardByConditions = false;
|
bool showCardByConditions = true;
|
||||||
for (var condition in card.conditions) {
|
for (var condition in card.conditions) {
|
||||||
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
Entity conditionEntity = HomeAssistant().entities.get(condition['entity']);
|
||||||
if (conditionEntity != null &&
|
if (conditionEntity != null &&
|
||||||
(condition['state'] != null && conditionEntity.state == condition['state']) ||
|
((condition['state'] != null && conditionEntity.state != condition['state']) ||
|
||||||
(condition['state_not'] != null && conditionEntity.state != condition['state_not'])
|
(condition['state_not'] != null && conditionEntity.state == condition['state_not']))
|
||||||
) {
|
) {
|
||||||
showCardByConditions = true;
|
showCardByConditions = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!showCardByConditions) {
|
if (!showCardByConditions) {
|
||||||
@ -42,31 +43,39 @@ class CardWidget extends StatelessWidget {
|
|||||||
|
|
||||||
switch (card.type) {
|
switch (card.type) {
|
||||||
|
|
||||||
case CardType.entities: {
|
case CardType.ENTITIES: {
|
||||||
return _buildEntitiesCard(context);
|
return _buildEntitiesCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.glance: {
|
case CardType.GLANCE: {
|
||||||
return _buildGlanceCard(context);
|
return _buildGlanceCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.mediaControl: {
|
case CardType.MEDIA_CONTROL: {
|
||||||
return _buildMediaControlsCard(context);
|
return _buildMediaControlsCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.entityButton: {
|
case CardType.ENTITY_BUTTON: {
|
||||||
return _buildEntityButtonCard(context);
|
return _buildEntityButtonCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.markdown: {
|
case CardType.GAUGE: {
|
||||||
|
return _buildGaugeCard(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* case CardType.LIGHT: {
|
||||||
|
return _buildLightCard(context);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
case CardType.MARKDOWN: {
|
||||||
return _buildMarkdownCard(context);
|
return _buildMarkdownCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.alarmPanel: {
|
case CardType.ALARM_PANEL: {
|
||||||
return _buildAlarmPanelCard(context);
|
return _buildAlarmPanelCard(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.horizontalStack: {
|
case CardType.HORIZONTAL_STACK: {
|
||||||
if (card.childCards.isNotEmpty) {
|
if (card.childCards.isNotEmpty) {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
card.childCards.forEach((card) {
|
card.childCards.forEach((card) {
|
||||||
@ -89,7 +98,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
|
|
||||||
case CardType.verticalStack: {
|
case CardType.VERTICAL_STACK: {
|
||||||
if (card.childCards.isNotEmpty) {
|
if (card.childCards.isNotEmpty) {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
card.childCards.forEach((card) {
|
card.childCards.forEach((card) {
|
||||||
@ -123,7 +132,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(name: card.name));
|
body.add(CardHeader(name: card.name));
|
||||||
entitiesToShow.forEach((EntityWrapper entity) {
|
entitiesToShow.forEach((EntityWrapper entity) {
|
||||||
if (!entity.entity.isHidden) {
|
if (!entity.entity.isHidden) {
|
||||||
body.add(
|
body.add(
|
||||||
@ -150,7 +159,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(name: card.name));
|
body.add(CardHeader(name: card.name));
|
||||||
body.add(MarkdownBody(data: card.content));
|
body.add(MarkdownBody(data: card.content));
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -162,7 +171,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildAlarmPanelCard(BuildContext context) {
|
Widget _buildAlarmPanelCard(BuildContext context) {
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(
|
body.add(CardHeader(
|
||||||
name: card.name ?? "",
|
name: card.name ?? "",
|
||||||
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
subtitle: Text("${card.linkedEntityWrapper.entity.displayState}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -214,39 +223,51 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Container(height: 0.0, width: 0.0,);
|
return Container(height: 0.0, width: 0.0,);
|
||||||
}
|
}
|
||||||
List<Widget> rows = [];
|
List<Widget> rows = [];
|
||||||
rows.add(CardHeaderWidget(name: card.name));
|
rows.add(CardHeader(name: card.name));
|
||||||
|
|
||||||
List<Widget> result = [];
|
|
||||||
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
int columnsCount = entitiesToShow.length >= card.columnsCount ? card.columnsCount : entitiesToShow.length;
|
||||||
|
|
||||||
entitiesToShow.forEach((EntityWrapper entity) {
|
|
||||||
result.add(
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1/columnsCount,
|
|
||||||
child: EntityModel(
|
|
||||||
entityWrapper: entity,
|
|
||||||
child: GlanceEntityContainer(
|
|
||||||
showName: card.showName,
|
|
||||||
showState: card.showState,
|
|
||||||
),
|
|
||||||
handleTap: true
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
rows.add(
|
rows.add(
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(0.0, Sizes.rowPadding, 0.0, 2*Sizes.rowPadding),
|
padding: EdgeInsets.only(bottom: Sizes.rowPadding, top: Sizes.rowPadding),
|
||||||
child: Wrap(
|
child: FractionallySizedBox(
|
||||||
//alignment: WrapAlignment.spaceAround,
|
widthFactor: 1,
|
||||||
runSpacing: Sizes.rowPadding*2,
|
child: LayoutBuilder(
|
||||||
children: result,
|
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(
|
return Card(
|
||||||
child: new Column(mainAxisSize: MainAxisSize.min, children: rows)
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: rows
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +287,41 @@ class CardWidget extends StatelessWidget {
|
|||||||
return Card(
|
return Card(
|
||||||
child: EntityModel(
|
child: EntityModel(
|
||||||
entityWrapper: card.linkedEntityWrapper,
|
entityWrapper: card.linkedEntityWrapper,
|
||||||
child: ButtonEntityContainer(),
|
child: EntityButtonCardBody(),
|
||||||
|
handleTap: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGaugeCard(BuildContext context) {
|
||||||
|
card.linkedEntityWrapper.displayName = card.name ??
|
||||||
|
card.linkedEntityWrapper.displayName;
|
||||||
|
card.linkedEntityWrapper.unitOfMeasurement = 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.displayName = 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
|
handleTap: true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -274,7 +329,7 @@ class CardWidget extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildUnsupportedCard(BuildContext context) {
|
Widget _buildUnsupportedCard(BuildContext context) {
|
||||||
List<Widget> body = [];
|
List<Widget> body = [];
|
||||||
body.add(CardHeaderWidget(name: card.name ?? ""));
|
body.add(CardHeader(name: card.name ?? ""));
|
||||||
List<Widget> result = [];
|
List<Widget> result = [];
|
||||||
if (card.linkedEntityWrapper != null) {
|
if (card.linkedEntityWrapper != null) {
|
||||||
result.addAll(<Widget>[
|
result.addAll(<Widget>[
|
@ -1,12 +1,12 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class CardHeaderWidget extends StatelessWidget {
|
class CardHeader extends StatelessWidget {
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final Widget trailing;
|
final Widget trailing;
|
||||||
final Widget subtitle;
|
final Widget subtitle;
|
||||||
|
|
||||||
const CardHeaderWidget({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
const CardHeader({Key key, this.name, this.trailing, this.subtitle}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -1,8 +1,8 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class ButtonEntityContainer extends StatelessWidget {
|
class EntityButtonCardBody extends StatelessWidget {
|
||||||
|
|
||||||
ButtonEntityContainer({
|
EntityButtonCardBody({
|
||||||
Key key,
|
Key key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -15,24 +15,25 @@ class ButtonEntityContainer extends StatelessWidget {
|
|||||||
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
if (entityWrapper.entity.statelessType > StatelessEntityType.MISSED) {
|
||||||
return Container(width: 0.0, height: 0.0,);
|
return Container(width: 0.0, height: 0.0,);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
||||||
child: Column(
|
child: FractionallySizedBox(
|
||||||
mainAxisSize: MainAxisSize.min,
|
widthFactor: 1,
|
||||||
children: <Widget>[
|
child: Column(
|
||||||
FractionallySizedBox(
|
children: <Widget>[
|
||||||
widthFactor: 0.4,
|
LayoutBuilder(
|
||||||
child: FittedBox(
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
fit: BoxFit.fitHeight,
|
return EntityIcon(
|
||||||
child: EntityIcon(
|
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
||||||
padding: EdgeInsets.fromLTRB(2.0, 6.0, 2.0, 2.0),
|
size: constraints.maxWidth / 2.5,
|
||||||
size: Sizes.iconSize,
|
);
|
||||||
)
|
}
|
||||||
),
|
),
|
||||||
),
|
_buildName()
|
||||||
_buildName()
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
153
lib/cards/widgets/gauge_card_body.dart
Normal file
153
lib/cards/widgets/gauge_card_body.dart
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
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(),
|
||||||
|
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,6 +1,6 @@
|
|||||||
part of '../main.dart';
|
part of '../../main.dart';
|
||||||
|
|
||||||
class GlanceEntityContainer extends StatelessWidget {
|
class GlanceCardEntityContainer extends StatelessWidget {
|
||||||
|
|
||||||
final bool showName;
|
final bool showName;
|
||||||
final bool showState;
|
final bool showState;
|
||||||
@ -9,7 +9,7 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
final double nameFontSize;
|
final double nameFontSize;
|
||||||
final bool wordsWrapInName;
|
final bool wordsWrapInName;
|
||||||
|
|
||||||
GlanceEntityContainer({
|
GlanceCardEntityContainer({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.showName,
|
@required this.showName,
|
||||||
@required this.showState,
|
@required this.showState,
|
||||||
@ -39,10 +39,10 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.add(
|
result.add(
|
||||||
EntityIcon(
|
EntityIcon(
|
||||||
padding: EdgeInsets.all(0.0),
|
padding: EdgeInsets.all(0.0),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (!nameInTheBottom) {
|
if (!nameInTheBottom) {
|
||||||
if (showState) {
|
if (showState) {
|
||||||
@ -54,14 +54,9 @@ class GlanceEntityContainer extends StatelessWidget {
|
|||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: InkResponse(
|
child: InkResponse(
|
||||||
child: ConstrainedBox(
|
child: Column(
|
||||||
constraints: BoxConstraints(minWidth: Sizes.iconSize * 2),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Column(
|
children: result,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
//mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
//crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: result,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onTap: () => entityWrapper.handleTap(),
|
onTap: () => entityWrapper.handleTap(),
|
||||||
onLongPress: () => entityWrapper.handleHold(),
|
onLongPress: () => entityWrapper.handleHold(),
|
90
lib/cards/widgets/light_card_body.dart
Normal file
90
lib/cards/widgets/light_card_body.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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(),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -77,23 +77,40 @@ class EntityUIAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CardType {
|
class CardType {
|
||||||
static const horizontalStack = "horizontal-stack";
|
static const HORIZONTAL_STACK = "horizontal-stack";
|
||||||
static const verticalStack = "vertical-stack";
|
static const VERTICAL_STACK = "vertical-stack";
|
||||||
static const entities = "entities";
|
static const ENTITIES = "entities";
|
||||||
static const glance = "glance";
|
static const GLANCE = "glance";
|
||||||
static const mediaControl = "media-control";
|
static const MEDIA_CONTROL = "media-control";
|
||||||
static const weatherForecast = "weather-forecast";
|
static const WEATHER_FORECAST = "weather-forecast";
|
||||||
static const thermostat = "thermostat";
|
static const THERMOSTAT = "thermostat";
|
||||||
static const sensor = "sensor";
|
static const SENSOR = "sensor";
|
||||||
static const plantStatus = "plant-status";
|
static const PLANT_STATUS = "plant-status";
|
||||||
static const pictureEntity = "picture-entity";
|
static const PICTURE_ENTITY = "picture-entity";
|
||||||
static const pictureElements = "picture-elements";
|
static const PICTURE_ELEMENTS = "picture-elements";
|
||||||
static const picture = "picture";
|
static const PICTURE = "picture";
|
||||||
static const map = "map";
|
static const MAP = "map";
|
||||||
static const iframe = "iframe";
|
static const IFRAME = "iframe";
|
||||||
static const gauge = "gauge";
|
static const GAUGE = "gauge";
|
||||||
static const entityButton = "entity-button";
|
static const ENTITY_BUTTON = "entity-button";
|
||||||
static const conditional = "conditional";
|
static const CONDITIONAL = "conditional";
|
||||||
static const alarmPanel = "alarm-panel";
|
static const ALARM_PANEL = "alarm-panel";
|
||||||
static const markdown = "markdown";
|
static const MARKDOWN = "markdown";
|
||||||
|
static const LIGHT = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sizes {
|
||||||
|
static const rightWidgetPadding = 10.0;
|
||||||
|
static const leftWidgetPadding = 10.0;
|
||||||
|
static const buttonPadding = 4.0;
|
||||||
|
static const extendedWidgetHeight = 50.0;
|
||||||
|
static const iconSize = 28.0;
|
||||||
|
static const largeIconSize = 46.0;
|
||||||
|
static const stateFontSize = 15.0;
|
||||||
|
static const nameFontSize = 15.0;
|
||||||
|
static const smallFontSize = 14.0;
|
||||||
|
static const largeFontSize = 24.0;
|
||||||
|
static const inputWidth = 160.0;
|
||||||
|
static const rowPadding = 10.0;
|
||||||
|
static const doubleRowPadding = rowPadding*2;
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ class EntityWrapper {
|
|||||||
|
|
||||||
String displayName;
|
String displayName;
|
||||||
String icon;
|
String icon;
|
||||||
|
String unitOfMeasurement;
|
||||||
String entityPicture;
|
String entityPicture;
|
||||||
EntityUIAction uiAction;
|
EntityUIAction uiAction;
|
||||||
Entity entity;
|
Entity entity;
|
||||||
@ -24,6 +25,7 @@ class EntityWrapper {
|
|||||||
if (uiAction == null) {
|
if (uiAction == null) {
|
||||||
uiAction = EntityUIAction();
|
uiAction = EntityUIAction();
|
||||||
}
|
}
|
||||||
|
unitOfMeasurement = entity.unitOfMeasurement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +149,17 @@ class EntityCollection {
|
|||||||
return _allEntities[entityId] != null;
|
return _allEntities[entityId] != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Entity> getByDomains(List<String> domains) {
|
||||||
|
List<Entity> result = [];
|
||||||
|
_allEntities.forEach((id, entity) {
|
||||||
|
if (domains.contains(entity.domain)) {
|
||||||
|
Logger.d("getByDomain: ${entity.isHidden}");
|
||||||
|
result.add(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
List<Entity> filterEntitiesForDefaultView() {
|
List<Entity> filterEntitiesForDefaultView() {
|
||||||
List<Entity> result = [];
|
List<Entity> result = [];
|
||||||
List<Entity> groups = [];
|
List<Entity> groups = [];
|
||||||
|
@ -57,7 +57,7 @@ class BadgeWidget extends StatelessWidget {
|
|||||||
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
} else if (entityModel.entityWrapper.entity.displayState.length <= 10) {
|
||||||
stateFontSize = 8.0;
|
stateFontSize = 8.0;
|
||||||
}
|
}
|
||||||
onBadgeTextValue = entityModel.entityWrapper.entity.unitOfMeasurement;
|
onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement;
|
||||||
badgeIcon = Center(
|
badgeIcon = Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"${entityModel.entityWrapper.entity.displayState}",
|
"${entityModel.entityWrapper.entity.displayState}",
|
||||||
|
@ -20,21 +20,9 @@ class _CameraStreamViewState extends State<CameraStreamView> {
|
|||||||
String streamUrl = "";
|
String streamUrl = "";
|
||||||
|
|
||||||
launchStream() {
|
launchStream() {
|
||||||
Navigator.push(
|
Launcher.launchURLInCustomTab(
|
||||||
context,
|
context: context,
|
||||||
MaterialPageRoute(
|
url: streamUrl
|
||||||
builder: (context) => WebviewScaffold(
|
|
||||||
url: "$streamUrl",
|
|
||||||
withZoom: true,
|
|
||||||
appBar: new AppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(Icons.close),
|
|
||||||
onPressed: () => Navigator.pop(context)
|
|
||||||
),
|
|
||||||
title: new Text("${_entity.displayName}"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +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 bool bold;
|
||||||
|
|
||||||
const SimpleEntityState({Key key, this.maxLines: 10, 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.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);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -21,18 +23,22 @@ class SimpleEntityState extends StatelessWidget {
|
|||||||
state = customValue;
|
state = customValue;
|
||||||
}
|
}
|
||||||
TextStyle textStyle = TextStyle(
|
TextStyle textStyle = TextStyle(
|
||||||
fontSize: Sizes.stateFontSize,
|
fontSize: this.fontSize,
|
||||||
|
fontWeight: FontWeight.normal
|
||||||
);
|
);
|
||||||
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
if (entityModel.entityWrapper.entity.statelessType == StatelessEntityType.CALL_SERVICE) {
|
||||||
textStyle = textStyle.apply(color: Colors.blue);
|
textStyle = textStyle.apply(color: Colors.blue);
|
||||||
}
|
}
|
||||||
|
if (this.bold) {
|
||||||
|
textStyle = textStyle.apply(fontWeightDelta: 100);
|
||||||
|
}
|
||||||
while (state.contains(" ")){
|
while (state.contains(" ")){
|
||||||
state = state.replaceAll(" ", " ");
|
state = state.replaceAll(" ", " ");
|
||||||
}
|
}
|
||||||
Widget result = Padding(
|
Widget result = Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Text(
|
child: Text(
|
||||||
"$state ${entityModel.entityWrapper.entity.unitOfMeasurement}",
|
"$state ${entityModel.entityWrapper.unitOfMeasurement}",
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
@ -34,32 +34,37 @@ class DefaultEntityContainer extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return InkWell(
|
Widget result = Row(
|
||||||
onLongPress: () {
|
mainAxisSize: MainAxisSize.max,
|
||||||
if (entityModel.handleTap) {
|
children: <Widget>[
|
||||||
entityModel.entityWrapper.handleHold();
|
EntityIcon(),
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
if (entityModel.handleTap) {
|
|
||||||
entityModel.entityWrapper.handleTap();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: <Widget>[
|
|
||||||
EntityIcon(),
|
|
||||||
|
|
||||||
Flexible(
|
Flexible(
|
||||||
fit: FlexFit.tight,
|
fit: FlexFit.tight,
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: EntityName(
|
child: EntityName(
|
||||||
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
padding: EdgeInsets.fromLTRB(10.0, 2.0, 10.0, 2.0),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
state
|
),
|
||||||
],
|
state
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
return InkWell(
|
||||||
|
onLongPress: () {
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
entityModel.entityWrapper.handleHold();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
if (entityModel.handleTap) {
|
||||||
|
entityModel.entityWrapper.handleTap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,6 +14,7 @@ class EntityColor {
|
|||||||
"auto": Colors.amber,
|
"auto": Colors.amber,
|
||||||
EntityState.active: Colors.amber,
|
EntityState.active: Colors.amber,
|
||||||
EntityState.playing: Colors.amber,
|
EntityState.playing: Colors.amber,
|
||||||
|
EntityState.paused: Colors.amber,
|
||||||
"above_horizon": Colors.amber,
|
"above_horizon": Colors.amber,
|
||||||
EntityState.home: Colors.amber,
|
EntityState.home: Colors.amber,
|
||||||
EntityState.open: Colors.amber,
|
EntityState.open: Colors.amber,
|
||||||
|
@ -11,6 +11,7 @@ class HomeAssistant {
|
|||||||
EntityCollection entities;
|
EntityCollection entities;
|
||||||
HomeAssistantUI ui;
|
HomeAssistantUI ui;
|
||||||
Map _instanceConfig = {};
|
Map _instanceConfig = {};
|
||||||
|
Map services;
|
||||||
String _userName;
|
String _userName;
|
||||||
HSVColor savedColor;
|
HSVColor savedColor;
|
||||||
|
|
||||||
@ -115,7 +116,11 @@ class HomeAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future _getServices() async {
|
Future _getServices() async {
|
||||||
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => Logger.d("Services received")).catchError((e) {
|
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) {
|
||||||
|
Logger.d("Got ${data.length} services");
|
||||||
|
Logger.d("Media extractor: ${data["media_extractor"]}");
|
||||||
|
services = data;
|
||||||
|
}).catchError((e) {
|
||||||
Logger.w("Can't get services: ${e}");
|
Logger.w("Can't get services: ${e}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -162,7 +167,8 @@ class HomeAssistant {
|
|||||||
count: viewCounter,
|
count: viewCounter,
|
||||||
id: "${rawView['id']}",
|
id: "${rawView['id']}",
|
||||||
name: rawView['title'],
|
name: rawView['title'],
|
||||||
iconName: rawView['icon']
|
iconName: rawView['icon'],
|
||||||
|
panel: rawView['panel'] ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rawView['badges'] != null && rawView['badges'] is List) {
|
if (rawView['badges'] != null && rawView['badges'] is List) {
|
||||||
@ -191,7 +197,7 @@ class HomeAssistant {
|
|||||||
HACard card = HACard(
|
HACard card = HACard(
|
||||||
id: "card",
|
id: "card",
|
||||||
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
name: rawCardInfo["title"] ?? rawCardInfo["name"],
|
||||||
type: rawCardInfo['type'] ?? CardType.entities,
|
type: rawCardInfo['type'] ?? CardType.ENTITIES,
|
||||||
columnsCount: rawCardInfo['columns'] ?? 4,
|
columnsCount: rawCardInfo['columns'] ?? 4,
|
||||||
showName: rawCardInfo['show_name'] ?? true,
|
showName: rawCardInfo['show_name'] ?? true,
|
||||||
showState: rawCardInfo['show_state'] ?? true,
|
showState: rawCardInfo['show_state'] ?? true,
|
||||||
@ -199,7 +205,11 @@ class HomeAssistant {
|
|||||||
stateFilter: rawCardInfo['state_filter'] ?? [],
|
stateFilter: rawCardInfo['state_filter'] ?? [],
|
||||||
states: rawCardInfo['states'],
|
states: rawCardInfo['states'],
|
||||||
conditions: rawCard['conditions'] ?? [],
|
conditions: rawCard['conditions'] ?? [],
|
||||||
content: rawCardInfo['content']
|
content: rawCardInfo['content'],
|
||||||
|
min: rawCardInfo['min'] ?? 0,
|
||||||
|
max: rawCardInfo['max'] ?? 100,
|
||||||
|
unit: rawCardInfo['unit'],
|
||||||
|
severity: rawCardInfo['severity']
|
||||||
);
|
);
|
||||||
if (rawCardInfo["cards"] != null) {
|
if (rawCardInfo["cards"] != null) {
|
||||||
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
card.childCards = _createLovelaceCards(rawCardInfo["cards"]);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -21,6 +22,11 @@ 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 'package:share/receive_share_state.dart';
|
||||||
|
import 'package:share/share.dart';
|
||||||
|
|
||||||
|
import 'utils/logger.dart';
|
||||||
|
|
||||||
part 'const.dart';
|
part 'const.dart';
|
||||||
part 'utils/launcher.dart';
|
part 'utils/launcher.dart';
|
||||||
@ -49,8 +55,8 @@ part 'entity_widgets/common/badge.dart';
|
|||||||
part 'entity_widgets/model_widgets.dart';
|
part 'entity_widgets/model_widgets.dart';
|
||||||
part 'entity_widgets/default_entity_container.dart';
|
part 'entity_widgets/default_entity_container.dart';
|
||||||
part 'entity_widgets/missed_entity.dart';
|
part 'entity_widgets/missed_entity.dart';
|
||||||
part 'entity_widgets/glance_entity_container.dart';
|
part 'cards/widgets/glance_card_entity_container.dart';
|
||||||
part 'entity_widgets/button_entity_container.dart';
|
part 'cards/widgets/entity_button_card_body.widget.dart';
|
||||||
part 'entity_widgets/common/entity_attributes_list.dart';
|
part 'entity_widgets/common/entity_attributes_list.dart';
|
||||||
part 'entity_widgets/entity_icon.dart';
|
part 'entity_widgets/entity_icon.dart';
|
||||||
part 'entity_widgets/entity_name.dart';
|
part 'entity_widgets/entity_name.dart';
|
||||||
@ -104,26 +110,27 @@ 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 'ui_class/ui.dart';
|
part 'ui.dart';
|
||||||
part 'ui_class/view.class.dart';
|
part 'view.class.dart';
|
||||||
part 'ui_class/card.class.dart';
|
part 'cards/card.class.dart';
|
||||||
part 'ui_class/sizes_class.dart';
|
part 'panels/panel_class.dart';
|
||||||
part 'ui_class/panel_class.dart';
|
part 'view.dart';
|
||||||
part 'ui_widgets/view.dart';
|
part 'cards/card_widget.dart';
|
||||||
part 'ui_widgets/card_widget.dart';
|
part 'cards/widgets/card_header.widget.dart';
|
||||||
part 'ui_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 'utils/logger.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/widgets/light_card_body.dart';
|
||||||
|
part 'pages/play_media.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.5";
|
const appVersion = "0.6.7";
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
FlutterError.onError = (errorDetails) {
|
FlutterError.onError = (errorDetails) {
|
||||||
@ -163,6 +170,7 @@ 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'] : ''}",),
|
||||||
"/log-view": (context) => LogViewPage(title: "Log"),
|
"/log-view": (context) => LogViewPage(title: "Log"),
|
||||||
"/login": (context) => WebviewScaffold(
|
"/login": (context) => WebviewScaffold(
|
||||||
url: "${ConnectionManager().oauthUrl}",
|
url: "${ConnectionManager().oauthUrl}",
|
||||||
|
@ -12,13 +12,18 @@ class StartupUserMessagesManager {
|
|||||||
StartupUserMessagesManager._internal() {}
|
StartupUserMessagesManager._internal() {}
|
||||||
|
|
||||||
bool _supportAppDevelopmentMessageShown;
|
bool _supportAppDevelopmentMessageShown;
|
||||||
|
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";
|
||||||
|
|
||||||
void checkMessagesToShow() async {
|
void checkMessagesToShow() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.reload();
|
await prefs.reload();
|
||||||
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
_supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false;
|
||||||
if (!_supportAppDevelopmentMessageShown) {
|
_whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false;
|
||||||
|
if (!_whatsNewMessageShown) {
|
||||||
|
_showWhatsNewMessage();
|
||||||
|
} else if (!_supportAppDevelopmentMessageShown) {
|
||||||
_showSupportAppDevelopmentMessage();
|
_showSupportAppDevelopmentMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,4 +48,24 @@ class StartupUserMessagesManager {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
prefs.setBool(_whatsNewMessageKey, true);
|
||||||
|
Launcher.launchURL("https://github.com/estevez-dev/ha_client/releases");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onNegative: () {
|
||||||
|
SharedPreferences.getInstance().then((prefs) {
|
||||||
|
prefs.setBool(_whatsNewMessageKey, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ class MainPage extends StatefulWidget {
|
|||||||
_MainPageState createState() => new _MainPageState();
|
_MainPageState createState() => new _MainPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MainPageState extends State<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
class _MainPageState extends ReceiveShareState<MainPage> with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||||
|
|
||||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||||
StreamSubscription _stateSubscription;
|
StreamSubscription _stateSubscription;
|
||||||
@ -25,6 +25,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
int _previousViewCount;
|
int _previousViewCount;
|
||||||
bool _showLoginButton = false;
|
bool _showLoginButton = false;
|
||||||
bool _preventAppRefresh = false;
|
bool _preventAppRefresh = false;
|
||||||
|
String _savedSharedText;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -34,6 +35,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
_handlePurchaseUpdates(purchases);
|
_handlePurchaseUpdates(purchases);
|
||||||
});
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
|
enableShareReceiving();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
_firebaseMessaging.configure(
|
_firebaseMessaging.configure(
|
||||||
@ -76,6 +78,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override void receiveShare(Share shared) {
|
||||||
|
if (shared.mimeType == ShareType.TYPE_PLAIN_TEXT) {
|
||||||
|
_savedSharedText = shared.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future onSelectNotification(String payload) async {
|
Future onSelectNotification(String payload) async {
|
||||||
if (payload != null) {
|
if (payload != null) {
|
||||||
Logger.d('Notification clicked: ' + payload);
|
Logger.d('Notification clicked: ' + payload);
|
||||||
@ -121,6 +129,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fetchData() async {
|
_fetchData() async {
|
||||||
|
if (_savedSharedText != null && !HomeAssistant().isNoEntities) {
|
||||||
|
Logger.d("Got shared text: $_savedSharedText");
|
||||||
|
Navigator.pushNamed(context, "/play-media", arguments: {"url": _savedSharedText});
|
||||||
|
_savedSharedText = null;
|
||||||
|
}
|
||||||
await HomeAssistant().fetchData().then((_) {
|
await HomeAssistant().fetchData().then((_) {
|
||||||
_hideBottomBar();
|
_hideBottomBar();
|
||||||
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
int currentViewCount = HomeAssistant().ui?.views?.length ?? 0;
|
||||||
@ -659,6 +672,11 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
|||||||
primary: true,
|
primary: true,
|
||||||
title: Text(HomeAssistant().locationName ?? ""),
|
title: Text(HomeAssistant().locationName ?? ""),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
|
"mdi:television"), color: Colors.white,),
|
||||||
|
onPressed: () => Navigator.pushNamed(context, "/play-media", arguments: {"url": ""})
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
icon: Icon(MaterialDesignIcons.getIconDataFromIconName(
|
||||||
"mdi:dots-vertical"), color: Colors.white,),
|
"mdi:dots-vertical"), color: Colors.white,),
|
||||||
|
225
lib/pages/play_media.page.dart
Normal file
225
lib/pages/play_media.page.dart
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
part of '../main.dart';
|
||||||
|
|
||||||
|
class PlayMediaPage extends StatefulWidget {
|
||||||
|
|
||||||
|
final String mediaUrl;
|
||||||
|
|
||||||
|
PlayMediaPage({Key key, this.mediaUrl}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PlayMediaPageState createState() => new _PlayMediaPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlayMediaPageState extends State<PlayMediaPage> {
|
||||||
|
|
||||||
|
bool _loaded = false;
|
||||||
|
String _error = "";
|
||||||
|
String _validationMessage = "";
|
||||||
|
List<Entity> _players;
|
||||||
|
String _mediaUrl;
|
||||||
|
String _contentType;
|
||||||
|
bool _useMediaExtractor = false;
|
||||||
|
bool _isMediaExtractorExist = false;
|
||||||
|
StreamSubscription _stateSubscription;
|
||||||
|
StreamSubscription _refreshDataSubscription;
|
||||||
|
final List<String> _contentTypes = ["movie", "video", "music", "image", "image/jpg", "playlist"];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_mediaUrl = widget.mediaUrl;
|
||||||
|
_contentType = _contentTypes[0];
|
||||||
|
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
|
||||||
|
if (event.entityId.contains("media_player")) {
|
||||||
|
Logger.d("State change event handled by play media page: ${event.entityId}");
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_refreshDataSubscription = eventBus.on<RefreshDataFinishedEvent>().listen((event) {
|
||||||
|
_loadMediaEntities();
|
||||||
|
});
|
||||||
|
_loadMediaEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadMediaEntities() async {
|
||||||
|
if (HomeAssistant().isNoEntities) {
|
||||||
|
setState(() {
|
||||||
|
_loaded = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_isMediaExtractorExist = HomeAssistant().services.containsKey("media_extractor");
|
||||||
|
//_useMediaExtractor = _isMediaExtractorExist;
|
||||||
|
_players = HomeAssistant().entities.getByDomains(["media_player"]);
|
||||||
|
setState(() {
|
||||||
|
if (_players.isNotEmpty) {
|
||||||
|
_loaded = true;
|
||||||
|
} else {
|
||||||
|
_loaded = false;
|
||||||
|
_error = "Looks like you don't have any media player";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _playMedia(Entity entity) {
|
||||||
|
if (_mediaUrl == null || _mediaUrl.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_validationMessage = "Media url must be specified";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
String serviceDomain;
|
||||||
|
if (_useMediaExtractor) {
|
||||||
|
serviceDomain = "media_extractor";
|
||||||
|
} else {
|
||||||
|
serviceDomain = "media_player";
|
||||||
|
}
|
||||||
|
Navigator.pop(context);
|
||||||
|
ConnectionManager().callService(
|
||||||
|
domain: serviceDomain,
|
||||||
|
entityId: entity.entityId,
|
||||||
|
service: "play_media",
|
||||||
|
additionalServiceData: {
|
||||||
|
"media_content_id": _mediaUrl,
|
||||||
|
"media_content_type": _contentType
|
||||||
|
}
|
||||||
|
);
|
||||||
|
eventBus.fire(ShowEntityPageEvent(entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget body;
|
||||||
|
if (!_loaded) {
|
||||||
|
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||||
|
} else {
|
||||||
|
List<Widget> children = [];
|
||||||
|
children.add(CardHeader(name: "Media:"));
|
||||||
|
children.add(
|
||||||
|
TextField(
|
||||||
|
maxLines: 5,
|
||||||
|
minLines: 1,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Media url"
|
||||||
|
),
|
||||||
|
controller: TextEditingController.fromValue(TextEditingValue(text: _mediaUrl)),
|
||||||
|
onChanged: (value) {
|
||||||
|
_mediaUrl = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (_validationMessage.isNotEmpty) {
|
||||||
|
children.add(Text(
|
||||||
|
"$_validationMessage",
|
||||||
|
style: TextStyle(color: Colors.red)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
children.addAll(<Widget>[
|
||||||
|
Container(height: Sizes.rowPadding,),
|
||||||
|
DropdownButton<String>(
|
||||||
|
value: _contentType,
|
||||||
|
isExpanded: true,
|
||||||
|
items: _contentTypes.map((String value) {
|
||||||
|
return new DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: new Text(value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_contentType = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (_isMediaExtractorExist) {
|
||||||
|
children.addAll(<Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Text("Use media extractor"),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: _useMediaExtractor,
|
||||||
|
onChanged: (value) => setState((){_useMediaExtractor = value;}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: Sizes.rowPadding,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children.addAll(<Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Text("You can use media extractor here"),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Launcher.launchURLInCustomTab(
|
||||||
|
context: context,
|
||||||
|
url: "https://www.home-assistant.io/components/media_extractor/"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"How?",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: Sizes.doubleRowPadding,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
children.add(CardHeader(name: "Play on:"));
|
||||||
|
children.addAll(
|
||||||
|
_players.map((player) => InkWell(
|
||||||
|
child: EntityModel(
|
||||||
|
entityWrapper: EntityWrapper(entity: player),
|
||||||
|
handleTap: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: Sizes.doubleRowPadding),
|
||||||
|
child: DefaultEntityContainer(state: player._buildStatePart(context)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onTap: () => _playMedia(player),
|
||||||
|
))
|
||||||
|
);
|
||||||
|
body = ListView(
|
||||||
|
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
children: children
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(
|
||||||
|
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
title: new Text("Play media"),
|
||||||
|
),
|
||||||
|
body: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose(){
|
||||||
|
_stateSubscription?.cancel();
|
||||||
|
_refreshDataSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -23,7 +23,6 @@ class Panel {
|
|||||||
if (icon == null || !icon.startsWith("mdi:")) {
|
if (icon == null || !icon.startsWith("mdi:")) {
|
||||||
icon = Panel.iconsByComponent[type];
|
icon = Panel.iconsByComponent[type];
|
||||||
}
|
}
|
||||||
Logger.d("New panel '$title'. type=$type, icon=$icon, urlPath=$urlPath");
|
|
||||||
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
|
isHidden = (type == 'lovelace' || type == 'kiosk' || type == 'states' || type == 'profile' || type == 'developer-tools');
|
||||||
isWebView = (type != 'config');
|
isWebView = (type != 'config');
|
||||||
}
|
}
|
@ -17,7 +17,7 @@ class LinkToWebConfig extends StatelessWidget {
|
|||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: Sizes.largeFontSize)),
|
||||||
subtitle: Text("Tap to opne 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);
|
||||||
},
|
},
|
||||||
|
77
lib/plugins/circular_slider/base_painter.dart
Normal file
77
lib/plugins/circular_slider/base_painter.dart
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
366
lib/plugins/circular_slider/circular_slider_paint.dart
Normal file
366
lib/plugins/circular_slider/circular_slider_paint.dart
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
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) {}
|
||||||
|
}
|
148
lib/plugins/circular_slider/double_circular_slider.dart
Normal file
148
lib/plugins/circular_slider/double_circular_slider.dart
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
147
lib/plugins/circular_slider/single_circular_slider.dart
Normal file
147
lib/plugins/circular_slider/single_circular_slider.dart
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
77
lib/plugins/circular_slider/slider_painter.dart
Normal file
77
lib/plugins/circular_slider/slider_painter.dart
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
75
lib/plugins/circular_slider/utils.dart
Normal file
75
lib/plugins/circular_slider/utils.dart
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of '../main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class HomeAssistantUI {
|
class HomeAssistantUI {
|
||||||
List<HAView> views;
|
List<HAView> views;
|
@ -1,16 +0,0 @@
|
|||||||
part of '../main.dart';
|
|
||||||
|
|
||||||
class Sizes {
|
|
||||||
static const rightWidgetPadding = 10.0;
|
|
||||||
static const leftWidgetPadding = 10.0;
|
|
||||||
static const buttonPadding = 4.0;
|
|
||||||
static const extendedWidgetHeight = 50.0;
|
|
||||||
static const iconSize = 28.0;
|
|
||||||
static const largeIconSize = 46.0;
|
|
||||||
static const stateFontSize = 15.0;
|
|
||||||
static const nameFontSize = 15.0;
|
|
||||||
static const smallFontSize = 14.0;
|
|
||||||
static const largeFontSize = 24.0;
|
|
||||||
static const inputWidth = 160.0;
|
|
||||||
static const rowPadding = 10.0;
|
|
||||||
}
|
|
@ -1,4 +1,7 @@
|
|||||||
part of '../main.dart';
|
import 'package:date_format/date_format.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
|
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
part of '../main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class HAView {
|
class HAView {
|
||||||
List<HACard> cards = [];
|
List<HACard> cards = [];
|
||||||
List<Entity> badges = [];
|
List<Entity> badges = [];
|
||||||
Entity linkedEntity;
|
Entity linkedEntity;
|
||||||
String name;
|
final String name;
|
||||||
String id;
|
final String id;
|
||||||
String iconName;
|
final String iconName;
|
||||||
int count;
|
final int count;
|
||||||
|
final bool panel;
|
||||||
|
|
||||||
HAView({
|
HAView({
|
||||||
this.name,
|
this.name,
|
||||||
this.id,
|
this.id,
|
||||||
this.count,
|
this.count,
|
||||||
this.iconName,
|
this.iconName,
|
||||||
|
this.panel: false,
|
||||||
List<Entity> childEntities
|
List<Entity> childEntities
|
||||||
}) {
|
}) {
|
||||||
if (childEntities != null) {
|
if (childEntities != null) {
|
||||||
@ -29,7 +31,7 @@ class HAView {
|
|||||||
name: e.displayName,
|
name: e.displayName,
|
||||||
id: e.entityId,
|
id: e.entityId,
|
||||||
linkedEntityWrapper: EntityWrapper(entity: e),
|
linkedEntityWrapper: EntityWrapper(entity: e),
|
||||||
type: CardType.mediaControl
|
type: CardType.MEDIA_CONTROL
|
||||||
);
|
);
|
||||||
cards.add(card);
|
cards.add(card);
|
||||||
});
|
});
|
||||||
@ -40,7 +42,7 @@ class HAView {
|
|||||||
HACard card = HACard(
|
HACard card = HACard(
|
||||||
id: groupIdToAdd,
|
id: groupIdToAdd,
|
||||||
name: entity.domain,
|
name: entity.domain,
|
||||||
type: CardType.entities
|
type: CardType.ENTITIES
|
||||||
);
|
);
|
||||||
card.entities.add(EntityWrapper(entity: entity));
|
card.entities.add(EntityWrapper(entity: entity));
|
||||||
autoGeneratedCards.add(card);
|
autoGeneratedCards.add(card);
|
||||||
@ -52,7 +54,7 @@ class HAView {
|
|||||||
name: entity.displayName,
|
name: entity.displayName,
|
||||||
id: entity.entityId,
|
id: entity.entityId,
|
||||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
linkedEntityWrapper: EntityWrapper(entity: entity),
|
||||||
type: CardType.entities
|
type: CardType.ENTITIES
|
||||||
);
|
);
|
||||||
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
|
card.entities.addAll(entity.childEntities.where((entity) {return entity.domain != "media_player";}).map((e) {return EntityWrapper(entity: e);}));
|
||||||
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
|
entity.childEntities.where((entity) {return entity.domain == "media_player";}).forEach((entity){
|
||||||
@ -60,7 +62,7 @@ class HAView {
|
|||||||
name: entity.displayName,
|
name: entity.displayName,
|
||||||
id: entity.entityId,
|
id: entity.entityId,
|
||||||
linkedEntityWrapper: EntityWrapper(entity: entity),
|
linkedEntityWrapper: EntityWrapper(entity: entity),
|
||||||
type: CardType.mediaControl
|
type: CardType.MEDIA_CONTROL
|
||||||
);
|
);
|
||||||
cards.add(mediaCard);
|
cards.add(mediaCard);
|
||||||
});
|
});
|
||||||
@ -85,7 +87,7 @@ class HAView {
|
|||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
Tab(
|
Tab(
|
||||||
text: name.toUpperCase(),
|
text: "${name?.toUpperCase() ?? "UNNAMED VIEW"}",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -99,7 +101,7 @@ class HAView {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Tab(
|
return Tab(
|
||||||
text: linkedEntity.displayName.toUpperCase(),
|
text: "${linkedEntity.displayName?.toUpperCase()}",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
part of '../main.dart';
|
part of 'main.dart';
|
||||||
|
|
||||||
class ViewWidget extends StatefulWidget {
|
class ViewWidget extends StatefulWidget {
|
||||||
final HAView view;
|
final HAView view;
|
||||||
@ -24,11 +24,27 @@ class ViewWidgetState extends State<ViewWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
if (widget.view.panel) {
|
||||||
padding: EdgeInsets.all(0.0),
|
return FractionallySizedBox(
|
||||||
//physics: const AlwaysScrollableScrollPhysics(),
|
widthFactor: 1,
|
||||||
children: _buildChildren(context),
|
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> _buildChildren(BuildContext context) {
|
37
pubspec.lock
37
pubspec.lock
@ -21,14 +21,14 @@ packages:
|
|||||||
name: async
|
name: async
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.3.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -77,7 +77,7 @@ packages:
|
|||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
date_format:
|
date_format:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -105,7 +105,7 @@ packages:
|
|||||||
name: firebase_messaging
|
name: firebase_messaging
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.4"
|
version: "5.1.5"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -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.2"
|
version: "0.8.3"
|
||||||
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.2.1+1"
|
version: "3.3.1+1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -164,7 +164,7 @@ packages:
|
|||||||
name: flutter_webview_plugin
|
name: flutter_webview_plugin
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.7"
|
version: "0.3.8"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -234,28 +234,28 @@ packages:
|
|||||||
name: meta
|
name: meta
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.6"
|
version: "1.1.7"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.2"
|
version: "1.6.4"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pedantic
|
name: pedantic
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.8.0+1"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -283,7 +283,16 @@ packages:
|
|||||||
name: quiver
|
name: quiver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.5"
|
||||||
|
share:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: HEAD
|
||||||
|
resolved-ref: "1f8b139ca0bd35b643ef4f5ccce3a1b09931f16a"
|
||||||
|
url: "https://github.com/d-silveira/flutter-share.git"
|
||||||
|
source: git
|
||||||
|
version: "0.6.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -330,7 +339,7 @@ packages:
|
|||||||
name: string_scanner
|
name: string_scanner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -365,7 +374,7 @@ packages:
|
|||||||
name: url_launcher
|
name: url_launcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.2"
|
version: "5.1.3"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: hass_client
|
name: hass_client
|
||||||
description: Home Assistant Android Client
|
description: Home Assistant Android Client
|
||||||
|
|
||||||
version: 0.6.5+652
|
version: 0.6.7+675
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||||
@ -16,9 +16,9 @@ dependencies:
|
|||||||
cached_network_image: any
|
cached_network_image: any
|
||||||
url_launcher: any
|
url_launcher: any
|
||||||
date_format: any
|
date_format: any
|
||||||
charts_flutter: any
|
charts_flutter: ^0.8.0
|
||||||
flutter_markdown: any
|
flutter_markdown: any
|
||||||
in_app_purchase: ^0.2.1+2
|
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.4
|
||||||
@ -26,6 +26,9 @@ dependencies:
|
|||||||
flutter_secure_storage: ^3.2.1+1
|
flutter_secure_storage: ^3.2.1+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
|
||||||
|
share:
|
||||||
|
git:
|
||||||
|
url: https://github.com/d-silveira/flutter-share.git
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user