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

@@ -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,