diff --git a/assets/images/dashboard-placeholder.png b/assets/images/dashboard-placeholder.png index 4f98c9c..5387405 100644 Binary files a/assets/images/dashboard-placeholder.png and b/assets/images/dashboard-placeholder.png differ diff --git a/assets/images/device-profile-placeholder.png b/assets/images/device-profile-placeholder.png index 36acff2..908525b 100644 Binary files a/assets/images/device-profile-placeholder.png and b/assets/images/device-profile-placeholder.png differ diff --git a/assets/images/thingsboard.svg b/assets/images/thingsboard.svg new file mode 100644 index 0000000..dfc7ba6 --- /dev/null +++ b/assets/images/thingsboard.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/thingsboard_center.svg b/assets/images/thingsboard_center.svg new file mode 100644 index 0000000..13f6d48 --- /dev/null +++ b/assets/images/thingsboard_center.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/thingsboard_logo_blue.svg b/assets/images/thingsboard_logo_blue.svg deleted file mode 100644 index 03e3524..0000000 --- a/assets/images/thingsboard_logo_blue.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/thingsboard_outer.svg b/assets/images/thingsboard_outer.svg new file mode 100644 index 0000000..8dfa678 --- /dev/null +++ b/assets/images/thingsboard_outer.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/thingsboard_with_title.svg b/assets/images/thingsboard_with_title.svg new file mode 100644 index 0000000..2d98f2f --- /dev/null +++ b/assets/images/thingsboard_with_title.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/config/themes/tb_theme.dart b/lib/config/themes/tb_theme.dart index 7a9daae..bc01db1 100644 --- a/lib/config/themes/tb_theme.dart +++ b/lib/config/themes/tb_theme.dart @@ -1,43 +1,73 @@ import 'package:flutter/material.dart'; -const int _tbPrimaryColor = 0xFF305680; -const int _tbSecondaryColor = 0xFF527dad; -const int _tbDarkPrimaryColor = 0xFF9fa8da; +const int _tbPrimaryColorValue = 0xFF305680; +const Color _tbPrimaryColor = Color(_tbPrimaryColorValue); +const Color _tbSecondaryColor = Color(0xFF527dad); +const Color _tbDarkPrimaryColor = Color(0xFF9fa8da); + +const int _tbTextColorValue = 0xFF282828; +const Color _tbTextColor = Color(_tbTextColorValue); + +var tbTypography = Typography.material2018(); const tbMatIndigo = MaterialColor( - _tbPrimaryColor, + _tbPrimaryColorValue, { 50: Color(0xFFE8EAF6), 100: Color(0xFFC5CAE9), 200: Color(0xFF9FA8DA), 300: Color(0xFF7986CB), 400: Color(0xFF5C6BC0), - 500: Color(_tbPrimaryColor), - 600: Color(_tbSecondaryColor), + 500: _tbPrimaryColor, + 600: _tbSecondaryColor, 700: Color(0xFF303F9F), 800: Color(0xFF283593), 900: Color(0xFF1A237E), },); const tbDarkMatIndigo = MaterialColor( - _tbPrimaryColor, + _tbPrimaryColorValue, { 50: Color(0xFFE8EAF6), 100: Color(0xFFC5CAE9), 200: Color(0xFF9FA8DA), 300: Color(0xFF7986CB), 400: Color(0xFF5C6BC0), - 500: Color(_tbDarkPrimaryColor), - 600: Color(_tbSecondaryColor), + 500: _tbDarkPrimaryColor, + 600: _tbSecondaryColor, 700: Color(0xFF303F9F), - 800: Color(_tbPrimaryColor), + 800: _tbPrimaryColor, 900: Color(0xFF1A237E), },); ThemeData tbTheme = ThemeData( primarySwatch: tbMatIndigo, accentColor: Colors.deepOrange, - scaffoldBackgroundColor: Color(0xFFF0F4F9) + scaffoldBackgroundColor: Color(0xFFFAFAFA), + textTheme: tbTypography.black, + primaryTextTheme: tbTypography.black, + typography: tbTypography, + appBarTheme: AppBarTheme( + backgroundColor: Colors.white, + foregroundColor: _tbTextColor, + /* titleTextStyle: TextStyle( + color: _tbTextColor + ), + toolbarTextStyle: TextStyle( + color: _tbTextColor + ), */ + iconTheme: IconThemeData( + color: _tbTextColor + ) + + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: Colors.white, + selectedItemColor: _tbPrimaryColor, + unselectedItemColor: _tbPrimaryColor.withAlpha((255 * 0.38).ceil()), + showSelectedLabels: true, + showUnselectedLabels: true + ) ); ThemeData tbDarkTheme = ThemeData( diff --git a/lib/constants/assets_path.dart b/lib/constants/assets_path.dart index cba6835..e43bbf2 100644 --- a/lib/constants/assets_path.dart +++ b/lib/constants/assets_path.dart @@ -1,6 +1,8 @@ abstract class ThingsboardImage { - static final thingsBoardLogoBlue = 'assets/images/thingsboard_logo_blue.svg'; - static final thingsboard = 'assets/images/thingsboard.png'; + static final thingsBoardWithTitle = 'assets/images/thingsboard_with_title.svg'; + static final thingsboard = 'assets/images/thingsboard.svg'; + static final thingsboardOuter = 'assets/images/thingsboard_outer.svg'; + static final thingsboardCenter = 'assets/images/thingsboard_center.svg'; static final dashboardPlaceholder = 'assets/images/dashboard-placeholder.png'; static final deviceProfilePlaceholder = 'assets/images/device-profile-placeholder.png'; } diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index 9a937c0..c4c13a9 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -1,10 +1,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:thingsboard_app/constants/assets_path.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; @@ -12,12 +12,7 @@ import 'package:thingsboard_app/core/context/tb_context_widget.dart'; class LoginPage extends TbPageWidget { - LoginPage(TbContext tbContext) : super(tbContext) { - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - systemNavigationBarColor: Colors.white, - systemNavigationBarIconBrightness: Brightness.light - )); - } + LoginPage(TbContext tbContext) : super(tbContext); @override _LoginPageState createState() => _LoginPageState(); @@ -26,6 +21,8 @@ class LoginPage extends TbPageWidget { class _LoginPageState extends TbPageState { + final _isLoginNotifier = ValueNotifier(false); + final usernameController = TextEditingController(); final passwordController = TextEditingController(); @@ -49,7 +46,7 @@ class _LoginPageState extends TbPageState { title: const Text('Login to ThingsBoard'), ), body: ValueListenableBuilder( - valueListenable: loadingNotifier, + valueListenable: _isLoginNotifier, builder: (BuildContext context, bool loading, child) { List children = [ SingleChildScrollView( @@ -61,7 +58,8 @@ class _LoginPageState extends TbPageState { child: Container( width: 300, height: 150, - child: SvgPicture.asset(ThingsboardImage.thingsBoardLogoBlue, + child: SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle, + color: Theme.of(context).primaryColor, semanticsLabel: 'ThingsBoard Logo') ) ) @@ -107,9 +105,15 @@ class _LoginPageState extends TbPageState { decoration: BoxDecoration( color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(4)), child: TextButton( - onPressed: loading ? null : () { - tbClient.login( - LoginRequest(usernameController.text, passwordController.text)); + onPressed: loading ? null : () async { + _isLoginNotifier.value = true; + try { + await tbClient.login( + LoginRequest(usernameController.text, + passwordController.text)); + } catch (e) { + _isLoginNotifier.value = false; + } }, child: Text( 'Login', @@ -126,6 +130,9 @@ class _LoginPageState extends TbPageState { ) ]; if (loading) { + var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window); + var bottomPadding = data.padding.top; + bottomPadding += kToolbarHeight; children.add( SizedBox.expand( child: ClipRect( @@ -135,15 +142,16 @@ class _LoginPageState extends TbPageState { decoration: new BoxDecoration( color: Colors.grey.shade200.withOpacity(0.2) ), - child: Center( - child: CircularProgressIndicator(), + child: Container( + padding: EdgeInsets.only(bottom: bottomPadding), + alignment: Alignment.center, + child: TbProgressIndicator(size: 50.0), ), ) ) ) ) ); - //children.add(Center(child: CircularProgressIndicator())); } return Stack( children: children, diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index 75edf60..933ec61 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -81,12 +81,31 @@ class TbLogger { } } +typedef OpenDashboardCallback = void Function(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar}); + +abstract class TbMainDashboardHolder { + + Future navigateToDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar, bool animate = true}); + + Future openMain({bool animate}); + + Future closeMain({bool animate}); + + Future openDashboard({bool animate}); + + Future closeDashboard({bool animate}); + + bool isDashboardOpen(); + + Future dashboardGoBack(); + +} class TbContext { static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); bool _initialized = false; bool isUserLoaded = false; - bool isAuthenticated = false; + final ValueNotifier _isAuthenticated = ValueNotifier(false); User? userDetails; HomeDashboardInfo? homeDashboard; final _isLoadingNotifier = ValueNotifier(false); @@ -94,6 +113,7 @@ class TbContext { late final _widgetActionHandler; late final AndroidDeviceInfo? _androidInfo; late final IosDeviceInfo? _iosInfo; + TbMainDashboardHolder? _mainDashboardHolder; GlobalKey messengerKey = GlobalKey(); late ThingsboardClient tbClient; @@ -137,6 +157,10 @@ class TbContext { } } + void setMainDashboardHolder(TbMainDashboardHolder holder) { + _mainDashboardHolder = holder; + } + void onError(ThingsboardError tbError) { log.error('onError', tbError, tbError.getStackTrace()); showErrorNotification(tbError.message!); @@ -214,8 +238,8 @@ class TbContext { try { log.debug('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}'); isUserLoaded = true; - isAuthenticated = tbClient.isAuthenticated(); - if (tbClient.isAuthenticated()) { + _isAuthenticated.value = tbClient.isAuthenticated(); + if (isAuthenticated) { log.debug('authUser: ${tbClient.getAuthUser()}'); if (tbClient.getAuthUser()!.userId != null) { try { @@ -230,20 +254,33 @@ class TbContext { userDetails = null; homeDashboard = null; } - updateRouteState(); + await updateRouteState(); } catch (e, s) { log.error('Error: $e', e, s); } } - void updateRouteState() { + Listenable get isAuthenticatedListenable => _isAuthenticated; + + bool get isAuthenticated => _isAuthenticated.value; + + Future updateRouteState() async { if (currentState != null) { if (tbClient.isAuthenticated()) { var defaultDashboardId = _defaultDashboardId(); if (defaultDashboardId != null) { bool fullscreen = _userForceFullscreen(); - navigateTo('/dashboard/$defaultDashboardId?fullscreen=$fullscreen', replace: true, transition: TransitionType.fadeIn, transitionDuration: Duration(milliseconds: 750)); + if (!fullscreen) { + await navigateToDashboard(defaultDashboardId, animate: false); + navigateTo('/home', + replace: true, + transition: TransitionType.none); + } else { + navigateTo('/fullscreenDashboard/$defaultDashboardId', + replace: true, + transition: TransitionType.fadeIn); + } } else { navigateTo('/home', replace: true, transition: TransitionType.fadeIn, transitionDuration: Duration(milliseconds: 750)); } @@ -276,9 +313,24 @@ class TbContext { } } - Future navigateTo(String path, {bool replace = false, bool clearStack = false, TransitionType? transition, Duration? transitionDuration}) async { + bool isHomePage() { + if (currentState != null) { + if (currentState is TbMainState) { + var mainState = currentState as TbMainState; + return mainState.isHomePage(); + } + } + return false; + } + + Future navigateTo(String path, {bool replace = false, bool clearStack = false, + TransitionType? transition, Duration? transitionDuration, bool restoreDashboard = true}) async { if (currentState != null) { hideNotification(); + bool isOpenedDashboard = _mainDashboardHolder?.isDashboardOpen() == true; + if (isOpenedDashboard) { + _mainDashboardHolder?.openMain(); + } if (currentState is TbMainState) { var mainState = currentState as TbMainState; if (mainState.canNavigate(path) && !replace) { @@ -290,23 +342,48 @@ class TbContext { replace = true; clearStack = true; } - if (transition == null) { + if (isOpenedDashboard) { + transition = TransitionType.none; + } else if (transition == null) { if (replace) { transition = TransitionType.fadeIn; } else { transition = TransitionType.inFromRight; } } - return await router.navigateTo(currentState!.context, path, transition: transition, transitionDuration: transitionDuration, replace: replace, clearStack: clearStack); + var res = await router.navigateTo(currentState!.context, path, transition: transition, transitionDuration: transitionDuration, replace: replace, clearStack: clearStack); + if (isOpenedDashboard) { + await _mainDashboardHolder?.closeMain(); + } + return res; } } + Future navigateToDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar, bool animate = true}) async { + await _mainDashboardHolder?.navigateToDashboard(dashboardId, dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar, animate: animate); + } + void pop([T? result]) { if (currentState != null) { router.pop(currentState!.context, result); } } + Future maybePop([ T? result ]) async { + if (currentState != null) { + return Navigator.of(currentState!.context).maybePop(result); + } else { + return true; + } + } + + Future willPop() async { + if (_mainDashboardHolder != null) { + return await _mainDashboardHolder!.dashboardGoBack(); + } + return true; + } + Future confirm({required String title, required String message, String cancel = 'Cancel', String ok = 'Ok'}) { return showDialog(context: currentState!.context, builder: (context) => AlertDialog( @@ -330,7 +407,13 @@ mixin HasTbContext { } void setupCurrentState(TbContextState currentState) { + if (_tbContext.currentState != null) { + ModalRoute.of(_tbContext.currentState!.context)?.removeScopedWillPopCallback(_tbContext.willPop); + } _tbContext.currentState = currentState; + if (_tbContext.currentState != null) { + ModalRoute.of(_tbContext.currentState!.context)?.addScopedWillPopCallback(_tbContext.willPop); + } } void setupTbContext(TbContextState currentState) { @@ -357,6 +440,11 @@ mixin HasTbContext { void pop([T? result]) => _tbContext.pop(result); + Future maybePop([ T? result ]) => _tbContext.maybePop(result); + + Future navigateToDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar, bool animate = true}) => + _tbContext.navigateToDashboard(dashboardId, dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar, animate: animate); + Future confirm({required String title, required String message, String cancel = 'Cancel', String ok = 'Ok'}) => _tbContext.confirm(title: title, message: message, cancel: cancel, ok: ok); void hideNotification() => _tbContext.hideNotification(); diff --git a/lib/core/context/tb_context_widget.dart b/lib/core/context/tb_context_widget.dart index 8014ffa..dd4072d 100644 --- a/lib/core/context/tb_context_widget.dart +++ b/lib/core/context/tb_context_widget.dart @@ -43,6 +43,8 @@ mixin TbMainState { navigateToPath(String path); + bool isHomePage(); + } abstract class TbPageWidget, S extends TbPageState> extends TbContextWidget { diff --git a/lib/core/entity/entities_base.dart b/lib/core/entity/entities_base.dart index 0de69e7..3fe2eaf 100644 --- a/lib/core/entity/entities_base.dart +++ b/lib/core/entity/entities_base.dart @@ -6,6 +6,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.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/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; typedef EntityTapFunction = Function(T entity); @@ -44,6 +45,8 @@ mixin EntitiesBase on HasTbContext { return Text('Not implemented!'); } + double? gridChildAspectRatio() => null; + EntityCardSettings entityListCardSettings(T entity) => EntityCardSettings(); EntityCardSettings entityGridCardSettings(T entity) => EntityCardSettings(); diff --git a/lib/core/entity/entities_grid.dart b/lib/core/entity/entities_grid.dart index 3c4aded..bcfae9e 100644 --- a/lib/core/entity/entities_grid.dart +++ b/lib/core/entity/entities_grid.dart @@ -19,6 +19,7 @@ class _EntitiesGridState extends BaseEntitiesState { @override Widget pagedViewBuilder(BuildContext context) { var heading = widget.buildHeading(context); + var gridChildAspectRatio = widget.gridChildAspectRatio() ?? 156 / 150; List slivers = []; if (heading != null) { slivers.add(SliverPadding( @@ -35,8 +36,8 @@ class _EntitiesGridState extends BaseEntitiesState { showNoMoreItemsIndicatorAsGridChild: false, pagingController: pagingController, // padding: EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 156 / 150, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: gridChildAspectRatio, crossAxisSpacing: 16, mainAxisSpacing: 16, crossAxisCount: 2, diff --git a/lib/core/entity/entity_details_page.dart b/lib/core/entity/entity_details_page.dart index a42f5fa..ea1193c 100644 --- a/lib/core/entity/entity_details_page.dart +++ b/lib/core/entity/entity_details_page.dart @@ -4,6 +4,7 @@ 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/widgets/tb_app_bar.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; abstract class EntityDetailsPage extends TbPageWidget, _EntityDetailsPageState> { @@ -87,7 +88,9 @@ class _EntityDetailsPageState extends TbPageState extends StatelessWidget { child: Card( margin: EdgeInsets.zero, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(4), ), elevation: 0, - child: Padding( - padding: const EdgeInsets.all(4), - child: _entityCardWidgetBuilder(context, _entity) - ) + child: _entityCardWidgetBuilder(context, _entity) ), decoration: _settings.dropShadow ? BoxDecoration( boxShadow: [ BoxShadow( - color: Colors.black.withAlpha(25), - blurRadius: 10.0, + color: Colors.black.withAlpha((255 * 0.05).ceil()), + blurRadius: 6.0, offset: Offset(0, 4) - ), - BoxShadow( - color: Colors.black.withAlpha(18), - blurRadius: 30.0, - offset: Offset(0, 10) - ), + ) ], ) : null, ), diff --git a/lib/core/entity/entity_list_card.dart b/lib/core/entity/entity_list_card.dart index a34e27e..bfda649 100644 --- a/lib/core/entity/entity_list_card.dart +++ b/lib/core/entity/entity_list_card.dart @@ -33,13 +33,10 @@ class EntityListCard extends StatelessWidget { child: Card( margin: EdgeInsets.zero, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(_listWidgetCard ? 4 : 6), + borderRadius: BorderRadius.circular(4), ), elevation: 0, - child: Padding( - padding: const EdgeInsets.all(2), - child: _entityCardWidgetBuilder(context, _entity) - ) + child: _entityCardWidgetBuilder(context, _entity) ), decoration: _listWidgetCard ? BoxDecoration( border: Border.all( @@ -51,15 +48,10 @@ class EntityListCard extends StatelessWidget { ) : BoxDecoration( boxShadow: [ BoxShadow( - color: Colors.black.withAlpha(25), - blurRadius: 10.0, + color: Colors.black.withAlpha((255 * 0.05).ceil()), + blurRadius: 6.0, offset: Offset(0, 4) ), - BoxShadow( - color: Colors.black.withAlpha(18), - blurRadius: 30.0, - offset: Offset(0, 10) - ), ], ), ), diff --git a/lib/core/init/init_app.dart b/lib/core/init/init_app.dart index 1786f53..625a56a 100644 --- a/lib/core/init/init_app.dart +++ b/lib/core/init/init_app.dart @@ -1,50 +1,24 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; -import 'package:flutter/services.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/widgets/tb_progress_indicator.dart'; class ThingsboardInitApp extends TbPageWidget { - ThingsboardInitApp(TbContext tbContext, {Key? key}) : super(tbContext, key: key) { - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - systemNavigationBarColor: Colors.white, - systemNavigationBarIconBrightness: Brightness.light - )); - } + ThingsboardInitApp(TbContext tbContext, {Key? key}) : super(tbContext, key: key); @override _ThingsboardInitAppState createState() => _ThingsboardInitAppState(); } -class _ThingsboardInitAppState extends TbPageState with TickerProviderStateMixin { - - late final AnimationController rotationController; - late final CurvedAnimation animation; +class _ThingsboardInitAppState extends TbPageState { @override void initState() { - rotationController = AnimationController(duration: Duration(milliseconds: 2000), - vsync: this, upperBound: 1, animationBehavior: AnimationBehavior.preserve); - animation = CurvedAnimation(parent: rotationController, curve: Curves.easeInOutCirc); super.initState(); initTbContext(); - rotationController.forward(from: 0.0); - rotationController.addListener(() { - if (rotationController.status == AnimationStatus.completed) { - rotationController.repeat(); - } - }); - } - - @override - void dispose() { - rotationController.dispose(); - super.dispose(); } @override @@ -52,20 +26,10 @@ class _ThingsboardInitAppState extends TbPageState { +class ThingsboardAppState extends State with TickerProviderStateMixin implements TbMainDashboardHolder { + + final TransitionIndexedStackController _mainStackController = TransitionIndexedStackController(); + final MainDashboardPageController _mainDashboardPageController = MainDashboardPageController(); + + final GlobalKey mainAppKey = GlobalKey(); + final GlobalKey dashboardKey = GlobalKey(); + @override void initState() { super.initState(); + appRouter.tbContext.setMainDashboardHolder(this); } + @override + Future navigateToDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar, bool animate = true}) async { + await _mainDashboardPageController.openDashboard(dashboardId, dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar); + await _openDashboard(animate: animate); + } + + @override + Future dashboardGoBack() async { + if (_mainStackController.index == 1) { + var canGoBack = await _mainDashboardPageController.dashboardGoBack(); + if (canGoBack) { + closeDashboard(); + } + return false; + } + return true; + } + + @override + Future openMain({bool animate = true}) async { + return _openMain(animate: animate); + } + + @override + Future closeMain({bool animate = true}) async { + return _closeMain(animate: animate); + } + + @override + Future openDashboard({bool animate = true}) async { + return _openDashboard(animate: animate); + } + + @override + Future closeDashboard({bool animate = true}) { + return _closeDashboard(animate: animate); + } + + bool isDashboardOpen() { + return _mainStackController.index == 1; + } + + Future _openMain({bool animate: true}) async { + return _mainStackController.open(0, animate: animate); + } + + Future _closeMain({bool animate: true}) async { + return _mainStackController.close(0, animate: animate); + } + + Future _openDashboard({bool animate: true}) async { + return _mainStackController.open(1, animate: animate); + } + + Future _closeDashboard({bool animate: true}) async { + return _mainStackController.close(1, animate: animate); + } + + @override Widget build(BuildContext context) { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + systemNavigationBarColor: Colors.white, + systemNavigationBarIconBrightness: Brightness.light + )); return MaterialApp( - scaffoldMessengerKey: appRouter.tbContext.messengerKey, title: 'ThingsBoard', - theme: tbTheme, - darkTheme: tbDarkTheme, - onGenerateRoute: appRouter.router.generator, - navigatorObservers: [appRouter.tbContext.routeObserver], + home: TransitionIndexedStack( + controller: _mainStackController, + first: MaterialApp( + key: mainAppKey, + scaffoldMessengerKey: appRouter.tbContext.messengerKey, + title: 'ThingsBoard', + theme: tbTheme, + darkTheme: tbDarkTheme, + onGenerateRoute: appRouter.router.generator, + navigatorObservers: [appRouter.tbContext.routeObserver], + ), + second: MaterialApp( + key: dashboardKey, + // scaffoldMessengerKey: appRouter.tbContext.messengerKey, + title: 'ThingsBoard', + theme: tbTheme, + darkTheme: tbDarkTheme, + home: MainDashboardPage(appRouter.tbContext, controller: _mainDashboardPageController), + ) + ) ); } + } diff --git a/lib/modules/alarm/alarms_base.dart b/lib/modules/alarm/alarms_base.dart index 420efb6..4799444 100644 --- a/lib/modules/alarm/alarms_base.dart +++ b/lib/modules/alarm/alarms_base.dart @@ -114,109 +114,126 @@ class _AlarmCardState extends TbContextState { 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: [ - 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) - ) - ] - ), - SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - fit: FlexFit.tight, - child: Text(alarm.originatorName != null ? alarm.originatorName! : '', + return Stack( + children: [ + Positioned.fill( + child: Container( + alignment: Alignment.centerLeft, + child: Container( + width: 4, + decoration: BoxDecoration( + color: alarmSeverityColors[alarm.severity]!, + borderRadius: BorderRadius.only(topLeft: Radius.circular(4), bottomLeft: Radius.circular(4)) + ), + ) + ) + ), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox(width: 4), + Flexible( + fit: FlexFit.tight, + child: + Padding( + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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(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: 4), Row( - 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: () => _ackAlarm(alarm)) + 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) + ) ), - 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)) - ) - ] + Text(alarmSeverityTranslations[alarm.severity]!, + style: TextStyle( + color: alarmSeverityColors[alarm.severity]!, + fontWeight: FontWeight.w500, + 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) + ) + ), + Row( + children: [ + if ([AlarmStatus.CLEARED_UNACK, AlarmStatus.ACTIVE_UNACK].contains(alarm.status)) + CircleAvatar( + radius: 16, + backgroundColor: Color(0xffF0F4F9), + child: IconButton(icon: Icon(Icons.done, size: 18), padding: EdgeInsets.all(7.0), onPressed: () => _ackAlarm(alarm)) + ), + if ([AlarmStatus.ACTIVE_UNACK, AlarmStatus.ACTIVE_ACK].contains(alarm.status)) + Row( + children: [ + SizedBox(width: 4), + CircleAvatar( + radius: 16, + backgroundColor: Color(0xffF0F4F9), + child: IconButton(icon: Icon(Icons.clear, size: 18), padding: EdgeInsets.all(7.0), onPressed: () => _clearAlarm(alarm)) + ) + ] + ) + ], + ) ], ) - ], - ) - ] + ] + ) ) - ) - ) - ] + ) + ] + ) + ], ); } } diff --git a/lib/modules/alarm/alarms_page.dart b/lib/modules/alarm/alarms_page.dart index 244c789..839c58a 100644 --- a/lib/modules/alarm/alarms_page.dart +++ b/lib/modules/alarm/alarms_page.dart @@ -17,12 +17,18 @@ class AlarmsPage extends TbContextWidget { } -class _AlarmsPageState extends TbContextState { +class _AlarmsPageState extends TbContextState with AutomaticKeepAliveClientMixin { final AlarmQueryController _alarmQueryController = AlarmQueryController(); + @override + bool get wantKeepAlive { + return true; + } + @override Widget build(BuildContext context) { + super.build(context); var alarmsList = AlarmsList(tbContext, _alarmQueryController, searchMode: widget.searchMode); PreferredSizeWidget appBar; if (widget.searchMode) { diff --git a/lib/modules/dashboard/dashboard.dart b/lib/modules/dashboard/dashboard.dart index 23f1dbe..f35a083 100644 --- a/lib/modules/dashboard/dashboard.dart +++ b/lib/modules/dashboard/dashboard.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -7,29 +8,47 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:thingsboard_app/constants/api_path.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; -import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; import 'package:url_launcher/url_launcher.dart'; +class DashboardController { + + final ValueNotifier canGoBack = ValueNotifier(false); + final _DashboardState dashboardState; + DashboardController(this.dashboardState); + + Future openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async { + return await dashboardState._openDashboard(dashboardId, state: state, hideToolbar: hideToolbar, fullscreen: fullscreen); + } + + Future goBack() async { + return dashboardState._goBack(); + } + + onHistoryUpdated(Future canGoBackFuture) async { + canGoBack.value = await canGoBackFuture; + } + + dispose() { + canGoBack.dispose(); + } + +} + typedef DashboardTitleCallback = void Function(String title); +typedef DashboardControllerCallback = void Function(DashboardController controller); + class Dashboard extends TbContextWidget { - final String _dashboardId; - final String? _state; final bool? _home; - final bool? _hideToolbar; - final bool _fullscreen; final DashboardTitleCallback? _titleCallback; + final DashboardControllerCallback? _controllerCallback; - Dashboard(TbContext tbContext, {required String dashboardId, required bool fullscreen, - DashboardTitleCallback? titleCallback, String? state, bool? home, - bool? hideToolbar}): - this._dashboardId = dashboardId, - this._fullscreen = fullscreen, - this._titleCallback = titleCallback, - this._state = state, + Dashboard(TbContext tbContext, {Key? key, bool? home, DashboardTitleCallback? titleCallback, DashboardControllerCallback? controllerCallback}): this._home = home, - this._hideToolbar = hideToolbar, + this._titleCallback = titleCallback, + this._controllerCallback = controllerCallback, super(tbContext); @override @@ -41,15 +60,23 @@ class _DashboardState extends TbContextState { final Completer _controller = Completer(); - final ValueNotifier webViewLoading = ValueNotifier(true); + bool webViewLoading = true; + final ValueNotifier dashboardLoading = ValueNotifier(true); + final ValueNotifier readyState = ValueNotifier(false); final GlobalKey webViewKey = GlobalKey(); + late final DashboardController _dashboardController; + + bool _fullscreen = false; + InAppWebViewGroupOptions options = InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( useShouldOverrideUrlLoading: true, mediaPlaybackRequiresUserGesture: false, javaScriptEnabled: true, + cacheEnabled: true, + supportZoom: false, // useOnDownloadStart: true ), android: AndroidInAppWebViewOptions( @@ -60,37 +87,88 @@ class _DashboardState extends TbContextState { allowsInlineMediaPlayback: true, )); - late String _dashboardUrl; - late String _currentDashboardId; - late String? _currentDashboardState; + late Uri _initialUrl; @override void initState() { super.initState(); - _dashboardUrl = thingsBoardApiEndpoint + '/dashboard/' + widget._dashboardId; - List params = []; - params.add("accessToken=${tbClient.getJwtToken()!}"); - params.add("refreshToken=${tbClient.getRefreshToken()!}"); - if (widget._state != null) { - params.add('state=${widget._state}'); + _dashboardController = DashboardController(this); + if (widget._controllerCallback != null) { + widget._controllerCallback!(_dashboardController); + } + tbContext.isAuthenticatedListenable.addListener(_onAuthenticated); + if (tbContext.isAuthenticated) { + _onAuthenticated(); + } + } + + void _onAuthenticated() async { + if (tbContext.isAuthenticated) { + if (!readyState.value) { + _initialUrl = Uri.parse(thingsBoardApiEndpoint + '?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}'); + readyState.value = true; + } else { + var windowMessage = { + 'type': 'reloadUserMessage', + 'data': { + 'accessToken': tbClient.getJwtToken()!, + 'refreshToken': tbClient.getRefreshToken()! + } + }; + var controller = await _controller.future; + await controller.postWebMessage(message: WebMessage(data: jsonEncode(windowMessage)), targetOrigin: Uri.parse('*')); + } + } + } + + Future _goBack() async { + var controller = await _controller.future; + if (await controller.canGoBack()) { + await controller.goBack(); + return false; + } + return true; + } + + @override + void dispose() { + tbContext.isAuthenticatedListenable.removeListener(_onAuthenticated); + readyState.dispose(); + dashboardLoading.dispose(); + _dashboardController.dispose(); + super.dispose(); + } + + Future _openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async { + _fullscreen = fullscreen; + dashboardLoading.value = true; + var controller = await _controller.future; + var windowMessage = { + 'type': 'openDashboardMessage', + 'data': { + 'dashboardId': dashboardId + } + }; + if (state != null) { + windowMessage['data']['state'] = state; } if (widget._home == true) { - params.add('embedded=true'); + windowMessage['data']['embedded'] = true; } - if (widget._hideToolbar == true) { - params.add('hideToolbar=true'); + if (hideToolbar == true) { + windowMessage['data']['hideToolbar'] = true; } - if (params.isNotEmpty) { - _dashboardUrl += '?${params.join('&')}'; - } - _currentDashboardId = widget._dashboardId; - _currentDashboardState = widget._state; + var webMessage = WebMessage(data: jsonEncode(windowMessage)); + await controller.postWebMessage(message: webMessage, targetOrigin: Uri.parse('*')); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { + if (widget._home == true && !tbContext.isHomePage()) { + return true; + } var controller = await _controller.future; if (await controller.canGoBack()) { await controller.goBack(); @@ -98,191 +176,147 @@ class _DashboardState extends TbContextState { } return true; }, - child: SafeArea( - child: Stack( - children: [ - InAppWebView( - key: webViewKey, - initialUrlRequest: URLRequest(url: Uri.parse(_dashboardUrl)), - initialOptions: options, - onWebViewCreated: (webViewController) { - webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardStateNameHandler", callback: (args) async { - log.debug("Invoked tbMobileDashboardStateNameHandler: $args"); - webViewLoading.value = false; - if (args.isNotEmpty && args[0] is String) { - if (widget._titleCallback != null) { - widget._titleCallback!(args[0]); - } - } - }); - webViewController.addJavaScriptHandler(handlerName: "tbMobileHandler", callback: (args) async { - log.debug("Invoked tbMobileHandler: $args"); - return await widgetActionHandler.handleWidgetMobileAction(args, webViewController); - }); - _controller.complete(webViewController); - }, - shouldOverrideUrlLoading: (controller, navigationAction) async { - var uri = navigationAction.request.url!; - var uriString = uri.toString(); - log.debug('shouldOverrideUrlLoading $uriString'); - if (![ - "http", - "https", - "file", - "chrome", - "data", - "javascript", - "about" - ].contains(uri.scheme)) { - if (await canLaunch(uriString)) { - // Launch the App - await launch( - uriString, - ); - // and cancel the request - return NavigationActionPolicy.CANCEL; - } - } + child: + ValueListenableBuilder( + valueListenable: readyState, + builder: (BuildContext context, bool ready, child) { + if (!ready) { + return SizedBox.shrink(); + } else { + return Container( + decoration: BoxDecoration(color: Colors.white), + child: SafeArea( + child: Stack( + children: [ + InAppWebView( + key: webViewKey, + initialUrlRequest: URLRequest(url: _initialUrl), + initialOptions: options, + onWebViewCreated: (webViewController) { + log.debug("onWebViewCreated"); + webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardLoadedHandler", callback: (args) async { + log.debug("Invoked tbMobileDashboardLoadedHandler"); + dashboardLoading.value = false; + }); + webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardStateNameHandler", callback: (args) async { + log.debug("Invoked tbMobileDashboardStateNameHandler: $args"); + if (args.isNotEmpty && args[0] is String) { + if (widget._titleCallback != null) { + widget._titleCallback!(args[0]); + } + } + }); + webViewController.addJavaScriptHandler(handlerName: "tbMobileNavigationHandler", callback: (args) async { + log.debug("Invoked tbMobileNavigationHandler: $args"); + if (args.length > 0) { + String? path = args[0]; + Map? params; + if (args.length > 1) { + params = args[1]; + } + log.debug("path: $path"); + log.debug("params: $params"); + if (path != null) { + if ([ + 'profile', + 'devices', + 'assets', + 'dashboards', + 'customers', + 'auditLogs' + ].contains(path)) { + var targetPath = '/$path'; + if (path == 'devices' && widget._home != true) { + targetPath = '/devicesPage'; + } + navigateTo(targetPath); + } + } + } + }); + webViewController.addJavaScriptHandler(handlerName: "tbMobileHandler", callback: (args) async { + log.debug("Invoked tbMobileHandler: $args"); + return await widgetActionHandler.handleWidgetMobileAction(args, webViewController); + }); + }, + shouldOverrideUrlLoading: (controller, navigationAction) async { + var uri = navigationAction.request.url!; + var uriString = uri.toString(); + log.debug('shouldOverrideUrlLoading $uriString'); + if (![ + "http", + "https", + "file", + "chrome", + "data", + "javascript", + "about" + ].contains(uri.scheme)) { + if (await canLaunch(uriString)) { + // Launch the App + await launch( + uriString, + ); + // and cancel the request + return NavigationActionPolicy.CANCEL; + } + } - return Platform.isIOS ? NavigationActionPolicy.ALLOW : NavigationActionPolicy.CANCEL; - }, - onUpdateVisitedHistory: (controller, url, androidIsReload) async { - if (url != null) { - String newStateId = url.pathSegments.last; - log.debug('onUpdateVisitedHistory: $newStateId'); - if (newStateId == 'profile') { - webViewLoading.value = true; - await controller.goBack(); - await navigateTo('/profile'); - webViewLoading.value = false; - return; - } else if (newStateId == 'login') { - webViewLoading.value = true; - await controller.pauseTimers(); - await controller.stopLoading(); - await tbClient.logout(); - return; - } else if (['devices', 'assets', 'dashboards'].contains(newStateId)) { - var controller = await _controller.future; - await controller.goBack(); - navigateTo('/$newStateId'); - return; - } else { - if (url.pathSegments.length > 1) { - var segmentName = url.pathSegments[url.pathSegments.length-2]; - if (segmentName == 'dashboards' && widget._home != true) { - webViewLoading.value = true; - var targetPath = _createDashboardNavigationPath(newStateId, fullscreen: widget._fullscreen); - await navigateTo(targetPath, replace: true); - return; - } else if (segmentName == 'dashboard') { - _currentDashboardId = newStateId; - _currentDashboardState = url.queryParameters['state']; - return; - } - } - webViewLoading.value = true; - if (widget._home == true) { - await navigateTo('/home', replace: true); - } else { - var targetPath = _createDashboardNavigationPath(_currentDashboardId, state: _currentDashboardState, fullscreen: widget._fullscreen); - await navigateTo(targetPath, replace: true); - } - } - } - }, - onConsoleMessage: (controller, consoleMessage) { - log.debug('[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}'); - }, - onLoadStart: (controller, url) async { - log.debug('onLoadStart: $url'); - // await _setTokens(controller.webStorage.localStorage); - }, - onLoadStop: (controller, url) async { - log.debug('onLoadStop: $url'); - // await _setTokens(controller.webStorage.localStorage); - }, - androidOnPermissionRequest: (controller, origin, resources) async { - log.debug('androidOnPermissionRequest origin: $origin, resources: $resources'); - return PermissionRequestResponse( - resources: resources, - action: PermissionRequestResponseAction.GRANT); - }, - /* onDownloadStart: (controller, url) async { - log.debug("onDownloadStart $url"); - final taskId = await FlutterDownloader.enqueue( - url: url.toString(), - savedDir: (await getExternalStorageDirectory())!.path, - showNotification: true, - openFileFromNotification: true, - ); - } */ - ), - ValueListenableBuilder( - valueListenable: webViewLoading, - builder: (BuildContext context, bool loading, child) { - if (!loading) { - return SizedBox.shrink(); - } else { - return Container( - decoration: BoxDecoration(color: Colors.white), - child: Center( - child: RefreshProgressIndicator() - ), - ); - } - } - ) - ] - ) - ) + return Platform.isIOS ? NavigationActionPolicy.ALLOW : NavigationActionPolicy.CANCEL; + }, + onUpdateVisitedHistory: (controller, url, androidIsReload) async { + log.debug('onUpdateVisitedHistory: url'); + _dashboardController.onHistoryUpdated(controller.canGoBack()); + }, + onConsoleMessage: (controller, consoleMessage) { + log.debug('[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}'); + }, + onLoadStart: (controller, url) async { + log.debug('onLoadStart: $url'); + }, + onLoadStop: (controller, url) async { + log.debug('onLoadStop: $url'); + if (webViewLoading) { + webViewLoading = false; + _controller.complete(controller); + } + }, + androidOnPermissionRequest: (controller, origin, resources) async { + log.debug('androidOnPermissionRequest origin: $origin, resources: $resources'); + return PermissionRequestResponse( + resources: resources, + action: PermissionRequestResponseAction.GRANT); + }, + ), + ValueListenableBuilder( + valueListenable: dashboardLoading, + builder: (BuildContext context, bool loading, child) { + if (!loading) { + return SizedBox.shrink(); + } else { + var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window); + var bottomPadding = data.padding.top; + if (widget._home != true) { + bottomPadding += kToolbarHeight; + } + return Container( + padding: EdgeInsets.only(bottom: bottomPadding), + alignment: Alignment.center, + color: Colors.white, + child: TbProgressIndicator( + size: 50.0 + ), + ); + } + } + ) + ] + ) + ), + ); + } + } + ) ); } - - String _createDashboardNavigationPath(String dashboardId, {bool? fullscreen, String? state}) { - var targetPath = '/dashboard/$dashboardId'; - List params = []; - if (state != null) { - params.add('state=$state'); - } - if (fullscreen != null) { - params.add('fullscreen=$fullscreen'); - } - if (params.isNotEmpty) { - targetPath += '?${params.join('&')}'; - } - return targetPath; - } - - Future _setTokens(Storage storage) async { - String jwtToken = tbClient.getJwtToken()!; - int jwtTokenExpiration = _getClientExpiration(jwtToken); - String refreshToken = tbClient.getRefreshToken()!; - int refreshTokenExpiration = _getClientExpiration(refreshToken); - await storage.setItem(key: 'jwt_token', value: jwtToken); - await storage.setItem(key: 'jwt_token_expiration', value: jwtTokenExpiration); - await storage.setItem(key: 'refresh_token', value: refreshToken); - await storage.setItem(key: 'refresh_token_expiration', value: refreshTokenExpiration); - } - -/* String _setTokensJavaScript() { - String jwtToken = tbClient.getJwtToken()!; - int jwtTokenExpiration = _getClientExpiration(jwtToken); - String refreshToken = tbClient.getRefreshToken()!; - int refreshTokenExpiration = _getClientExpiration(refreshToken); - return "window.localStorage.setItem('jwt_token','$jwtToken');\n"+ - "window.localStorage.setItem('jwt_token_expiration','$jwtTokenExpiration');\n"+ - "window.localStorage.setItem('refresh_token','$refreshToken');\n"+ - "window.localStorage.setItem('refresh_token_expiration','$refreshTokenExpiration');"; - } */ - - int _getClientExpiration(String token) { - var decodedToken = JwtDecoder.decode(tbClient.getJwtToken()!); - int issuedAt = decodedToken['iat']; - int expTime = decodedToken['exp']; - int ttl = expTime - issuedAt; - int clientExpiration = DateTime.now().millisecondsSinceEpoch + ttl * 1000; - return clientExpiration; - } - } diff --git a/lib/modules/dashboard/dashboard_page.dart b/lib/modules/dashboard/dashboard_page.dart index 0466e31..0144974 100644 --- a/lib/modules/dashboard/dashboard_page.dart +++ b/lib/modules/dashboard/dashboard_page.dart @@ -8,11 +8,11 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; class DashboardPage extends TbPageWidget { final String? _dashboardTitle; - final String _dashboardId; + final String? _dashboardId; final String? _state; - final bool _fullscreen; + final bool? _fullscreen; - DashboardPage(TbContext tbContext, {required String dashboardId, required bool fullscreen, String? dashboardTitle, String? state}): + DashboardPage(TbContext tbContext, {String? dashboardId, bool? fullscreen, String? dashboardTitle, String? state}): _dashboardId = dashboardId, _fullscreen = fullscreen, _dashboardTitle = dashboardTitle, @@ -52,10 +52,11 @@ class _DashboardPageState extends TbPageState params) { + return FullscreenDashboardPage(tbContext, params["id"]![0]); + }); + DashboardRoutes(TbContext tbContext) : super(tbContext); @override void doRegisterRoutes(router) { router.define("/dashboards", handler: dashboardsHandler); router.define("/dashboard/:id", handler: dashboardDetailsHandler); + router.define("/fullscreenDashboard/:id", handler: fullscreenDashboardHandler); } } diff --git a/lib/modules/dashboard/dashboards_base.dart b/lib/modules/dashboard/dashboards_base.dart index 62ba9f8..3b3c1fd 100644 --- a/lib/modules/dashboard/dashboards_base.dart +++ b/lib/modules/dashboard/dashboards_base.dart @@ -26,7 +26,8 @@ mixin DashboardsBase on EntitiesBase { @override void onEntityTap(DashboardInfo dashboard) { - navigateTo('/dashboard/${dashboard.id!.id}?title=${dashboard.title}'); + navigateToDashboard(dashboard.id!.id!, dashboardTitle: dashboard.title); + // navigateTo('/dashboard/${dashboard.id!.id}?title=${dashboard.title}'); } @override @@ -152,57 +153,50 @@ class _DashboardGridCardState extends TbContextState { @@ -17,23 +16,19 @@ class DashboardsPage extends TbPageWidget class _DashboardsPageState extends TbPageState { - final PageLinkController _pageLinkController = PageLinkController(); - @override Widget build(BuildContext context) { - var dashboardsList = DashboardsList(tbContext, _pageLinkController); return Scaffold( appBar: TbAppBar( tbContext, - title: Text(dashboardsList.title) + title: Text('Dashboards') ), - body: dashboardsList + body: DashboardsGridWidget(tbContext) ); } @override void dispose() { - _pageLinkController.dispose(); super.dispose(); } diff --git a/lib/modules/dashboard/fullscreen_dashboard_page.dart b/lib/modules/dashboard/fullscreen_dashboard_page.dart new file mode 100644 index 0000000..5b364d4 --- /dev/null +++ b/lib/modules/dashboard/fullscreen_dashboard_page.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +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/dashboard/dashboard.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class FullscreenDashboardPage extends TbPageWidget { + + final String fullscreenDashboardId; + final String? _dashboardTitle; + + FullscreenDashboardPage(TbContext tbContext, this.fullscreenDashboardId, {String? dashboardTitle}): + _dashboardTitle = dashboardTitle, + super(tbContext); + + @override + _FullscreenDashboardPageState createState() => _FullscreenDashboardPageState(); + +} + +class _FullscreenDashboardPageState extends TbPageState { + + late ValueNotifier dashboardTitleValue; + final ValueNotifier showBackValue = ValueNotifier(false); + + @override + void initState() { + super.initState(); + dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard'); + } + + @override + void dispose() { + super.dispose(); + } + + _onCanGoBack(bool canGoBack) { + showBackValue.value = canGoBack; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: ValueListenableBuilder( + valueListenable: showBackValue, + builder: (context, canGoBack, widget) { + return TbAppBar( + tbContext, + leading: canGoBack ? BackButton( + onPressed: () { + maybePop(); + } + ) : null, + showLoadingIndicator: false, + elevation: 0, + title: ValueListenableBuilder( + valueListenable: dashboardTitleValue, + builder: (context, title, widget) { + return FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text(title) + ); + }, + ), + actions: [ + IconButton(icon: Icon(Icons.settings), onPressed: () => navigateTo('/profile?fullscreen=true')) + ] + ); + } + ), + ), + body: Dashboard( + tbContext, + titleCallback: (title) { + dashboardTitleValue.value = title; + }, + controllerCallback: (controller) { + controller.canGoBack.addListener(() { + _onCanGoBack(controller.canGoBack.value); + }); + controller.openDashboard(widget.fullscreenDashboardId, fullscreen: true); + } + ) + ); + } +} diff --git a/lib/modules/dashboard/main_dashboard_page.dart b/lib/modules/dashboard/main_dashboard_page.dart new file mode 100644 index 0000000..a268ed4 --- /dev/null +++ b/lib/modules/dashboard/main_dashboard_page.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +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/dashboard/dashboard.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; + +class MainDashboardPageController { + + DashboardController? _dashboardController; + _MainDashboardPageState? _mainDashboardPageState; + + _setMainDashboardPageState(_MainDashboardPageState state) { + _mainDashboardPageState = state; + } + + _setDashboardController(DashboardController controller) { + _dashboardController = controller; + } + + Future dashboardGoBack() { + if (_dashboardController != null) { + return _dashboardController!.goBack(); + } else { + return Future.value(true); + } + } + + Future openDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar}) async { + if (dashboardTitle != null) { + _mainDashboardPageState?._updateTitle(dashboardTitle); + } + await _dashboardController?.openDashboard(dashboardId, state: state, hideToolbar: hideToolbar); + } +} + +class MainDashboardPage extends TbContextWidget { + + final String? _dashboardTitle; + final MainDashboardPageController? _controller; + + MainDashboardPage(TbContext tbContext, + {MainDashboardPageController? controller, + String? dashboardTitle}): + _controller = controller, + _dashboardTitle = dashboardTitle, + super(tbContext); + + @override + _MainDashboardPageState createState() => _MainDashboardPageState(); + +} + +class _MainDashboardPageState extends TbContextState { + + late ValueNotifier dashboardTitleValue; + + @override + void initState() { + super.initState(); + if (widget._controller != null) { + widget._controller!._setMainDashboardPageState(this); + } + dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard'); + } + + @override + void dispose() { + super.dispose(); + } + + _updateTitle(String newTitle) { + dashboardTitleValue.value = newTitle; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: TbAppBar( + tbContext, + leading: BackButton( + onPressed: () { + maybePop(); + } + ), + showLoadingIndicator: false, + elevation: 0, + title: ValueListenableBuilder( + valueListenable: dashboardTitleValue, + builder: (context, title, widget) { + return FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.centerLeft, + child: Text(title) + ); + }, + ) + ), + body: Dashboard( + tbContext, + titleCallback: (title) { + dashboardTitleValue.value = title; + }, + controllerCallback: (controller) { + if (widget._controller != null) { + widget._controller!._setDashboardController(controller); + } + } + ) + ); + } + +} diff --git a/lib/modules/device/device_profiles_base.dart b/lib/modules/device/device_profiles_base.dart index 4d0dc83..f4f8222 100644 --- a/lib/modules/device/device_profiles_base.dart +++ b/lib/modules/device/device_profiles_base.dart @@ -9,6 +9,7 @@ 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_app/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; mixin DeviceProfilesBase on EntitiesBase { @@ -50,6 +51,11 @@ mixin DeviceProfilesBase on EntitiesBase { return DeviceProfileCard(tbContext, deviceProfile); } + @override + double? gridChildAspectRatio() { + return 156 / 200; + } + } class RefreshDeviceCounts { @@ -93,8 +99,10 @@ class _AllDevicesCardState extends TbContextState inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, active: false); Future> countsFuture = Future.wait([activeDevicesCount, inactiveDevicesCount]); countsFuture.then((counts) { - _activeDevicesCount.add(counts[0]); - _inactiveDevicesCount.add(counts[1]); + if (this.mounted) { + _activeDevicesCount.add(counts[0]); + _inactiveDevicesCount.add(counts[1]); + } }); return countsFuture; } @@ -107,32 +115,31 @@ class _AllDevicesCardState extends TbContextState( - future: activeDevicesCount, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { - var deviceCount = snapshot.data!; - return _buildDeviceCount(context, true, deviceCount); - } else { - return Center(child: - Container(height: 20, width: 20, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary), - strokeWidth: 2.5))); - } - }, - ) - ), - onTap: () { - navigateTo('/deviceList?active=true&deviceType=${entity.name}'); - } - ), - ), - SizedBox(width: 4), - Flexible(fit: FlexFit.tight, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - ), - child: FutureBuilder( - future: inactiveDevicesCount, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { - var deviceCount = snapshot.data!; - return _buildDeviceCount(context, false, deviceCount); - } else { - return Center(child: - Container(height: 20, width: 20, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary), - strokeWidth: 2.5))); - } - }, - ) - ), - onTap: () { - navigateTo('/deviceList?active=false&deviceType=${entity.name}'); - } + Container( + height: 44, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: Center( + child: AutoSizeText(entity.name, + textAlign: TextAlign.center, + maxLines: 1, + minFontSize: 12, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + height: 20 / 14 ), - ), - ], + ) + ) ) - ) - ], + ), + Divider(height: 1), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: FutureBuilder( + future: activeDevicesCount, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { + var deviceCount = snapshot.data!; + return _buildDeviceCount(context, true, deviceCount); + } else { + return Container(height: 40, + child: 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}'); + } + ), + Divider(height: 1), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: FutureBuilder( + future: inactiveDevicesCount, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { + var deviceCount = snapshot.data!; + return _buildDeviceCount(context, false, deviceCount); + } else { + return Container(height: 40, + child: 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}) { +Widget _buildDeviceCount(BuildContext context, bool active, int count) { Color color = active ? Color(0xFF008A00) : Color(0xFFAFAFAF); return Padding( padding: EdgeInsets.all(12), @@ -412,23 +392,23 @@ Widget _buildDeviceCount(BuildContext context, bool active, int count, {bool dis ) ], ), - if (displayStatusText) SizedBox(width: 8.67), - if (displayStatusText) Text(active ? 'Active' : 'Inactive', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, height: 16 / 12, color: color + )), + SizedBox(width: 8.67), + Text(count.toString(), 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 - )) + Icon(Icons.chevron_right, size: 16, color: Color(0xFFACACAC)) ], ), ); diff --git a/lib/modules/device/device_routes.dart b/lib/modules/device/device_routes.dart index 090fd03..59c8794 100644 --- a/lib/modules/device/device_routes.dart +++ b/lib/modules/device/device_routes.dart @@ -2,10 +2,11 @@ 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/device/devices_page.dart'; import 'package:thingsboard_app/modules/main/main_page.dart'; import 'device_details_page.dart'; -import 'devices_page.dart'; +import 'devices_list_page.dart'; class DeviceRoutes extends TbRoutes { @@ -13,12 +14,16 @@ class DeviceRoutes extends TbRoutes { return MainPage(tbContext, path: '/devices'); }); + late var devicesPageHandler = Handler(handlerFunc: (BuildContext? context, Map params) { + return DevicesPage(tbContext); + }); + late var deviceListHandler = Handler(handlerFunc: (BuildContext? context, Map params) { var searchMode = params['search']?.first == 'true'; var deviceType = params['deviceType']?.first; String? activeStr = params['active']?.first; bool? active = activeStr != null ? activeStr == 'true' : null; - return DevicesPage(tbContext, searchMode: searchMode, deviceType: deviceType, active: active); + return DevicesListPage(tbContext, searchMode: searchMode, deviceType: deviceType, active: active); }); late var deviceDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map params) { @@ -30,6 +35,7 @@ class DeviceRoutes extends TbRoutes { @override void doRegisterRoutes(router) { router.define("/devices", handler: devicesHandler); + router.define("/devicesPage", handler: devicesPageHandler); router.define("/deviceList", handler: deviceListHandler); router.define("/device/:id", handler: deviceDetailsHandler); } diff --git a/lib/modules/device/devices_base.dart b/lib/modules/device/devices_base.dart index 3074c2f..2060af6 100644 --- a/lib/modules/device/devices_base.dart +++ b/lib/modules/device/devices_base.dart @@ -3,6 +3,7 @@ import 'dart:core'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.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'; @@ -30,7 +31,8 @@ mixin DevicesBase on EntitiesBase { if (profile.defaultDashboardId != null) { var dashboardId = profile.defaultDashboardId!.id!; var state = Utils.createDashboardEntityState(device.entityId, entityName: device.field('name')!, entityLabel: device.field('label')!); - navigateTo('/dashboard/$dashboardId?title=${device.field('name')!}&state=$state'); + // navigateTo('/dashboard/$dashboardId?title=${device.field('name')!}&state=$state'); + navigateToDashboard(dashboardId, dashboardTitle: device.field('name'), state: state); } else { // navigateTo('/device/${device.entityId.id}'); if (tbClient.isTenantAdmin()) { @@ -127,49 +129,37 @@ class _DeviceCardState extends TbContextState { width: widget.listWidgetCard ? 58 : 60, height: widget.listWidgetCard ? 58 : 60, decoration: BoxDecoration( - color: Color(0xFFEEEEEE), - borderRadius: BorderRadius.horizontal(left: Radius.circular(widget.listWidgetCard ? 4 : 6)) + // color: Color(0xFFEEEEEE), + borderRadius: BorderRadius.horizontal(left: Radius.circular(4)) ), child: FutureBuilder( future: deviceProfileFuture, builder: (context, snapshot) { if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { var profile = snapshot.data!; + Widget image; + BoxFit imageFit; 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.contain, - 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] - ) - ) - ), - ) - ], - ) - ); + image = Image.memory(uriData.contentAsBytes()); + imageFit = BoxFit.contain; } else { - return Center( - child: Icon(Icons.devices_other, color: Color(0xFFC2C2C2)) - ); + image = Image.asset(ThingsboardImage.deviceProfilePlaceholder); + imageFit = BoxFit.cover; } + return ClipRRect( + borderRadius: BorderRadius.horizontal(left: Radius.circular(4)), + child: Stack( + children: [ + Positioned.fill( + child: FittedBox( + fit: imageFit, + child: image, + ) + ) + ], + ) + ); } else { return Center(child: RefreshProgressIndicator( valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary) @@ -200,12 +190,12 @@ class _DeviceCardState extends TbContextState { height: 20 / 14 )) ), - if (!widget.listWidgetCard) Text(widget.device.attribute('active') == 'true' ? 'Active' : 'Inactive', + if (!widget.listWidgetCard) Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(widget.device.createdTime!)), style: TextStyle( - color: widget.device.attribute('active') == 'true' ? Color(0xFF008A00) : Color(0xFFAFAFAF), - fontSize: 12, - height: 12 /12, - fontWeight: FontWeight.w500, + color: Color(0xFFAFAFAF), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 )) ] ), @@ -221,12 +211,12 @@ class _DeviceCardState extends TbContextState { fontWeight: FontWeight.normal, height: 16 / 12 )), - if (!widget.listWidgetCard) Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(widget.device.createdTime!)), + if (!widget.listWidgetCard) Text(widget.device.attribute('active') == 'true' ? 'Active' : 'Inactive', style: TextStyle( - color: Color(0xFFAFAFAF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12 + color: widget.device.attribute('active') == 'true' ? Color(0xFF008A00) : Color(0xFFAFAFAF), + fontSize: 12, + height: 16 / 12, + fontWeight: FontWeight.normal, )) ], ) diff --git a/lib/modules/device/devices_list_page.dart b/lib/modules/device/devices_list_page.dart new file mode 100644 index 0000000..7cee485 --- /dev/null +++ b/lib/modules/device/devices_list_page.dart @@ -0,0 +1,98 @@ +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 DevicesListPage extends TbPageWidget { + + final String? deviceType; + final bool? active; + final bool searchMode; + + DevicesListPage(TbContext tbContext, {this.deviceType, this.active, this.searchMode = false}) : super(tbContext); + + @override + _DevicesListPageState createState() => _DevicesListPageState(); + +} + +class _DevicesListPageState extends TbPageState { + + late final DeviceQueryController _deviceQueryController; + + @override + void initState() { + super.initState(); + _deviceQueryController = DeviceQueryController(deviceType: widget.deviceType, active: widget.active); + } + + @override + Widget build(BuildContext context) { + var devicesList = DevicesList(tbContext, _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( + fontWeight: FontWeight.w500, + fontSize: subTitleText != null ? 16 : 20, + height: subTitleText != null ? 20 / 16 : 24 / 20 + )), + if (subTitleText != null) + Text(subTitleText, style: TextStyle( + color: Theme.of(context).primaryTextTheme.headline6!.color!.withAlpha((0.38 * 255).ceil()), + fontSize: 12, + fontWeight: FontWeight.normal, + height: 16 / 12 + )) + ] + ); + + appBar = TbAppBar( + tbContext, + title: title, + actions: [ + IconButton( + icon: Icon( + Icons.search + ), + onPressed: () { + List params = []; + params.add('search=true'); + if (widget.deviceType != null) { + params.add('deviceType=${widget.deviceType}'); + } + if (widget.active != null) { + params.add('active=${widget.active}'); + } + navigateTo('/deviceList?${params.join('&')}'); + }, + ) + ]); + } + return Scaffold( + appBar: appBar, + body: devicesList + ); + } + + @override + void dispose() { + _deviceQueryController.dispose(); + super.dispose(); + } + +} diff --git a/lib/modules/device/devices_main_page.dart b/lib/modules/device/devices_main_page.dart index 625eb7b..6015a1d 100644 --- a/lib/modules/device/devices_main_page.dart +++ b/lib/modules/device/devices_main_page.dart @@ -14,12 +14,18 @@ class DevicesMainPage extends TbContextWidget { +class _DevicesMainPageState extends TbContextState with AutomaticKeepAliveClientMixin { final PageLinkController _pageLinkController = PageLinkController(); + @override + bool get wantKeepAlive { + return true; + } + @override Widget build(BuildContext context) { + super.build(context); var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController); return Scaffold( appBar: TbAppBar( diff --git a/lib/modules/device/devices_page.dart b/lib/modules/device/devices_page.dart index 7bc4bd6..822d0e5 100644 --- a/lib/modules/device/devices_page.dart +++ b/lib/modules/device/devices_page.dart @@ -1,17 +1,13 @@ 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/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 DevicesPage extends TbPageWidget { - final String? deviceType; - final bool? active; - final bool searchMode; - - DevicesPage(TbContext tbContext, {this.deviceType, this.active, this.searchMode = false}) : super(tbContext); + DevicesPage(TbContext tbContext) : super(tbContext); @override _DevicesPageState createState() => _DevicesPageState(); @@ -20,79 +16,23 @@ class DevicesPage extends TbPageWidget { class _DevicesPageState extends TbPageState { - late final DeviceQueryController _deviceQueryController; - - @override - void initState() { - super.initState(); - _deviceQueryController = DeviceQueryController(deviceType: widget.deviceType, active: widget.active); - } + final PageLinkController _pageLinkController = PageLinkController(); @override Widget build(BuildContext context) { - var devicesList = DevicesList(tbContext, _deviceQueryController, searchMode: widget.searchMode, displayDeviceImage: widget.deviceType == null); - PreferredSizeWidget appBar; - if (widget.searchMode) { - appBar = TbAppSearchBar( - tbContext, - onSearch: (searchText) => _deviceQueryController.onSearchText(searchText), - ); - } else { - String titleText = widget.deviceType != null ? widget.deviceType! : 'All devices'; - String? subTitleText; - if (widget.active != null) { - subTitleText = widget.active == true ? 'Active' : 'Inactive'; - } - Column title = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(titleText, style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w500, - fontSize: subTitleText != null ? 16 : 20, - height: subTitleText != null ? 20 / 16 : 24 / 20 - )), - if (subTitleText != null) - Text(subTitleText, style: TextStyle( - color: Color(0x61FFFFFF), - fontSize: 12, - fontWeight: FontWeight.normal, - height: 16 / 12 - )) - ] - ); - - appBar = TbAppBar( - tbContext, - title: title, - actions: [ - IconButton( - icon: Icon( - Icons.search - ), - onPressed: () { - List params = []; - params.add('search=true'); - if (widget.deviceType != null) { - params.add('deviceType=${widget.deviceType}'); - } - if (widget.active != null) { - params.add('active=${widget.active}'); - } - navigateTo('/deviceList?${params.join('&')}'); - }, - ) - ]); - } + var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController); return Scaffold( - appBar: appBar, - body: devicesList + appBar: TbAppBar( + tbContext, + title: Text(deviceProfilesList.title) + ), + body: deviceProfilesList ); } @override void dispose() { - _deviceQueryController.dispose(); + _pageLinkController.dispose(); super.dispose(); } diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index f3e9f6e..aa0716a 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.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_list_widget.dart'; @@ -45,8 +47,15 @@ class _HomePageState extends TbContextState with Autom return Scaffold( appBar: TbAppBar( tbContext, - elevation: dashboardState ? 0 : null, - title: const Text('Home'), + elevation: dashboardState ? 0 : 8, + title: Center( + child: Container( + height: 24, + child: SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle, + color: Theme.of(context).primaryColor, + semanticsLabel: 'ThingsBoard Logo') + ) + ), ), body: Builder( builder: (context) { @@ -61,8 +70,7 @@ class _HomePageState extends TbContextState with Autom } Widget _buildDashboardHome(BuildContext context, HomeDashboardInfo dashboard) { - return dashboardUi.Dashboard(tbContext, dashboardId: dashboard.dashboardId!.id!, - fullscreen: false, home: true, hideToolbar: dashboard.hideDashboardToolbar); + return HomeDashboard(tbContext, dashboard); } Widget _buildDefaultHome(BuildContext context) { @@ -108,3 +116,29 @@ class _HomePageState extends TbContextState with Autom ]; } */ } + +class HomeDashboard extends TbContextWidget { + + final HomeDashboardInfo dashboard; + + HomeDashboard(TbContext tbContext, this.dashboard) : super(tbContext); + + @override + _HomeDashboardState createState() => _HomeDashboardState(); + +} + +class _HomeDashboardState extends TbContextState { + + @override + Widget build(BuildContext context) { + return dashboardUi.Dashboard(tbContext, + home: true, + controllerCallback: (controller) { + controller.openDashboard(widget.dashboard.dashboardId!.id!, + hideToolbar: widget.dashboard.hideDashboardToolbar); + } + ); + } + +} diff --git a/lib/modules/main/main_page.dart b/lib/modules/main/main_page.dart index cd5dfa2..49e7732 100644 --- a/lib/modules/main/main_page.dart +++ b/lib/modules/main/main_page.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; 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/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'; @@ -97,12 +95,7 @@ class MainPage extends TbPageWidget { final String _path; MainPage(TbContext tbContext, {required String path}): - _path = path, super(tbContext) { - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - systemNavigationBarColor: Theme.of(tbContext.currentState!.context).colorScheme.primary, - systemNavigationBarIconBrightness: Brightness.dark - )); - } + _path = path, super(tbContext); @override _MainPageState createState() => _MainPageState(); @@ -122,9 +115,24 @@ class _MainPageState extends TbPageState with TbMainSt int currentIndex = _indexFromPath(widget._path); _tabController = TabController(initialIndex: currentIndex, length: _tabItems.length, vsync: this); _currentIndexNotifier = ValueNotifier(currentIndex); - _tabController.addListener(() { - _currentIndexNotifier.value = _tabController.index; - }); + _tabController.animation!.addListener(_onTabAnimation); + } + + @override + void dispose() { + _tabController.animation!.removeListener(_onTabAnimation); + super.dispose(); + } + + _onTabAnimation () { + var value = _tabController.animation!.value; + var targetIndex; + if (value >= _tabController.previousIndex) { + targetIndex = value.round(); + } else { + targetIndex = value.floor(); + } + _currentIndexNotifier.value = targetIndex; } @override @@ -142,25 +150,18 @@ class _MainPageState extends TbPageState with TbMainSt controller: _tabController, children: _tabItems.map((item) => item.page).toList(), ), - bottomNavigationBar: Theme( - data: Theme.of(context).copyWith( - canvasColor: Theme.of(context).colorScheme.primary + bottomNavigationBar: ValueListenableBuilder( + valueListenable: _currentIndexNotifier, + builder: (context, index, child) => BottomNavigationBar( + type: BottomNavigationBarType.fixed, + currentIndex: index, + onTap: (int index) => _setIndex(index) /*_currentIndex = index*/, + items: _tabItems.map((item) => BottomNavigationBarItem( + icon: item.icon, + label: item.title + )).toList() ), - child: ValueListenableBuilder( - valueListenable: _currentIndexNotifier, - builder: (context, index, child) => BottomNavigationBar( - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white.withAlpha(97), - currentIndex: index, - onTap: (int index) => _setIndex(index) /*_currentIndex = index*/, - items: _tabItems.map((item) => BottomNavigationBarItem( - icon: item.icon, - label: item.title - )).toList() - ), - ) - ) + ) ), ); } @@ -180,6 +181,11 @@ class _MainPageState extends TbPageState with TbMainSt _setIndex(targetIndex); } + @override + bool isHomePage() { + return _tabController.index == 0; + } + _setIndex(int index) { _tabController.index = index; } diff --git a/lib/modules/profile/profile_page.dart b/lib/modules/profile/profile_page.dart index b06307a..df5f845 100644 --- a/lib/modules/profile/profile_page.dart +++ b/lib/modules/profile/profile_page.dart @@ -4,11 +4,14 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/widgets/tb_progress_indicator.dart'; import 'package:thingsboard_client/thingsboard_client.dart'; class ProfilePage extends TbPageWidget { - ProfilePage(TbContext tbContext) : super(tbContext); + final bool _fullscreen; + + ProfilePage(TbContext tbContext, {bool fullscreen = false}) : _fullscreen = fullscreen, super(tbContext); @override _ProfilePageState createState() => _ProfilePageState(); @@ -30,7 +33,17 @@ class _ProfilePageState extends TbPageState { return Scaffold( appBar: TbAppBar( tbContext, - title: const Text('Profile') + title: const Text('Profile'), + actions: [ + if (widget._fullscreen) IconButton( + icon: Icon( + Icons.logout + ), + onPressed: () { + tbClient.logout(); + } + ) + ], ), body: FutureBuilder( future: userFuture, @@ -42,7 +55,9 @@ class _ProfilePageState extends TbPageState { subtitle: Text('${user.firstName} ${user.lastName}'), ); } else { - return Center(child: CircularProgressIndicator()); + return Center(child: TbProgressIndicator( + size: 50.0, + )); } }, ) diff --git a/lib/modules/profile/profile_routes.dart b/lib/modules/profile/profile_routes.dart index a914d87..11c2587 100644 --- a/lib/modules/profile/profile_routes.dart +++ b/lib/modules/profile/profile_routes.dart @@ -8,7 +8,8 @@ import 'profile_page.dart'; class ProfileRoutes extends TbRoutes { late var profileHandler = Handler(handlerFunc: (BuildContext? context, Map params) { - return ProfilePage(tbContext); + var fullscreen = params['fullscreen']?.first == 'true'; + return ProfilePage(tbContext, fullscreen: fullscreen); }); ProfileRoutes(TbContext tbContext) : super(tbContext); diff --git a/lib/utils/ui/qr_code_scanner.dart b/lib/utils/ui/qr_code_scanner.dart index 20f375c..38599c4 100644 --- a/lib/utils/ui/qr_code_scanner.dart +++ b/lib/utils/ui/qr_code_scanner.dart @@ -61,8 +61,11 @@ class _QrCodeScannerPageState extends TbPageState[ IconButton( diff --git a/lib/widgets/tb_app_bar.dart b/lib/widgets/tb_app_bar.dart index 6081298..bafe793 100644 --- a/lib/widgets/tb_app_bar.dart +++ b/lib/widgets/tb_app_bar.dart @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:stream_transform/stream_transform.dart'; import 'package:flutter/material.dart'; 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'; class TbAppBar extends TbContextWidget implements PreferredSizeWidget { + final Widget? leading; final Widget? title; final List? actions; final double? elevation; @@ -17,7 +17,7 @@ class TbAppBar extends TbContextWidget implements Pref @override final Size preferredSize; - TbAppBar(TbContext tbContext, {this.title, this.actions, this.elevation, + TbAppBar(TbContext tbContext, {this.leading, this.title, this.actions, this.elevation = 8, this.showLoadingIndicator = false}) : preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)), super(tbContext); @@ -64,9 +64,11 @@ class _TbAppBarState extends TbContextState { AppBar buildDefaultBar() { return AppBar( + leading: widget.leading, title: widget.title, actions: widget.actions, elevation: widget.elevation, + shadowColor: Color(0xFFFFFFFF).withAlpha(150), ); } } @@ -137,16 +139,17 @@ class _TbAppSearchBarState extends TbContextState? valueColor, + String? semanticsLabel, + String? semanticsValue, + }) : super( + key: key, + value: null, + valueColor: valueColor, + semanticsLabel: semanticsLabel, + semanticsValue: semanticsValue, + ); + + @override + _TbProgressIndicatorState createState() => _TbProgressIndicatorState(); + + Color _getValueColor(BuildContext context) => valueColor?.value ?? Theme.of(context).primaryColor; + +} + +class _TbProgressIndicatorState extends State with SingleTickerProviderStateMixin { + + late AnimationController _controller; + late CurvedAnimation _rotation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, upperBound: 1, animationBehavior: AnimationBehavior.preserve); + _rotation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); + _controller.repeat(); + } + + @override + void didUpdateWidget(TbProgressIndicator oldWidget) { + super.didUpdateWidget(oldWidget); + if (!_controller.isAnimating) + _controller.repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + SvgPicture.asset(ThingsboardImage.thingsboardCenter, + height: widget.size, + width: widget.size, + color: widget._getValueColor(context)), + AnimatedBuilder( + animation: _rotation, + child: SvgPicture.asset(ThingsboardImage.thingsboardOuter, + height: widget.size, + width: widget.size, + color: widget._getValueColor(context)), + builder: (BuildContext context, Widget? child) { + return Transform.rotate( + angle: _rotation.value * pi * 2, + child: child + ); + }, + ) + ], + ); + } + +} diff --git a/lib/widgets/transition_indexed_stack.dart b/lib/widgets/transition_indexed_stack.dart new file mode 100644 index 0000000..271c88e --- /dev/null +++ b/lib/widgets/transition_indexed_stack.dart @@ -0,0 +1,134 @@ +import 'package:flutter/widgets.dart'; + +class TransitionIndexedStackController { + + _TransitionIndexedStackState? _state; + + setTransitionIndexedStackState(_TransitionIndexedStackState state) { + _state = state; + } + + Future open(int index, {bool animate = true}) async { + if (_state != null) { + return _state!._open(index, animate: animate); + } + return false; + } + + Future close(int index, {bool animate = true}) async { + if (_state != null) { + return _state!._close(index, animate: animate); + } + return false; + } + + int? get index => _state?._selectedIndex; + +} + +class TransitionIndexedStack extends StatefulWidget { + final Widget first; + final Widget second; + final Duration duration; + final TransitionIndexedStackController? controller; + + const TransitionIndexedStack({ + Key? key, + required this.first, + required this.second, + this.controller, + this.duration = const Duration(milliseconds: 250) + }) : super(key: key); + + @override + _TransitionIndexedStackState createState() => _TransitionIndexedStackState(); + +} + +class _TransitionIndexedStackState extends State with TickerProviderStateMixin { + + late List _pages; + List _animationControllers = []; + int _selectedIndex = 0; + + @override + void initState() { + widget.controller?.setTransitionIndexedStackState(this); + final _duration = widget.duration; + _animationControllers = [ + AnimationController( + vsync: this, + duration: _duration, + ), + AnimationController( + vsync: this, + duration: _duration, + ) + ]; + _pages = [ + pageBuilder(UniqueKey(), widget.second, context, _animationControllers[1]), + pageBuilder(UniqueKey(), widget.first, context, _animationControllers[0]), + ]; + super.initState(); + } + + Future _open(int index, {bool animate = true}) async { + if (_selectedIndex != index) { + _selectedIndex = index; + setState(() { + _pages = _pages.reversed.toList(); + }); + if (animate) { + await _animationControllers[_selectedIndex].reverse(from: _animationControllers[_selectedIndex].upperBound); + } + return true; + } + return false; + } + + Future _close(int index, {bool animate = true}) async { + if (_selectedIndex == index) { + _selectedIndex = index == 1 ? 0 : 1; + if (animate) { + await _animationControllers[index].forward(from: _animationControllers[index].lowerBound); + } + setState(() { + _pages = _pages.reversed.toList(); + }); + if (animate) { + _animationControllers[index].value = _animationControllers[index].lowerBound; + } + return true; + } + return false; + } + + + + @override + void dispose() { + _animationControllers.forEach((controller) => controller.dispose()); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: Stack( + children: _pages, + ), + ); + } + + Widget pageBuilder(Key key, Widget widget, BuildContext context, Animation animation) { + return SlideTransition( + key: key, + position: Tween( + begin: Offset.zero, + end: const Offset(1, 0), + ).animate(animation), + child: widget + ); + } + +} diff --git a/pubspec.lock b/pubspec.lock index ded5357..279f41e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -190,7 +190,7 @@ packages: name: geolocator_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" geolocator_web: dependency: transitive description: @@ -356,7 +356,7 @@ packages: name: sliver_tools url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.4" source_span: dependency: transitive description: @@ -428,7 +428,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.4" + version: "6.0.6" url_launcher_linux: dependency: transitive description: @@ -456,7 +456,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" url_launcher_windows: dependency: transitive description: @@ -487,4 +487,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.12.0 <3.0.0" - flutter: ">=1.26.0-17.6.pre" + flutter: ">=2.0.0"