From 5e536ab21771840512f4b0df79ce6206db174043 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 25 May 2021 20:19:00 +0300 Subject: [PATCH] Alarms, Devices and More pages --- lib/config/routes/router.dart | 13 + lib/core/context/tb_context.dart | 16 + lib/core/context/tb_context_widget.dart | 25 + lib/core/entity/entities_base.dart | 97 +++- lib/core/entity/entities_grid.dart | 2 + lib/core/entity/entities_list.dart | 2 + lib/core/entity/entities_list_widget.dart | 17 +- lib/modules/alarm/alarm_routes.dart | 8 +- lib/modules/alarm/alarms_base.dart | 272 +++++++---- lib/modules/alarm/alarms_list.dart | 3 +- lib/modules/alarm/alarms_page.dart | 41 +- lib/modules/asset/asset_routes.dart | 3 +- lib/modules/asset/assets_base.dart | 2 +- lib/modules/asset/assets_list.dart | 4 +- lib/modules/asset/assets_page.dart | 41 +- lib/modules/dashboard/dashboards_base.dart | 2 +- lib/modules/dashboard/dashboards_grid.dart | 33 +- lib/modules/dashboard/dashboards_list.dart | 4 +- .../dashboard/dashboards_list_widget.dart | 1 + lib/modules/dashboard/dashboards_page.dart | 11 +- lib/modules/device/device_profiles_base.dart | 455 ++++++++++++++++++ lib/modules/device/device_profiles_grid.dart | 12 + lib/modules/device/device_routes.dart | 10 + lib/modules/device/devices_base.dart | 227 ++++++--- lib/modules/device/devices_list.dart | 10 +- lib/modules/device/devices_list_widget.dart | 6 +- lib/modules/device/devices_main_page.dart | 39 ++ lib/modules/device/devices_page.dart | 85 +++- lib/modules/home/home_page.dart | 10 +- lib/modules/main/main_page.dart | 70 +-- lib/modules/more/more_page.dart | 228 +++++++++ lib/modules/profile/profile_page.dart | 4 +- lib/utils/services/device_profile_cache.dart | 25 + lib/utils/services/entity_query_api.dart | 61 +++ lib/widgets/tb_app_bar.dart | 175 ++++--- pubspec.lock | 14 +- 36 files changed, 1690 insertions(+), 338 deletions(-) create mode 100644 lib/modules/device/device_profiles_base.dart create mode 100644 lib/modules/device/device_profiles_grid.dart create mode 100644 lib/modules/device/devices_main_page.dart create mode 100644 lib/modules/more/more_page.dart create mode 100644 lib/utils/services/device_profile_cache.dart create mode 100644 lib/utils/services/entity_query_api.dart diff --git a/lib/config/routes/router.dart b/lib/config/routes/router.dart index cff4eff..196d57d 100644 --- a/lib/config/routes/router.dart +++ b/lib/config/routes/router.dart @@ -1,4 +1,6 @@ import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/core/auth/auth_routes.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/init/init_routes.dart'; @@ -15,6 +17,17 @@ class ThingsboardAppRouter { late final _tbContext = TbContext(router); ThingsboardAppRouter() { + router.notFoundHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { + var settings = context!.settings; + return Scaffold( + appBar: AppBar( + title: Text('Not Found') + ), + body: Center( + child: Text('Route not defined: ${settings!.name}') + ), + ); + }); InitRoutes(_tbContext).registerRoutes(); AuthRoutes(_tbContext).registerRoutes(); UiUtilsRoutes(_tbContext).registerRoutes(); diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index 8b335a4..75edf60 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -306,6 +306,20 @@ class TbContext { router.pop(currentState!.context, result); } } + + Future confirm({required String title, required String message, String cancel = 'Cancel', String ok = 'Ok'}) { + return showDialog(context: currentState!.context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton(onPressed: () => pop(false), + child: Text(cancel)), + TextButton(onPressed: () => pop(true), + child: Text(ok)) + ], + )); + } } mixin HasTbContext { @@ -343,6 +357,8 @@ mixin HasTbContext { void pop([T? result]) => _tbContext.pop(result); + Future confirm({required String title, required String message, String cancel = 'Cancel', String ok = 'Ok'}) => _tbContext.confirm(title: title, message: message, cancel: cancel, ok: ok); + void hideNotification() => _tbContext.hideNotification(); void showErrorNotification(String message, {Duration? duration}) => _tbContext.showErrorNotification(message, duration: duration); diff --git a/lib/core/context/tb_context_widget.dart b/lib/core/context/tb_context_widget.dart index 4e08d29..8014ffa 100644 --- a/lib/core/context/tb_context_widget.dart +++ b/lib/core/context/tb_context_widget.dart @@ -1,6 +1,11 @@ +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; +abstract class RefreshableWidget extends Widget { + refresh(); +} + abstract class TbContextStatelessWidget extends StatelessWidget with HasTbContext { TbContextStatelessWidget(TbContext tbContext, {Key? key}) : super(key: key) { setTbContext(tbContext); @@ -71,3 +76,23 @@ abstract class TbPageState, S extends TbPageState { + + final String text; + + TextContextWidget(TbContext tbContext, this.text) : super(tbContext); + + @override + _TextContextWidgetState createState() => _TextContextWidgetState(); + +} + +class _TextContextWidgetState extends TbContextState { + + @override + Widget build(BuildContext context) { + return Scaffold(body: Center(child: Text(widget.text))); + } + +} diff --git a/lib/core/entity/entities_base.dart b/lib/core/entity/entities_base.dart index 5434084..4f42f74 100644 --- a/lib/core/entity/entities_base.dart +++ b/lib/core/entity/entities_base.dart @@ -42,10 +42,6 @@ mixin EntitiesBase on HasTbContext { return Text('Not implemented!'); } - P createFirstKey({int pageSize = 10}) => throw UnimplementedError('Not implemented'); - - P nextPageKey(P pageKey) => throw UnimplementedError('Not implemented'); - EntityCardSettings entityListCardSettings(T entity) => EntityCardSettings(); EntityCardSettings entityGridCardSettings(T entity) => EntityCardSettings(); @@ -54,37 +50,67 @@ mixin EntitiesBase on HasTbContext { } -mixin EntitiesBaseWithPageLink on EntitiesBase { +abstract class PageKeyController

extends ValueNotifier> { - @override - PageLink createFirstKey({int pageSize = 10}) => PageLink(pageSize, 0, null, SortOrder('createdTime', Direction.DESC)); + PageKeyController(P initialPageKey) : super(PageKeyValue(initialPageKey)); + + P nextPageKey(P pageKey); + +} + +class PageKeyValue

{ + + final P pageKey; + + PageKeyValue(this.pageKey); + +} + +class PageLinkController extends PageKeyController { + + PageLinkController({int pageSize = 10, String? searchText}) : super(PageLink(pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC))); @override PageLink nextPageKey(PageLink pageKey) => pageKey.nextPageLink(); + onSearchText(String searchText) { + value.pageKey.page = 0; + value.pageKey.textSearch = searchText; + notifyListeners(); + } + } -mixin EntitiesBaseWithTimePageLink on EntitiesBase { +class TimePageLinkController extends PageKeyController { - @override - TimePageLink createFirstKey({int pageSize = 10}) => TimePageLink(pageSize, 0, null, SortOrder('createdTime', Direction.DESC)); + TimePageLinkController({int pageSize = 10, String? searchText}) : super(TimePageLink(pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC))); @override TimePageLink nextPageKey(TimePageLink pageKey) => pageKey.nextPageLink(); -} + onSearchText(String searchText) { + value.pageKey.page = 0; + value.pageKey.textSearch = searchText; + notifyListeners(); + } -abstract class BaseEntitiesPageLinkWidget extends BaseEntitiesWidget with EntitiesBaseWithPageLink { - BaseEntitiesPageLinkWidget(TbContext tbContext): super(tbContext); -} - -abstract class BaseEntitiesTimePageLinkWidget extends BaseEntitiesWidget with EntitiesBaseWithTimePageLink { - BaseEntitiesTimePageLinkWidget(TbContext tbContext): super(tbContext); } abstract class BaseEntitiesWidget extends TbContextWidget, BaseEntitiesState> with EntitiesBase { - BaseEntitiesWidget(TbContext tbContext): super(tbContext); + final bool searchMode; + final PageKeyController

pageKeyController; + + BaseEntitiesWidget(TbContext tbContext, this.pageKeyController, {this.searchMode = false}): + super(tbContext); + + @override + Widget? buildHeading(BuildContext context) => searchMode ? Text('Search results', style: TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 16, + height: 24 / 16 + )) : null; + } @@ -92,24 +118,42 @@ abstract class BaseEntitiesState extends TbContextState pagingController; Completer? _refreshCompleter; + bool _dataLoading = false; + bool _scheduleRefresh = false; + bool _reloadData = false; + + BaseEntitiesState(); @override void initState() { super.initState(); - pagingController = PagingController(firstPageKey: widget.createFirstKey()); + pagingController = PagingController(firstPageKey: widget.pageKeyController.value.pageKey); + widget.pageKeyController.addListener(_didChangePageKeyValue); pagingController.addPageRequestListener((pageKey) { _fetchPage(pageKey); }); } + @override + void didUpdateWidget(BaseEntitiesWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.pageKeyController != oldWidget.pageKeyController) { + oldWidget.pageKeyController.removeListener(_didChangePageKeyValue); + widget.pageKeyController.addListener(_didChangePageKeyValue); + } + } + @override void dispose() { + widget.pageKeyController.removeListener(_didChangePageKeyValue); pagingController.dispose(); super.dispose(); } - bool _dataLoading = false; - bool _scheduleRefresh = false; + void _didChangePageKeyValue() { + _reloadData = true; + _refresh(); + } Future _refresh() { if (_refreshCompleter == null) { @@ -124,7 +168,12 @@ abstract class BaseEntitiesState extends TbContextState _fetchPage(P pageKey, {bool refresh = false}) async { @@ -143,7 +192,7 @@ abstract class BaseEntitiesState extends TbContextState extends TbContextState pagingController.refresh(), + onTryAgain: widget.searchMode ? null : () => pagingController.refresh(), ); } diff --git a/lib/core/entity/entities_grid.dart b/lib/core/entity/entities_grid.dart index 1ca5bb3..dfa8ec2 100644 --- a/lib/core/entity/entities_grid.dart +++ b/lib/core/entity/entities_grid.dart @@ -14,6 +14,8 @@ mixin EntitiesGridStateBase on StatefulWidget { class _EntitiesGridState extends BaseEntitiesState { + _EntitiesGridState() : super(); + @override Widget pagedViewBuilder(BuildContext context) { var heading = widget.buildHeading(context); diff --git a/lib/core/entity/entities_list.dart b/lib/core/entity/entities_list.dart index 764fc26..cbf8a20 100644 --- a/lib/core/entity/entities_list.dart +++ b/lib/core/entity/entities_list.dart @@ -14,6 +14,8 @@ mixin EntitiesListStateBase on StatefulWidget { class _EntitiesListState extends BaseEntitiesState { + _EntitiesListState() : super(); + @override Widget pagedViewBuilder(BuildContext context) { var heading = widget.buildHeading(context); diff --git a/lib/core/entity/entities_list_widget.dart b/lib/core/entity/entities_list_widget.dart index a45d3a4..56bfe38 100644 --- a/lib/core/entity/entities_list_widget.dart +++ b/lib/core/entity/entities_list_widget.dart @@ -32,8 +32,13 @@ class EntitiesListWidgetController { } -abstract class EntitiesListPageLinkWidget extends EntitiesListWidget with EntitiesBaseWithPageLink { - EntitiesListPageLinkWidget(TbContext tbContext, {EntitiesListWidgetController? controller}): super(tbContext, controller: controller); +abstract class EntitiesListPageLinkWidget extends EntitiesListWidget { + + EntitiesListPageLinkWidget(TbContext tbContext, {EntitiesListWidgetController? controller}) : super(tbContext, controller: controller); + + @override + PageKeyController createPageKeyController() => PageLinkController(pageSize: 5); + } abstract class EntitiesListWidget extends TbContextWidget, _EntitiesListWidgetState> with EntitiesBase { @@ -47,6 +52,8 @@ abstract class EntitiesListWidget extends TbContextWidget _EntitiesListWidgetState(_controller); + PageKeyController

createPageKeyController(); + void onViewAll(); } @@ -55,6 +62,8 @@ class _EntitiesListWidgetState extends TbContextState _pageKeyController; + final StreamController?> _entitiesStreamController = StreamController.broadcast(); _EntitiesListWidgetState(EntitiesListWidgetController? controller): @@ -63,6 +72,7 @@ class _EntitiesListWidgetState extends TbContextState extends TbContextState _refresh() { _entitiesStreamController.add(null); - var entitiesFuture = widget.fetchEntities(widget.createFirstKey(pageSize: 5)); + var entitiesFuture = widget.fetchEntities(_pageKeyController.value.pageKey); entitiesFuture.then((value) => _entitiesStreamController.add(value)); return entitiesFuture; } diff --git a/lib/modules/alarm/alarm_routes.dart b/lib/modules/alarm/alarm_routes.dart index cbc0ac3..36b479d 100644 --- a/lib/modules/alarm/alarm_routes.dart +++ b/lib/modules/alarm/alarm_routes.dart @@ -2,12 +2,18 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/config/routes/router.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/modules/alarm/alarms_page.dart'; import 'package:thingsboard_app/modules/main/main_page.dart'; class AlarmRoutes extends TbRoutes { late var alarmsHandler = Handler(handlerFunc: (BuildContext? context, Map params) { - return MainPage(tbContext, path: '/alarms'); + var searchMode = params['search']?.first == 'true'; + if (searchMode) { + return AlarmsPage(tbContext, searchMode: true); + } else { + return MainPage(tbContext, path: '/alarms'); + } }); AlarmRoutes(TbContext tbContext) : super(tbContext); diff --git a/lib/modules/alarm/alarms_base.dart b/lib/modules/alarm/alarms_base.dart index d4bfeb1..21e76ac 100644 --- a/lib/modules/alarm/alarms_base.dart +++ b/lib/modules/alarm/alarms_base.dart @@ -1,6 +1,9 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; @@ -36,15 +39,6 @@ mixin AlarmsBase on EntitiesBase { @override String get noItemsFoundText => 'No alarms found'; - @override - AlarmQuery createFirstKey({int pageSize = 10}) => AlarmQuery(TimePageLink(pageSize, 0, null, SortOrder('createdTime', Direction.DESC)), fetchOriginator: true); - - @override - AlarmQuery nextPageKey(AlarmQuery query) { - query.pageLink = query.pageLink.nextPageLink(); - return query; - } - @override Future> fetchEntities(AlarmQuery query) { return tbClient.getAlarmService().getAllAlarms(query); @@ -61,110 +55,202 @@ mixin AlarmsBase on EntitiesBase { } Widget _buildEntityListCard(BuildContext context, AlarmInfo alarm) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - fit: FlexFit.tight, - child: - Padding( - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + return AlarmCard(tbContext, alarm: alarm); + } +} + +class AlarmQueryController extends PageKeyController { + + AlarmQueryController({int pageSize = 10, String? searchText}) : super(AlarmQuery(TimePageLink(pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC)), fetchOriginator: true)); + + @override + AlarmQuery nextPageKey(AlarmQuery pageKey) { + return AlarmQuery(pageKey.pageLink.nextPageLink()); + } + + onSearchText(String searchText) { + var query = value.pageKey; + query.pageLink.page = 0; + query.pageLink.textSearch = searchText; + notifyListeners(); + } + +} + +class AlarmCard extends TbContextWidget { + + final AlarmInfo alarm; + + AlarmCard(TbContext tbContext, {required this.alarm}) : super(tbContext); + + @override + _AlarmCardState createState() => _AlarmCardState(alarm); + +} + +class _AlarmCardState extends TbContextState { + + bool loading = false; + AlarmInfo alarm; + + final entityDateFormat = DateFormat('yyyy-MM-dd'); + + _AlarmCardState(this.alarm): super(); + + @override + void initState() { + super.initState(); + } + + @override + void didUpdateWidget(AlarmCard oldWidget) { + super.didUpdateWidget(oldWidget); + this.loading = false; + this.alarm = widget.alarm; + } + + @override + Widget build(BuildContext context) { + if (this.loading) { + return Container( height: 134, alignment: Alignment.center, child: RefreshProgressIndicator()); + } else { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + fit: FlexFit.tight, + child: + Padding( + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible( - fit: FlexFit.tight, - child: AutoSizeText(alarm.type, - maxLines: 2, - minFontSize: 8, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Color(0xFF282828), - fontWeight: FontWeight.w500, - fontSize: 14, - height: 20 / 14) - ) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + fit: FlexFit.tight, + child: AutoSizeText(alarm.type, + maxLines: 2, + minFontSize: 8, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14) + ) + ), + Text(alarmSeverityTranslations[alarm.severity]!, + style: TextStyle( + color: alarmSeverityColors[alarm.severity]!, + fontWeight: FontWeight.w500, + fontSize: 12, + height: 16 / 12) + ) + ] ), - Text(alarmSeverityTranslations[alarm.severity]!, - style: TextStyle( - color: alarmSeverityColors[alarm.severity]!, - fontWeight: FontWeight.w500, - fontSize: 12, - height: 16 / 12) - ) - ] - ), - SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - fit: FlexFit.tight, - child: Text(alarm.originatorName != null ? alarm.originatorName! : '', + SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + fit: FlexFit.tight, + child: Text(alarm.originatorName != null ? alarm.originatorName! : '', + style: TextStyle( + color: Color(0xFFAFAFAF), + fontWeight: FontWeight.normal, + fontSize: 12, + height: 16 / 12) + ) + ), + Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(alarm.createdTime!)), style: TextStyle( color: Color(0xFFAFAFAF), fontWeight: FontWeight.normal, fontSize: 12, height: 16 / 12) ) - ), - Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(alarm.createdTime!)), - style: TextStyle( - color: Color(0xFFAFAFAF), - fontWeight: FontWeight.normal, - fontSize: 12, - height: 16 / 12) - ) - ] - ), - SizedBox(height: 22), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - fit: FlexFit.tight, - child: Text(alarmStatusTranslations[alarm.status]!, - style: TextStyle( - color: Color(0xFF282828), - fontWeight: FontWeight.normal, - fontSize: 14, - height: 20 / 14) - ) + ] ), + SizedBox(height: 22), Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if ([AlarmStatus.CLEARED_UNACK, AlarmStatus.ACTIVE_UNACK].contains(alarm.status)) - CircleAvatar( - radius: 24, - backgroundColor: Color(0xffF0F4F9), - child: IconButton(icon: Icon(Icons.done), padding: EdgeInsets.all(6.0), onPressed: () => {}) + Flexible( + fit: FlexFit.tight, + child: Text(alarmStatusTranslations[alarm.status]!, + style: TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.normal, + fontSize: 14, + height: 20 / 14) + ) ), - if ([AlarmStatus.ACTIVE_UNACK, AlarmStatus.ACTIVE_ACK].contains(alarm.status)) Row( children: [ - SizedBox(width: 4), - CircleAvatar( - radius: 24, - backgroundColor: Color(0xffF0F4F9), - child: IconButton(icon: Icon(Icons.clear), padding: EdgeInsets.all(6.0), onPressed: () => {}) - ) - ] + if ([AlarmStatus.CLEARED_UNACK, AlarmStatus.ACTIVE_UNACK].contains(alarm.status)) + CircleAvatar( + radius: 24, + backgroundColor: Color(0xffF0F4F9), + child: IconButton(icon: Icon(Icons.done), padding: EdgeInsets.all(6.0), onPressed: () => _ackAlarm(alarm)) + ), + if ([AlarmStatus.ACTIVE_UNACK, AlarmStatus.ACTIVE_ACK].contains(alarm.status)) + Row( + children: [ + SizedBox(width: 4), + CircleAvatar( + radius: 24, + backgroundColor: Color(0xffF0F4F9), + child: IconButton(icon: Icon(Icons.clear), padding: EdgeInsets.all(6.0), onPressed: () => _clearAlarm(alarm)) + ) + ] + ) + ], ) ], ) - ], - ) - ] + ] + ) ) - ) - ) - ] - ); + ) + ] + ); + } } -} + _clearAlarm(AlarmInfo alarm) async { + var res = await confirm(title: 'Clear Alarm', message: 'Are you sure you want to clear Alarm?', cancel: 'No', ok: 'Yes'); + if (res != null && res) { + setState(() { + loading = true; + }); + await tbClient.getAlarmService().clearAlarm(alarm.id!.id!); + var newAlarm = await tbClient.getAlarmService().getAlarmInfo( + alarm.id!.id!); + setState(() { + loading = false; + this.alarm = newAlarm; + }); + } + } + + _ackAlarm(AlarmInfo alarm) async { + var res = await confirm(title: 'Acknowledge Alarm', message: 'Are you sure you want to acknowledge Alarm?', cancel: 'No', ok: 'Yes'); + if (res != null && res) { + setState(() { + loading = true; + }); + await tbClient.getAlarmService().ackAlarm(alarm.id!.id!); + var newAlarm = await tbClient.getAlarmService().getAlarmInfo( + alarm.id!.id!); + setState(() { + loading = false; + this.alarm = newAlarm; + }); + } + } + +} diff --git a/lib/modules/alarm/alarms_list.dart b/lib/modules/alarm/alarms_list.dart index d0bd7d1..7f56af2 100644 --- a/lib/modules/alarm/alarms_list.dart +++ b/lib/modules/alarm/alarms_list.dart @@ -1,3 +1,4 @@ +import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list.dart'; @@ -7,7 +8,7 @@ import 'alarms_base.dart'; class AlarmsList extends BaseEntitiesWidget with AlarmsBase, EntitiesListStateBase { - AlarmsList(TbContext tbContext) : super(tbContext); + AlarmsList(TbContext tbContext, PageKeyController pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode); } diff --git a/lib/modules/alarm/alarms_page.dart b/lib/modules/alarm/alarms_page.dart index af58f4e..244c789 100644 --- a/lib/modules/alarm/alarms_page.dart +++ b/lib/modules/alarm/alarms_page.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/modules/alarm/alarms_base.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'alarms_list.dart'; class AlarmsPage extends TbContextWidget { - AlarmsPage(TbContext tbContext) : super(tbContext); + final bool searchMode; + + AlarmsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext); @override _AlarmsPageState createState() => _AlarmsPageState(); @@ -16,16 +19,42 @@ class AlarmsPage extends TbContextWidget { class _AlarmsPageState extends TbContextState { + final AlarmQueryController _alarmQueryController = AlarmQueryController(); + @override Widget build(BuildContext context) { - var alarmsList = AlarmsList(tbContext); + var alarmsList = AlarmsList(tbContext, _alarmQueryController, searchMode: widget.searchMode); + PreferredSizeWidget appBar; + if (widget.searchMode) { + appBar = TbAppSearchBar( + tbContext, + onSearch: (searchText) => _alarmQueryController.onSearchText(searchText), + ); + } else { + appBar = TbAppBar( + tbContext, + title: Text(alarmsList.title), + actions: [ + IconButton( + icon: Icon( + Icons.search + ), + onPressed: () { + navigateTo('/alarms?search=true'); + }, + ) + ]); + } return Scaffold( - appBar: TbAppBar( - tbContext, - title: Text(alarmsList.title) - ), + appBar: appBar, body: alarmsList ); } + @override + void dispose() { + _alarmQueryController.dispose(); + super.dispose(); + } + } diff --git a/lib/modules/asset/asset_routes.dart b/lib/modules/asset/asset_routes.dart index 49bf522..a2ca78f 100644 --- a/lib/modules/asset/asset_routes.dart +++ b/lib/modules/asset/asset_routes.dart @@ -9,7 +9,8 @@ import 'asset_details_page.dart'; class AssetRoutes extends TbRoutes { late var assetsHandler = Handler(handlerFunc: (BuildContext? context, Map params) { - return AssetsPage(tbContext); + var searchMode = params['search']?.first == 'true'; + return AssetsPage(tbContext, searchMode: searchMode); }); late var assetDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map params) { diff --git a/lib/modules/asset/assets_base.dart b/lib/modules/asset/assets_base.dart index beea529..4596c1b 100644 --- a/lib/modules/asset/assets_base.dart +++ b/lib/modules/asset/assets_base.dart @@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; -mixin AssetsBase on EntitiesBaseWithPageLink { +mixin AssetsBase on EntitiesBase { @override String get title => 'Assets'; diff --git a/lib/modules/asset/assets_list.dart b/lib/modules/asset/assets_list.dart index 190b95d..aa3da35 100644 --- a/lib/modules/asset/assets_list.dart +++ b/lib/modules/asset/assets_list.dart @@ -5,9 +5,9 @@ import 'package:thingsboard_client/thingsboard_client.dart'; import 'assets_base.dart'; -class AssetsList extends BaseEntitiesPageLinkWidget with AssetsBase, EntitiesListStateBase { +class AssetsList extends BaseEntitiesWidget with AssetsBase, EntitiesListStateBase { - AssetsList(TbContext tbContext) : super(tbContext); + AssetsList(TbContext tbContext, PageKeyController pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode); } diff --git a/lib/modules/asset/assets_page.dart b/lib/modules/asset/assets_page.dart index ce254bd..d569d9b 100644 --- a/lib/modules/asset/assets_page.dart +++ b/lib/modules/asset/assets_page.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'assets_list.dart'; class AssetsPage extends TbPageWidget { - AssetsPage(TbContext tbContext) : super(tbContext); + final bool searchMode; + + AssetsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext); @override _AssetsPageState createState() => _AssetsPageState(); @@ -16,16 +19,42 @@ class AssetsPage extends TbPageWidget { class _AssetsPageState extends TbPageState { + final PageLinkController _pageLinkController = PageLinkController(); + @override Widget build(BuildContext context) { - var assetsList = AssetsList(tbContext); + var assetsList = AssetsList(tbContext, _pageLinkController, searchMode: widget.searchMode); + PreferredSizeWidget appBar; + if (widget.searchMode) { + appBar = TbAppSearchBar( + tbContext, + onSearch: (searchText) => _pageLinkController.onSearchText(searchText), + ); + } else { + appBar = TbAppBar( + tbContext, + title: Text(assetsList.title), + actions: [ + IconButton( + icon: Icon( + Icons.search + ), + onPressed: () { + navigateTo('/assets?search=true'); + }, + ) + ]); + } return Scaffold( - appBar: TbAppBar( - tbContext, - title: Text(assetsList.title) - ), + appBar: appBar, body: assetsList ); } + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } + } diff --git a/lib/modules/dashboard/dashboards_base.dart b/lib/modules/dashboard/dashboards_base.dart index d26fb89..a1a8692 100644 --- a/lib/modules/dashboard/dashboards_base.dart +++ b/lib/modules/dashboard/dashboards_base.dart @@ -5,7 +5,7 @@ import 'package:thingsboard_app/constants/assets_path.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; -mixin DashboardsBase on EntitiesBaseWithPageLink { +mixin DashboardsBase on EntitiesBase { @override String get title => 'Dashboards'; diff --git a/lib/modules/dashboard/dashboards_grid.dart b/lib/modules/dashboard/dashboards_grid.dart index ab312b8..c7c8554 100644 --- a/lib/modules/dashboard/dashboards_grid.dart +++ b/lib/modules/dashboard/dashboards_grid.dart @@ -1,13 +1,42 @@ +import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_grid.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; import 'dashboards_base.dart'; -class DashboardsGrid extends BaseEntitiesPageLinkWidget with DashboardsBase, EntitiesGridStateBase { +class DashboardsGridWidget extends TbContextWidget { - DashboardsGrid(TbContext tbContext) : super(tbContext); + DashboardsGridWidget(TbContext tbContext) : super(tbContext); + + @override + _DashboardsGridWidgetState createState() => _DashboardsGridWidgetState(); + +} + +class _DashboardsGridWidgetState extends TbContextState { + + final PageLinkController _pageLinkController = PageLinkController(); + + @override + Widget build(BuildContext context) { + return DashboardsGrid(tbContext, _pageLinkController); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } + +} + + +class DashboardsGrid extends BaseEntitiesWidget with DashboardsBase, EntitiesGridStateBase { + + DashboardsGrid(TbContext tbContext, PageKeyController pageKeyController) : super(tbContext, pageKeyController); } diff --git a/lib/modules/dashboard/dashboards_list.dart b/lib/modules/dashboard/dashboards_list.dart index 5c510bb..050a603 100644 --- a/lib/modules/dashboard/dashboards_list.dart +++ b/lib/modules/dashboard/dashboards_list.dart @@ -5,9 +5,9 @@ import 'package:thingsboard_client/thingsboard_client.dart'; import 'dashboards_base.dart'; -class DashboardsList extends BaseEntitiesPageLinkWidget with DashboardsBase, EntitiesListStateBase { +class DashboardsList extends BaseEntitiesWidget with DashboardsBase, EntitiesListStateBase { - DashboardsList(TbContext tbContext) : super(tbContext); + DashboardsList(TbContext tbContext, PageKeyController pageKeyController) : super(tbContext, pageKeyController); } diff --git a/lib/modules/dashboard/dashboards_list_widget.dart b/lib/modules/dashboard/dashboards_list_widget.dart index bec58d1..cb43962 100644 --- a/lib/modules/dashboard/dashboards_list_widget.dart +++ b/lib/modules/dashboard/dashboards_list_widget.dart @@ -13,3 +13,4 @@ class DashboardsListWidget extends EntitiesListPageLinkWidget wit } } + diff --git a/lib/modules/dashboard/dashboards_page.dart b/lib/modules/dashboard/dashboards_page.dart index 5c93924..8f12634 100644 --- a/lib/modules/dashboard/dashboards_page.dart +++ b/lib/modules/dashboard/dashboards_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'dashboards_list.dart'; @@ -16,9 +17,11 @@ class DashboardsPage extends TbPageWidget class _DashboardsPageState extends TbPageState { + final PageLinkController _pageLinkController = PageLinkController(); + @override Widget build(BuildContext context) { - var dashboardsList = DashboardsList(tbContext); + var dashboardsList = DashboardsList(tbContext, _pageLinkController); return Scaffold( appBar: TbAppBar( tbContext, @@ -28,4 +31,10 @@ class _DashboardsPageState extends TbPageState { + + final RefreshDeviceCounts refreshDeviceCounts = RefreshDeviceCounts(); + + @override + String get title => 'Devices'; + + @override + String get noItemsFoundText => 'No devices found'; + + @override + Future> fetchEntities(PageLink pageLink) { + return DeviceProfileCache.getDeviceProfileInfos(tbClient, pageLink); + } + + @override + void onEntityTap(DeviceProfileInfo deviceProfile) { + navigateTo('/deviceList?deviceType=${deviceProfile.name}'); + } + + @override + Future onRefresh() { + if (refreshDeviceCounts.onRefresh != null) { + return refreshDeviceCounts.onRefresh!(); + } else { + return Future.value(); + } + } + + @override + Widget? buildHeading(BuildContext context) { + return AllDevicesCard(tbContext, refreshDeviceCounts); + } + + @override + Widget buildEntityGridCard(BuildContext context, DeviceProfileInfo deviceProfile) { + return DeviceProfileCard(tbContext, deviceProfile); + } + +} + +class RefreshDeviceCounts { + Future Function()? onRefresh; +} + +class AllDevicesCard extends TbContextWidget { + + final RefreshDeviceCounts refreshDeviceCounts; + + AllDevicesCard(TbContext tbContext, this.refreshDeviceCounts) : super(tbContext); + + @override + _AllDevicesCardState createState() => _AllDevicesCardState(); + +} + +class _AllDevicesCardState extends TbContextState { + + final StreamController _activeDevicesCount = StreamController.broadcast(); + final StreamController _inactiveDevicesCount = StreamController.broadcast(); + + @override + void initState() { + super.initState(); + widget.refreshDeviceCounts.onRefresh = _countDevices; + _countDevices(); + } + + @override + void dispose() { + _activeDevicesCount.close(); + _inactiveDevicesCount.close(); + super.dispose(); + } + + Future _countDevices() { + _activeDevicesCount.add(null); + _inactiveDevicesCount.add(null); + Future activeDevicesCount = EntityQueryApi.countDevices(tbClient, active: true); + Future inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, active: false); + Future> countsFuture = Future.wait([activeDevicesCount, inactiveDevicesCount]); + countsFuture.then((counts) { + _activeDevicesCount.add(counts[0]); + _inactiveDevicesCount.add(counts[1]); + }); + return countsFuture; + } + + @override + Widget build(BuildContext context) { + return + GestureDetector( + behavior: HitTestBehavior.opaque, + child: + Container( + child: Card( + color: Theme.of(tbContext.currentState!.context).colorScheme.primary, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + child: Column( + children: [ + Padding(padding: EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('All devices', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14 + ) + ), + Icon(Icons.arrow_forward, color: Colors.white) + ], + ) + ), + Padding(padding: EdgeInsets.all(8), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible(fit: FlexFit.tight, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + height: 40, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + child: StreamBuilder( + stream: _activeDevicesCount.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + var deviceCount = snapshot.data!; + return _buildDeviceCount(context, true, deviceCount, displayStatusText: true); + } else { + return Center(child: + Container(height: 20, width: 20, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary), + strokeWidth: 2.5))); + } + }, + ) + ), + onTap: () { + navigateTo('/deviceList?active=true'); + } + ), + ), + SizedBox(width: 4), + Flexible(fit: FlexFit.tight, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + height: 40, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + child: StreamBuilder( + stream: _inactiveDevicesCount.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + var deviceCount = snapshot.data!; + return _buildDeviceCount(context, false, deviceCount, displayStatusText: true); + } else { + return Center(child: + Container(height: 20, width: 20, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary), + strokeWidth: 2.5))); + } + }, + ) + ), + onTap: () { + navigateTo('/deviceList?active=false'); + } + ), + ) + ], + ) + ) + ], + ) + ), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(25), + blurRadius: 10.0, + offset: Offset(0, 4) + ), + BoxShadow( + color: Colors.black.withAlpha(18), + blurRadius: 30.0, + offset: Offset(0, 10) + ), + ], + ), + ), + onTap: () { + navigateTo('/deviceList'); + } + ); + } + +} + +class DeviceProfileCard extends TbContextWidget { + + final DeviceProfileInfo deviceProfile; + + DeviceProfileCard(TbContext tbContext, this.deviceProfile) : super(tbContext); + + @override + _DeviceProfileCardState createState() => _DeviceProfileCardState(); + +} + +class _DeviceProfileCardState extends TbContextState { + + late Future activeDevicesCount; + late Future inactiveDevicesCount; + + @override + void initState() { + super.initState(); + _countDevices(); + } + + @override + void didUpdateWidget(DeviceProfileCard oldWidget) { + super.didUpdateWidget(oldWidget); + _countDevices(); + } + + _countDevices() { + activeDevicesCount = EntityQueryApi.countDevices(tbClient, deviceType: widget.deviceProfile.name, active: true); + inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, deviceType: widget.deviceProfile.name, active: false); + } + + @override + Widget build(BuildContext context) { + var entity = widget.deviceProfile; + var hasImage = entity.image != null; + Widget image; + if (hasImage) { + var uriData = UriData.parse(entity.image!); + image = Image.memory(uriData.contentAsBytes()); + } else { + image = Image.asset(ThingsboardImage.deviceProfilePlaceholder); + } + return + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Stack( + children: [ + Positioned.fill( + child: FittedBox( + fit: BoxFit.cover, + child: image, + ) + ), + hasImage ? Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x00000000), + Color(0xb7000000) + ], + stops: [0.4219, 1] + ) + ) + ), + ) : Container(), + Positioned( + bottom: 56, + left: 16, + right: 16, + child: AutoSizeText(entity.name, + textAlign: TextAlign.center, + maxLines: 2, + minFontSize: 8, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: hasImage ? Colors.white : Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14 + ), + ) + ), + Positioned( + bottom: 4, + left: 4, + right: 4, + height: 40, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible(fit: FlexFit.tight, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + child: FutureBuilder( + future: activeDevicesCount, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { + var deviceCount = snapshot.data!; + return _buildDeviceCount(context, true, deviceCount); + } else { + return Center(child: + Container(height: 20, width: 20, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary), + strokeWidth: 2.5))); + } + }, + ) + ), + onTap: () { + navigateTo('/deviceList?active=true&deviceType=${entity.name}'); + } + ), + ), + SizedBox(width: 4), + Flexible(fit: FlexFit.tight, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + child: FutureBuilder( + future: inactiveDevicesCount, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { + var deviceCount = snapshot.data!; + return _buildDeviceCount(context, false, deviceCount); + } else { + return Center(child: + Container(height: 20, width: 20, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary), + strokeWidth: 2.5))); + } + }, + ) + ), + onTap: () { + navigateTo('/deviceList?active=false&deviceType=${entity.name}'); + } + ), + ), + ], + ) + ) + ], + ) + ); + } +} + +Widget _buildDeviceCount(BuildContext context, bool active, int count, {bool displayStatusText = false}) { + Color color = active ? Color(0xFF008A00) : Color(0xFFAFAFAF); + return Padding( + padding: EdgeInsets.all(12), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Stack( + children: [ + Icon(Icons.devices_other, size: 16, color: color), + if (!active) CustomPaint( + size: Size.square(16), + painter: StrikeThroughPainter(color: color, offset: 2), + ) + ], + ), + if (displayStatusText) + SizedBox(width: 8.67), + if (displayStatusText) + Text(active ? 'Active' : 'Inactive', style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 16 / 12, + color: color + )) + ], + ), + Text(count.toString(), style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 16 / 12, + color: color + )) + ], + ), + ); +} + +class StrikeThroughPainter extends CustomPainter { + + final Color color; + final double offset; + + StrikeThroughPainter({required this.color, this.offset = 0}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + paint.strokeWidth = 1.5; + canvas.drawLine(Offset(offset, offset), Offset(size.width - offset, size.height - offset), paint); + paint.color = Colors.white; + canvas.drawLine(Offset(2, 0), Offset(size.width + 2, size.height), paint); + } + + @override + bool shouldRepaint(covariant StrikeThroughPainter oldDelegate) { + return color != oldDelegate.color; + } + +} diff --git a/lib/modules/device/device_profiles_grid.dart b/lib/modules/device/device_profiles_grid.dart new file mode 100644 index 0000000..78282eb --- /dev/null +++ b/lib/modules/device/device_profiles_grid.dart @@ -0,0 +1,12 @@ +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/core/entity/entities_grid.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +import 'device_profiles_base.dart'; + +class DeviceProfilesGrid extends BaseEntitiesWidget with DeviceProfilesBase, EntitiesGridStateBase { + + DeviceProfilesGrid(TbContext tbContext, PageKeyController pageKeyController) : super(tbContext, pageKeyController); + +} diff --git a/lib/modules/device/device_routes.dart b/lib/modules/device/device_routes.dart index 532ab18..090fd03 100644 --- a/lib/modules/device/device_routes.dart +++ b/lib/modules/device/device_routes.dart @@ -5,6 +5,7 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/modules/main/main_page.dart'; import 'device_details_page.dart'; +import 'devices_page.dart'; class DeviceRoutes extends TbRoutes { @@ -12,6 +13,14 @@ class DeviceRoutes extends TbRoutes { return MainPage(tbContext, path: '/devices'); }); + late var deviceListHandler = Handler(handlerFunc: (BuildContext? context, Map params) { + var searchMode = params['search']?.first == 'true'; + var deviceType = params['deviceType']?.first; + String? activeStr = params['active']?.first; + bool? active = activeStr != null ? activeStr == 'true' : null; + return DevicesPage(tbContext, searchMode: searchMode, deviceType: deviceType, active: active); + }); + late var deviceDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map params) { return DeviceDetailsPage(tbContext, params["id"][0]); }); @@ -21,6 +30,7 @@ class DeviceRoutes extends TbRoutes { @override void doRegisterRoutes(router) { router.define("/devices", handler: devicesHandler); + router.define("/deviceList", handler: deviceListHandler); router.define("/device/:id", handler: deviceDetailsHandler); } diff --git a/lib/modules/device/devices_base.dart b/lib/modules/device/devices_base.dart index 8bca318..40ddd15 100644 --- a/lib/modules/device/devices_base.dart +++ b/lib/modules/device/devices_base.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/utils/services/device_profile_cache.dart'; +import 'package:thingsboard_app/utils/services/entity_query_api.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; -mixin DevicesBase on EntitiesBaseWithPageLink { +mixin DevicesBase on EntitiesBase { @override String get title => 'Devices'; @@ -12,108 +17,206 @@ mixin DevicesBase on EntitiesBaseWithPageLink { String get noItemsFoundText => 'No devices found'; @override - Future> fetchEntities(PageLink pageLink) { - if (tbClient.isTenantAdmin()) { - return tbClient.getDeviceService().getTenantDeviceInfos(pageLink); - } else { - return tbClient.getDeviceService().getCustomerDeviceInfos(tbClient.getAuthUser()!.customerId, pageLink); - } + Future> fetchEntities(EntityDataQuery dataQuery) { + return tbClient.getEntityQueryService().findEntityDataByQuery(dataQuery); } @override - void onEntityTap(DeviceInfo device) { - navigateTo('/device/${device.id!.id}'); + void onEntityTap(EntityData device) { + navigateTo('/device/${device.entityId.id}'); } @override - Widget? buildHeading(BuildContext context) { - return Text('Hobo Devices!'); - } - - - @override - Widget buildEntityListCard(BuildContext context, DeviceInfo device) { + Widget buildEntityListCard(BuildContext context, EntityData device) { return _buildEntityListCard(context, device, false); } @override - Widget buildEntityListWidgetCard(BuildContext context, DeviceInfo device) { + Widget buildEntityListWidgetCard(BuildContext context, EntityData device) { return _buildEntityListCard(context, device, true); } @override - Widget buildEntityGridCard(BuildContext context, DeviceInfo device) { - return Text(device.name); + Widget buildEntityGridCard(BuildContext context, EntityData device) { + return Text(device.field('name')!); } - Widget _buildEntityListCard(BuildContext context, DeviceInfo device, bool listWidgetCard) { + bool displayCardImage(bool listWidgetCard) => listWidgetCard; + + Widget _buildEntityListCard(BuildContext context, EntityData device, bool listWidgetCard) { + return DeviceCard(tbContext, device: device, listWidgetCard: listWidgetCard, displayImage: displayCardImage(listWidgetCard)); + } +} + +class DeviceQueryController extends PageKeyController { + + DeviceQueryController({int pageSize = 10, String? searchText, String? deviceType, bool? active}): + super(EntityQueryApi.createDefaultDeviceQuery(pageSize: pageSize, searchText: searchText, deviceType: deviceType, active: active)); + + @override + EntityDataQuery nextPageKey(EntityDataQuery deviceQuery) => deviceQuery.next(); + + onSearchText(String searchText) { + value.pageKey.pageLink.page = 0; + value.pageKey.pageLink.textSearch = searchText; + notifyListeners(); + } + +} + +class DeviceCard extends TbContextWidget { + + final EntityData device; + final bool listWidgetCard; + final bool displayImage; + + DeviceCard(TbContext tbContext, {required this.device, this.listWidgetCard = false, this.displayImage = false}) : super(tbContext); + + @override + _DeviceCardState createState() => _DeviceCardState(); + +} + +class _DeviceCardState extends TbContextState { + + final entityDateFormat = DateFormat('yyyy-MM-dd'); + + late Future deviceProfileFuture; + + @override + void initState() { + super.initState(); + if (widget.displayImage) { + deviceProfileFuture = DeviceProfileCache.getDeviceProfileInfo( + tbClient, widget.device.field('type')!, widget.device.entityId.id!); + } + } + + @override + void didUpdateWidget(DeviceCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.displayImage) { + deviceProfileFuture = DeviceProfileCache.getDeviceProfileInfo( + tbClient, widget.device.field('type')!, widget.device.entityId.id!); + } + } + + @override + Widget build(BuildContext context) { return Row( - mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max, + mainAxisSize: widget.listWidgetCard ? MainAxisSize.min : MainAxisSize.max, children: [ - Container( - width: listWidgetCard ? 58 : 60, - height: listWidgetCard ? 58 : 60, + if (widget.displayImage) Container( + width: widget.listWidgetCard ? 58 : 60, + height: widget.listWidgetCard ? 58 : 60, decoration: BoxDecoration( color: Color(0xFFEEEEEE), - borderRadius: BorderRadius.horizontal(left: Radius.circular(listWidgetCard ? 4 : 6)) + borderRadius: BorderRadius.horizontal(left: Radius.circular(widget.listWidgetCard ? 4 : 6)) ), - child: Center( - child: Icon(Icons.devices_other, color: Color(0xFFC2C2C2)) + child: FutureBuilder( + future: deviceProfileFuture, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { + var profile = snapshot.data!; + if (profile.image != null) { + var uriData = UriData.parse(profile.image!); + return ClipRRect( + borderRadius: BorderRadius.horizontal(left: Radius.circular(widget.listWidgetCard ? 4 : 6)), + child: Stack( + children: [ + Positioned.fill( + child: FittedBox( + fit: BoxFit.cover, + child: Image.memory(uriData.contentAsBytes()), + ) + ), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x00000000), + Color(0xb7000000) + ], + stops: [0.4219, 1] + ) + ) + ), + ) + ], + ) + ); + } else { + return Center( + child: Icon(Icons.devices_other, color: Color(0xFFC2C2C2)) + ); + } + } else { + return Center(child: RefreshProgressIndicator()); + } + }, ), ), Flexible( - fit: listWidgetCard ? FlexFit.loose : FlexFit.tight, + fit: widget.listWidgetCard ? FlexFit.loose : FlexFit.tight, child: Container( - padding: EdgeInsets.symmetric(vertical: listWidgetCard ? 9 : 10, horizontal: 16), - child: Row( - mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max, - children: [ - Flexible( - fit: listWidgetCard ? FlexFit.loose : FlexFit.tight, - child: - Column( - crossAxisAlignment: CrossAxisAlignment.start, + padding: EdgeInsets.symmetric(vertical: widget.listWidgetCard ? 9 : 10, horizontal: 16), + child: Column( + children: [ + Row( + mainAxisSize: widget.listWidgetCard ? MainAxisSize.min : MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ FittedBox( fit: BoxFit.fitWidth, alignment: Alignment.centerLeft, - child: Text('${device.name}', + child: Text('${widget.device.field('name')!}', style: TextStyle( color: Color(0xFF282828), fontSize: 14, fontWeight: FontWeight.w500, - height: 1.7 + height: 20 / 14 )) ), - Text('${device.type}', + if (!widget.listWidgetCard) Text(widget.device.attribute('active') == 'true' ? 'Active' : 'Inactive', style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 1.33 + color: widget.device.attribute('active') == 'true' ? Color(0xFF008A00) : Color(0xFFAFAFAF), + fontSize: 12, + height: 12 /12, + fontWeight: FontWeight.w500, )) - ], - ) - ), - (!listWidgetCard ? Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(device.createdTime!)), - style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 1.33 - )) - ], - ) : Container()) - ], - ), + ] + ), + SizedBox(height: 4), + Row( + mainAxisSize: widget.listWidgetCard ? MainAxisSize.min : MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${widget.device.field('type')!}', + style: TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 + )), + if (!widget.listWidgetCard) Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(widget.device.createdTime!)), + style: TextStyle( + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 + )) + ], + ) + ], + ) ) - ) ] ); } + } diff --git a/lib/modules/device/devices_list.dart b/lib/modules/device/devices_list.dart index 5913055..0697109 100644 --- a/lib/modules/device/devices_list.dart +++ b/lib/modules/device/devices_list.dart @@ -4,9 +4,15 @@ import 'package:thingsboard_app/core/entity/entities_list.dart'; import 'package:thingsboard_app/modules/device/devices_base.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; -class DevicesList extends BaseEntitiesPageLinkWidget with DevicesBase, EntitiesListStateBase { +class DevicesList extends BaseEntitiesWidget with DevicesBase, EntitiesListStateBase { - DevicesList(TbContext tbContext) : super(tbContext); + final bool displayDeviceImage; + + DevicesList(TbContext tbContext, PageKeyController pageKeyController, {searchMode = false, this.displayDeviceImage = false}): + super(tbContext, pageKeyController, searchMode: searchMode); + + @override + bool displayCardImage(bool listWidgetCard) => displayDeviceImage; } diff --git a/lib/modules/device/devices_list_widget.dart b/lib/modules/device/devices_list_widget.dart index 7ee28a9..7d01f70 100644 --- a/lib/modules/device/devices_list_widget.dart +++ b/lib/modules/device/devices_list_widget.dart @@ -1,9 +1,10 @@ import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; import 'package:thingsboard_app/core/entity/entities_list_widget.dart'; import 'package:thingsboard_app/modules/device/devices_base.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; -class DevicesListWidget extends EntitiesListPageLinkWidget with DevicesBase { +class DevicesListWidget extends EntitiesListWidget with DevicesBase { DevicesListWidget(TbContext tbContext, {EntitiesListWidgetController? controller}): super(tbContext, controller: controller); @@ -12,4 +13,7 @@ class DevicesListWidget extends EntitiesListPageLinkWidget with Devi navigateTo('/devices'); } + @override + PageKeyController createPageKeyController() => DeviceQueryController(pageSize: 5); + } diff --git a/lib/modules/device/devices_main_page.dart b/lib/modules/device/devices_main_page.dart new file mode 100644 index 0000000..625eb7b --- /dev/null +++ b/lib/modules/device/devices_main_page.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/core/entity/entities_base.dart'; +import 'package:thingsboard_app/modules/device/device_profiles_grid.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class DevicesMainPage extends TbContextWidget { + + DevicesMainPage(TbContext tbContext) : super(tbContext); + + @override + _DevicesMainPageState createState() => _DevicesMainPageState(); + +} + +class _DevicesMainPageState extends TbContextState { + + final PageLinkController _pageLinkController = PageLinkController(); + + @override + Widget build(BuildContext context) { + var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController); + return Scaffold( + appBar: TbAppBar( + tbContext, + title: Text(deviceProfilesList.title) + ), + body: deviceProfilesList + ); + } + + @override + void dispose() { + _pageLinkController.dispose(); + super.dispose(); + } + +} diff --git a/lib/modules/device/devices_page.dart b/lib/modules/device/devices_page.dart index 5f53d14..7bc4bd6 100644 --- a/lib/modules/device/devices_page.dart +++ b/lib/modules/device/devices_page.dart @@ -1,30 +1,99 @@ import 'package:flutter/material.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/modules/device/devices_base.dart'; import 'package:thingsboard_app/modules/device/devices_list.dart'; import 'package:thingsboard_app/widgets/tb_app_bar.dart'; -class DevicesPage extends TbContextWidget { +class DevicesPage extends TbPageWidget { - DevicesPage(TbContext tbContext) : super(tbContext); + final String? deviceType; + final bool? active; + final bool searchMode; + + DevicesPage(TbContext tbContext, {this.deviceType, this.active, this.searchMode = false}) : super(tbContext); @override _DevicesPageState createState() => _DevicesPageState(); } -class _DevicesPageState extends TbContextState { +class _DevicesPageState extends TbPageState { + + late final DeviceQueryController _deviceQueryController; + + @override + void initState() { + super.initState(); + _deviceQueryController = DeviceQueryController(deviceType: widget.deviceType, active: widget.active); + } @override Widget build(BuildContext context) { - var devicesList = DevicesList(tbContext); + var devicesList = DevicesList(tbContext, _deviceQueryController, searchMode: widget.searchMode, displayDeviceImage: widget.deviceType == null); + PreferredSizeWidget appBar; + if (widget.searchMode) { + appBar = TbAppSearchBar( + tbContext, + onSearch: (searchText) => _deviceQueryController.onSearchText(searchText), + ); + } else { + String titleText = widget.deviceType != null ? widget.deviceType! : 'All devices'; + String? subTitleText; + if (widget.active != null) { + subTitleText = widget.active == true ? 'Active' : 'Inactive'; + } + Column title = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(titleText, style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: subTitleText != null ? 16 : 20, + height: subTitleText != null ? 20 / 16 : 24 / 20 + )), + if (subTitleText != null) + Text(subTitleText, style: TextStyle( + color: Color(0x61FFFFFF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 + )) + ] + ); + + appBar = TbAppBar( + tbContext, + title: title, + actions: [ + IconButton( + icon: Icon( + Icons.search + ), + onPressed: () { + List params = []; + params.add('search=true'); + if (widget.deviceType != null) { + params.add('deviceType=${widget.deviceType}'); + } + if (widget.active != null) { + params.add('active=${widget.active}'); + } + navigateTo('/deviceList?${params.join('&')}'); + }, + ) + ]); + } return Scaffold( - appBar: TbAppBar( - tbContext, - title: Text(devicesList.title) - ), + appBar: appBar, body: devicesList ); } + @override + void dispose() { + _deviceQueryController.dispose(); + super.dispose(); + } + } diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index f78dfae..f3e9f6e 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -17,7 +17,7 @@ class HomePage extends TbContextWidget { } -class _HomePageState extends TbContextState { +class _HomePageState extends TbContextState with AutomaticKeepAliveClientMixin { final EntitiesListWidgetController _entitiesWidgetController = EntitiesListWidgetController(); @@ -26,6 +26,11 @@ class _HomePageState extends TbContextState { super.initState(); } + @override + bool get wantKeepAlive { + return true; + } + @override void dispose() { _entitiesWidgetController.dispose(); @@ -34,6 +39,7 @@ class _HomePageState extends TbContextState { @override Widget build(BuildContext context) { + super.build(context); var homeDashboard = tbContext.homeDashboard; var dashboardState = homeDashboard != null; return Scaffold( @@ -63,7 +69,7 @@ class _HomePageState extends TbContextState { if (tbClient.isSystemAdmin()) { return _buildSysAdminHome(context); } else { - return DashboardsGrid(tbContext); + return DashboardsGridWidget(tbContext); } } diff --git a/lib/modules/main/main_page.dart b/lib/modules/main/main_page.dart index 19ee742..cd5dfa2 100644 --- a/lib/modules/main/main_page.dart +++ b/lib/modules/main/main_page.dart @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; import 'package:thingsboard_app/modules/alarm/alarms_page.dart'; -import 'package:thingsboard_app/modules/dashboard/dashboards_page.dart'; +import 'package:thingsboard_app/modules/device/devices_main_page.dart'; import 'package:thingsboard_app/modules/device/devices_page.dart'; import 'package:thingsboard_app/modules/home/home_page.dart'; +import 'package:thingsboard_app/modules/more/more_page.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class TbMainNavigationItem { @@ -24,8 +25,8 @@ class TbMainNavigationItem { static Map> mainPageStateMap = { Authority.SYS_ADMIN: Set.unmodifiable(['/home', '/tenants', '/more']), - Authority.TENANT_ADMIN: Set.unmodifiable(['/home', '/devices', '/dashboards', '/more']), - Authority.CUSTOMER_USER: Set.unmodifiable(['/home', '/devices', '/dashboards', '/more']), + Authority.TENANT_ADMIN: Set.unmodifiable(['/home', '/alarms', '/devices', '/more']), + Authority.CUSTOMER_USER: Set.unmodifiable(['/home', '/alarms', '/devices', '/more']), }; static bool isMainPageState(TbContext tbContext, String path) { @@ -50,7 +51,7 @@ class TbMainNavigationItem { switch(tbContext.tbClient.getAuthUser()!.authority) { case Authority.SYS_ADMIN: items.add(TbMainNavigationItem( - page: Scaffold(body: Center(child: Text('Tenants TODO'))), + page: TextContextWidget(tbContext, 'Tenants TODO'), title: 'Tenants', icon: Icon(Icons.supervisor_account), path: '/tenants' @@ -66,7 +67,7 @@ class TbMainNavigationItem { path: '/alarms' ), TbMainNavigationItem( - page: DevicesPage(tbContext), + page: DevicesMainPage(tbContext), title: 'Devices', icon: Icon(Icons.devices_other), path: '/devices' @@ -79,7 +80,7 @@ class TbMainNavigationItem { break; } items.add(TbMainNavigationItem( - page: Scaffold(body: Center(child: Text('TODO'))), + page: MorePage(tbContext), title: 'More', icon: Icon(Icons.menu), path: '/more' @@ -108,49 +109,56 @@ class MainPage extends TbPageWidget { } -class _MainPageState extends TbPageState with TbMainState { - - late int _currentIndex; +class _MainPageState extends TbPageState with TbMainState, TickerProviderStateMixin { + late ValueNotifier _currentIndexNotifier; late final List _tabItems; + late TabController _tabController; @override void initState() { super.initState(); _tabItems = TbMainNavigationItem.getItems(tbContext); - _currentIndex = _indexFromPath(widget._path); + int currentIndex = _indexFromPath(widget._path); + _tabController = TabController(initialIndex: currentIndex, length: _tabItems.length, vsync: this); + _currentIndexNotifier = ValueNotifier(currentIndex); + _tabController.addListener(() { + _currentIndexNotifier.value = _tabController.index; + }); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { - if (_currentIndex > 0) { - setState(() => _currentIndex = 0); + if (_tabController.index > 0) { + _setIndex(0); return false; } return true; }, child: Scaffold( - /* body: IndexedStack( - index: _currentIndex, - children: _tabItems.map((item) => item.page).toList(), - ),*/ - body: _tabItems.elementAt(_currentIndex).page, + body: TabBarView( + controller: _tabController, + children: _tabItems.map((item) => item.page).toList(), + ), bottomNavigationBar: Theme( data: Theme.of(context).copyWith( canvasColor: Theme.of(context).colorScheme.primary ), - child: BottomNavigationBar( - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white.withAlpha(97), - currentIndex: _currentIndex, - onTap: (int index) => setState(() => _currentIndex = index), - items: _tabItems.map((item) => BottomNavigationBarItem( - icon: item.icon, - label: item.title - )).toList() + child: ValueListenableBuilder( + valueListenable: _currentIndexNotifier, + builder: (context, index, child) => BottomNavigationBar( + type: BottomNavigationBarType.fixed, + selectedItemColor: Colors.white, + unselectedItemColor: Colors.white.withAlpha(97), + currentIndex: index, + onTap: (int index) => _setIndex(index) /*_currentIndex = index*/, + items: _tabItems.map((item) => BottomNavigationBarItem( + icon: item.icon, + label: item.title + )).toList() + ), ) ) ), @@ -169,9 +177,11 @@ class _MainPageState extends TbPageState with TbMainSt @override navigateToPath(String path) { int targetIndex = _indexFromPath(path); - if (_currentIndex != targetIndex) { - setState(() => _currentIndex = targetIndex); - } + _setIndex(targetIndex); + } + + _setIndex(int index) { + _tabController.index = index; } } diff --git a/lib/modules/more/more_page.dart b/lib/modules/more/more_page.dart new file mode 100644 index 0000000..e1710a3 --- /dev/null +++ b/lib/modules/more/more_page.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; + +class MorePage extends TbContextWidget { + + MorePage(TbContext tbContext) : super(tbContext); + + @override + _MorePageState createState() => _MorePageState(); + +} + +class _MorePageState extends TbContextState { + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Container( + padding: EdgeInsets.fromLTRB(16, 40, 16, 20), + child: + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.account_circle, size: 48, color: Color(0xFFAFAFAF)), + Spacer(), + IconButton(icon: Icon(Icons.settings, color: Color(0xFFAFAFAF)), onPressed: () => navigateTo('/profile')) + ], + ), + SizedBox(height: 22), + Text(_getUserDisplayName(), + style: TextStyle( + color: Color(0xFF282828), + fontWeight: FontWeight.w500, + fontSize: 20, + height: 23 / 20 + ) + ), + SizedBox(height: 2), + Text(_getAuthorityName(), + style: TextStyle( + color: Color(0xFFAFAFAF), + fontWeight: FontWeight.normal, + fontSize: 14, + height: 16 / 14 + ) + ), + SizedBox(height: 24), + Divider(color: Color(0xFFEDEDED)), + SizedBox(height: 8), + buildMoreMenuItems(context), + SizedBox(height: 8), + Divider(color: Color(0xFFEDEDED)), + SizedBox(height: 8), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + height: 48, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 0, horizontal: 18), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Icon(Icons.logout, color: Color(0xFFE04B2F)), + SizedBox(width: 34), + Text('Log out', + style: TextStyle( + color: Color(0xFFE04B2F), + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14 + )) + ] + ) + ) + ), + onTap: () { + tbClient.logout( + requestConfig: RequestConfig(ignoreErrors: true)); + } + ) + ], + ), + ) + ); + } + + Widget buildMoreMenuItems(BuildContext context) { + List items = MoreMenuItem.getItems(tbContext).map((menuItem) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + height: 48, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 0, horizontal: 18), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Icon(menuItem.icon, color: Color(0xFF282828)), + SizedBox(width: 34), + Text(menuItem.title, + style: TextStyle( + color: Color(0xFF282828), + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14 + )) + ] + ) + ) + ), + onTap: () { + navigateTo(menuItem.path); + } + ); + }).toList(); + return Column( + children: items + ); + } + + String _getUserDisplayName() { + var user = tbContext.userDetails; + var name = ''; + if (user != null) { + if ((user.firstName != null && user.firstName!.isNotEmpty) || + (user.lastName != null && user.lastName!.isNotEmpty)) { + if (user.firstName != null) { + name += user.firstName!; + } + if (user.lastName != null) { + if (name.isNotEmpty) { + name += ' '; + } + name += user.lastName!; + } + } else { + name = user.email; + } + } + return name; + } + + String _getAuthorityName() { + var user = tbContext.userDetails; + var name = ''; + if (user != null) { + var authority = user.authority; + switch(authority) { + case Authority.SYS_ADMIN: + name = 'System Administrator'; + break; + case Authority.TENANT_ADMIN: + name = 'Tenant Administrator'; + break; + case Authority.CUSTOMER_USER: + name = 'Customer'; + break; + } + } + return name; + } + +} + +class MoreMenuItem { + final String title; + final IconData icon; + final String path; + + MoreMenuItem({ + required this.title, + required this.icon, + required this.path + }); + + static List getItems(TbContext tbContext) { + if (tbContext.isAuthenticated) { + List items = []; + switch (tbContext.tbClient.getAuthUser()!.authority) { + case Authority.SYS_ADMIN: + break; + case Authority.TENANT_ADMIN: + items.addAll([ + MoreMenuItem( + title: 'Customers', + icon: Icons.supervisor_account, + path: '/customers' + ), + MoreMenuItem( + title: 'Assets', + icon: Icons.domain, + path: '/assets' + ), + MoreMenuItem( + title: 'Audit Logs', + icon: Icons.track_changes, + path: '/auditLogs' + ) + ]); + break; + case Authority.CUSTOMER_USER: + items.addAll([ + MoreMenuItem( + title: 'Assets', + icon: Icons.domain, + path: '/assets' + ) + ]); + break; + case Authority.REFRESH_TOKEN: + break; + case Authority.ANONYMOUS: + break; + } + return items; + } else { + return []; + } + } +} diff --git a/lib/modules/profile/profile_page.dart b/lib/modules/profile/profile_page.dart index 2823e23..b06307a 100644 --- a/lib/modules/profile/profile_page.dart +++ b/lib/modules/profile/profile_page.dart @@ -30,9 +30,7 @@ class _ProfilePageState extends TbPageState { return Scaffold( appBar: TbAppBar( tbContext, - title: const Text('Profile'), - showProfile: false, - showLogout: true, + title: const Text('Profile') ), body: FutureBuilder( future: userFuture, diff --git a/lib/utils/services/device_profile_cache.dart b/lib/utils/services/device_profile_cache.dart new file mode 100644 index 0000000..19d9ebf --- /dev/null +++ b/lib/utils/services/device_profile_cache.dart @@ -0,0 +1,25 @@ +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract class DeviceProfileCache { + + static final _cache = Map(); + + static Future getDeviceProfileInfo(ThingsboardClient tbClient, String name, String deviceId) async { + var deviceProfile = _cache[name]; + if (deviceProfile == null) { + var device = await tbClient.getDeviceService().getDevice(deviceId); + deviceProfile = await tbClient.getDeviceProfileService().getDeviceProfileInfo(device.deviceProfileId!.id!); + _cache[name] = deviceProfile; + } + return deviceProfile; + } + + static Future> getDeviceProfileInfos(ThingsboardClient tbClient, PageLink pageLink) async { + var deviceProfileInfos = await tbClient.getDeviceProfileService().getDeviceProfileInfos(pageLink); + deviceProfileInfos.data.forEach((deviceProfile) { + _cache[deviceProfile.name] = deviceProfile; + }); + return deviceProfileInfos; + } + +} diff --git a/lib/utils/services/entity_query_api.dart b/lib/utils/services/entity_query_api.dart new file mode 100644 index 0000000..fdfd763 --- /dev/null +++ b/lib/utils/services/entity_query_api.dart @@ -0,0 +1,61 @@ +import 'package:thingsboard_client/thingsboard_client.dart'; + +abstract class EntityQueryApi { + + static final activeDeviceKeyFilter = KeyFilter( + key: EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'), + valueType: EntityKeyValueType.BOOLEAN, + predicate: BooleanFilterPredicate( + operation: BooleanOperation.EQUAL, + value: FilterPredicateValue(true))); + + static final inactiveDeviceKeyFilter = KeyFilter( + key: EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'), + valueType: EntityKeyValueType.BOOLEAN, + predicate: BooleanFilterPredicate( + operation: BooleanOperation.EQUAL, + value: FilterPredicateValue(false))); + + static final defaultDeviceFields = [ + EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'name'), + EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'type'), + EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'createdTime') + ]; + + static final defaultDeviceAttributes = [ + EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active') + ]; + + static Future countDevices(ThingsboardClient tbClient, {String? deviceType, bool? active}) { + EntityFilter deviceFilter; + if (deviceType != null) { + deviceFilter = DeviceTypeFilter(deviceType: deviceType); + } else { + deviceFilter = EntityTypeFilter(entityType: EntityType.DEVICE); + } + EntityCountQuery deviceCountQuery = EntityCountQuery(entityFilter: deviceFilter); + if (active != null) { + deviceCountQuery.keyFilters = [active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter]; + } + return tbClient.getEntityQueryService().countEntitiesByQuery(deviceCountQuery); + } + + static EntityDataQuery createDefaultDeviceQuery({int pageSize = 10, String? searchText, String? deviceType, bool? active}) { + EntityFilter deviceFilter; + List? keyFilters; + if (deviceType != null) { + deviceFilter = DeviceTypeFilter(deviceType: deviceType); + } else { + deviceFilter = EntityTypeFilter(entityType: EntityType.DEVICE); + } + if (active != null) { + keyFilters = [active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter]; + } + return EntityDataQuery(entityFilter: deviceFilter, keyFilters: keyFilters, + entityFields: defaultDeviceFields, latestValues: defaultDeviceAttributes, pageLink: EntityDataPageLink(pageSize: pageSize, + textSearch: searchText, + sortOrder: EntityDataSortOrder(key: EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'createdTime'), + direction: EntityDataSortOrderDirection.DESC))); + } + +} diff --git a/lib/widgets/tb_app_bar.dart b/lib/widgets/tb_app_bar.dart index 409d78a..6081298 100644 --- a/lib/widgets/tb_app_bar.dart +++ b/lib/widgets/tb_app_bar.dart @@ -6,25 +6,19 @@ import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/config/themes/tb_theme.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:thingsboard_client/thingsboard_client.dart'; class TbAppBar extends TbContextWidget implements PreferredSizeWidget { final Widget? title; - final bool? showProfile; - final bool? showLogout; + final List? actions; final double? elevation; final bool showLoadingIndicator; - final ValueNotifier? searchModeNotifier; - final String? searchHint; - final void Function(String searchText)? onSearch; - final void Function()? onSearchClosed; @override final Size preferredSize; - TbAppBar(TbContext tbContext, {this.title, this.elevation, this.showProfile = true, this.showLogout = false, - this.showLoadingIndicator = false, this.searchModeNotifier, this.searchHint, this.onSearch, this.onSearchClosed}) : + TbAppBar(TbContext tbContext, {this.title, this.actions, this.elevation, + this.showLoadingIndicator = false}) : preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)), super(tbContext); @@ -35,19 +29,80 @@ class TbAppBar extends TbContextWidget implements Pref class _TbAppBarState extends TbContextState { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + List children = []; + children.add(buildDefaultBar()); + if (widget.showLoadingIndicator) { + children.add( + ValueListenableBuilder( + valueListenable: loadingNotifier, + builder: (context, bool loading, child) { + if (loading) { + return LinearProgressIndicator(); + } else { + return Container(height: 4); + } + } + ) + ); + } + return Column( + children: children, + ); + } + + AppBar buildDefaultBar() { + return AppBar( + title: widget.title, + actions: widget.actions, + elevation: widget.elevation, + ); + } +} + +class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget { + + final double? elevation; + final bool showLoadingIndicator; + final String? searchHint; + final void Function(String searchText)? onSearch; + + @override + final Size preferredSize; + + TbAppSearchBar(TbContext tbContext, {this.elevation, + this.showLoadingIndicator = false, this.searchHint, this.onSearch}) : + preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)), + super(tbContext); + + @override + _TbAppSearchBarState createState() => _TbAppSearchBarState(); +} + +class _TbAppSearchBarState extends TbContextState { + final TextEditingController _filter = new TextEditingController(); final _textUpdates = StreamController(); @override void initState() { super.initState(); - if (widget.searchModeNotifier != null) { - _textUpdates.add(''); - _filter.addListener(() { - _textUpdates.add(_filter.text); - }); - _textUpdates.stream.debounce(const Duration(milliseconds: 150)).distinct().forEach((element) => widget.onSearch!(element)); - } + // _textUpdates.add(''); + _filter.addListener(() { + _textUpdates.add(_filter.text); + }); + _textUpdates.stream.skip(1).debounce(const Duration(milliseconds: 150)).distinct().forEach((element) => widget.onSearch!(element)); } @override @@ -59,20 +114,7 @@ class _TbAppBarState extends TbContextState { @override Widget build(BuildContext context) { List children = []; - if (widget.searchModeNotifier != null) { - children.add(ValueListenableBuilder( - valueListenable: widget.searchModeNotifier!, - builder: (context, bool searchMode, child) { - if (searchMode) { - return buildSearchBar(); - } else { - return buildDefaultBar(); - } - } - )); - } else { - children.add(buildDefaultBar()); - } + children.add(buildSearchBar()); if (widget.showLoadingIndicator) { children.add( ValueListenableBuilder( @@ -98,58 +140,33 @@ class _TbAppBarState extends TbContextState { title: Theme( data: tbDarkTheme, child: TextField( - controller: _filter, - cursorColor: Colors.white, - decoration: new InputDecoration( + controller: _filter, + cursorColor: Colors.white, + decoration: new InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.only(left: 15, bottom: 11, top: 15, right: 15), - suffixIcon: new Icon(Icons.search, color: Colors.white), - hintText: widget.searchHint ?? 'Search...', - ), - ) - ), - leading: new IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { - _filter.text = ''; - widget.searchModeNotifier!.value = false; - }, - ), - ); - } - - AppBar buildDefaultBar() { - List actions = []; - if (widget.showProfile!) { - actions.add(IconButton( - icon: Icon( - Icons.account_circle, - color: Colors.white, - ), - onPressed: () { - navigateTo('/profile'); - }, - )); - } - if (widget.showLogout!) { - actions.add( - TextButton( - child: const Text('Logout', - style: TextStyle( - color: Colors.white - ), - ), - onPressed: () { - tbClient.logout( - requestConfig: RequestConfig(ignoreErrors: true)); - } + hintText: widget.searchHint ?? 'Search', + ), ) - ); - } - return AppBar( - title: widget.title, - actions: actions, - elevation: widget.elevation, + ), + actions: [ + ValueListenableBuilder(valueListenable: _filter, + builder: (context, value, child) { + if (_filter.text.isNotEmpty) { + return IconButton( + icon: Icon( + Icons.clear + ), + onPressed: () { + _filter.text = ''; + }, + ); + } else { + return Container(); + } + } + ) + ] ); } } diff --git a/pubspec.lock b/pubspec.lock index 96d8533..e72d97c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -84,7 +84,7 @@ packages: name: device_info url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" device_info_platform_interface: dependency: transitive description: @@ -225,7 +225,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.7.5" + version: "0.7.5+2" image_picker_for_web: dependency: transitive description: @@ -246,7 +246,7 @@ packages: name: infinite_scroll_pagination url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.1+1" intl: dependency: "direct main" description: @@ -411,7 +411,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "0627f3aea8e9252d7c178cec864908e564ddee0a" + resolved-ref: "39f0c98fc6543436d0fc388aad71688505f48d65" url: "git@github.com:thingsboard/dart_thingsboard_client.git" source: git version: "1.0.0" @@ -428,7 +428,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.3" + version: "6.0.4" url_launcher_linux: dependency: transitive description: @@ -449,7 +449,7 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" url_launcher_web: dependency: transitive description: @@ -477,7 +477,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.1.0" + version: "5.1.1" yaml: dependency: transitive description: