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"