Redesign. Improve dashboard page loading.

This commit is contained in:
Igor Kulikov
2021-06-03 18:53:17 +03:00
parent 00038f6c35
commit 068dbbdf0c
43 changed files with 1573 additions and 811 deletions

View File

@@ -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,
<int, Color>{
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,
<int, Color>{
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(

View File

@@ -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';
}

View File

@@ -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, _LoginPageState> {
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<LoginPage, _LoginPageState> {
class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
final _isLoginNotifier = ValueNotifier<bool>(false);
final usernameController = TextEditingController();
final passwordController = TextEditingController();
@@ -49,7 +46,7 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
title: const Text('Login to ThingsBoard'),
),
body: ValueListenableBuilder(
valueListenable: loadingNotifier,
valueListenable: _isLoginNotifier,
builder: (BuildContext context, bool loading, child) {
List<Widget> children = [
SingleChildScrollView(
@@ -61,7 +58,8 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
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<LoginPage, _LoginPageState> {
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<LoginPage, _LoginPageState> {
)
];
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<LoginPage, _LoginPageState> {
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,

View File

@@ -81,12 +81,31 @@ class TbLogger {
}
}
typedef OpenDashboardCallback = void Function(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar});
abstract class TbMainDashboardHolder {
Future<void> navigateToDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar, bool animate = true});
Future<bool> openMain({bool animate});
Future<bool> closeMain({bool animate});
Future<bool> openDashboard({bool animate});
Future<bool> closeDashboard({bool animate});
bool isDashboardOpen();
Future<bool> dashboardGoBack();
}
class TbContext {
static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
bool _initialized = false;
bool isUserLoaded = false;
bool isAuthenticated = false;
final ValueNotifier<bool> _isAuthenticated = ValueNotifier(false);
User? userDetails;
HomeDashboardInfo? homeDashboard;
final _isLoadingNotifier = ValueNotifier<bool>(false);
@@ -94,6 +113,7 @@ class TbContext {
late final _widgetActionHandler;
late final AndroidDeviceInfo? _androidInfo;
late final IosDeviceInfo? _iosInfo;
TbMainDashboardHolder? _mainDashboardHolder;
GlobalKey<ScaffoldMessengerState> messengerKey = GlobalKey<ScaffoldMessengerState>();
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<void> 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<dynamic> 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<dynamic> 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<void> 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>([T? result]) {
if (currentState != null) {
router.pop<T>(currentState!.context, result);
}
}
Future<bool> maybePop<T extends Object?>([ T? result ]) async {
if (currentState != null) {
return Navigator.of(currentState!.context).maybePop(result);
} else {
return true;
}
}
Future<bool> willPop() async {
if (_mainDashboardHolder != null) {
return await _mainDashboardHolder!.dashboardGoBack();
}
return true;
}
Future<bool?> confirm({required String title, required String message, String cancel = 'Cancel', String ok = 'Ok'}) {
return showDialog<bool>(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>([T? result]) => _tbContext.pop<T>(result);
Future<bool> maybePop<T extends Object?>([ T? result ]) => _tbContext.maybePop<T>(result);
Future<void> navigateToDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar, bool animate = true}) =>
_tbContext.navigateToDashboard(dashboardId, dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar, animate: animate);
Future<bool?> confirm({required String title, required String message, String cancel = 'Cancel', String ok = 'Ok'}) => _tbContext.confirm(title: title, message: message, cancel: cancel, ok: ok);
void hideNotification() => _tbContext.hideNotification();

View File

@@ -43,6 +43,8 @@ mixin TbMainState {
navigateToPath(String path);
bool isHomePage();
}
abstract class TbPageWidget<W extends TbPageWidget<W,S>, S extends TbPageState<W,S>> extends TbContextWidget<W,S> {

View File

@@ -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<T> = Function(T entity);
@@ -44,6 +45,8 @@ mixin EntitiesBase<T, P> on HasTbContext {
return Text('Not implemented!');
}
double? gridChildAspectRatio() => null;
EntityCardSettings entityListCardSettings(T entity) => EntityCardSettings();
EntityCardSettings entityGridCardSettings(T entity) => EntityCardSettings();

View File

@@ -19,6 +19,7 @@ class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
@override
Widget pagedViewBuilder(BuildContext context) {
var heading = widget.buildHeading(context);
var gridChildAspectRatio = widget.gridChildAspectRatio() ?? 156 / 150;
List<Widget> slivers = [];
if (heading != null) {
slivers.add(SliverPadding(
@@ -35,8 +36,8 @@ class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
showNoMoreItemsIndicatorAsGridChild: false,
pagingController: pagingController,
// padding: EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 156 / 150,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: gridChildAspectRatio,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
crossAxisCount: 2,

View File

@@ -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<T extends BaseData> extends TbPageWidget<EntityDetailsPage<T>, _EntityDetailsPageState<T>> {
@@ -87,7 +88,9 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
var entity = snapshot.data!;
return widget.buildEntityDetails(context, entity);
} else {
return Center(child: CircularProgressIndicator());
return Center(child: TbProgressIndicator(
size: 50.0,
));
}
},
),

View File

@@ -30,26 +30,18 @@ class EntityGridCard<T> 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,
),

View File

@@ -33,13 +33,10 @@ class EntityListCard<T> 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<T> 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)
),
],
),
),

View File

@@ -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, _ThingsboardInitAppState> {
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<ThingsboardInitApp, _ThingsboardInitAppState> with TickerProviderStateMixin {
late final AnimationController rotationController;
late final CurvedAnimation animation;
class _ThingsboardInitAppState extends TbPageState<ThingsboardInitApp, _ThingsboardInitAppState> {
@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<ThingsboardInitApp, _Thingsbo
return Container(
alignment: Alignment.center,
color: Colors.white,
child: AnimatedBuilder(
animation: animation,
child: Container(
height: 50.0,
width: 50.0,
child: Image.asset(ThingsboardImage.thingsboard),
),
builder: (BuildContext context, Widget? _widget) {
return Transform.rotate(
angle: animation.value * pi * 2,
child: _widget,
);
},
child: TbProgressIndicator(
size: 50.0
),
);
}
}

View File

@@ -2,8 +2,13 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/dashboard/main_dashboard_page.dart';
import 'package:thingsboard_app/widgets/transition_indexed_stack.dart';
import 'config/themes/tb_theme.dart';
final appRouter = ThingsboardAppRouter();
@@ -30,21 +35,108 @@ class ThingsboardApp extends StatefulWidget {
}
class ThingsboardAppState extends State<ThingsboardApp> {
class ThingsboardAppState extends State<ThingsboardApp> 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<void> 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<bool> dashboardGoBack() async {
if (_mainStackController.index == 1) {
var canGoBack = await _mainDashboardPageController.dashboardGoBack();
if (canGoBack) {
closeDashboard();
}
return false;
}
return true;
}
@override
Future<bool> openMain({bool animate = true}) async {
return _openMain(animate: animate);
}
@override
Future<bool> closeMain({bool animate = true}) async {
return _closeMain(animate: animate);
}
@override
Future<bool> openDashboard({bool animate = true}) async {
return _openDashboard(animate: animate);
}
@override
Future<bool> closeDashboard({bool animate = true}) {
return _closeDashboard(animate: animate);
}
bool isDashboardOpen() {
return _mainStackController.index == 1;
}
Future<bool> _openMain({bool animate: true}) async {
return _mainStackController.open(0, animate: animate);
}
Future<bool> _closeMain({bool animate: true}) async {
return _mainStackController.close(0, animate: animate);
}
Future<bool> _openDashboard({bool animate: true}) async {
return _mainStackController.open(1, animate: animate);
}
Future<bool> _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),
)
)
);
}
}

View File

@@ -114,109 +114,126 @@ class _AlarmCardState extends TbContextState<AlarmCard, _AlarmCardState> {
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))
)
]
)
],
)
],
)
],
)
]
]
)
)
)
)
]
)
]
)
],
);
}
}

View File

@@ -17,12 +17,18 @@ class AlarmsPage extends TbContextWidget<AlarmsPage, _AlarmsPageState> {
}
class _AlarmsPageState extends TbContextState<AlarmsPage, _AlarmsPageState> {
class _AlarmsPageState extends TbContextState<AlarmsPage, _AlarmsPageState> with AutomaticKeepAliveClientMixin<AlarmsPage> {
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) {

View File

@@ -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<bool> canGoBack = ValueNotifier(false);
final _DashboardState dashboardState;
DashboardController(this.dashboardState);
Future<void> openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async {
return await dashboardState._openDashboard(dashboardId, state: state, hideToolbar: hideToolbar, fullscreen: fullscreen);
}
Future<bool> goBack() async {
return dashboardState._goBack();
}
onHistoryUpdated(Future<bool> 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<Dashboard, _DashboardState> {
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<Dashboard, _DashboardState> {
final Completer<InAppWebViewController> _controller = Completer<InAppWebViewController>();
final ValueNotifier<bool> webViewLoading = ValueNotifier(true);
bool webViewLoading = true;
final ValueNotifier<bool> dashboardLoading = ValueNotifier(true);
final ValueNotifier<bool> 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<Dashboard, _DashboardState> {
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<String> 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 = <String, dynamic>{
'type': 'reloadUserMessage',
'data': <String, dynamic>{
'accessToken': tbClient.getJwtToken()!,
'refreshToken': tbClient.getRefreshToken()!
}
};
var controller = await _controller.future;
await controller.postWebMessage(message: WebMessage(data: jsonEncode(windowMessage)), targetOrigin: Uri.parse('*'));
}
}
}
Future<bool> _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<void> _openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async {
_fullscreen = fullscreen;
dashboardLoading.value = true;
var controller = await _controller.future;
var windowMessage = <String, dynamic>{
'type': 'openDashboardMessage',
'data': <String, dynamic>{
'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<Dashboard, _DashboardState> {
}
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<String, dynamic>? 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<String> params = [];
if (state != null) {
params.add('state=$state');
}
if (fullscreen != null) {
params.add('fullscreen=$fullscreen');
}
if (params.isNotEmpty) {
targetPath += '?${params.join('&')}';
}
return targetPath;
}
Future<void> _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;
}
}

View File

@@ -8,11 +8,11 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class DashboardPage extends TbPageWidget<DashboardPage, _DashboardPageState> {
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<DashboardPage, _DashboardPageState
},
),
),
body: Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state,
fullscreen: widget._fullscreen, titleCallback: (title) {
dashboardTitleValue.value = title;
}),
body: Text('Deprecated') //Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state,
//fullscreen: widget._fullscreen, titleCallback: (title) {
//dashboardTitleValue.value = title;
//}
//),
);
}

View File

@@ -3,6 +3,7 @@ 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/dashboard/dashboards_page.dart';
import 'package:thingsboard_app/modules/dashboard/fullscreen_dashboard_page.dart';
import 'dashboard_page.dart';
@@ -20,12 +21,17 @@ class DashboardRoutes extends TbRoutes {
dashboardTitle: dashboardTitle, state: state);
});
late var fullscreenDashboardHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> 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);
}
}

View File

@@ -26,7 +26,8 @@ mixin DashboardsBase on EntitiesBase<DashboardInfo, PageLink> {
@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<DashboardGridCard, _Dashboa
Widget build(BuildContext context) {
var hasImage = widget.dashboard.image != null;
Widget image;
BoxFit imageFit;
if (hasImage) {
var uriData = UriData.parse(widget.dashboard.image!);
image = Image.memory(uriData.contentAsBytes());
imageFit = BoxFit.contain;
} else {
image = Image.asset(ThingsboardImage.dashboardPlaceholder);
imageFit = BoxFit.cover;
}
return
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Stack(
borderRadius: BorderRadius.circular(4),
child: Column(
children: [
Positioned.fill(
child: FittedBox(
fit: imageFit,
child: image,
Expanded(
child: Stack (
children: [
SizedBox.expand(
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.cover,
child: image
)
)
]
)
),
hasImage ? Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0x00000000),
Color(0xb7000000)
],
stops: [0.4219, 1]
Divider(height: 1),
Container(
height: 44,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child:
Center(
child: AutoSizeText(widget.dashboard.title,
textAlign: TextAlign.center,
maxLines: 1,
minFontSize: 12,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
),
)
)
),
) : Container(),
Positioned(
bottom: 16,
left: 16,
right: 16,
child: AutoSizeText(widget.dashboard.title,
textAlign: TextAlign.center,
maxLines: 2,
minFontSize: 8,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: hasImage ? Colors.white : Color(0xFF282828),
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
),
)
)
],
)

View File

@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'dashboards_list.dart';
import 'dashboards_grid.dart';
class DashboardsPage extends TbPageWidget<DashboardsPage, _DashboardsPageState> {
@@ -17,23 +16,19 @@ class DashboardsPage extends TbPageWidget<DashboardsPage, _DashboardsPageState>
class _DashboardsPageState extends TbPageState<DashboardsPage, _DashboardsPageState> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
var dashboardsList = DashboardsList(tbContext, _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();
}

View File

@@ -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<FullscreenDashboardPage, _FullscreenDashboardPageState> {
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<FullscreenDashboardPage, _FullscreenDashboardPageState> {
late ValueNotifier<String> dashboardTitleValue;
final ValueNotifier<bool> 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<bool>(
valueListenable: showBackValue,
builder: (context, canGoBack, widget) {
return TbAppBar(
tbContext,
leading: canGoBack ? BackButton(
onPressed: () {
maybePop();
}
) : null,
showLoadingIndicator: false,
elevation: 0,
title: ValueListenableBuilder<String>(
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);
}
)
);
}
}

View File

@@ -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<bool> dashboardGoBack() {
if (_dashboardController != null) {
return _dashboardController!.goBack();
} else {
return Future.value(true);
}
}
Future<void> 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<MainDashboardPage, _MainDashboardPageState> {
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<MainDashboardPage, _MainDashboardPageState> {
late ValueNotifier<String> 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<String>(
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);
}
}
)
);
}
}

View File

@@ -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<DeviceProfileInfo, PageLink> {
@@ -50,6 +51,11 @@ mixin DeviceProfilesBase on EntitiesBase<DeviceProfileInfo, PageLink> {
return DeviceProfileCard(tbContext, deviceProfile);
}
@override
double? gridChildAspectRatio() {
return 156 / 200;
}
}
class RefreshDeviceCounts {
@@ -93,8 +99,10 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
Future<int> inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, active: false);
Future<List<int>> countsFuture = Future.wait([activeDevicesCount, inactiveDevicesCount]);
countsFuture.then((counts) {
_activeDevicesCount.add(counts[0]);
_inactiveDevicesCount.add(counts[1]);
if (this.mounted) {
_activeDevicesCount.add(counts[0]);
_inactiveDevicesCount.add(counts[1]);
}
});
return countsFuture;
}
@@ -107,32 +115,31 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
child:
Container(
child: Card(
color: Theme.of(tbContext.currentState!.context).colorScheme.primary,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
child: Column(
children: [
Padding(padding: EdgeInsets.fromLTRB(16, 12, 16, 8),
Padding(padding: EdgeInsets.fromLTRB(16, 12, 16, 15),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('All devices',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
)
),
Icon(Icons.arrow_forward, color: Colors.white)
Icon(Icons.arrow_forward, size: 18)
],
)
),
Padding(padding: EdgeInsets.all(8),
Divider(height: 1),
Padding(padding: EdgeInsets.all(0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
@@ -150,7 +157,7 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
builder: (context, snapshot) {
if (snapshot.hasData) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, true, deviceCount, displayStatusText: true);
return _buildDeviceCount(context, true, deviceCount);
} else {
return Center(child:
Container(height: 20, width: 20,
@@ -166,7 +173,11 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
}
),
),
SizedBox(width: 4),
// SizedBox(width: 4),
Container(width: 1,
height: 40,
child: VerticalDivider(width: 1)
),
Flexible(fit: FlexFit.tight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
@@ -181,7 +192,7 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
builder: (context, snapshot) {
if (snapshot.hasData) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, false, deviceCount, displayStatusText: true);
return _buildDeviceCount(context, false, deviceCount);
} else {
return Center(child:
Container(height: 20, width: 20,
@@ -206,15 +217,10 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard, _AllDevicesCar
decoration: 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)
),
)
],
),
),
@@ -275,124 +281,98 @@ class _DeviceProfileCardState extends TbContextState<DeviceProfileCard, _DeviceP
}
return
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Stack(
children: [
Positioned.fill(
child: FittedBox(
fit: imageFit,
child: image,
)
),
hasImage ? Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0x00000000),
Color(0xb7000000)
],
stops: [0.4219, 1]
)
borderRadius: BorderRadius.circular(4),
child: Column(
children: [
Expanded(
child: Stack (
children: [
SizedBox.expand(
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: imageFit,
child: image
)
)
]
)
),
) : Container(),
Positioned(
bottom: 56,
left: 16,
right: 16,
child: AutoSizeText(entity.name,
textAlign: TextAlign.center,
maxLines: 2,
minFontSize: 8,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: hasImage ? Colors.white : Color(0xFF282828),
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
),
)
),
Positioned(
bottom: 4,
left: 4,
right: 4,
height: 40,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(fit: FlexFit.tight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: FutureBuilder<int>(
future: activeDevicesCount,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, true, deviceCount);
} else {
return Center(child:
Container(height: 20, width: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
strokeWidth: 2.5)));
}
},
)
),
onTap: () {
navigateTo('/deviceList?active=true&deviceType=${entity.name}');
}
),
),
SizedBox(width: 4),
Flexible(fit: FlexFit.tight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: FutureBuilder<int>(
future: inactiveDevicesCount,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, false, deviceCount);
} else {
return Center(child:
Container(height: 20, width: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
strokeWidth: 2.5)));
}
},
)
),
onTap: () {
navigateTo('/deviceList?active=false&deviceType=${entity.name}');
}
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<int>(
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<int>(
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))
],
),
);

View File

@@ -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<String, dynamic> params) {
return DevicesPage(tbContext);
});
late var deviceListHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
var deviceType = params['deviceType']?.first;
String? activeStr = params['active']?.first;
bool? active = activeStr != null ? activeStr == 'true' : null;
return DevicesPage(tbContext, searchMode: searchMode, deviceType: deviceType, active: active);
return DevicesListPage(tbContext, searchMode: searchMode, deviceType: deviceType, active: active);
});
late var deviceDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> 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);
}

View File

@@ -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<EntityData, EntityDataQuery> {
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<DeviceCard, _DeviceCardState> {
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<DeviceProfileInfo>(
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<DeviceCard, _DeviceCardState> {
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<DeviceCard, _DeviceCardState> {
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,
))
],
)

View File

@@ -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<DevicesListPage, _DevicesListPageState> {
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<DevicesListPage, _DevicesListPageState> {
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<String> params = [];
params.add('search=true');
if (widget.deviceType != null) {
params.add('deviceType=${widget.deviceType}');
}
if (widget.active != null) {
params.add('active=${widget.active}');
}
navigateTo('/deviceList?${params.join('&')}');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: devicesList
);
}
@override
void dispose() {
_deviceQueryController.dispose();
super.dispose();
}
}

View File

@@ -14,12 +14,18 @@ class DevicesMainPage extends TbContextWidget<DevicesMainPage, _DevicesMainPageS
}
class _DevicesMainPageState extends TbContextState<DevicesMainPage, _DevicesMainPageState> {
class _DevicesMainPageState extends TbContextState<DevicesMainPage, _DevicesMainPageState> with AutomaticKeepAliveClientMixin<DevicesMainPage> {
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(

View File

@@ -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<DevicesPage, _DevicesPageState> {
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<DevicesPage, _DevicesPageState> {
class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
late final DeviceQueryController _deviceQueryController;
@override
void initState() {
super.initState();
_deviceQueryController = DeviceQueryController(deviceType: widget.deviceType, active: widget.active);
}
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<String> params = [];
params.add('search=true');
if (widget.deviceType != null) {
params.add('deviceType=${widget.deviceType}');
}
if (widget.active != null) {
params.add('active=${widget.active}');
}
navigateTo('/deviceList?${params.join('&')}');
},
)
]);
}
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();
}

View File

@@ -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<HomePage, _HomePageState> 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<HomePage, _HomePageState> 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<HomePage, _HomePageState> with Autom
];
} */
}
class HomeDashboard extends TbContextWidget<HomeDashboard, _HomeDashboardState> {
final HomeDashboardInfo dashboard;
HomeDashboard(TbContext tbContext, this.dashboard) : super(tbContext);
@override
_HomeDashboardState createState() => _HomeDashboardState();
}
class _HomeDashboardState extends TbContextState<HomeDashboard, _HomeDashboardState> {
@override
Widget build(BuildContext context) {
return dashboardUi.Dashboard(tbContext,
home: true,
controllerCallback: (controller) {
controller.openDashboard(widget.dashboard.dashboardId!.id!,
hideToolbar: widget.dashboard.hideDashboardToolbar);
}
);
}
}

View File

@@ -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<MainPage, _MainPageState> {
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<MainPage, _MainPageState> 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<MainPage, _MainPageState> 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<int>(
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<int>(
valueListenable: _currentIndexNotifier,
builder: (context, index, child) => BottomNavigationBar(
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.white,
unselectedItemColor: Colors.white.withAlpha(97),
currentIndex: index,
onTap: (int index) => _setIndex(index) /*_currentIndex = index*/,
items: _tabItems.map((item) => BottomNavigationBarItem(
icon: item.icon,
label: item.title
)).toList()
),
)
)
)
),
);
}
@@ -180,6 +181,11 @@ class _MainPageState extends TbPageState<MainPage, _MainPageState> with TbMainSt
_setIndex(targetIndex);
}
@override
bool isHomePage() {
return _tabController.index == 0;
}
_setIndex(int index) {
_tabController.index = index;
}

View File

@@ -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, _ProfilePageState> {
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<ProfilePage, _ProfilePageState> {
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<User>(
future: userFuture,
@@ -42,7 +55,9 @@ class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
subtitle: Text('${user.firstName} ${user.lastName}'),
);
} else {
return Center(child: CircularProgressIndicator());
return Center(child: TbProgressIndicator(
size: 50.0,
));
}
},
)

View File

@@ -8,7 +8,8 @@ import 'profile_page.dart';
class ProfileRoutes extends TbRoutes {
late var profileHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return ProfilePage(tbContext);
var fullscreen = params['fullscreen']?.first == 'true';
return ProfilePage(tbContext, fullscreen: fullscreen);
});
ProfileRoutes(TbContext tbContext) : super(tbContext);

View File

@@ -61,8 +61,11 @@ class _QrCodeScannerPageState extends TbPageState<QrCodeScannerPage, _QrCodeScan
Positioned(
child:
AppBar(
leading: Container(),
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
iconTheme: IconThemeData(
color: Colors.white
),
elevation: 0,
actions: <Widget>[
IconButton(

View File

@@ -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<TbAppBar, _TbAppBarState> implements PreferredSizeWidget {
final Widget? leading;
final Widget? title;
final List<Widget>? actions;
final double? elevation;
@@ -17,7 +17,7 @@ class TbAppBar extends TbContextWidget<TbAppBar, _TbAppBarState> 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<TbAppBar, _TbAppBarState> {
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<TbAppSearchBar, _TbAppSearchBa
AppBar buildSearchBar() {
return AppBar(
centerTitle: true,
title: Theme(
data: tbDarkTheme,
child: TextField(
controller: _filter,
cursorColor: Colors.white,
decoration: new InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.only(left: 15, bottom: 11, top: 15, right: 15),
hintText: widget.searchHint ?? 'Search',
title: TextField(
controller: _filter,
autofocus: true,
// cursorColor: Colors.white,
decoration: new InputDecoration(
border: InputBorder.none,
hintStyle: TextStyle(
color: Color(0xFF282828).withAlpha((255 * 0.38).ceil()),
),
contentPadding: EdgeInsets.only(left: 15, bottom: 11, top: 15, right: 15),
hintText: widget.searchHint ?? 'Search',
)
),
actions: [

View File

@@ -0,0 +1,86 @@
import 'dart:math';
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';
class TbProgressIndicator extends ProgressIndicator {
final double size;
const TbProgressIndicator({
Key? key,
this.size = 36.0,
Animation<Color?>? 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<TbProgressIndicator> 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
);
},
)
],
);
}
}

View File

@@ -0,0 +1,134 @@
import 'package:flutter/widgets.dart';
class TransitionIndexedStackController {
_TransitionIndexedStackState? _state;
setTransitionIndexedStackState(_TransitionIndexedStackState state) {
_state = state;
}
Future<bool> open(int index, {bool animate = true}) async {
if (_state != null) {
return _state!._open(index, animate: animate);
}
return false;
}
Future<bool> 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<TransitionIndexedStack> with TickerProviderStateMixin {
late List<Widget> _pages;
List<AnimationController> _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<bool> _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<bool> _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<double> animation) {
return SlideTransition(
key: key,
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(1, 0),
).animate(animation),
child: widget
);
}
}