Alarms, Devices and More pages

This commit is contained in:
Igor Kulikov
2021-05-25 20:19:00 +03:00
parent bbe8c0c0d8
commit 5e536ab217
36 changed files with 1690 additions and 338 deletions

View File

@@ -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<String, List<String>> 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();

View File

@@ -306,6 +306,20 @@ class TbContext {
router.pop<T>(currentState!.context, result);
}
}
Future<bool?> confirm({required String title, required String message, String cancel = 'Cancel', String ok = 'Ok'}) {
return showDialog<bool>(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>([T? result]) => _tbContext.pop<T>(result);
Future<bool?> 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);

View File

@@ -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<W extends TbPageWidget<W,S>, S extends TbPageState<W,
}
}
class TextContextWidget extends TbContextWidget<TextContextWidget, _TextContextWidgetState> {
final String text;
TextContextWidget(TbContext tbContext, this.text) : super(tbContext);
@override
_TextContextWidgetState createState() => _TextContextWidgetState();
}
class _TextContextWidgetState extends TbContextState<TextContextWidget, _TextContextWidgetState> {
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text(widget.text)));
}
}

View File

@@ -42,10 +42,6 @@ mixin EntitiesBase<T, P> 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<T, P> on HasTbContext {
}
mixin EntitiesBaseWithPageLink<T> on EntitiesBase<T, PageLink> {
abstract class PageKeyController<P> extends ValueNotifier<PageKeyValue<P>> {
@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<P> {
final P pageKey;
PageKeyValue(this.pageKey);
}
class PageLinkController extends PageKeyController<PageLink> {
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<T> on EntitiesBase<T, TimePageLink> {
class TimePageLinkController extends PageKeyController<TimePageLink> {
@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<T> extends BaseEntitiesWidget<T, PageLink> with EntitiesBaseWithPageLink<T> {
BaseEntitiesPageLinkWidget(TbContext tbContext): super(tbContext);
}
abstract class BaseEntitiesTimePageLinkWidget<T> extends BaseEntitiesWidget<T, TimePageLink> with EntitiesBaseWithTimePageLink<T> {
BaseEntitiesTimePageLinkWidget(TbContext tbContext): super(tbContext);
}
abstract class BaseEntitiesWidget<T, P> extends TbContextWidget<BaseEntitiesWidget<T, P>, BaseEntitiesState<T, P>> with EntitiesBase<T, P> {
BaseEntitiesWidget(TbContext tbContext): super(tbContext);
final bool searchMode;
final PageKeyController<P> 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<T, P> extends TbContextState<BaseEntitiesWidget
late final PagingController<P, T> pagingController;
Completer<void>? _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<T, P> 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<void> _refresh() {
if (_refreshCompleter == null) {
@@ -124,7 +168,12 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
}
void _refreshPagingController() {
_fetchPage(widget.createFirstKey(), refresh: true);
if (_reloadData) {
pagingController.refresh();
_reloadData = false;
} else {
_fetchPage(widget.pageKeyController.value.pageKey, refresh: true);
}
}
Future<void> _fetchPage(P pageKey, {bool refresh = false}) async {
@@ -143,7 +192,7 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
if (isLastPage) {
pagingController.appendLastPage(pageData.data);
} else {
final nextPageKey = widget.nextPageKey(pageKey);
final nextPageKey = widget.pageKeyController.nextPageKey(pageKey);
pagingController.appendPage(pageData.data, nextPageKey);
}
} catch (error) {
@@ -207,7 +256,7 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
return FirstPageExceptionIndicator(
title: widget.noItemsFoundText,
message: 'The list is currently empty.',
onTryAgain: () => pagingController.refresh(),
onTryAgain: widget.searchMode ? null : () => pagingController.refresh(),
);
}

View File

@@ -14,6 +14,8 @@ mixin EntitiesGridStateBase on StatefulWidget {
class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
_EntitiesGridState() : super();
@override
Widget pagedViewBuilder(BuildContext context) {
var heading = widget.buildHeading(context);

View File

@@ -14,6 +14,8 @@ mixin EntitiesListStateBase on StatefulWidget {
class _EntitiesListState<T,P> extends BaseEntitiesState<T, P> {
_EntitiesListState() : super();
@override
Widget pagedViewBuilder(BuildContext context) {
var heading = widget.buildHeading(context);

View File

@@ -32,8 +32,13 @@ class EntitiesListWidgetController {
}
abstract class EntitiesListPageLinkWidget<T> extends EntitiesListWidget<T, PageLink> with EntitiesBaseWithPageLink<T> {
EntitiesListPageLinkWidget(TbContext tbContext, {EntitiesListWidgetController? controller}): super(tbContext, controller: controller);
abstract class EntitiesListPageLinkWidget<T> extends EntitiesListWidget<T, PageLink> {
EntitiesListPageLinkWidget(TbContext tbContext, {EntitiesListWidgetController? controller}) : super(tbContext, controller: controller);
@override
PageKeyController<PageLink> createPageKeyController() => PageLinkController(pageSize: 5);
}
abstract class EntitiesListWidget<T, P> extends TbContextWidget<EntitiesListWidget<T,P>, _EntitiesListWidgetState<T,P>> with EntitiesBase<T,P> {
@@ -47,6 +52,8 @@ abstract class EntitiesListWidget<T, P> extends TbContextWidget<EntitiesListWidg
@override
_EntitiesListWidgetState createState() => _EntitiesListWidgetState(_controller);
PageKeyController<P> createPageKeyController();
void onViewAll();
}
@@ -55,6 +62,8 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
final EntitiesListWidgetController? _controller;
late final PageKeyController<P> _pageKeyController;
final StreamController<PageData<T>?> _entitiesStreamController = StreamController.broadcast();
_EntitiesListWidgetState(EntitiesListWidgetController? controller):
@@ -63,6 +72,7 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
@override
void initState() {
super.initState();
_pageKeyController = widget.createPageKeyController();
if (_controller != null) {
_controller!._registerEntitiesWidgetState(this);
}
@@ -74,13 +84,14 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
if (_controller != null) {
_controller!._unregisterEntitiesWidgetState(this);
}
_pageKeyController.dispose();
_entitiesStreamController.close();
super.dispose();
}
Future<void> _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;
}

View File

@@ -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<String, dynamic> 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);

View File

@@ -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<AlarmInfo, AlarmQuery> {
@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<PageData<AlarmInfo>> fetchEntities(AlarmQuery query) {
return tbClient.getAlarmService().getAllAlarms(query);
@@ -61,110 +55,202 @@ mixin AlarmsBase on EntitiesBase<AlarmInfo, AlarmQuery> {
}
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<AlarmQuery> {
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<AlarmCard, _AlarmCardState> {
final AlarmInfo alarm;
AlarmCard(TbContext tbContext, {required this.alarm}) : super(tbContext);
@override
_AlarmCardState createState() => _AlarmCardState(alarm);
}
class _AlarmCardState extends TbContextState<AlarmCard, _AlarmCardState> {
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;
});
}
}
}

View File

@@ -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<AlarmInfo, AlarmQuery> with AlarmsBase, EntitiesListStateBase {
AlarmsList(TbContext tbContext) : super(tbContext);
AlarmsList(TbContext tbContext, PageKeyController<AlarmQuery> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -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, _AlarmsPageState> {
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<AlarmsPage, _AlarmsPageState> {
class _AlarmsPageState extends TbContextState<AlarmsPage, _AlarmsPageState> {
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();
}
}

View File

@@ -9,7 +9,8 @@ import 'asset_details_page.dart';
class AssetRoutes extends TbRoutes {
late var assetsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return AssetsPage(tbContext);
var searchMode = params['search']?.first == 'true';
return AssetsPage(tbContext, searchMode: searchMode);
});
late var assetDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {

View File

@@ -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<AssetInfo> {
mixin AssetsBase on EntitiesBase<AssetInfo, PageLink> {
@override
String get title => 'Assets';

View File

@@ -5,9 +5,9 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'assets_base.dart';
class AssetsList extends BaseEntitiesPageLinkWidget<AssetInfo> with AssetsBase, EntitiesListStateBase {
class AssetsList extends BaseEntitiesWidget<AssetInfo, PageLink> with AssetsBase, EntitiesListStateBase {
AssetsList(TbContext tbContext) : super(tbContext);
AssetsList(TbContext tbContext, PageKeyController<PageLink> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -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, _AssetsPageState> {
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<AssetsPage, _AssetsPageState> {
class _AssetsPageState extends TbPageState<AssetsPage, _AssetsPageState> {
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();
}
}

View File

@@ -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<DashboardInfo> {
mixin DashboardsBase on EntitiesBase<DashboardInfo, PageLink> {
@override
String get title => 'Dashboards';

View File

@@ -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<DashboardInfo> with DashboardsBase, EntitiesGridStateBase {
class DashboardsGridWidget extends TbContextWidget<DashboardsGridWidget, _DashboardsGridWidgetState> {
DashboardsGrid(TbContext tbContext) : super(tbContext);
DashboardsGridWidget(TbContext tbContext) : super(tbContext);
@override
_DashboardsGridWidgetState createState() => _DashboardsGridWidgetState();
}
class _DashboardsGridWidgetState extends TbContextState<DashboardsGridWidget, _DashboardsGridWidgetState> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
return DashboardsGrid(tbContext, _pageLinkController);
}
@override
void dispose() {
_pageLinkController.dispose();
super.dispose();
}
}
class DashboardsGrid extends BaseEntitiesWidget<DashboardInfo, PageLink> with DashboardsBase, EntitiesGridStateBase {
DashboardsGrid(TbContext tbContext, PageKeyController<PageLink> pageKeyController) : super(tbContext, pageKeyController);
}

View File

@@ -5,9 +5,9 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'dashboards_base.dart';
class DashboardsList extends BaseEntitiesPageLinkWidget<DashboardInfo> with DashboardsBase, EntitiesListStateBase {
class DashboardsList extends BaseEntitiesWidget<DashboardInfo, PageLink> with DashboardsBase, EntitiesListStateBase {
DashboardsList(TbContext tbContext) : super(tbContext);
DashboardsList(TbContext tbContext, PageKeyController<PageLink> pageKeyController) : super(tbContext, pageKeyController);
}

View File

@@ -13,3 +13,4 @@ class DashboardsListWidget extends EntitiesListPageLinkWidget<DashboardInfo> wit
}
}

View File

@@ -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<DashboardsPage, _DashboardsPageState>
class _DashboardsPageState extends TbPageState<DashboardsPage, _DashboardsPageState> {
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<DashboardsPage, _DashboardsPageSt
);
}
@override
void dispose() {
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,455 @@
import 'dart:async';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/constants/assets_path.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 DeviceProfilesBase on EntitiesBase<DeviceProfileInfo, PageLink> {
final RefreshDeviceCounts refreshDeviceCounts = RefreshDeviceCounts();
@override
String get title => 'Devices';
@override
String get noItemsFoundText => 'No devices found';
@override
Future<PageData<DeviceProfileInfo>> fetchEntities(PageLink pageLink) {
return DeviceProfileCache.getDeviceProfileInfos(tbClient, pageLink);
}
@override
void onEntityTap(DeviceProfileInfo deviceProfile) {
navigateTo('/deviceList?deviceType=${deviceProfile.name}');
}
@override
Future<void> 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<void> Function()? onRefresh;
}
class AllDevicesCard extends TbContextWidget<AllDevicesCard, _AllDevicesCardState> {
final RefreshDeviceCounts refreshDeviceCounts;
AllDevicesCard(TbContext tbContext, this.refreshDeviceCounts) : super(tbContext);
@override
_AllDevicesCardState createState() => _AllDevicesCardState();
}
class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCardState> {
final StreamController<int?> _activeDevicesCount = StreamController.broadcast();
final StreamController<int?> _inactiveDevicesCount = StreamController.broadcast();
@override
void initState() {
super.initState();
widget.refreshDeviceCounts.onRefresh = _countDevices;
_countDevices();
}
@override
void dispose() {
_activeDevicesCount.close();
_inactiveDevicesCount.close();
super.dispose();
}
Future<void> _countDevices() {
_activeDevicesCount.add(null);
_inactiveDevicesCount.add(null);
Future<int> activeDevicesCount = EntityQueryApi.countDevices(tbClient, active: true);
Future<int> inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, active: false);
Future<List<int>> 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<int?>(
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<int?>(
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<DeviceProfileCard, _DeviceProfileCardState> {
final DeviceProfileInfo deviceProfile;
DeviceProfileCard(TbContext tbContext, this.deviceProfile) : super(tbContext);
@override
_DeviceProfileCardState createState() => _DeviceProfileCardState();
}
class _DeviceProfileCardState extends TbContextState<DeviceProfileCard, _DeviceProfileCardState> {
late Future<int> activeDevicesCount;
late Future<int> 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<int>(
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<int>(
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;
}
}

View File

@@ -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<DeviceProfileInfo, PageLink> with DeviceProfilesBase, EntitiesGridStateBase {
DeviceProfilesGrid(TbContext tbContext, PageKeyController<PageLink> pageKeyController) : super(tbContext, pageKeyController);
}

View File

@@ -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<String, dynamic> 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<String, dynamic> 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);
}

View File

@@ -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<DeviceInfo> {
mixin DevicesBase on EntitiesBase<EntityData, EntityDataQuery> {
@override
String get title => 'Devices';
@@ -12,108 +17,206 @@ mixin DevicesBase on EntitiesBaseWithPageLink<DeviceInfo> {
String get noItemsFoundText => 'No devices found';
@override
Future<PageData<DeviceInfo>> fetchEntities(PageLink pageLink) {
if (tbClient.isTenantAdmin()) {
return tbClient.getDeviceService().getTenantDeviceInfos(pageLink);
} else {
return tbClient.getDeviceService().getCustomerDeviceInfos(tbClient.getAuthUser()!.customerId, pageLink);
}
Future<PageData<EntityData>> 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<EntityDataQuery> {
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<DeviceCard, _DeviceCardState> {
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<DeviceCard, _DeviceCardState> {
final entityDateFormat = DateFormat('yyyy-MM-dd');
late Future<DeviceProfileInfo> 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<DeviceProfileInfo>(
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
))
],
)
],
)
)
)
]
);
}
}

View File

@@ -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<DeviceInfo> with DevicesBase, EntitiesListStateBase {
class DevicesList extends BaseEntitiesWidget<EntityData, EntityDataQuery> with DevicesBase, EntitiesListStateBase {
DevicesList(TbContext tbContext) : super(tbContext);
final bool displayDeviceImage;
DevicesList(TbContext tbContext, PageKeyController<EntityDataQuery> pageKeyController, {searchMode = false, this.displayDeviceImage = false}):
super(tbContext, pageKeyController, searchMode: searchMode);
@override
bool displayCardImage(bool listWidgetCard) => displayDeviceImage;
}

View File

@@ -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<DeviceInfo> with DevicesBase {
class DevicesListWidget extends EntitiesListWidget<EntityData, EntityDataQuery> with DevicesBase {
DevicesListWidget(TbContext tbContext, {EntitiesListWidgetController? controller}): super(tbContext, controller: controller);
@@ -12,4 +13,7 @@ class DevicesListWidget extends EntitiesListPageLinkWidget<DeviceInfo> with Devi
navigateTo('/devices');
}
@override
PageKeyController<EntityDataQuery> createPageKeyController() => DeviceQueryController(pageSize: 5);
}

View File

@@ -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, _DevicesMainPageState> {
DevicesMainPage(TbContext tbContext) : super(tbContext);
@override
_DevicesMainPageState createState() => _DevicesMainPageState();
}
class _DevicesMainPageState extends TbContextState<DevicesMainPage, _DevicesMainPageState> {
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();
}
}

View File

@@ -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<DevicesPage, _DevicesPageState> {
class DevicesPage extends TbPageWidget<DevicesPage, _DevicesPageState> {
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<DevicesPage, _DevicesPageState> {
class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
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<String> 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();
}
}

View File

@@ -17,7 +17,7 @@ class HomePage extends TbContextWidget<HomePage, _HomePageState> {
}
class _HomePageState extends TbContextState<HomePage, _HomePageState> {
class _HomePageState extends TbContextState<HomePage, _HomePageState> with AutomaticKeepAliveClientMixin<HomePage> {
final EntitiesListWidgetController _entitiesWidgetController = EntitiesListWidgetController();
@@ -26,6 +26,11 @@ class _HomePageState extends TbContextState<HomePage, _HomePageState> {
super.initState();
}
@override
bool get wantKeepAlive {
return true;
}
@override
void dispose() {
_entitiesWidgetController.dispose();
@@ -34,6 +39,7 @@ class _HomePageState extends TbContextState<HomePage, _HomePageState> {
@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<HomePage, _HomePageState> {
if (tbClient.isSystemAdmin()) {
return _buildSysAdminHome(context);
} else {
return DashboardsGrid(tbContext);
return DashboardsGridWidget(tbContext);
}
}

View File

@@ -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<Authority, Set<String>> 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<MainPage, _MainPageState> {
}
class _MainPageState extends TbPageState<MainPage, _MainPageState> with TbMainState {
late int _currentIndex;
class _MainPageState extends TbPageState<MainPage, _MainPageState> with TbMainState, TickerProviderStateMixin {
late ValueNotifier<int> _currentIndexNotifier;
late final List<TbMainNavigationItem> _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<int>(
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<MainPage, _MainPageState> with TbMainSt
@override
navigateToPath(String path) {
int targetIndex = _indexFromPath(path);
if (_currentIndex != targetIndex) {
setState(() => _currentIndex = targetIndex);
}
_setIndex(targetIndex);
}
_setIndex(int index) {
_tabController.index = index;
}
}

View File

@@ -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, _MorePageState> {
MorePage(TbContext tbContext) : super(tbContext);
@override
_MorePageState createState() => _MorePageState();
}
class _MorePageState extends TbContextState<MorePage, _MorePageState> {
@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<Widget> 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<MoreMenuItem> getItems(TbContext tbContext) {
if (tbContext.isAuthenticated) {
List<MoreMenuItem> 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 [];
}
}
}

View File

@@ -30,9 +30,7 @@ class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
return Scaffold(
appBar: TbAppBar(
tbContext,
title: const Text('Profile'),
showProfile: false,
showLogout: true,
title: const Text('Profile')
),
body: FutureBuilder<User>(
future: userFuture,

View File

@@ -0,0 +1,25 @@
import 'package:thingsboard_client/thingsboard_client.dart';
abstract class DeviceProfileCache {
static final _cache = Map<String, DeviceProfileInfo>();
static Future<DeviceProfileInfo> 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<PageData<DeviceProfileInfo>> getDeviceProfileInfos(ThingsboardClient tbClient, PageLink pageLink) async {
var deviceProfileInfos = await tbClient.getDeviceProfileService().getDeviceProfileInfos(pageLink);
deviceProfileInfos.data.forEach((deviceProfile) {
_cache[deviceProfile.name] = deviceProfile;
});
return deviceProfileInfos;
}
}

View File

@@ -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>[
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>[
EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active')
];
static Future<int> 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<KeyFilter>? 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)));
}
}

View File

@@ -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<TbAppBar, _TbAppBarState> implements PreferredSizeWidget {
final Widget? title;
final bool? showProfile;
final bool? showLogout;
final List<Widget>? actions;
final double? elevation;
final bool showLoadingIndicator;
final ValueNotifier<bool>? 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<TbAppBar, _TbAppBarState> implements Pref
class _TbAppBarState extends TbContextState<TbAppBar, _TbAppBarState> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
List<Widget> children = <Widget>[];
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<TbAppSearchBar, _TbAppSearchBarState> 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<TbAppSearchBar, _TbAppSearchBarState> {
final TextEditingController _filter = new TextEditingController();
final _textUpdates = StreamController<String>();
@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<TbAppBar, _TbAppBarState> {
@override
Widget build(BuildContext context) {
List<Widget> children = <Widget>[];
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<TbAppBar, _TbAppBarState> {
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<Widget> actions = <Widget>[];
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();
}
}
)
]
);
}
}