Alarms, Devices and More pages
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -13,3 +13,4 @@ class DashboardsListWidget extends EntitiesListPageLinkWidget<DashboardInfo> wit
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
455
lib/modules/device/device_profiles_base.dart
Normal file
455
lib/modules/device/device_profiles_base.dart
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
12
lib/modules/device/device_profiles_grid.dart
Normal file
12
lib/modules/device/device_profiles_grid.dart
Normal 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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
))
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
39
lib/modules/device/devices_main_page.dart
Normal file
39
lib/modules/device/devices_main_page.dart
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
228
lib/modules/more/more_page.dart
Normal file
228
lib/modules/more/more_page.dart
Normal 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
25
lib/utils/services/device_profile_cache.dart
Normal file
25
lib/utils/services/device_profile_cache.dart
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
61
lib/utils/services/entity_query_api.dart
Normal file
61
lib/utils/services/entity_query_api.dart
Normal 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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user