Compare commits
	
		
			36 Commits
		
	
	
		
			1.0.0
			...
			foreground
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c844e21e76 | ||
|  | 24d42c9597 | ||
|  | 9078ad81e8 | ||
|  | 7cba6c8a10 | ||
|  | c1f9c8c16d | ||
|  | 1d1d132b33 | ||
|  | e258b3bc2c | ||
|  | 13508ee92f | ||
|  | 4fbf58e707 | ||
|  | a3442f84ca | ||
|  | 6a6ab3b2cb | ||
|  | d9fa553e2f | ||
|  | cde5d9b912 | ||
|  | 3468446b5b | ||
|  | 326434273a | ||
|  | 470d3be946 | ||
|  | d1032be6a6 | ||
|  | cffac8e1f8 | ||
|  | 870bc25dd9 | ||
|  | de713024f6 | ||
|  | 4d4add4581 | ||
|  | 1670c8e505 | ||
|  | 55eb1b5125 | ||
|  | dbeaaaf91e | ||
|  | 8166d8ce6d | ||
|  | 35bcf0c1fa | ||
|  | 9c1d240962 | ||
|  | a76652b552 | ||
|  | a140f993d0 | ||
|  | ded60a2867 | ||
|  | b86602bcdb | ||
|  | 02ea45469f | ||
|  | 90105c3b09 | ||
|  | 3d828914cc | ||
|  | 8cd5776bc6 | ||
|  | 17ec73b176 | 
| @@ -5,8 +5,13 @@ | ||||
|         android:required="false" /> | ||||
|     <uses-permission android:name="android.permission.INTERNET"/> | ||||
|     <uses-permission android:name="android.permission.VIBRATE" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> | ||||
|     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> | ||||
|     <!-- | ||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | ||||
|     --> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||
|  | ||||
|  | ||||
| @@ -53,6 +58,17 @@ | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <receiver android:name="rekab.app.background_locator.LocatorBroadcastReceiver" | ||||
|             android:enabled="true" | ||||
|             android:exported="true"/> | ||||
|         <service android:name="rekab.app.background_locator.LocatorService" | ||||
|             android:permission="android.permission.BIND_JOB_SERVICE" | ||||
|             android:exported="true"/> | ||||
|         <service android:name="rekab.app.background_locator.IsolateHolderService" | ||||
|             android:permission="android.permission.FOREGROUND_SERVICE" | ||||
|             android:exported="true"/> | ||||
|  | ||||
|         <!--  | ||||
|         <service | ||||
|             android:name="io.flutter.plugins.androidalarmmanager.AlarmService" | ||||
|             android:permission="android.permission.BIND_JOB_SERVICE" | ||||
| @@ -67,5 +83,6 @@ | ||||
|                 <action android:name="android.intent.action.BOOT_COMPLETED"></action> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|     --> | ||||
|     </application> | ||||
| </manifest> | ||||
|   | ||||
| @@ -11,9 +11,10 @@ window.externalApp.getExternalAuth = function(options) { | ||||
|         setTimeout(function(){ | ||||
|             console.log("Calling a callback"); | ||||
|             window[options.callback](true, responseData); | ||||
|         }, 500); | ||||
|         }, 900); | ||||
|     } | ||||
| }; | ||||
| /* | ||||
| window.externalApp.externalBus = function(message) { | ||||
|     console.log("External bus message: " + message); | ||||
|     var messageObj = JSON.parse(message); | ||||
| @@ -32,4 +33,5 @@ window.externalApp.externalBus = function(message) { | ||||
|     } else if (messageObj.type == "config_screen/show") { | ||||
|         HAClient.postMessage('show-settings'); | ||||
|     } | ||||
| }; | ||||
| }; | ||||
| */ | ||||
							
								
								
									
										196
									
								
								lib/cards/badges.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								lib/cards/badges.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class Badges extends StatelessWidget { | ||||
|   final BadgesData badges; | ||||
|  | ||||
|   const Badges({Key key, this.badges}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     List<EntityWrapper> entitiesToShow = badges.getEntitiesToShow(); | ||||
|      | ||||
|     if (entitiesToShow.isNotEmpty) { | ||||
|       if (ConnectionManager().scrollBadges) { | ||||
|         return ConstrainedBox( | ||||
|           constraints: BoxConstraints.tightFor(height: 112), | ||||
|           child: SingleChildScrollView( | ||||
|             scrollDirection: Axis.horizontal, | ||||
|             child: Row( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               mainAxisAlignment: MainAxisAlignment.center, | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: entitiesToShow.map((entity) => | ||||
|                 EntityModel( | ||||
|                   entityWrapper: entity, | ||||
|                   child: Padding( | ||||
|                     padding: EdgeInsets.fromLTRB(5, 10, 5, 10), | ||||
|                     child: BadgeWidget(), | ||||
|                   ), | ||||
|                   handleTap: true, | ||||
|                 )).toList() | ||||
|             ), | ||||
|           ) | ||||
|         ); | ||||
|       } else { | ||||
|         return Padding( | ||||
|           padding: EdgeInsets.fromLTRB(5, 10, 5, 10), | ||||
|           child: Wrap( | ||||
|             alignment: WrapAlignment.center, | ||||
|             spacing: 10.0, | ||||
|             runSpacing: 5, | ||||
|             children: entitiesToShow.map((entity) => | ||||
|                 EntityModel( | ||||
|                   entityWrapper: entity, | ||||
|                   child: BadgeWidget(), | ||||
|                   handleTap: true, | ||||
|                 )).toList(), | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|     return Container(height: 0.0, width: 0.0,); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class BadgeWidget extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     Widget badgeIcon; | ||||
|     String onBadgeTextValue; | ||||
|     Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain); | ||||
|     switch (entityModel.entityWrapper.entity.domain) { | ||||
|       case "sun": | ||||
|         { | ||||
|           IconData iconData; | ||||
|           if (entityModel.entityWrapper.entity.state == "below_horizon") { | ||||
|             iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf0dc); | ||||
|           } else { | ||||
|             iconData = MaterialDesignIcons.getIconDataFromIconCode(0xf5a8); | ||||
|           } | ||||
|           badgeIcon = Padding( | ||||
|             padding: EdgeInsets.all(10), | ||||
|             child: Icon( | ||||
|               iconData, | ||||
|             ) | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "camera": | ||||
|       case "media_player": | ||||
|       case "binary_sensor": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|             imagePadding: EdgeInsets.all(0.0), | ||||
|             iconPadding: EdgeInsets.all(10), | ||||
|             color: Theme.of(context).textTheme.body2.color | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "device_tracker": | ||||
|       case "person": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|             imagePadding: EdgeInsets.all(0.0), | ||||
|             iconPadding: EdgeInsets.all(10), | ||||
|             color: Theme.of(context).textTheme.body2.color | ||||
|           ); | ||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.displayState; | ||||
|           break; | ||||
|         } | ||||
|       default: | ||||
|         { | ||||
|           onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement; | ||||
|           badgeIcon = Padding( | ||||
|             padding: EdgeInsets.all(4), | ||||
|             child: Text( | ||||
|               "${entityModel.entityWrapper.entity.displayState}", | ||||
|               overflow: TextOverflow.fade, | ||||
|               softWrap: false, | ||||
|               textAlign: TextAlign.center, | ||||
|               style: Theme.of(context).textTheme.body1 | ||||
|             ) | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|     } | ||||
|     Widget onBadgeText; | ||||
|     if (onBadgeTextValue == null || onBadgeTextValue.length == 0) { | ||||
|       onBadgeText = Container(width: 0.0, height: 0.0); | ||||
|     } else { | ||||
|       onBadgeText = Container( | ||||
|         constraints: BoxConstraints(maxWidth: 50), | ||||
|         padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0), | ||||
|         child: Text("$onBadgeTextValue", | ||||
|             style: Theme.of(context).textTheme.overline.copyWith( | ||||
|               color: HAClientTheme().getOnBadgeTextColor() | ||||
|             ), | ||||
|             textAlign: TextAlign.center, | ||||
|             softWrap: false, | ||||
|             overflow: TextOverflow.ellipsis | ||||
|           ), | ||||
|         decoration: new BoxDecoration( | ||||
|           color: iconColor, | ||||
|           borderRadius: BorderRadius.circular(9.0), | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|     return GestureDetector( | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: <Widget>[ | ||||
|             Stack( | ||||
|               overflow: Overflow.visible, | ||||
|               alignment: Alignment.center, | ||||
|               children: <Widget>[ | ||||
|                 Container( | ||||
|                   width: 45, | ||||
|                   height: 45, | ||||
|                   decoration: new BoxDecoration( | ||||
|                     // Circle shape | ||||
|                     shape: BoxShape.circle, | ||||
|                     color: Theme.of(context).cardColor, | ||||
|                     // The border you want | ||||
|                     border: Border.all( | ||||
|                       width: 2.0, | ||||
|                       color: iconColor, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 SizedBox( | ||||
|                   width: 41, | ||||
|                   height: 41, | ||||
|                   child: FittedBox( | ||||
|                     fit: BoxFit.contain, | ||||
|                     alignment: Alignment.center, | ||||
|                     child: badgeIcon, | ||||
|                   ) | ||||
|                 ), | ||||
|                 Positioned( | ||||
|                   bottom: -6, | ||||
|                   child: onBadgeText | ||||
|                 ) | ||||
|               ], | ||||
|             ), | ||||
|             Container( | ||||
|               constraints: BoxConstraints(maxWidth: 45), | ||||
|               padding: EdgeInsets.only(top: 10), | ||||
|               child: Text( | ||||
|                 "${entityModel.entityWrapper.displayName}", | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: Theme.of(context).textTheme.caption.copyWith( | ||||
|                   fontSize: 10 | ||||
|                 ), | ||||
|                 softWrap: true, | ||||
|                 maxLines: 3, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|         onTap: () => entityModel.entityWrapper.handleTap(), | ||||
|         onDoubleTap: () => entityModel.entityWrapper.handleDoubleTap(), | ||||
|         onLongPress: () => entityModel.entityWrapper.handleHold(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -13,17 +13,32 @@ class CardData { | ||||
|  | ||||
|   factory CardData.parse(rawData) { | ||||
|     try { | ||||
|       if (rawData['type'] == null) { | ||||
|         rawData['type'] = CardType.ENTITIES; | ||||
|       } else if (!(rawData['type'] is String)) { | ||||
|         return CardData(null); | ||||
|       } | ||||
|       switch (rawData['type']) { | ||||
|           case CardType.ENTITIES: | ||||
|           case CardType.HISTORY_GRAPH: | ||||
|           case CardType.MAP: | ||||
|           case CardType.PICTURE_GLANCE: | ||||
|           case CardType.SENSOR: | ||||
|           case CardType.ENTITY: | ||||
|           case CardType.WEATHER_FORECAST: | ||||
|           case CardType.PLANT_STATUS: | ||||
|             if (rawData['entity'] != null) { | ||||
|               rawData['entities'] = [rawData['entity']]; | ||||
|             } | ||||
|             return EntitiesCardData(rawData); | ||||
|             break; | ||||
|           case CardType.ALARM_PANEL: | ||||
|             return AlarmPanelCardData(rawData); | ||||
|             break; | ||||
|           case CardType.BUTTON: | ||||
|             return ButtonCardData(rawData); | ||||
|             break; | ||||
|           case CardType.ENTITY_BUTTON: | ||||
|           case CardType.LIGHT: | ||||
|           case CardType.BUTTON: | ||||
|           case CardType.PICTURE_ENTITY: | ||||
|             return ButtonCardData(rawData); | ||||
|             break; | ||||
|           case CardType.CONDITIONAL: | ||||
| @@ -42,6 +57,10 @@ class CardData { | ||||
|             return GaugeCardData(rawData); | ||||
|             break; | ||||
|           case CardType.GLANCE: | ||||
|           case CardType.THERMOSTAT: | ||||
|             if (rawData['entity'] != null) { | ||||
|               rawData['entities'] = [rawData['entity']]; | ||||
|             } | ||||
|             return GlanceCardData(rawData); | ||||
|             break; | ||||
|           case CardType.HORIZONTAL_STACK: | ||||
| @@ -56,14 +75,11 @@ class CardData { | ||||
|           case CardType.MEDIA_CONTROL: | ||||
|             return MediaControlCardData(rawData); | ||||
|             break; | ||||
|           case CardType.BADGES: | ||||
|             return BadgesData(rawData); | ||||
|             break; | ||||
|           default: | ||||
|             if (rawData.containsKey('entities')) { | ||||
|               return EntitiesCardData(rawData); | ||||
|             } else if (rawData.containsKey('entity')) { | ||||
|               rawData['entities'] = [rawData['entity']]; | ||||
|               return EntitiesCardData(rawData); | ||||
|             } | ||||
|             return CardData(rawData); | ||||
|             return CardData(null); | ||||
|         } | ||||
|     } catch (error, stacktrace) { | ||||
|       Logger.e('Error parsing card $rawData: $error', stacktrace: stacktrace); | ||||
| @@ -73,10 +89,10 @@ class CardData { | ||||
|  | ||||
|   CardData(rawData) { | ||||
|     if (rawData != null && rawData is Map) { | ||||
|       type = rawData['type'] ?? CardType.ENTITIES; | ||||
|       type = rawData['type']; | ||||
|       conditions = rawData['conditions'] ?? []; | ||||
|       showEmpty = rawData['show_empty'] ?? true; | ||||
|       stateFilter = rawData['state_filter'] ?? []; | ||||
|       stateFilter = rawData['state_filter']  ?? []; | ||||
|     } else { | ||||
|       type = CardType.UNKNOWN; | ||||
|       conditions = []; | ||||
| @@ -159,6 +175,64 @@ class CardData { | ||||
|  | ||||
| } | ||||
|  | ||||
| class BadgesData extends CardData { | ||||
|  | ||||
|   String title; | ||||
|   String icon; | ||||
|   bool showHeaderToggle; | ||||
|  | ||||
|   @override | ||||
|   Widget buildCardWidget() { | ||||
|     return Badges(badges: this); | ||||
|   } | ||||
|    | ||||
|   BadgesData(rawData) : super(rawData) { | ||||
|     if (rawData['badges'] is List) { | ||||
|       rawData['badges'].forEach((dynamic rawBadge) { | ||||
|         if (rawBadge is String && HomeAssistant().entities.isExist(rawBadge)) {   | ||||
|           entities.add(EntityWrapper(entity: HomeAssistant().entities.get(rawBadge))); | ||||
|         } else if (rawBadge is Map && rawBadge.containsKey('entity') && HomeAssistant().entities.isExist(rawBadge['entity'])) { | ||||
|           entities.add( | ||||
|             EntityWrapper( | ||||
|               entity: HomeAssistant().entities.get(rawBadge['entity']), | ||||
|               overrideName: rawBadge["name"], | ||||
|               overrideIcon: rawBadge["icon"], | ||||
|             ) | ||||
|           ); | ||||
|         } else if (rawBadge is Map && rawBadge.containsKey('entities')) { | ||||
|           _parseEntities(rawBadge); | ||||
|         } | ||||
|       });     | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _parseEntities(rawData) { | ||||
|     var rawEntities = rawData['entities'] ?? []; | ||||
|     rawEntities.forEach((rawEntity) { | ||||
|       if (rawEntity is String) { | ||||
|         if (HomeAssistant().entities.isExist(rawEntity)) { | ||||
|           entities.add(EntityWrapper( | ||||
|             entity: HomeAssistant().entities.get(rawEntity), | ||||
|             stateFilter: rawData['state_filter'] ?? [], | ||||
|           )); | ||||
|         } | ||||
|       } else if (HomeAssistant().entities.isExist('${rawEntity['entity']}')) { | ||||
|         Entity e = HomeAssistant().entities.get(rawEntity["entity"]); | ||||
|         entities.add( | ||||
|           EntityWrapper( | ||||
|               entity: e, | ||||
|               overrideName: rawEntity["name"], | ||||
|               overrideIcon: rawEntity["icon"], | ||||
|               stateFilter: rawEntity['state_filter'] ?? (rawData['state_filter'] ?? []), | ||||
|               uiAction: EntityUIAction(rawEntityData: rawEntity) | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class EntitiesCardData extends CardData { | ||||
|  | ||||
|   String title; | ||||
| @@ -172,12 +246,12 @@ class EntitiesCardData extends CardData { | ||||
|    | ||||
|   EntitiesCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     title = rawData["title"]; | ||||
|     icon = rawData['icon']; | ||||
|     title = rawData['title']; | ||||
|     icon = rawData['icon'] is String ? rawData['icon'] : null; | ||||
|     stateColor = rawData['state_color'] ?? false; | ||||
|     showHeaderToggle = rawData['show_header_toggle'] ?? false; | ||||
|     //Parsing entities | ||||
|     var rawEntities = rawData["entities"] ?? []; | ||||
|     var rawEntities = rawData['entities'] ?? []; | ||||
|     rawEntities.forEach((rawEntity) { | ||||
|       if (rawEntity is String) { | ||||
|         if (HomeAssistant().entities.isExist(rawEntity)) { | ||||
| @@ -299,7 +373,7 @@ class ButtonCardData extends CardData { | ||||
|   ButtonCardData(rawData) : super(rawData) { | ||||
|     //Parsing card data | ||||
|     name = rawData['name']; | ||||
|     icon = rawData['icon']; | ||||
|     icon = rawData['icon'] is String ? rawData['icon'] : null; | ||||
|     showName = rawData['show_name'] ?? true; | ||||
|     showIcon = rawData['show_icon'] ?? true; | ||||
|     stateColor = rawData['state_color'] ?? true; | ||||
| @@ -379,7 +453,7 @@ class GaugeCardData extends CardData { | ||||
|     } | ||||
|     severity = rawData['severity']; | ||||
|     //Parsing entity | ||||
|     var entitiId = rawData["entity"]; | ||||
|     var entitiId = rawData["entity"] is List ? rawData["entity"][0] : rawData["entity"]; | ||||
|     if (entitiId != null && entitiId is String) { | ||||
|       if (HomeAssistant().entities.isExist(entitiId)) { | ||||
|         entities.add(EntityWrapper( | ||||
| @@ -390,7 +464,7 @@ class GaugeCardData extends CardData { | ||||
|         entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||
|       } | ||||
|     } else { | ||||
|       entities.add(EntityWrapper(entity: Entity.missed(entitiId))); | ||||
|       entities.add(EntityWrapper(entity: Entity.missed('$entitiId'))); | ||||
|     } | ||||
|      | ||||
|   } | ||||
| @@ -460,7 +534,7 @@ class HorizontalStackCardData extends CardData { | ||||
|   } | ||||
|    | ||||
|   HorizontalStackCardData(rawData) : super(rawData) { | ||||
|     if (rawData.containsKey('cards')) { | ||||
|     if (rawData.containsKey('cards') && rawData['cards'] is List) { | ||||
|       childCards = rawData['cards'].map<CardData>((childCard) { | ||||
|         return CardData.parse(childCard); | ||||
|       }).toList(); | ||||
| @@ -481,7 +555,7 @@ class VerticalStackCardData extends CardData { | ||||
|   } | ||||
|    | ||||
|   VerticalStackCardData(rawData) : super(rawData) { | ||||
|     if (rawData.containsKey('cards')) { | ||||
|     if (rawData.containsKey('cards') && rawData['cards'] is List) { | ||||
|       childCards = rawData['cards'].map<CardData>((childCard) { | ||||
|         return CardData.parse(childCard); | ||||
|       }).toList(); | ||||
|   | ||||
| @@ -62,13 +62,16 @@ class EntityButtonCard extends StatelessWidget { | ||||
|           onLongPress: () => entityWrapper.handleHold(), | ||||
|           onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||
|           child: Center( | ||||
|             child: Column( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: <Widget>[ | ||||
|                 buttonIcon, | ||||
|                 _buildName(context) | ||||
|               ], | ||||
|             child: Padding( | ||||
|               padding: EdgeInsets.only(top: 5), | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                 children: <Widget>[ | ||||
|                   buttonIcon, | ||||
|                   _buildName(context) | ||||
|                 ], | ||||
|               ) | ||||
|             ) | ||||
|           ), | ||||
|         ), | ||||
|   | ||||
| @@ -30,10 +30,13 @@ class GlanceCard extends StatelessWidget { | ||||
|           start, end | ||||
|         ).map( | ||||
|           (EntityWrapper entity){ | ||||
|             return EntityModel( | ||||
|               entityWrapper: entity, | ||||
|               child: _buildEntityContainer(context, entity), | ||||
|               handleTap: true | ||||
|             return Padding( | ||||
|               padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||
|               child: EntityModel( | ||||
|                 entityWrapper: entity, | ||||
|                 child: _buildEntityContainer(context, entity), | ||||
|                 handleTap: true | ||||
|               ) | ||||
|             ); | ||||
|           } | ||||
|         ).toList() | ||||
| @@ -50,17 +53,22 @@ class GlanceCard extends StatelessWidget { | ||||
|       ); | ||||
|     } | ||||
|     return CardWrapper( | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: <Widget>[ | ||||
|           CardHeader(name: card.title), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||
|             child: Table( | ||||
|               children: rows | ||||
|             ) | ||||
|       child: Center( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|               CardHeader( | ||||
|                 name: card.title, | ||||
|                 emptyPadding: Sizes.rowPadding, | ||||
|               ), | ||||
|               Table( | ||||
|                 children: rows | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|         ], | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
| @@ -85,16 +93,14 @@ class GlanceCard extends StatelessWidget { | ||||
|       result.add(_buildState()); | ||||
|     } | ||||
|  | ||||
|     return Center( | ||||
|       child: InkResponse( | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: result, | ||||
|         ), | ||||
|         onTap: () => entityWrapper.handleTap(), | ||||
|         onLongPress: () => entityWrapper.handleHold(), | ||||
|         onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||
|     return InkResponse( | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: result, | ||||
|       ), | ||||
|       onTap: () => entityWrapper.handleTap(), | ||||
|       onLongPress: () => entityWrapper.handleHold(), | ||||
|       onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -7,11 +7,6 @@ class UnsupportedCard extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return CardWrapper( | ||||
|       child: Padding( | ||||
|         padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), | ||||
|         child: Text("'${card.type}' card is not supported yet"), | ||||
|       ) | ||||
|     ); | ||||
|     return Container(); | ||||
|   }   | ||||
| } | ||||
| @@ -36,66 +36,6 @@ class EntityState { | ||||
|  | ||||
| } | ||||
|  | ||||
| class EntityUIAction { | ||||
|   static const moreInfo = 'more-info'; | ||||
|   static const toggle = 'toggle'; | ||||
|   static const callService = 'call-service'; | ||||
|   static const navigate = 'navigate'; | ||||
|   static const none = 'none'; | ||||
|  | ||||
|   String tapAction = EntityUIAction.moreInfo; | ||||
|   String tapNavigationPath; | ||||
|   String tapService; | ||||
|   Map<String, dynamic> tapServiceData; | ||||
|   String holdAction = EntityUIAction.none; | ||||
|   String holdNavigationPath; | ||||
|   String holdService; | ||||
|   Map<String, dynamic> holdServiceData; | ||||
|   String doubleTapAction = EntityUIAction.none; | ||||
|   String doubleTapNavigationPath; | ||||
|   String doubleTapService; | ||||
|   Map<String, dynamic> doubleTapServiceData; | ||||
|  | ||||
|   EntityUIAction({rawEntityData}) { | ||||
|     if (rawEntityData != null) { | ||||
|       if (rawEntityData["tap_action"] != null) { | ||||
|         if (rawEntityData["tap_action"] is String) { | ||||
|           tapAction = rawEntityData["tap_action"]; | ||||
|         } else { | ||||
|           tapAction = | ||||
|               rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo; | ||||
|           tapNavigationPath = rawEntityData["tap_action"]["navigation_path"]; | ||||
|           tapService = rawEntityData["tap_action"]["service"]; | ||||
|           tapServiceData = rawEntityData["tap_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|       if (rawEntityData["hold_action"] != null) { | ||||
|         if (rawEntityData["hold_action"] is String) { | ||||
|           holdAction = rawEntityData["hold_action"]; | ||||
|         } else { | ||||
|           holdAction = | ||||
|               rawEntityData["hold_action"]["action"] ?? EntityUIAction.none; | ||||
|           holdNavigationPath = rawEntityData["hold_action"]["navigation_path"]; | ||||
|           holdService = rawEntityData["hold_action"]["service"]; | ||||
|           holdServiceData = rawEntityData["hold_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|       if (rawEntityData["double_tap_action"] != null) { | ||||
|         if (rawEntityData["double_tap_action"] is String) { | ||||
|           doubleTapAction = rawEntityData["double_tap_action"]; | ||||
|         } else { | ||||
|           doubleTapAction = | ||||
|               rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none; | ||||
|           doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"]; | ||||
|           doubleTapService = rawEntityData["double_tap_action"]["service"]; | ||||
|           doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class CardType { | ||||
|   static const HORIZONTAL_STACK = "horizontal-stack"; | ||||
|   static const VERTICAL_STACK = "vertical-stack"; | ||||
| @@ -113,6 +53,7 @@ class CardType { | ||||
|   static const IFRAME = "iframe"; | ||||
|   static const GAUGE = "gauge"; | ||||
|   static const ENTITY_BUTTON = "entity-button"; | ||||
|   static const ENTITY = "entity"; | ||||
|   static const BUTTON = "button"; | ||||
|   static const CONDITIONAL = "conditional"; | ||||
|   static const ALARM_PANEL = "alarm-panel"; | ||||
| @@ -120,6 +61,9 @@ class CardType { | ||||
|   static const LIGHT = "light"; | ||||
|   static const ENTITY_FILTER = "entity-filter"; | ||||
|   static const UNKNOWN = "unknown"; | ||||
|   static const HISTORY_GRAPH = "history-graph"; | ||||
|   static const PICTURE_GLANCE = "picture-glance"; | ||||
|   static const BADGES = "badges"; | ||||
| } | ||||
|  | ||||
| class Sizes { | ||||
|   | ||||
| @@ -1,148 +0,0 @@ | ||||
| part of '../main.dart'; | ||||
|  | ||||
| class BadgeWidget extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final entityModel = EntityModel.of(context); | ||||
|     double iconSize = 26.0; | ||||
|     Widget badgeIcon; | ||||
|     String onBadgeTextValue; | ||||
|     Color iconColor = HAClientTheme().getBadgeColor(entityModel.entityWrapper.entity.domain); | ||||
|     switch (entityModel.entityWrapper.entity.domain) { | ||||
|       case "sun": | ||||
|         { | ||||
|           badgeIcon = entityModel.entityWrapper.entity.state == "below_horizon" | ||||
|               ? Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconCode(0xf0dc), | ||||
|             size: iconSize, | ||||
|           ) | ||||
|               : Icon( | ||||
|             MaterialDesignIcons.getIconDataFromIconCode(0xf5a8), | ||||
|             size: iconSize, | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "camera": | ||||
|       case "media_player": | ||||
|       case "binary_sensor": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|             padding: EdgeInsets.all(0.0), | ||||
|             size: iconSize, | ||||
|             color: Theme.of(context).textTheme.body1.color | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       case "device_tracker": | ||||
|       case "person": | ||||
|         { | ||||
|           badgeIcon = EntityIcon( | ||||
|               padding: EdgeInsets.all(0.0), | ||||
|               size: iconSize, | ||||
|               color: Theme.of(context).textTheme.body1.color | ||||
|           ); | ||||
|           onBadgeTextValue = entityModel.entityWrapper.entity.displayState; | ||||
|           break; | ||||
|         } | ||||
|       default: | ||||
|         { | ||||
|           double stateFontSize; | ||||
|           if (entityModel.entityWrapper.entity.displayState.length <= 3) { | ||||
|             stateFontSize = 18.0; | ||||
|           } else if (entityModel.entityWrapper.entity.displayState.length <= 4) { | ||||
|             stateFontSize = 15.0; | ||||
|           } else if (entityModel.entityWrapper.entity.displayState.length <= 6) { | ||||
|             stateFontSize = 10.0; | ||||
|           } else if (entityModel.entityWrapper.entity.displayState.length <= 10) { | ||||
|             stateFontSize = 8.0; | ||||
|           } | ||||
|           onBadgeTextValue = entityModel.entityWrapper.unitOfMeasurement; | ||||
|           badgeIcon = Center( | ||||
|             child: Text( | ||||
|               "${entityModel.entityWrapper.entity.displayState}", | ||||
|               overflow: TextOverflow.fade, | ||||
|               softWrap: false, | ||||
|               textAlign: TextAlign.center, | ||||
|               style: Theme.of(context).textTheme.body1.copyWith( | ||||
|                 fontSize: stateFontSize | ||||
|               ) | ||||
|             ), | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|     } | ||||
|     Widget onBadgeText; | ||||
|     if (onBadgeTextValue == null || onBadgeTextValue.length == 0) { | ||||
|       onBadgeText = Container(width: 0.0, height: 0.0); | ||||
|     } else { | ||||
|       onBadgeText = Container( | ||||
|           padding: EdgeInsets.fromLTRB(6.0, 2.0, 6.0, 2.0), | ||||
|           child: Text("$onBadgeTextValue", | ||||
|               style: Theme.of(context).textTheme.overline.copyWith( | ||||
|                 color: HAClientTheme().getOnBadgeTextColor() | ||||
|               ), | ||||
|               textAlign: TextAlign.center, | ||||
|               softWrap: false, | ||||
|               overflow: TextOverflow.fade), | ||||
|           decoration: new BoxDecoration( | ||||
|             // Circle shape | ||||
|             //shape: BoxShape.circle, | ||||
|             color: iconColor, | ||||
|             borderRadius: BorderRadius.circular(9.0), | ||||
|           )); | ||||
|     } | ||||
|     return GestureDetector( | ||||
|         child: Column( | ||||
|           children: <Widget>[ | ||||
|             Container( | ||||
|               margin: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0), | ||||
|               width: 50.0, | ||||
|               height: 50.0, | ||||
|               decoration: new BoxDecoration( | ||||
|                 // Circle shape | ||||
|                 shape: BoxShape.circle, | ||||
|                 color: Theme.of(context).cardColor, | ||||
|                 // The border you want | ||||
|                 border: new Border.all( | ||||
|                   width: 2.0, | ||||
|                   color: iconColor, | ||||
|                 ), | ||||
|               ), | ||||
|               child: Stack( | ||||
|                 overflow: Overflow.visible, | ||||
|                 children: <Widget>[ | ||||
|                   Positioned( | ||||
|                     width: 46.0, | ||||
|                     height: 46.0, | ||||
|                     top: 0.0, | ||||
|                     left: 0.0, | ||||
|                     child: badgeIcon, | ||||
|                   ), | ||||
|                   Positioned( | ||||
|                     //width: 50.0, | ||||
|                       bottom: -9.0, | ||||
|                       left: -10.0, | ||||
|                       right: -10.0, | ||||
|                       child: Center( | ||||
|                         child: onBadgeText, | ||||
|                       )) | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|             Container( | ||||
|               width: 60.0, | ||||
|               child: Text( | ||||
|                 "${entityModel.entityWrapper.displayName}", | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: Theme.of(context).textTheme.caption, | ||||
|                 softWrap: true, | ||||
|                 maxLines: 3, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         onTap: () => | ||||
|             eventBus.fire(new ShowEntityPageEvent(entityId: entityModel.entityWrapper.entity.entityId))); | ||||
|   } | ||||
| } | ||||
| @@ -11,7 +11,7 @@ class ModeSelectorWidget extends StatelessWidget { | ||||
|   ModeSelectorWidget({ | ||||
|     Key key, | ||||
|     @required this.caption, | ||||
|     @required this.options, | ||||
|     this.options: const [], | ||||
|     this.value, | ||||
|     @required this.onChange, | ||||
|     this.padding: const EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, 0.0), | ||||
|   | ||||
| @@ -79,7 +79,8 @@ class Entity { | ||||
|   ); | ||||
|  | ||||
|   String get displayName => | ||||
|       attributes["friendly_name"] ?? (attributes["name"] ?? entityId.split(".")[1].replaceAll("_", " ")); | ||||
|       attributes["friendly_name"] ?? | ||||
|       (attributes["name"] ?? (entityId != null && entityId.contains('.')) ? entityId.split(".")[1].replaceAll("_", " ") : ""); | ||||
|  | ||||
|   bool get isView => | ||||
|       (domain == "group") && | ||||
| @@ -209,14 +210,6 @@ class Entity { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget buildBadgeWidget(BuildContext context) { | ||||
|     return EntityModel( | ||||
|       entityWrapper: EntityWrapper(entity: this), | ||||
|       child: BadgeWidget(), | ||||
|       handleTap: true, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String getAttribute(String attributeName) { | ||||
|     if (attributes != null) { | ||||
|       return attributes["$attributeName"].toString(); | ||||
|   | ||||
| @@ -3,10 +3,12 @@ part of '../main.dart'; | ||||
| class EntityIcon extends StatelessWidget { | ||||
|  | ||||
|   final EdgeInsetsGeometry padding; | ||||
|   final EdgeInsetsGeometry iconPadding; | ||||
|   final EdgeInsetsGeometry imagePadding; | ||||
|   final double size; | ||||
|   final Color color; | ||||
|  | ||||
|   const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0)}) : super(key: key); | ||||
|   const EntityIcon({Key key, this.color, this.size: Sizes.iconSize, this.padding: const EdgeInsets.all(0.0), this.iconPadding, this.imagePadding}) : super(key: key); | ||||
|  | ||||
|   int getDefaultIconByEntityId(String entityId, String deviceClass, String state) { | ||||
|     if (entityId == null) { | ||||
| @@ -26,76 +28,6 @@ class EntityIcon extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget buildIcon(BuildContext context, EntityWrapper data, Color color) { | ||||
|     Widget result; | ||||
|     if (data == null) { | ||||
|       return null; | ||||
|     } | ||||
|     if (data.entityPicture != null) { | ||||
|       result = Container( | ||||
|         height: size+12, | ||||
|         width: size+12, | ||||
|         decoration: BoxDecoration( | ||||
|             shape: BoxShape.circle, | ||||
|             image: DecorationImage( | ||||
|               fit:BoxFit.cover, | ||||
|               image: CachedNetworkImageProvider( | ||||
|                 "${data.entityPicture}" | ||||
|               ), | ||||
|             ) | ||||
|         ), | ||||
|       ); | ||||
|     } else { | ||||
|       String iconName = data.icon; | ||||
|       int iconCode = 0; | ||||
|       if (iconName.length > 0) { | ||||
|         iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); | ||||
|       } else { | ||||
|         iconCode = getDefaultIconByEntityId(data.entity.entityId, | ||||
|             data.entity.deviceClass, data.entity.state); // | ||||
|       } | ||||
|       result = Icon( | ||||
|         IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||
|         size: size, | ||||
|         color: color, | ||||
|       ); | ||||
|       if (data.entity is LightEntity && | ||||
|         (data.entity as LightEntity).supportColor && | ||||
|         (data.entity as LightEntity).color != null | ||||
|         ) { | ||||
|         Color lightColor = (data.entity as LightEntity).color.toColor(); | ||||
|         if (lightColor == Colors.white) { | ||||
|           return result; | ||||
|         }   | ||||
|         result = Stack( | ||||
|           children: <Widget>[ | ||||
|             result, | ||||
|             Positioned( | ||||
|               bottom: 0, | ||||
|               right: 0, | ||||
|               child: Container( | ||||
|                 width: size / 3, | ||||
|                 height: size / 3, | ||||
|                 decoration: BoxDecoration( | ||||
|                   color: lightColor, | ||||
|                   shape: BoxShape.circle, | ||||
|                   boxShadow: <BoxShadow>[ | ||||
|                     BoxShadow( | ||||
|                       spreadRadius: 0, | ||||
|                       blurRadius: 0, | ||||
|                       offset: Offset(0.3, 0.3) | ||||
|                     ) | ||||
|                   ] | ||||
|                 ), | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
| @@ -107,13 +39,92 @@ class EntityIcon extends StatelessWidget { | ||||
|     } else { | ||||
|       iconColor = HAClientTheme().getOffStateColor(context); | ||||
|     } | ||||
|     Widget iconWidget; | ||||
|     bool isPicture = false; | ||||
|     if (entityWrapper == null) { | ||||
|       iconWidget = Container( | ||||
|         width: size, | ||||
|         height: size, | ||||
|       ); | ||||
|     } else { | ||||
|       if (entityWrapper.entityPicture != null) { | ||||
|         iconWidget = Container( | ||||
|           height: size+12, | ||||
|           width: size+12, | ||||
|           decoration: BoxDecoration( | ||||
|               shape: BoxShape.circle, | ||||
|               image: DecorationImage( | ||||
|                 fit:BoxFit.cover, | ||||
|                 image: CachedNetworkImageProvider( | ||||
|                   "${entityWrapper.entityPicture}" | ||||
|                 ), | ||||
|               ) | ||||
|           ), | ||||
|         ); | ||||
|         isPicture = true; | ||||
|       } else { | ||||
|         String iconName = entityWrapper.icon; | ||||
|         int iconCode = 0; | ||||
|         if (iconName.length > 0) { | ||||
|           iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); | ||||
|         } else { | ||||
|           iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId, | ||||
|               entityWrapper.entity.deviceClass, entityWrapper.entity.state); // | ||||
|         } | ||||
|         if (entityWrapper.entity is LightEntity && | ||||
|           (entityWrapper.entity as LightEntity).supportColor && | ||||
|           (entityWrapper.entity as LightEntity).color != null && | ||||
|           (entityWrapper.entity as LightEntity).color.toColor() != Colors.white | ||||
|           ) { | ||||
|           Color lightColor = (entityWrapper.entity as LightEntity).color.toColor();   | ||||
|           iconWidget = Stack( | ||||
|             children: <Widget>[ | ||||
|               Icon( | ||||
|                 IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||
|                 size: size, | ||||
|                 color: iconColor, | ||||
|               ), | ||||
|               Positioned( | ||||
|                 bottom: 0, | ||||
|                 right: 0, | ||||
|                 child: Container( | ||||
|                   width: size / 3, | ||||
|                   height: size / 3, | ||||
|                   decoration: BoxDecoration( | ||||
|                     color: lightColor, | ||||
|                     shape: BoxShape.circle, | ||||
|                     boxShadow: <BoxShadow>[ | ||||
|                       BoxShadow( | ||||
|                         spreadRadius: 0, | ||||
|                         blurRadius: 0, | ||||
|                         offset: Offset(0.3, 0.3) | ||||
|                       ) | ||||
|                     ] | ||||
|                   ), | ||||
|                 ), | ||||
|               ) | ||||
|             ], | ||||
|           ); | ||||
|         } else { | ||||
|           iconWidget = Icon( | ||||
|             IconData(iconCode, fontFamily: 'Material Design Icons'), | ||||
|             size: size, | ||||
|             color: iconColor, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     EdgeInsetsGeometry computedPadding; | ||||
|     if (isPicture && imagePadding != null) { | ||||
|       computedPadding = imagePadding; | ||||
|     } else if (!isPicture && iconPadding != null) { | ||||
|       computedPadding = iconPadding; | ||||
|     } else { | ||||
|       computedPadding = padding; | ||||
|     } | ||||
|     return Padding( | ||||
|       padding: padding, | ||||
|       child: buildIcon( | ||||
|         context, | ||||
|         entityWrapper, | ||||
|         iconColor  | ||||
|       ), | ||||
|       padding: computedPadding, | ||||
|       child: iconWidget, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -154,4 +154,64 @@ class EntityWrapper { | ||||
|       } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class EntityUIAction { | ||||
|   static const moreInfo = 'more-info'; | ||||
|   static const toggle = 'toggle'; | ||||
|   static const callService = 'call-service'; | ||||
|   static const navigate = 'navigate'; | ||||
|   static const none = 'none'; | ||||
|  | ||||
|   String tapAction = EntityUIAction.moreInfo; | ||||
|   String tapNavigationPath; | ||||
|   String tapService; | ||||
|   Map<String, dynamic> tapServiceData; | ||||
|   String holdAction = EntityUIAction.moreInfo; | ||||
|   String holdNavigationPath; | ||||
|   String holdService; | ||||
|   Map<String, dynamic> holdServiceData; | ||||
|   String doubleTapAction = EntityUIAction.none; | ||||
|   String doubleTapNavigationPath; | ||||
|   String doubleTapService; | ||||
|   Map<String, dynamic> doubleTapServiceData; | ||||
|  | ||||
|   EntityUIAction({rawEntityData}) { | ||||
|     if (rawEntityData != null) { | ||||
|       if (rawEntityData["tap_action"] != null) { | ||||
|         if (rawEntityData["tap_action"] is String) { | ||||
|           tapAction = rawEntityData["tap_action"]; | ||||
|         } else { | ||||
|           tapAction = | ||||
|               rawEntityData["tap_action"]["action"] ?? EntityUIAction.moreInfo; | ||||
|           tapNavigationPath = rawEntityData["tap_action"]["navigation_path"]; | ||||
|           tapService = rawEntityData["tap_action"]["service"]; | ||||
|           tapServiceData = rawEntityData["tap_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|       if (rawEntityData["hold_action"] != null) { | ||||
|         if (rawEntityData["hold_action"] is String) { | ||||
|           holdAction = rawEntityData["hold_action"]; | ||||
|         } else { | ||||
|           holdAction = | ||||
|               rawEntityData["hold_action"]["action"] ?? EntityUIAction.none; | ||||
|           holdNavigationPath = rawEntityData["hold_action"]["navigation_path"]; | ||||
|           holdService = rawEntityData["hold_action"]["service"]; | ||||
|           holdServiceData = rawEntityData["hold_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|       if (rawEntityData["double_tap_action"] != null) { | ||||
|         if (rawEntityData["double_tap_action"] is String) { | ||||
|           doubleTapAction = rawEntityData["double_tap_action"]; | ||||
|         } else { | ||||
|           doubleTapAction = | ||||
|               rawEntityData["double_tap_action"]["action"] ?? EntityUIAction.none; | ||||
|           doubleTapNavigationPath = rawEntityData["double_tap_action"]["navigation_path"]; | ||||
|           doubleTapService = rawEntityData["double_tap_action"]["service"]; | ||||
|           doubleTapServiceData = rawEntityData["double_tap_action"]["service_data"]; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -26,11 +26,11 @@ class TimerEntity extends Entity { | ||||
|               seconds: int.tryParse(durationList[2]) ?? 0 | ||||
|           ); | ||||
|         } else { | ||||
|           Logger.e("Strange $entityId duration format: $durationSource"); | ||||
|           Logger.e("Strange timer duration format: $durationSource"); | ||||
|           duration = Duration(seconds: 0); | ||||
|         } | ||||
|       } catch (e, stacktrace) { | ||||
|         Logger.e("Error parsing duration for $entityId: $e", stacktrace: stacktrace); | ||||
|         Logger.e("Error parsing timer duration \'$durationSource\': $e", stacktrace: stacktrace); | ||||
|         duration = Duration(seconds: 0); | ||||
|       } | ||||
|     } else { | ||||
|   | ||||
| @@ -140,9 +140,9 @@ class HomeAssistant { | ||||
|  | ||||
|   Future _getConfig(SharedPreferences sharedPrefs) async { | ||||
|     _instanceConfig?.clear(); | ||||
|     if (sharedPrefs != null) { | ||||
|     if (sharedPrefs != null && sharedPrefs.containsKey('cached_config')) { | ||||
|       try { | ||||
|         var data = json.decode(sharedPrefs.getString('cached_config') ?? '{}'); | ||||
|         var data = json.decode(sharedPrefs.getString('cached_config')); | ||||
|         _parseConfig(data); | ||||
|       } catch (e, stacktrace) { | ||||
|        Logger.e('Error gettong config from cache: $e', stacktrace: stacktrace); | ||||
| @@ -160,10 +160,10 @@ class HomeAssistant { | ||||
|   } | ||||
|  | ||||
|   Future _getStates(SharedPreferences sharedPrefs) async { | ||||
|     if (sharedPrefs != null) { | ||||
|     if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) { | ||||
|       try { | ||||
|         var data = json.decode(sharedPrefs.getString('cached_states') ?? '[]'); | ||||
|         _parseStates(data); | ||||
|         var data = json.decode(sharedPrefs.getString('cached_states')); | ||||
|         _parseStates(data ?? []); | ||||
|       } catch (e, stacktrace) { | ||||
|         Logger.e('Error getting cached states: $e', stacktrace: stacktrace); | ||||
|       } | ||||
| @@ -171,7 +171,7 @@ class HomeAssistant { | ||||
|       await ConnectionManager().sendSocketMessage(type: "get_states").then( | ||||
|               (data) => _parseStates(data) | ||||
|       ).catchError((e) { | ||||
|        Logger.e('get_states error: $e'); | ||||
|         Logger.e('get_states error: $e'); | ||||
|         throw HACException("Error getting states: $e"); | ||||
|       }); | ||||
|     } | ||||
| @@ -183,9 +183,9 @@ class HomeAssistant { | ||||
|   } | ||||
|  | ||||
|   Future _getLovelace(SharedPreferences sharedPrefs) { | ||||
|     if (sharedPrefs != null) { | ||||
|     if (sharedPrefs != null && sharedPrefs.containsKey('cached_lovelace')) { | ||||
|       try { | ||||
|         var data = json.decode(sharedPrefs.getString('cached_lovelace') ?? '{}'); | ||||
|         var data = json.decode(sharedPrefs.getString('cached_lovelace')); | ||||
|         _rawLovelaceData = data; | ||||
|       } catch (e) { | ||||
|         autoUi = true; | ||||
| @@ -194,7 +194,7 @@ class HomeAssistant { | ||||
|     } else { | ||||
|       Completer completer = Completer(); | ||||
|       var additionalData; | ||||
|       if (_lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) { | ||||
|       if (ConnectionManager().haVersion >= 107 && _lovelaceDashbordUrl != HomeAssistant.DEFAULT_DASHBOARD) { | ||||
|         additionalData = { | ||||
|           'url_path': _lovelaceDashbordUrl | ||||
|         }; | ||||
| @@ -221,10 +221,10 @@ class HomeAssistant { | ||||
|  | ||||
|   Future _getServices(SharedPreferences prefs) async { | ||||
|     services?.clear();	 | ||||
|     if (prefs != null) { | ||||
|     if (prefs != null && prefs.containsKey('cached_services')) { | ||||
|       try { | ||||
|         var data = json.decode(prefs.getString('cached_services') ?? '{}'); | ||||
|         _parseServices(data); | ||||
|         var data = json.decode(prefs.getString('cached_services')); | ||||
|         _parseServices(data ?? {}); | ||||
|       } catch (e, stacktrace) { | ||||
|        Logger.e(e, stacktrace: stacktrace);   | ||||
|       } | ||||
| @@ -240,9 +240,19 @@ class HomeAssistant { | ||||
|  | ||||
|   Future _getUserInfo(SharedPreferences sharedPrefs) async { | ||||
|     _userName = null; | ||||
|     await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) { | ||||
|       Logger.e('auth/current_user error: $e'); | ||||
|     }); | ||||
|     if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) { | ||||
|       try { | ||||
|         var data = json.decode(sharedPrefs.getString('cached_user')); | ||||
|         _parseUserInfo(data ?? {}); | ||||
|       } catch (e, stacktrace) { | ||||
|         Logger.e('Error getting cached user info: $e', stacktrace: stacktrace); | ||||
|       } | ||||
|       return Future.value(); | ||||
|     } else { | ||||
|       await ConnectionManager().sendSocketMessage(type: "auth/current_user").then((data) => _parseUserInfo(data)).catchError((e) { | ||||
|         Logger.e('auth/current_user error: $e'); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _parseUserInfo(data) { | ||||
| @@ -251,10 +261,10 @@ class HomeAssistant { | ||||
|   } | ||||
|  | ||||
|   Future _getPanels(SharedPreferences sharedPrefs) async { | ||||
|     if (sharedPrefs != null) { | ||||
|     if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) { | ||||
|       try { | ||||
|         var data = json.decode(sharedPrefs.getString('cached_panels') ?? '{}'); | ||||
|         _parsePanels(data); | ||||
|         var data = json.decode(sharedPrefs.getString('cached_panels')); | ||||
|         _parsePanels(data ?? {}); | ||||
|       } catch (e, stacktrace) { | ||||
|         Logger.e(e, stacktrace: stacktrace); | ||||
|         panels.clear(); | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:async'; | ||||
| import 'dart:isolate'; | ||||
| import 'dart:ui'; | ||||
| import 'dart:math' as math; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/rendering.dart'; | ||||
| @@ -23,8 +25,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||||
| import 'package:in_app_purchase/in_app_purchase.dart'; | ||||
| import 'plugins/dynamic_multi_column_layout.dart'; | ||||
| import 'plugins/spoiler_card.dart'; | ||||
| import 'package:workmanager/workmanager.dart' as workManager; | ||||
| import 'package:geolocator/geolocator.dart'; | ||||
| //import 'package:workmanager/workmanager.dart' as workManager; | ||||
| //import 'package:geolocator/geolocator.dart'; | ||||
| import 'package:background_locator/background_locator.dart'; | ||||
| import 'package:background_locator/location_dto.dart'; | ||||
| import 'package:background_locator/location_settings.dart'; | ||||
| import 'package:battery/battery.dart'; | ||||
| import 'package:firebase_crashlytics/firebase_crashlytics.dart'; | ||||
| import 'package:flutter_webview_plugin/flutter_webview_plugin.dart' as standaloneWebview; | ||||
| @@ -58,7 +63,6 @@ part 'entities/fan/fan_entity.class.dart'; | ||||
| part 'entities/automation/automation_entity.class.dart'; | ||||
| part 'entities/camera/camera_entity.class.dart'; | ||||
| part 'entities/alarm_control_panel/alarm_control_panel_entity.class.dart'; | ||||
| part 'entities/badge.widget.dart'; | ||||
| part 'entities/entity_model.widget.dart'; | ||||
| part 'entities/default_entity_container.widget.dart'; | ||||
| part 'entities/missed_entity.widget.dart'; | ||||
| @@ -153,14 +157,16 @@ part 'entities/media_player/widgets/media_player_progress_bar.widget.dart'; | ||||
| part 'pages/whats_new.page.dart'; | ||||
| part 'pages/fullscreen.page.dart'; | ||||
| part 'popups.dart'; | ||||
| part 'cards/badges.dart'; | ||||
|  | ||||
| EventBus eventBus = new EventBus(); | ||||
| final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); | ||||
| FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin(); | ||||
| const String appName = "HA Client"; | ||||
| const appVersionNumber = "1.0.0"; | ||||
| const appVersionAdd = ""; | ||||
| const appVersion = "$appVersionNumber$appVersionAdd"; | ||||
| const String appName = 'HA Client'; | ||||
| const appVersionNumber = '1.0.1'; | ||||
| final String appVersionAdd = secrets['version_type'] ?? ''; | ||||
| final String appVersion = '$appVersionNumber${appVersionAdd.isNotEmpty ? '-' : ''}$appVersionAdd'; | ||||
| const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.0.1.md'; | ||||
|  | ||||
| Future<void> _reportError(dynamic error, dynamic stackTrace) async { | ||||
|     // Print the exception to the console. | ||||
| @@ -212,9 +218,12 @@ class _HAClientAppState extends State<HAClientApp> { | ||||
|   StreamSubscription<List<PurchaseDetails>> _purchaseUpdateSubscription; | ||||
|   StreamSubscription _themeChangeSubscription; | ||||
|   AppTheme _currentTheme = AppTheme.defaultTheme; | ||||
|  | ||||
|   ReceivePort port = ReceivePort(); | ||||
|    | ||||
|   @override | ||||
|   void initState() { | ||||
|  | ||||
|     InAppPurchaseConnection.enablePendingPurchases(); | ||||
|     final Stream purchaseUpdates = | ||||
|         InAppPurchaseConnection.instance.purchaseUpdatedStream; | ||||
| @@ -227,11 +236,18 @@ class _HAClientAppState extends State<HAClientApp> { | ||||
|         _currentTheme = event.theme; | ||||
|       }); | ||||
|     }); | ||||
|     /* | ||||
|     workManager.Workmanager.initialize( | ||||
|       updateDeviceLocationIsolate, | ||||
|       isInDebugMode: false | ||||
|     ); | ||||
|     */ | ||||
|     super.initState(); | ||||
|     IsolateNameServer.registerPortWithName(port.sendPort, LocationManager.isolateName); | ||||
|     port.listen((dynamic data) { | ||||
|       // do something with data | ||||
|     }); | ||||
|     initPlatformState(); | ||||
|   } | ||||
|  | ||||
|   void _handlePurchaseUpdates(purchase) { | ||||
| @@ -252,6 +268,10 @@ class _HAClientAppState extends State<HAClientApp> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> initPlatformState() async { | ||||
|     await BackgroundLocator.initialize(); | ||||
|   } | ||||
|  | ||||
|   // This widget is the root of your application. | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   | ||||
| @@ -19,6 +19,8 @@ class ConnectionManager { | ||||
|   String _tempToken; | ||||
|   String oauthUrl; | ||||
|   String webhookId; | ||||
|   double haVersion; | ||||
|   bool scrollBadges; | ||||
|   String mobileAppDeviceName; | ||||
|   bool settingsLoaded = false; | ||||
|   int appIntegrationVersion; | ||||
| @@ -47,6 +49,7 @@ class ConnectionManager { | ||||
|       webhookId = prefs.getString('app-webhook-id'); | ||||
|       appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0; | ||||
|       mobileAppDeviceName = prefs.getString('app-integration-device-name'); | ||||
|       scrollBadges = prefs.getBool('scroll-badges') ?? true; | ||||
|       displayHostname = "$_domain:$_port"; | ||||
|       _webSocketAPIEndpoint = | ||||
|       "${prefs.getString('hassio-protocol')}://$_domain:$_port/api/websocket"; | ||||
| @@ -154,7 +157,12 @@ class ConnectionManager { | ||||
|                     if (!connecting.isCompleted) connecting.completeError(e); | ||||
|                   }); | ||||
|                 } else if (data["type"] == "auth_ok") { | ||||
|                   Logger.d("[Received] <== ${data.toString()}"); | ||||
|                   String v = data["ha_version"]; | ||||
|                   if (v != null && v.isNotEmpty) { | ||||
|                     haVersion = double.tryParse(v.replaceFirst('0.','')) ?? 0; | ||||
|                   } | ||||
|                   Logger.d("Home assistant version: $v ($haVersion)"); | ||||
|                   Crashlytics.instance.setString('ha_version', v); | ||||
|                   Logger.d("[Connection] Subscribing to events"); | ||||
|                   sendSocketMessage( | ||||
|                     type: "subscribe_events", | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class LocationManager { | ||||
|         defaultUpdateIntervalMinutes); | ||||
|     _isRunning = prefs.getBool("location-enabled") ?? false; | ||||
|     if (_isRunning) { | ||||
|       await _startLocationService(); | ||||
|       //await _startLocationService(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -57,6 +57,20 @@ class LocationManager { | ||||
|   } | ||||
|  | ||||
|   _startLocationService() async { | ||||
|     Logger.d('Starting location tracking'); | ||||
|     BackgroundLocator.registerLocationUpdate( | ||||
|         locationCallback, | ||||
|         //optional | ||||
|         androidNotificationCallback: locationNotificationCallback, | ||||
|         settings: LocationSettings( | ||||
|             notificationTitle: "HA Client location tracking", | ||||
|             notificationMsg: "HA Client is updating your device location", | ||||
|             wakeLockTime: 20, | ||||
|             autoStop: false, | ||||
|             interval: 10 | ||||
|         ), | ||||
|     ); | ||||
|     /* | ||||
|     String webhookId = ConnectionManager().webhookId; | ||||
|     String httpWebHost = ConnectionManager().httpWebHost; | ||||
|     if (webhookId != null && webhookId.isNotEmpty) { | ||||
| @@ -100,53 +114,129 @@ class LocationManager { | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|     */ | ||||
|   } | ||||
|  | ||||
|   _stopLocationService() async { | ||||
|     Logger.d("Canceling previous schedule if any..."); | ||||
|     await workManager.Workmanager.cancelAll(); | ||||
|     Logger.d('Stopping location tracking'); | ||||
|     IsolateNameServer.removePortNameMapping(isolateName); | ||||
|     BackgroundLocator.unRegisterLocationUpdate(); | ||||
|     /*Logger.d("Canceling previous schedule if any..."); | ||||
|     await workManager.Workmanager.cancelAll();*/ | ||||
|   } | ||||
|  | ||||
|   static const String isolateName = "HAClientLocatorIsolate"; | ||||
|  | ||||
|   static void locationCallback(LocationDto locationDto) async { | ||||
|     print('[Background location] Got location: $locationDto'); | ||||
|     sendLocationData(locationDto); | ||||
|     final SendPort send = IsolateNameServer.lookupPortByName(isolateName); | ||||
|     send?.send(locationDto); | ||||
|   } | ||||
|  | ||||
|   static Future<void> sendLocationData(LocationDto location) async { | ||||
|     print('[Background location] Loading settings...'); | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     String domain = prefs.getString('hassio-domain'); | ||||
|     String port = prefs.getString('hassio-port'); | ||||
|     String webhookId = prefs.getString('app-webhook-id'); | ||||
|     String httpWebHost = | ||||
|       "${prefs.getString('hassio-res-protocol')}://$domain:$port"; | ||||
|     if (webhookId != null && webhookId.isNotEmpty) { | ||||
|       String url = "$httpWebHost/api/webhook/$webhookId"; | ||||
|       Map<String, String> headers = {}; | ||||
|       headers["Content-Type"] = "application/json"; | ||||
|       Map data = { | ||||
|         "type": "update_location", | ||||
|         "data": { | ||||
|           "gps": [], | ||||
|           "gps_accuracy": 0, | ||||
|           "battery": 100 | ||||
|         } | ||||
|       }; | ||||
|       try { | ||||
|         if (location.longitude != null && location.latitude != null) { | ||||
|           data["data"]["gps"] = [location.latitude, location.longitude]; | ||||
|           data["data"]["gps_accuracy"] = location.accuracy; | ||||
|           print('[Background location] Sending...'); | ||||
|           try { | ||||
|             http.Response response = await http.post( | ||||
|                 url, | ||||
|                 headers: headers, | ||||
|                 body: json.encode(data) | ||||
|             ); | ||||
|             if (response.statusCode >= 200 && response.statusCode < 300) { | ||||
|               print('[Background location] Success!'); | ||||
|             } else { | ||||
|               print('[Background location] Error sending data: ${response.statusCode}'); | ||||
|             } | ||||
|           } catch(e) { | ||||
|             print('[Background location] Error sending data: $e'); | ||||
|           } | ||||
|         } else { | ||||
|           print('[Background location] Error. Location is null'); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         print('[Background location] Error: $e'); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
|   static void locationNotificationCallback() { | ||||
|     print('[Background location] User clicked on the notification'); | ||||
|   } | ||||
|  | ||||
|   updateDeviceLocation() async { | ||||
|     Logger.d("[Foreground location] Started"); | ||||
|     Geolocator geolocator = Geolocator(); | ||||
|     var battery = Battery(); | ||||
|     String webhookId = ConnectionManager().webhookId; | ||||
|     String httpWebHost = ConnectionManager().httpWebHost; | ||||
|     if (webhookId != null && webhookId.isNotEmpty) { | ||||
|         Logger.d("[Foreground location] Getting battery level..."); | ||||
|         int batteryLevel = await battery.batteryLevel; | ||||
|         Logger.d("[Foreground location] Getting device location..."); | ||||
|         Position position = await geolocator.getCurrentPosition( | ||||
|             desiredAccuracy: LocationAccuracy.high, | ||||
|             locationPermissionLevel: GeolocationPermission.locationAlways | ||||
|           ); | ||||
|         if (position != null) { | ||||
|           Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})"); | ||||
|           String url = "$httpWebHost/api/webhook/$webhookId"; | ||||
|           Map data = { | ||||
|             "type": "update_location", | ||||
|             "data": { | ||||
|               "gps": [position.latitude, position.longitude], | ||||
|               "gps_accuracy": position.accuracy, | ||||
|               "battery": batteryLevel ?? 100 | ||||
|     /* | ||||
|     try { | ||||
|       Logger.d("[Foreground location] Started"); | ||||
|       Geolocator geolocator = Geolocator(); | ||||
|       var battery = Battery(); | ||||
|       String webhookId = ConnectionManager().webhookId; | ||||
|       String httpWebHost = ConnectionManager().httpWebHost; | ||||
|       if (webhookId != null && webhookId.isNotEmpty) { | ||||
|           Logger.d("[Foreground location] Getting battery level..."); | ||||
|           int batteryLevel = await battery.batteryLevel; | ||||
|           Logger.d("[Foreground location] Getting device location..."); | ||||
|           Position position = await geolocator.getCurrentPosition( | ||||
|               desiredAccuracy: LocationAccuracy.high, | ||||
|               locationPermissionLevel: GeolocationPermission.locationAlways | ||||
|             ); | ||||
|           if (position != null) { | ||||
|             Logger.d("[Foreground location] Location: ${position.latitude} ${position.longitude}. Accuracy: ${position.accuracy}. (${position.timestamp})"); | ||||
|             String url = "$httpWebHost/api/webhook/$webhookId"; | ||||
|             Map data = { | ||||
|               "type": "update_location", | ||||
|               "data": { | ||||
|                 "gps": [position.latitude, position.longitude], | ||||
|                 "gps_accuracy": position.accuracy, | ||||
|                 "battery": batteryLevel ?? 100 | ||||
|               } | ||||
|             }; | ||||
|             Logger.d("[Foreground location] Sending data home..."); | ||||
|             http.Response response = await http.post( | ||||
|               url, | ||||
|               headers: {"Content-Type": "application/json"}, | ||||
|               body: json.encode(data) | ||||
|             ); | ||||
|             if (response.statusCode >= 300) { | ||||
|               Logger.e('Foreground location update error: ${response.body}'); | ||||
|             } | ||||
|           }; | ||||
|           Logger.d("[Foreground location] Sending data home..."); | ||||
|           var response = await http.post( | ||||
|             url, | ||||
|             headers: {"Content-Type": "application/json"}, | ||||
|             body: json.encode(data) | ||||
|           ); | ||||
|           Logger.d("[Foreground location] Got HTTP ${response.statusCode}"); | ||||
|         } else { | ||||
|           Logger.d("[Foreground location] No location. Aborting."); | ||||
|         } | ||||
|             Logger.d("[Foreground location] Got HTTP ${response.statusCode}"); | ||||
|           } else { | ||||
|             Logger.d("[Foreground location] No location. Aborting."); | ||||
|           } | ||||
|       } | ||||
|     } catch (e, stack) { | ||||
|       Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack); | ||||
|     } | ||||
|     */ | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| /* | ||||
| void updateDeviceLocationIsolate() { | ||||
|   workManager.Workmanager.executeTask((backgroundTask, data) async { | ||||
|     //print("[Background $backgroundTask] Started"); | ||||
| @@ -234,4 +324,5 @@ void updateDeviceLocationIsolate() { | ||||
|     print("[Background $backgroundTask] Finished.");*/ | ||||
|     return true; | ||||
|   }); | ||||
| } | ||||
| } | ||||
| */ | ||||
| @@ -31,12 +31,16 @@ class MobileAppIntegrationManager { | ||||
|       Logger.d("Mobile app was not registered yet. Registering..."); | ||||
|       var registrationData = Map.from(_appRegistrationData); | ||||
|       registrationData.addAll({ | ||||
|         "device_id": "${DeviceInfoManager().unicDeviceId}", | ||||
|         "app_id": "ha_client", | ||||
|         "app_name": "$appName", | ||||
|         "os_name": DeviceInfoManager().osName, | ||||
|         "supports_encryption": false, | ||||
|       }); | ||||
|       if (ConnectionManager().haVersion >= 104) { | ||||
|         registrationData.addAll({ | ||||
|           "device_id": "${DeviceInfoManager().unicDeviceId}" | ||||
|         }); | ||||
|       } | ||||
|       ConnectionManager().sendHTTPPost( | ||||
|           endPoint: "/api/mobile_app/registrations", | ||||
|           includeAuthHeader: true, | ||||
|   | ||||
| @@ -14,13 +14,13 @@ class StartupUserMessagesManager { | ||||
|   bool _supportAppDevelopmentMessageShown; | ||||
|   bool _whatsNewMessageShown; | ||||
|   static final _supportAppDevelopmentMessageKey = "user-message-shown-support-development_3"; | ||||
|   static final _whatsNewMessageKey = "user-message-shown-whats-new-1006"; | ||||
|   static final _whatsNewMessageKey = "user-msg-whats-new-url"; | ||||
|  | ||||
|   void checkMessagesToShow() async { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     await prefs.reload(); | ||||
|     _supportAppDevelopmentMessageShown = prefs.getBool(_supportAppDevelopmentMessageKey) ?? false; | ||||
|     _whatsNewMessageShown = prefs.getBool(_whatsNewMessageKey) ?? false; | ||||
|     _whatsNewMessageShown = '${prefs.getString(_whatsNewMessageKey)}' == whatsNewUrl; | ||||
|     if (!_whatsNewMessageShown) { | ||||
|       _showWhatsNewMessage(); | ||||
|     } else if (!_supportAppDevelopmentMessageShown) { | ||||
| @@ -52,7 +52,7 @@ class StartupUserMessagesManager { | ||||
|  | ||||
|   void _showWhatsNewMessage() { | ||||
|     SharedPreferences.getInstance().then((prefs) { | ||||
|       prefs.setBool(_whatsNewMessageKey, true); | ||||
|       prefs.setString(_whatsNewMessageKey, whatsNewUrl); | ||||
|       eventBus.fire(ShowPageEvent(path: "/whats-new")); | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -53,9 +53,11 @@ class _IntegrationSettingsPageState extends State<IntegrationSettingsPage> { | ||||
|   } | ||||
|  | ||||
|   _switchLocationTrackingState(bool state) async { | ||||
|     /* | ||||
|     if (state) { | ||||
|       await LocationManager().updateDeviceLocation(); | ||||
|     } | ||||
|     */ | ||||
|     await LocationManager().setSettings(_locationTrackingEnabled, _locationInterval); | ||||
|     setState(() { | ||||
|       _wait = false; | ||||
|   | ||||
| @@ -12,7 +12,7 @@ class LookAndFeelSettingsPage extends StatefulWidget { | ||||
| class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> { | ||||
|  | ||||
|   AppTheme _currentTheme; | ||||
|   bool _changed = false; | ||||
|   bool _scrollBadges = false; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
| @@ -26,13 +26,15 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> { | ||||
|     SharedPreferences.getInstance().then((prefs) { | ||||
|       setState(() { | ||||
|         _currentTheme = AppTheme.values[prefs.getInt("app-theme") ?? AppTheme.defaultTheme.index]; | ||||
|         _scrollBadges = prefs.getBool('scroll-badges') ?? true; | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   _saveSettings(AppTheme theme) { | ||||
|   _saveTheme(AppTheme theme) { | ||||
|     SharedPreferences.getInstance().then((prefs) { | ||||
|       prefs.setInt('app-theme', theme.index); | ||||
|       prefs.setBool('scroll-badges', _scrollBadges); | ||||
|       setState(() { | ||||
|         _currentTheme = theme; | ||||
|         eventBus.fire(ChangeThemeEvent(_currentTheme)); | ||||
| @@ -40,6 +42,12 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future _saveOther() async { | ||||
|     SharedPreferences prefs = await SharedPreferences.getInstance(); | ||||
|     ConnectionManager().scrollBadges = _scrollBadges; | ||||
|     await prefs.setBool('scroll-badges', _scrollBadges); | ||||
|   } | ||||
|  | ||||
|   Map appThemeName = { | ||||
|     AppTheme.defaultTheme: 'Default', | ||||
|     AppTheme.haTheme: 'Home Assistant theme', | ||||
| @@ -59,15 +67,35 @@ class _LookAndFeelSettingsPageState extends State<LookAndFeelSettingsPage> { | ||||
|             iconSize: 30.0, | ||||
|             isExpanded: true, | ||||
|             style: Theme.of(context).textTheme.title, | ||||
|             //hint: Text("Select ${caption.toLowerCase()}"), | ||||
|             items: AppTheme.values.map((value) { | ||||
|               return new DropdownMenuItem<AppTheme>( | ||||
|                 value: value, | ||||
|                 child: Text('${appThemeName[value]}'), | ||||
|               ); | ||||
|             }).toList(), | ||||
|             onChanged: (theme) => _saveSettings(theme), | ||||
|           ) | ||||
|             onChanged: (theme) => _saveTheme(theme), | ||||
|           ), | ||||
|           Container(height: Sizes.doubleRowPadding), | ||||
|           Text("Badges display:", style: Theme.of(context).textTheme.body2), | ||||
|           Container(height: Sizes.rowPadding), | ||||
|           DropdownButton<bool>( | ||||
|             value: _scrollBadges, | ||||
|             iconSize: 30.0, | ||||
|             isExpanded: true, | ||||
|             style: Theme.of(context).textTheme.title, | ||||
|             items: [true, false].map((value) { | ||||
|               return new DropdownMenuItem<bool>( | ||||
|                 value: value, | ||||
|                 child: Text('${value ? 'Horizontal scroll' : 'In rows'}'), | ||||
|               ); | ||||
|             }).toList(), | ||||
|             onChanged: (val) { | ||||
|               setState(() { | ||||
|                 _scrollBadges = val; | ||||
|               }); | ||||
|               _saveOther(); | ||||
|             }, | ||||
|           ), | ||||
|         ] | ||||
|       ); | ||||
|   } | ||||
|   | ||||
| @@ -24,7 +24,7 @@ class _WhatsNewPageState extends State<WhatsNewPage> { | ||||
|       error = ""; | ||||
|     }); | ||||
|     http.Response response; | ||||
|     response = await http.get("http://ha-client.app/service/whats_new_1.0.0_stable.md"); | ||||
|     response = await http.get(whatsNewUrl); | ||||
|     if (response.statusCode == 200) { | ||||
|       setState(() { | ||||
|         data = response.body; | ||||
|   | ||||
| @@ -54,7 +54,7 @@ class _EntityHistoryWidgetState extends State<EntityHistoryWidget> { | ||||
|           }); | ||||
|         } | ||||
|       }).catchError((e) { | ||||
|         Logger.e("Error loading $entityId history: $e"); | ||||
|         Logger.e("Error loading $entityId history: $e", skipCrashlytics: true); | ||||
|         if (!_disposed) { | ||||
|           setState(() { | ||||
|             _history = []; | ||||
|   | ||||
| @@ -18,8 +18,8 @@ class Logger { | ||||
|     print(data); | ||||
|   } | ||||
|  | ||||
|   static void e(String message, {dynamic stacktrace, bool skipCrashlytics: false}) { | ||||
|     _writeToLog(ErrorLevel.ERROR, message, stacktrace, skipCrashlytics); | ||||
|   static void e(dynamic message, {dynamic stacktrace, bool skipCrashlytics: false}) { | ||||
|     _writeToLog(ErrorLevel.ERROR, message.toString(), stacktrace, skipCrashlytics); | ||||
|   } | ||||
|  | ||||
|   static void w(String message) { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ part of 'main.dart'; | ||||
|  | ||||
| class HAView { | ||||
|   List<CardData> cards = []; | ||||
|   List<Entity> badges = []; | ||||
|   CardData badges; | ||||
|   Entity linkedEntity; | ||||
|   String name; | ||||
|   String id; | ||||
| @@ -16,28 +16,16 @@ class HAView { | ||||
|     iconName = rawData['icon']; | ||||
|     isPanel = rawData['panel'] ?? false; | ||||
|  | ||||
|     if (rawData['badges'] != null && rawData['badges'] is List) { | ||||
|         rawData['badges'].forEach((entity) { | ||||
|           if (entity is String) { | ||||
|             if (HomeAssistant().entities.isExist(entity)) { | ||||
|               Entity e = HomeAssistant().entities.get(entity); | ||||
|               badges.add(e); | ||||
|             } | ||||
|           } else { | ||||
|             String eId = '${entity['entity']}'; | ||||
|             if (HomeAssistant().entities.isExist(eId)) { | ||||
|               Entity e = HomeAssistant().entities.get(eId); | ||||
|               badges.add(e); | ||||
|             } | ||||
|           } | ||||
|     if (rawData['badges'] != null && !isPanel) { | ||||
|         badges = CardData.parse({ | ||||
|           'type': CardType.BADGES, | ||||
|           'badges': rawData['badges'] | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       (rawData["cards"] ?? []).forEach((rawCardData) { | ||||
|       (rawData['cards'] ?? []).forEach((rawCardData) { | ||||
|         cards.add(CardData.parse(rawCardData)); | ||||
|       }); | ||||
|  | ||||
|       //cards.addAll(_createLovelaceCards(rawData["cards"] ?? [], 1)); | ||||
|   } | ||||
|  | ||||
|   Widget buildTab() { | ||||
|   | ||||
| @@ -18,6 +18,12 @@ class ViewWidget extends StatelessWidget { | ||||
|       ); | ||||
|     } else { | ||||
|       Widget cardsContainer; | ||||
|       Widget badgesContainer; | ||||
|       if (this.view.badges != null && this.view.badges is BadgesData) { | ||||
|         badgesContainer = this.view.badges.buildCardWidget(); | ||||
|       } else { | ||||
|         badgesContainer = Container(width: 0, height: 0); | ||||
|       } | ||||
|       if (this.view.cards.isNotEmpty) { | ||||
|         cardsContainer = DynamicMultiColumnLayout( | ||||
|           minColumnWidth: Sizes.minViewColumnWidth, | ||||
| @@ -44,13 +50,15 @@ class ViewWidget extends StatelessWidget { | ||||
|       } else { | ||||
|         cardsContainer = Container(); | ||||
|       } | ||||
|       return ListView( | ||||
|           shrinkWrap: true, | ||||
|       return SingleChildScrollView( | ||||
|           padding: EdgeInsets.all(0), | ||||
|           children: <Widget>[ | ||||
|             _buildBadges(context), | ||||
|             cardsContainer | ||||
|           ] | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|               badgesContainer, | ||||
|               cardsContainer | ||||
|             ], | ||||
|           ), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| @@ -63,18 +71,4 @@ class ViewWidget extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildBadges(BuildContext context) { | ||||
|     if (this.view.badges.isNotEmpty) { | ||||
|       return Wrap( | ||||
|         alignment: WrapAlignment.center, | ||||
|         spacing: 10.0, | ||||
|         runSpacing: 1.0, | ||||
|         children: this.view.badges.map((badge) => | ||||
|             badge.buildBadgeWidget(context)).toList(), | ||||
|       ); | ||||
|     } else { | ||||
|       return Container(width: 0, height: 0,); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										11
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								pubspec.yaml
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| name: hass_client | ||||
| description: Home Assistant Android Client | ||||
|  | ||||
| version: 1.0.0+1008 | ||||
| version: 1.1.0+1100 | ||||
|  | ||||
|  | ||||
| environment: | ||||
| @@ -27,12 +27,13 @@ dependencies: | ||||
|   flutter_secure_storage: ^3.3.3 | ||||
|   device_info: ^0.4.1+4 | ||||
|   flutter_local_notifications: ^1.1.6 | ||||
|   geolocator: ^5.3.1 | ||||
|   workmanager: ^0.2.2 | ||||
|   #geolocator: ^5.3.1 | ||||
|   background_locator: ^1.1.3+1 | ||||
|   #workmanager: ^0.2.2 | ||||
|   battery: ^1.0.0 | ||||
|   firebase_crashlytics: ^0.1.3+3 | ||||
|   syncfusion_flutter_core: ^18.1.43 | ||||
|   syncfusion_flutter_gauges: ^18.1.43 | ||||
|   syncfusion_flutter_core: ^18.1.48 | ||||
|   syncfusion_flutter_gauges: ^18.1.48 | ||||
|    | ||||
|  | ||||
| dev_dependencies: | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'dart:io'; | ||||
| Future<void> main() async { | ||||
|   final config = { | ||||
|     'syncfusion_license_key': Platform.environment['SYNCFUSION_LICENSE_KEY'], | ||||
|     'version_type': Platform.environment['HA_CLIENT_VERSION_TYPE'] ?? '' | ||||
|   }; | ||||
|  | ||||
|   final filename = 'lib/.secrets.dart'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user