Compare commits
	
		
			26 Commits
		
	
	
		
			1.0.1
			...
			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 | 
| @@ -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); | ||||
| @@ -33,3 +34,4 @@ window.externalApp.externalBus = function(message) { | ||||
|         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,7 +89,7 @@ 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']  ?? []; | ||||
| @@ -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,6 +62,8 @@ class EntityButtonCard extends StatelessWidget { | ||||
|           onLongPress: () => entityWrapper.handleHold(), | ||||
|           onDoubleTap: () => entityWrapper.handleDoubleTap(), | ||||
|           child: Center( | ||||
|             child: Padding( | ||||
|               padding: EdgeInsets.only(top: 5), | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
| @@ -70,6 +72,7 @@ class EntityButtonCard extends StatelessWidget { | ||||
|                   _buildName(context) | ||||
|                 ], | ||||
|               ) | ||||
|             ) | ||||
|           ), | ||||
|         ), | ||||
|         handleTap: true | ||||
|   | ||||
| @@ -30,10 +30,13 @@ class GlanceCard extends StatelessWidget { | ||||
|           start, end | ||||
|         ).map( | ||||
|           (EntityWrapper entity){ | ||||
|             return EntityModel( | ||||
|             return Padding( | ||||
|               padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||
|               child: EntityModel( | ||||
|                 entityWrapper: entity, | ||||
|                 child: _buildEntityContainer(context, entity), | ||||
|                 handleTap: true | ||||
|               ) | ||||
|             ); | ||||
|           } | ||||
|         ).toList() | ||||
| @@ -50,18 +53,23 @@ class GlanceCard extends StatelessWidget { | ||||
|       ); | ||||
|     } | ||||
|     return CardWrapper( | ||||
|       child: Center( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.only(bottom: Sizes.rowPadding), | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|           CardHeader(name: card.title), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.symmetric(vertical: Sizes.rowPadding), | ||||
|             child: Table( | ||||
|               CardHeader( | ||||
|                 name: card.title, | ||||
|                 emptyPadding: Sizes.rowPadding, | ||||
|               ), | ||||
|               Table( | ||||
|                 children: rows | ||||
|               ) | ||||
|           ) | ||||
|             ], | ||||
|           ) | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -85,8 +93,7 @@ class GlanceCard extends StatelessWidget { | ||||
|       result.add(_buildState()); | ||||
|     } | ||||
|  | ||||
|     return Center( | ||||
|       child: InkResponse( | ||||
|     return InkResponse( | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: result, | ||||
| @@ -94,7 +101,6 @@ class GlanceCard extends StatelessWidget { | ||||
|       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,13 +28,27 @@ class EntityIcon extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget buildIcon(BuildContext context, EntityWrapper data, Color color) { | ||||
|     Widget result; | ||||
|     if (data == null) { | ||||
|       return null; | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     Color iconColor; | ||||
|     if (color != null) { | ||||
|       iconColor = color; | ||||
|     } else if (entityWrapper.stateColor) { | ||||
|       iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context); | ||||
|     } else { | ||||
|       iconColor = HAClientTheme().getOffStateColor(context); | ||||
|     } | ||||
|     if (data.entityPicture != null) { | ||||
|       result = Container( | ||||
|     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( | ||||
| @@ -40,36 +56,34 @@ class EntityIcon extends StatelessWidget { | ||||
|               image: DecorationImage( | ||||
|                 fit:BoxFit.cover, | ||||
|                 image: CachedNetworkImageProvider( | ||||
|                 "${data.entityPicture}" | ||||
|                   "${entityWrapper.entityPicture}" | ||||
|                 ), | ||||
|               ) | ||||
|           ), | ||||
|         ); | ||||
|         isPicture = true; | ||||
|       } else { | ||||
|       String iconName = data.icon; | ||||
|         String iconName = entityWrapper.icon; | ||||
|         int iconCode = 0; | ||||
|         if (iconName.length > 0) { | ||||
|           iconCode = MaterialDesignIcons.getIconCodeByIconName(iconName); | ||||
|         } else { | ||||
|         iconCode = getDefaultIconByEntityId(data.entity.entityId, | ||||
|             data.entity.deviceClass, data.entity.state); // | ||||
|           iconCode = getDefaultIconByEntityId(entityWrapper.entity.entityId, | ||||
|               entityWrapper.entity.deviceClass, entityWrapper.entity.state); // | ||||
|         } | ||||
|       result = Icon( | ||||
|         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: 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, | ||||
|                 color: iconColor, | ||||
|               ), | ||||
|               Positioned( | ||||
|                 bottom: 0, | ||||
|                 right: 0, | ||||
| @@ -91,29 +105,26 @@ class EntityIcon extends StatelessWidget { | ||||
|               ) | ||||
|             ], | ||||
|           ); | ||||
|       } | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final EntityWrapper entityWrapper = EntityModel.of(context).entityWrapper; | ||||
|     Color iconColor; | ||||
|     if (color != null) { | ||||
|       iconColor = color; | ||||
|     } else if (entityWrapper.stateColor) { | ||||
|       iconColor = HAClientTheme().getColorByEntityState(entityWrapper.entity.state, context); | ||||
|         } else { | ||||
|       iconColor = HAClientTheme().getOffStateColor(context); | ||||
|           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, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -155,3 +155,63 @@ 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 { | ||||
|   | ||||
| @@ -163,7 +163,7 @@ class HomeAssistant { | ||||
|     if (sharedPrefs != null && sharedPrefs.containsKey('cached_states')) { | ||||
|       try { | ||||
|         var data = json.decode(sharedPrefs.getString('cached_states')); | ||||
|         _parseStates(data); | ||||
|         _parseStates(data ?? []); | ||||
|       } catch (e, stacktrace) { | ||||
|         Logger.e('Error getting cached states: $e', stacktrace: stacktrace); | ||||
|       } | ||||
| @@ -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 | ||||
|         }; | ||||
| @@ -224,7 +224,7 @@ class HomeAssistant { | ||||
|     if (prefs != null && prefs.containsKey('cached_services')) { | ||||
|       try { | ||||
|         var data = json.decode(prefs.getString('cached_services')); | ||||
|         _parseServices(data); | ||||
|         _parseServices(data ?? {}); | ||||
|       } catch (e, stacktrace) { | ||||
|        Logger.e(e, stacktrace: stacktrace);   | ||||
|       } | ||||
| @@ -243,7 +243,7 @@ class HomeAssistant { | ||||
|     if (sharedPrefs != null && sharedPrefs.containsKey('cached_user')) { | ||||
|       try { | ||||
|         var data = json.decode(sharedPrefs.getString('cached_user')); | ||||
|         _parseUserInfo(data); | ||||
|         _parseUserInfo(data ?? {}); | ||||
|       } catch (e, stacktrace) { | ||||
|         Logger.e('Error getting cached user info: $e', stacktrace: stacktrace); | ||||
|       } | ||||
| @@ -264,7 +264,7 @@ class HomeAssistant { | ||||
|     if (sharedPrefs != null && sharedPrefs.containsKey('cached_panels')) { | ||||
|       try { | ||||
|         var data = json.decode(sharedPrefs.getString('cached_panels')); | ||||
|         _parsePanels(data); | ||||
|         _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,6 +157,7 @@ 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(); | ||||
| @@ -214,8 +219,11 @@ class _HAClientAppState extends State<HAClientApp> { | ||||
|   StreamSubscription _themeChangeSubscription; | ||||
|   AppTheme _currentTheme = AppTheme.defaultTheme; | ||||
|  | ||||
|   ReceivePort port = ReceivePort(); | ||||
|    | ||||
|   @override | ||||
|   void initState() { | ||||
|  | ||||
|     InAppPurchaseConnection.enablePendingPurchases(); | ||||
|     final Stream purchaseUpdates = | ||||
|         InAppPurchaseConnection.instance.purchaseUpdatedStream; | ||||
| @@ -228,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) { | ||||
| @@ -253,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) { | ||||
|   | ||||
| @@ -20,6 +20,7 @@ class ConnectionManager { | ||||
|   String oauthUrl; | ||||
|   String webhookId; | ||||
|   double haVersion; | ||||
|   bool scrollBadges; | ||||
|   String mobileAppDeviceName; | ||||
|   bool settingsLoaded = false; | ||||
|   int appIntegrationVersion; | ||||
| @@ -48,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"; | ||||
|   | ||||
| @@ -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,14 +114,81 @@ 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 { | ||||
|     /* | ||||
|     try { | ||||
|       Logger.d("[Foreground location] Started"); | ||||
|       Geolocator geolocator = Geolocator(); | ||||
| @@ -150,10 +231,12 @@ class LocationManager { | ||||
|     } catch (e, stack) { | ||||
|       Logger.e('Foreground location error: ${e.toSTring()}', stacktrace: stack); | ||||
|     } | ||||
|     */ | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| /* | ||||
| void updateDeviceLocationIsolate() { | ||||
|   workManager.Workmanager.executeTask((backgroundTask, data) async { | ||||
|     //print("[Background $backgroundTask] Started"); | ||||
| @@ -242,3 +325,4 @@ void updateDeviceLocationIsolate() { | ||||
|     return true; | ||||
|   }); | ||||
| } | ||||
| */ | ||||
| @@ -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(); | ||||
|             }, | ||||
|           ), | ||||
|         ] | ||||
|       ); | ||||
|   } | ||||
|   | ||||
| @@ -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), | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: <Widget>[ | ||||
|             _buildBadges(context), | ||||
|               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.1+1013 | ||||
| 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: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user