Add flutter 3+ support. Update dependencies. Fix code style and format issues.

This commit is contained in:
Igor Kulikov
2022-08-12 13:55:27 +03:00
parent 1a07bcd7a0
commit 944c36ce7b
94 changed files with 3167 additions and 3173 deletions

View File

@@ -8,12 +8,13 @@ import 'package:thingsboard_app/core/context/tb_context.dart';
import 'login/login_page.dart';
class AuthRoutes extends TbRoutes {
late var loginHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var loginHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return LoginPage(tbContext);
});
late var resetPasswordRequestHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var resetPasswordRequestHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return ResetPasswordRequestPage(tbContext);
});
@@ -22,7 +23,7 @@ class AuthRoutes extends TbRoutes {
@override
void doRegisterRoutes(router) {
router.define("/login", handler: loginHandler);
router.define("/login/resetPasswordRequest", handler: resetPasswordRequestHandler);
router.define("/login/resetPasswordRequest",
handler: resetPasswordRequestHandler);
}
}

View File

@@ -1,8 +1,6 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -16,23 +14,20 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'login_page_background.dart';
class LoginPage extends TbPageWidget {
LoginPage(TbContext tbContext) : super(tbContext);
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends TbPageState<LoginPage> {
final ButtonStyle _oauth2ButtonWithTextStyle = OutlinedButton.styleFrom(
padding: EdgeInsets.all(16),
alignment: Alignment.centerLeft,
primary: Colors.black87);
final ButtonStyle _oauth2ButtonWithTextStyle =
OutlinedButton.styleFrom(padding: EdgeInsets.all(16),
alignment: Alignment.centerLeft, primary: Colors.black87);
final ButtonStyle _oauth2IconButtonStyle =
OutlinedButton.styleFrom(padding: EdgeInsets.all(16),
alignment: Alignment.center);
final ButtonStyle _oauth2IconButtonStyle = OutlinedButton.styleFrom(
padding: EdgeInsets.all(16), alignment: Alignment.center);
final _isLoginNotifier = ValueNotifier<bool>(false);
final _showPasswordNotifier = ValueNotifier<bool>(false);
@@ -54,174 +49,185 @@ class _LoginPageState extends TbPageState<LoginPage> {
return Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
body: Stack(
children: [
LoginPageBackground(),
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(24, 71, 24, 24),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight - (71 + 24)),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle,
height: 25,
color: Theme.of(context).primaryColor,
semanticsLabel: 'ThingsBoard Logo')
]
),
SizedBox(height: 32),
Row(
children: [
Text(
'Login to your account',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 28,
height: 36 / 28
)
)]
),
SizedBox(height: 48),
if (tbContext.hasOAuthClients)
_buildOAuth2Buttons(tbContext.oauth2ClientInfos!),
if (tbContext.hasOAuthClients)
Padding(padding: EdgeInsets.only(top: 10, bottom: 16),
child: Row(
children: [
Flexible(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text('OR'),
),
Flexible(child: Divider())
],
)
body: Stack(children: [
LoginPageBackground(),
Positioned.fill(child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(24, 71, 24, 24),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - (71 + 24)),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(children: [
SvgPicture.asset(
ThingsboardImage.thingsBoardWithTitle,
height: 25,
color: Theme.of(context).primaryColor,
semanticsLabel: 'ThingsBoard Logo')
]),
SizedBox(height: 32),
Row(children: [
Text('Login to your account',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 28,
height: 36 / 28))
]),
SizedBox(height: 48),
if (tbContext.hasOAuthClients)
_buildOAuth2Buttons(
tbContext.oauth2ClientInfos!),
if (tbContext.hasOAuthClients)
Padding(
padding:
EdgeInsets.only(top: 10, bottom: 16),
child: Row(
children: [
Flexible(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 16),
child: Text('OR'),
),
Flexible(child: Divider())
],
)),
FormBuilder(
key: _loginFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
FormBuilderTextField(
name: 'username',
validator:
FormBuilderValidators.compose([
FormBuilderValidators.required(
errorText: 'Email is required.'),
FormBuilderValidators.email(
errorText:
'Invalid email format.')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email'),
),
FormBuilder(
key: _loginFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FormBuilderTextField(
name: 'username',
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Email is required.'),
FormBuilderValidators.email(context, errorText: 'Invalid email format.')
SizedBox(height: 28),
ValueListenableBuilder(
valueListenable:
_showPasswordNotifier,
builder: (BuildContext context,
bool showPassword, child) {
return FormBuilderTextField(
name: 'password',
obscureText: !showPassword,
validator: FormBuilderValidators
.compose([
FormBuilderValidators.required(
errorText:
'Password is required.')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () {
_showPasswordNotifier
.value =
!_showPasswordNotifier
.value;
},
),
border: OutlineInputBorder(),
labelText: 'Email'
),
),
SizedBox(height: 28),
ValueListenableBuilder(
valueListenable: _showPasswordNotifier,
builder: (BuildContext context, bool showPassword, child) {
return FormBuilderTextField(
name: 'password',
obscureText: !showPassword,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Password is required.')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
_showPasswordNotifier.value = !_showPasswordNotifier.value;
},
),
border: OutlineInputBorder(),
labelText: 'Password'
),
);
}
)
],
)
labelText: 'Password'),
);
})
],
)),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
_forgotPassword();
},
child: Text(
'Forgot Password?',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
letterSpacing: 1,
fontSize: 12,
height: 16 / 12),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
_forgotPassword();
},
child: Text(
'Forgot Password?',
style: TextStyle(color: Theme.of(context).colorScheme.primary,
letterSpacing: 1,
fontSize: 12,
height: 16 / 12),
),
)
],
),
Spacer(),
ElevatedButton(
child: Text('Log In'),
style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)),
onPressed: () {
_login();
},
),
SizedBox(height: 48)
]
)
],
),
)
)
);
},
)
),
ValueListenableBuilder<bool>(
Spacer(),
ElevatedButton(
child: Text('Log In'),
style: ElevatedButton.styleFrom(
padding:
EdgeInsets.symmetric(vertical: 16)),
onPressed: () {
_login();
},
),
SizedBox(height: 48)
]),
)));
},
)),
ValueListenableBuilder<bool>(
valueListenable: _isLoginNotifier,
builder: (BuildContext context, bool loading, child) {
if (loading) {
var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window);
var data =
MediaQueryData.fromWindow(WidgetsBinding.instance.window);
var bottomPadding = data.padding.top;
bottomPadding += kToolbarHeight;
return SizedBox.expand(
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
filter:
ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
child: Container(
decoration: new BoxDecoration(
color: Colors.grey.shade200.withOpacity(0.2)
),
color:
Colors.grey.shade200.withOpacity(0.2)),
child: Container(
padding: EdgeInsets.only(bottom: bottomPadding),
padding:
EdgeInsets.only(bottom: bottomPadding),
alignment: Alignment.center,
child: TbProgressIndicator(size: 50.0),
),
)
)
)
);
))));
} else {
return SizedBox.shrink();
}
}
)
]
)
);
})
]));
}
Widget _buildOAuth2Buttons(List<OAuth2ClientInfo> clients) {
if (clients.length == 1 || clients.length > 6) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: clients.asMap().map((index, client) =>
MapEntry(index, _buildOAuth2Button(client, 'Login with ${client.name}', false, index == clients.length - 1))).values.toList()
);
crossAxisAlignment: CrossAxisAlignment.stretch,
children: clients
.asMap()
.map((index, client) => MapEntry(
index,
_buildOAuth2Button(client, 'Login with ${client.name}', false,
index == clients.length - 1)))
.values
.toList());
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -231,18 +237,24 @@ class _LoginPageState extends TbPageState<LoginPage> {
child: Center(child: Text('LOGIN WITH')),
),
Row(
children: clients.asMap().map((index, client) =>
MapEntry(index, _buildOAuth2Button(client, clients.length == 2 ? client.name : null, true, index == clients.length - 1))).values.toList()
)
children: clients
.asMap()
.map((index, client) => MapEntry(
index,
_buildOAuth2Button(
client,
clients.length == 2 ? client.name : null,
true,
index == clients.length - 1)))
.values
.toList())
],
);
}
}
Widget _buildOAuth2Button(OAuth2ClientInfo client,
String? text,
bool expand,
bool isLast) {
Widget _buildOAuth2Button(
OAuth2ClientInfo client, String? text, bool expand, bool isLast) {
Widget? icon;
if (client.icon != null) {
if (ThingsboardImage.oauth2Logos.containsKey(client.icon)) {
@@ -255,7 +267,8 @@ class _LoginPageState extends TbPageState<LoginPage> {
}
var iconData = MdiIcons.fromString(strIcon);
if (iconData != null) {
icon = Icon(iconData, size: 24, color: Theme.of(context).primaryColor);
icon =
Icon(iconData, size: 24, color: Theme.of(context).primaryColor);
}
}
}
@@ -279,10 +292,9 @@ class _LoginPageState extends TbPageState<LoginPage> {
if (expand) {
return Expanded(
child: Padding(
padding: EdgeInsets.only(right: isLast ? 0 : 8),
child: button,
)
);
padding: EdgeInsets.only(right: isLast ? 0 : 8),
child: button,
));
} else {
return button;
}
@@ -293,7 +305,8 @@ class _LoginPageState extends TbPageState<LoginPage> {
try {
final result = await tbContext.oauth2Client.authenticate(client.url);
if (result.success) {
await tbClient.setUserFromJwtToken(result.accessToken, result.refreshToken, true);
await tbClient.setUserFromJwtToken(
result.accessToken, result.refreshToken, true);
} else {
_isLoginNotifier.value = false;
showErrorNotification(result.error!);

View File

@@ -1,21 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class LoginPageBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: CustomPaint(
painter: _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor),
)
);
painter:
_LoginPageBackgroundPainter(color: Theme.of(context).primaryColor),
));
}
}
class _LoginPageBackgroundPainter extends CustomPainter {
final Color color;
const _LoginPageBackgroundPainter({required this.color});

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:thingsboard_app/core/auth/login/login_page_background.dart';
@@ -9,16 +8,15 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
class ResetPasswordRequestPage extends TbPageWidget {
ResetPasswordRequestPage(TbContext tbContext) : super(tbContext);
@override
_ResetPasswordRequestPageState createState() => _ResetPasswordRequestPageState();
_ResetPasswordRequestPageState createState() =>
_ResetPasswordRequestPageState();
}
class _ResetPasswordRequestPageState extends TbPageState<ResetPasswordRequestPage> {
class _ResetPasswordRequestPageState
extends TbPageState<ResetPasswordRequestPage> {
final _isLoadingNotifier = ValueNotifier<bool>(false);
final _resetPasswordFormKey = GlobalKey<FormBuilderState>();
@@ -26,82 +24,74 @@ class _ResetPasswordRequestPageState extends TbPageState<ResetPasswordRequestPag
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack( children: [
LoginPageBackground(),
SizedBox.expand(
body: Stack(children: [
LoginPageBackground(),
SizedBox.expand(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: TbAppBar(
tbContext,
title: Text('Reset password'),
),
body: Stack(
children: [
SizedBox.expand(
body: Stack(children: [
SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(24),
padding: EdgeInsets.all(24),
child: FormBuilder(
key: _resetPasswordFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
Text('Enter the email associated with your account and we\'ll send an email with password reset link',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 14,
height: 24 / 14
),
),
SizedBox(height: 61),
FormBuilderTextField(
name: 'email',
autofocus: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Email is required.'),
FormBuilderValidators.email(context, errorText: 'Invalid email format.')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email *'
),
),
Spacer(),
ElevatedButton(
child: Text('Request password reset'),
style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)),
onPressed: () {
_requestPasswordReset();
},
)
]
)
)
)
),
ValueListenableBuilder<bool>(
valueListenable: _isLoadingNotifier,
builder: (BuildContext context, bool loading, child) {
if (loading) {
return SizedBox.expand(
child: Container(
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
)
);
} else {
return SizedBox.shrink();
}
key: _resetPasswordFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
Text(
'Enter the email associated with your account and we\'ll send an email with password reset link',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 14,
height: 24 / 14),
),
SizedBox(height: 61),
FormBuilderTextField(
name: 'email',
autofocus: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(
errorText: 'Email is required.'),
FormBuilderValidators.email(
errorText: 'Invalid email format.')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email *'),
),
Spacer(),
ElevatedButton(
child: Text('Request password reset'),
style: ElevatedButton.styleFrom(
padding:
EdgeInsets.symmetric(vertical: 16)),
onPressed: () {
_requestPasswordReset();
},
)
])))),
ValueListenableBuilder<bool>(
valueListenable: _isLoadingNotifier,
builder: (BuildContext context, bool loading, child) {
if (loading) {
return SizedBox.expand(
child: Container(
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
));
} else {
return SizedBox.shrink();
}
)
]
)
)
)
])
);
})
])))
]));
}
void _requestPasswordReset() async {
@@ -115,7 +105,7 @@ class _ResetPasswordRequestPageState extends TbPageState<ResetPasswordRequestPag
await tbClient.sendResetPasswordLink(email);
_isLoadingNotifier.value = false;
showSuccessNotification('Password reset link was successfully sent!');
} catch(e) {
} catch (e) {
_isLoadingNotifier.value = false;
}
}

View File

@@ -1,19 +1,15 @@
import 'package:thingsboard_app/constants/app_constants.dart';
abstract class AppSecretProvider {
Future<String> getAppSecret();
factory AppSecretProvider.local() => _LocalAppSecretProvider();
}
/// Not for production (only for debugging)
class _LocalAppSecretProvider implements AppSecretProvider {
@override
Future<String> getAppSecret() async {
return ThingsboardAppConstants.thingsboardOAuth2AppSecret;
}
}

View File

@@ -19,39 +19,42 @@ class TbOAuth2AuthenticateResult {
TbOAuth2AuthenticateResult.failed(this.error);
bool get success => error == null;
}
class TbOAuth2Client {
final TbContext _tbContext;
final AppSecretProvider _appSecretProvider;
TbOAuth2Client(
{ required TbContext tbContext,
required AppSecretProvider appSecretProvider} ):
_tbContext = tbContext,
_appSecretProvider = appSecretProvider;
{required TbContext tbContext,
required AppSecretProvider appSecretProvider})
: _tbContext = tbContext,
_appSecretProvider = appSecretProvider;
Future<TbOAuth2AuthenticateResult> authenticate(String oauth2Url) async {
final appSecret = await _appSecretProvider.getAppSecret();
final pkgName = _tbContext.packageName;
final jwt = JWT(
{
'callbackUrlScheme': ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme
'callbackUrlScheme':
ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme
},
issuer: pkgName,
);
final key = SecretKey(appSecret);
final appToken = jwt.sign(key, algorithm: _HMACBase64Algorithm.HS512, expiresIn: Duration(minutes: 2));
var url = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + oauth2Url);
final params = Map<String,String>.from(url.queryParameters);
final appToken = jwt.sign(key,
algorithm: _HMACBase64Algorithm.HS512, expiresIn: Duration(minutes: 2));
var url =
Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + oauth2Url);
final params = Map<String, String>.from(url.queryParameters);
params['pkg'] = pkgName;
params['appToken'] = appToken;
url = url.replace(queryParameters: params);
final result = await TbWebAuth.authenticate(
url: url.toString(),
callbackUrlScheme: ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme, saveHistory: false);
callbackUrlScheme:
ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme,
saveHistory: false);
final resultUri = Uri.parse(result);
final error = resultUri.queryParameters['error'];
if (error != null) {
@@ -62,14 +65,14 @@ class TbOAuth2Client {
if (accessToken != null && refreshToken != null) {
return TbOAuth2AuthenticateResult.success(accessToken, refreshToken);
} else {
return TbOAuth2AuthenticateResult.failed('No authentication credentials in response.');
return TbOAuth2AuthenticateResult.failed(
'No authentication credentials in response.');
}
}
}
}
class _HMACBase64Algorithm extends JWTAlgorithm {
static const HS512 = _HMACBase64Algorithm('HS512');
final String _name;

View File

@@ -19,13 +19,18 @@ class _OnAppLifecycleResumeObserver extends WidgetsBindingObserver {
class TbWebAuth {
static const MethodChannel _channel = const MethodChannel('tb_web_auth');
static final _OnAppLifecycleResumeObserver _resumedObserver = _OnAppLifecycleResumeObserver(() {
static final _OnAppLifecycleResumeObserver _resumedObserver =
_OnAppLifecycleResumeObserver(() {
_cleanUpDanglingCalls();
});
static Future<String> authenticate({required String url, required String callbackUrlScheme, bool? saveHistory}) async {
WidgetsBinding.instance?.removeObserver(_resumedObserver); // safety measure so we never add this observer twice
WidgetsBinding.instance?.addObserver(_resumedObserver);
static Future<String> authenticate(
{required String url,
required String callbackUrlScheme,
bool? saveHistory}) async {
WidgetsBinding.instance.removeObserver(
_resumedObserver); // safety measure so we never add this observer twice
WidgetsBinding.instance.addObserver(_resumedObserver);
return await _channel.invokeMethod('authenticate', <String, dynamic>{
'url': url,
'callbackUrlScheme': callbackUrlScheme,
@@ -35,6 +40,6 @@ class TbWebAuth {
static Future<void> _cleanUpDanglingCalls() async {
await _channel.invokeMethod('cleanUpDanglingCalls');
WidgetsBinding.instance?.removeObserver(_resumedObserver);
WidgetsBinding.instance.removeObserver(_resumedObserver);
}
}

View File

@@ -15,12 +15,7 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'package:thingsboard_app/utils/services/tb_app_storage.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
enum NotificationType {
info,
warn,
success,
error
}
enum NotificationType { info, warn, success, error }
class TbLogOutput extends LogOutput {
@override
@@ -45,18 +40,14 @@ class TbLogsFilter extends LogFilter {
class TbLogger {
final _logger = Logger(
filter: TbLogsFilter(),
printer: PrefixPrinter(
PrettyPrinter(
methodCount: 0,
errorMethodCount: 8,
lineLength: 200,
colors: false,
printEmojis: true,
printTime: false
)
),
output: TbLogOutput()
);
printer: PrefixPrinter(PrettyPrinter(
methodCount: 0,
errorMethodCount: 8,
lineLength: 200,
colors: false,
printEmojis: true,
printTime: false)),
output: TbLogOutput());
void verbose(dynamic message, [dynamic error, StackTrace? stackTrace]) {
_logger.v(message, error, stackTrace);
@@ -83,11 +74,15 @@ class TbLogger {
}
}
typedef OpenDashboardCallback = void Function(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar});
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<void> navigateToDashboard(String dashboardId,
{String? dashboardTitle,
String? state,
bool? hideToolbar,
bool animate = true});
Future<bool> openMain({bool animate});
@@ -100,7 +95,6 @@ abstract class TbMainDashboardHolder {
bool isDashboardOpen();
Future<bool> dashboardGoBack();
}
class TbContext {
@@ -121,7 +115,8 @@ class TbContext {
TbMainDashboardHolder? _mainDashboardHolder;
bool _closeMainFirst = false;
GlobalKey<ScaffoldMessengerState> messengerKey = GlobalKey<ScaffoldMessengerState>();
GlobalKey<ScaffoldMessengerState> messengerKey =
GlobalKey<ScaffoldMessengerState>();
late final ThingsboardClient tbClient;
late final TbOAuth2Client oauth2Client;
@@ -147,14 +142,15 @@ class TbContext {
_initialized = true;
var storage = createAppStorage();
tbClient = ThingsboardClient(ThingsboardAppConstants.thingsBoardApiEndpoint,
storage: storage,
onUserLoaded: onUserLoaded,
onError: onError,
onLoadStarted: onLoadStarted,
onLoadFinished: onLoadFinished,
computeFunc: <Q, R>(callback, message) => compute(callback, message));
storage: storage,
onUserLoaded: onUserLoaded,
onError: onError,
onLoadStarted: onLoadStarted,
onLoadFinished: onLoadFinished,
computeFunc: <Q, R>(callback, message) => compute(callback, message));
oauth2Client = TbOAuth2Client(tbContext: this, appSecretProvider: AppSecretProvider.local());
oauth2Client = TbOAuth2Client(
tbContext: this, appSecretProvider: AppSecretProvider.local());
try {
if (UniversalPlatform.isAndroid) {
@@ -203,11 +199,12 @@ class TbContext {
showNotification(message, NotificationType.success, duration: duration);
}
void showNotification(String message, NotificationType type, {Duration? duration}) {
void showNotification(String message, NotificationType type,
{Duration? duration}) {
duration ??= const Duration(days: 1);
Color backgroundColor;
var textColor = Color(0xFFFFFFFF);
switch(type) {
switch (type) {
case NotificationType.info:
backgroundColor = Color(0xFF323232);
break;
@@ -224,16 +221,16 @@ class TbContext {
final snackBar = SnackBar(
duration: duration,
backgroundColor: backgroundColor,
content: Text(message,
style: TextStyle(
color: textColor
),
content: Text(
message,
style: TextStyle(color: textColor),
),
action: SnackBarAction(
label: 'Close',
textColor: textColor,
onPressed: () {
messengerKey.currentState!.hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
messengerKey.currentState!
.hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
},
),
);
@@ -264,7 +261,8 @@ class TbContext {
if (tbClient.getAuthUser()!.userId != null) {
try {
userDetails = await tbClient.getUserService().getUser();
homeDashboard = await tbClient.getDashboardService().getHomeDashboardInfo();
homeDashboard =
await tbClient.getDashboardService().getHomeDashboardInfo();
} catch (e) {
if (!_isConnectionError(e)) {
tbClient.logout();
@@ -276,33 +274,44 @@ class TbContext {
} else {
userDetails = null;
homeDashboard = null;
oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients(pkgName: packageName, platform: _oauth2PlatformType);
oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients(
pkgName: packageName, platform: _oauth2PlatformType);
}
_isAuthenticated.value = tbClient.isAuthenticated();
await updateRouteState();
} catch (e, s) {
log.error('Error: $e', e, s);
if (_isConnectionError(e)) {
var res = await confirm(title: 'Connection error', message: 'Failed to connect to server', cancel: 'Cancel', ok: 'Retry');
var res = await confirm(
title: 'Connection error',
message: 'Failed to connect to server',
cancel: 'Cancel',
ok: 'Retry');
if (res == true) {
onUserLoaded();
} else {
navigateTo('/login', replace: true, clearStack: true, transition: TransitionType.fadeIn, transitionDuration: Duration(milliseconds: 750));
navigateTo('/login',
replace: true,
clearStack: true,
transition: TransitionType.fadeIn,
transitionDuration: Duration(milliseconds: 750));
}
}
}
}
bool _isConnectionError(e) {
return e is ThingsboardError && e.errorCode == ThingsBoardErrorCode.general && e.message == 'Unable to connect';
return e is ThingsboardError &&
e.errorCode == ThingsBoardErrorCode.general &&
e.message == 'Unable to connect';
}
Listenable get isAuthenticatedListenable => _isAuthenticated;
bool get isAuthenticated => _isAuthenticated.value;
bool get hasOAuthClients => oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty;
bool get hasOAuthClients =>
oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty;
Future<void> updateRouteState() async {
if (currentState != null) {
@@ -318,14 +327,20 @@ class TbContext {
transition: TransitionType.none);
} else {
navigateTo('/fullscreenDashboard/$defaultDashboardId',
replace: true,
transition: TransitionType.fadeIn);
replace: true, transition: TransitionType.fadeIn);
}
} else {
navigateTo('/home', replace: true, transition: TransitionType.fadeIn, transitionDuration: Duration(milliseconds: 750));
navigateTo('/home',
replace: true,
transition: TransitionType.fadeIn,
transitionDuration: Duration(milliseconds: 750));
}
} else {
navigateTo('/login', replace: true, clearStack: true, transition: TransitionType.fadeIn, transitionDuration: Duration(milliseconds: 750));
navigateTo('/login',
replace: true,
clearStack: true,
transition: TransitionType.fadeIn,
transitionDuration: Duration(milliseconds: 750));
}
}
}
@@ -338,9 +353,10 @@ class TbContext {
}
bool _userForceFullscreen() {
return tbClient.getAuthUser()!.isPublic ||
(userDetails != null && userDetails!.additionalInfo != null &&
userDetails!.additionalInfo!['defaultDashboardFullscreen'] == true);
return tbClient.getAuthUser()!.isPublic! ||
(userDetails != null &&
userDetails!.additionalInfo != null &&
userDetails!.additionalInfo!['defaultDashboardFullscreen'] == true);
}
bool isPhysicalDevice() {
@@ -356,11 +372,13 @@ class TbContext {
String userAgent() {
String userAgent = 'Mozilla/5.0';
if (UniversalPlatform.isAndroid) {
userAgent += ' (Linux; Android ${_androidInfo!.version.release}; ${_androidInfo!.model})';
userAgent +=
' (Linux; Android ${_androidInfo!.version.release}; ${_androidInfo!.model})';
} else if (UniversalPlatform.isIOS) {
userAgent += ' (${_iosInfo!.model})';
}
userAgent += ' AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36';
userAgent +=
' AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36';
return userAgent;
}
@@ -374,11 +392,17 @@ class TbContext {
return false;
}
Future<dynamic> navigateTo(String path, {bool replace = false, bool clearStack = false, closeDashboard = true,
TransitionType? transition, Duration? transitionDuration, bool restoreDashboard = true}) async {
Future<dynamic> navigateTo(String path,
{bool replace = false,
bool clearStack = false,
closeDashboard = true,
TransitionType? transition,
Duration? transitionDuration,
bool restoreDashboard = true}) async {
if (currentState != null) {
hideNotification();
bool isOpenedDashboard = _mainDashboardHolder?.isDashboardOpen() == true && closeDashboard;
bool isOpenedDashboard =
_mainDashboardHolder?.isDashboardOpen() == true && closeDashboard;
if (isOpenedDashboard) {
_mainDashboardHolder?.openMain();
}
@@ -403,12 +427,24 @@ class TbContext {
}
}
_closeMainFirst = isOpenedDashboard;
return await router.navigateTo(currentState!.context, path, transition: transition, transitionDuration: transitionDuration, replace: replace, clearStack: clearStack);
return await router.navigateTo(currentState!.context, path,
transition: transition,
transitionDuration: transitionDuration,
replace: replace,
clearStack: clearStack);
}
}
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);
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);
}
Future<T?> showFullScreenDialog<T>(Widget dialog) {
@@ -416,8 +452,7 @@ class TbContext {
builder: (BuildContext context) {
return dialog;
},
fullscreenDialog: true
));
fullscreenDialog: true));
}
void pop<T>([T? result, BuildContext? context]) async {
@@ -428,7 +463,7 @@ class TbContext {
}
}
Future<bool> maybePop<T extends Object?>([ T? result ]) async {
Future<bool> maybePop<T extends Object?>([T? result]) async {
if (currentState != null) {
return Navigator.of(currentState!.context).maybePop(result);
} else {
@@ -441,7 +476,7 @@ class TbContext {
return true;
}
if (_mainDashboardHolder != null) {
return await _mainDashboardHolder!.dashboardGoBack();
return await _mainDashboardHolder!.dashboardGoBack();
}
return true;
}
@@ -456,18 +491,22 @@ class TbContext {
return false;
}
Future<bool?> confirm({required String title, required String message, String cancel = 'Cancel', String ok = 'Ok'}) {
return showDialog<bool>(context: currentState!.context,
Future<bool?> confirm(
{required String title,
required String message,
String cancel = 'Cancel',
String ok = 'Ok'}) {
return showDialog<bool>(
context: currentState!.context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(onPressed: () => pop(false, context),
child: Text(cancel)),
TextButton(onPressed: () => pop(true, context),
child: Text(ok))
],
));
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => pop(false, context), child: Text(cancel)),
TextButton(onPressed: () => pop(true, context), child: Text(ok))
],
));
}
}
@@ -480,11 +519,13 @@ mixin HasTbContext {
void setupCurrentState(TbContextState currentState) {
if (_tbContext.currentState != null) {
ModalRoute.of(_tbContext.currentState!.context)?.removeScopedWillPopCallback(_tbContext.willPop);
ModalRoute.of(_tbContext.currentState!.context)
?.removeScopedWillPopCallback(_tbContext.willPop);
}
_tbContext.currentState = currentState;
if (_tbContext.currentState != null) {
ModalRoute.of(_tbContext.currentState!.context)?.addScopedWillPopCallback(_tbContext.willPop);
ModalRoute.of(_tbContext.currentState!.context)
?.addScopedWillPopCallback(_tbContext.willPop);
}
if (_tbContext._closeMainFirst) {
_tbContext._closeMainFirst = false;
@@ -514,33 +555,55 @@ mixin HasTbContext {
await _tbContext.init();
}
Future<dynamic> navigateTo(String path, {bool replace = false, bool clearStack = false}) => _tbContext.navigateTo(path, replace: replace, clearStack: clearStack);
Future<dynamic> navigateTo(String path,
{bool replace = false, bool clearStack = false}) =>
_tbContext.navigateTo(path, replace: replace, clearStack: clearStack);
void pop<T>([T? result, BuildContext? context]) => _tbContext.pop<T>(result, context);
void pop<T>([T? result, BuildContext? context]) =>
_tbContext.pop<T>(result, context);
Future<bool> maybePop<T extends Object?>([ T? result ]) => _tbContext.maybePop<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<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);
Future<bool?> confirm(
{required String title,
required String message,
String cancel = 'Cancel',
String ok = 'Ok'}) =>
_tbContext.confirm(
title: title, message: message, cancel: cancel, ok: ok);
void hideNotification() => _tbContext.hideNotification();
void showErrorNotification(String message, {Duration? duration}) => _tbContext.showErrorNotification(message, duration: duration);
void showErrorNotification(String message, {Duration? duration}) =>
_tbContext.showErrorNotification(message, duration: duration);
void showInfoNotification(String message, {Duration? duration}) => _tbContext.showInfoNotification(message, duration: duration);
void showInfoNotification(String message, {Duration? duration}) =>
_tbContext.showInfoNotification(message, duration: duration);
void showWarnNotification(String message, {Duration? duration}) => _tbContext.showWarnNotification(message, duration: duration);
void showWarnNotification(String message, {Duration? duration}) =>
_tbContext.showWarnNotification(message, duration: duration);
void showSuccessNotification(String message, {Duration? duration}) => _tbContext.showSuccessNotification(message, duration: duration);
void showSuccessNotification(String message, {Duration? duration}) =>
_tbContext.showSuccessNotification(message, duration: duration);
void subscribeRouteObserver(TbPageState pageState) {
_tbContext.routeObserver.subscribe(pageState, ModalRoute.of(pageState.context) as PageRoute);
_tbContext.routeObserver
.subscribe(pageState, ModalRoute.of(pageState.context) as PageRoute);
}
void unsubscribeRouteObserver(TbPageState pageState) {
_tbContext.routeObserver.unsubscribe(pageState);
}
}

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
abstract class RefreshableWidget extends Widget {
refresh();
}
abstract class TbContextStatelessWidget extends StatelessWidget with HasTbContext {
abstract class TbContextStatelessWidget extends StatelessWidget
with HasTbContext {
TbContextStatelessWidget(TbContext tbContext, {Key? key}) : super(key: key) {
setTbContext(tbContext);
}
@@ -18,8 +18,8 @@ abstract class TbContextWidget extends StatefulWidget with HasTbContext {
}
}
abstract class TbContextState<T extends TbContextWidget> extends State<T> with HasTbContext {
abstract class TbContextState<T extends TbContextWidget> extends State<T>
with HasTbContext {
final bool handleLoading;
bool closeMainFirst = false;
@@ -35,25 +35,23 @@ abstract class TbContextState<T extends TbContextWidget> extends State<T> with H
void dispose() {
super.dispose();
}
}
mixin TbMainState {
bool canNavigate(String path);
navigateToPath(String path);
bool isHomePage();
}
abstract class TbPageWidget extends TbContextWidget {
TbPageWidget(TbContext tbContext, {Key? key}) : super(tbContext, key: key);
}
abstract class TbPageState<W extends TbPageWidget> extends TbContextState<W> with RouteAware {
TbPageState({bool handleUserLoaded = false}): super(handleLoading: true);
abstract class TbPageState<W extends TbPageWidget> extends TbContextState<W>
with RouteAware {
TbPageState({bool handleUserLoaded = false}) : super(handleLoading: true);
@override
void didChangeDependencies() {
@@ -77,25 +75,20 @@ abstract class TbPageState<W extends TbPageWidget> extends TbContextState<W> wit
hideNotification();
setupCurrentState(this);
}
}
class TextContextWidget extends TbContextWidget {
final String text;
TextContextWidget(TbContext tbContext, this.text) : super(tbContext);
@override
_TextContextWidgetState createState() => _TextContextWidgetState();
}
class _TextContextWidgetState extends TbContextState<TextContextWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text(widget.text)));
}
}

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:intl/intl.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
@@ -31,7 +30,8 @@ const Map<EntityType, String> entityTypeTranslations = {
};
typedef EntityTapFunction<T> = Function(T entity);
typedef EntityCardWidgetBuilder<T> = Widget Function(BuildContext context, T entity);
typedef EntityCardWidgetBuilder<T> = Widget Function(
BuildContext context, T entity);
class EntityCardSettings {
bool dropShadow;
@@ -39,7 +39,6 @@ class EntityCardSettings {
}
mixin EntitiesBase<T, P> on HasTbContext {
final entityDateFormat = DateFormat('yyyy-MM-dd');
String get title;
@@ -73,11 +72,9 @@ mixin EntitiesBase<T, P> on HasTbContext {
EntityCardSettings entityGridCardSettings(T entity) => EntityCardSettings();
void onEntityTap(T entity);
}
mixin ContactBasedBase<T extends ContactBased, P> on EntitiesBase<T,P> {
mixin ContactBasedBase<T extends ContactBased, P> on EntitiesBase<T, P> {
@override
Widget buildEntityListCard(BuildContext context, T contact) {
var address = Utils.contactToShortAddress(contact);
@@ -89,8 +86,7 @@ mixin ContactBasedBase<T extends ContactBased, P> on EntitiesBase<T,P> {
children: [
Flexible(
fit: FlexFit.tight,
child:
Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
@@ -105,39 +101,36 @@ mixin ContactBasedBase<T extends ContactBased, P> on EntitiesBase<T,P> {
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14
))
),
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(contact.createdTime!)),
height: 20 / 14))),
Text(
entityDateFormat.format(
DateTime.fromMillisecondsSinceEpoch(
contact.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
),
height: 16 / 12))
]),
SizedBox(height: 4),
if (contact.email != null) Text(contact.email!,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
)),
if (contact.email == null)
SizedBox(height: 16),
if (contact.email != null)
Text(contact.email!,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12)),
if (contact.email == null) SizedBox(height: 16),
if (address != null) SizedBox(height: 4),
if (address != null) Text(address,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
)),
if (address != null)
Text(address,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12)),
],
)
),
)),
SizedBox(width: 16),
Icon(Icons.chevron_right, color: Color(0xFFACACAC)),
SizedBox(width: 8)
@@ -148,24 +141,21 @@ mixin ContactBasedBase<T extends ContactBased, P> on EntitiesBase<T,P> {
}
abstract class PageKeyController<P> extends ValueNotifier<PageKeyValue<P>> {
PageKeyController(P initialPageKey) : super(PageKeyValue(initialPageKey));
P nextPageKey(P pageKey);
}
class PageKeyValue<P> {
final P pageKey;
PageKeyValue(this.pageKey);
}
class PageLinkController extends PageKeyController<PageLink> {
PageLinkController({int pageSize = 20, String? searchText}) : super(PageLink(pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC)));
PageLinkController({int pageSize = 20, String? searchText})
: super(PageLink(
pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC)));
@override
PageLink nextPageKey(PageLink pageKey) => pageKey.nextPageLink();
@@ -175,12 +165,12 @@ class PageLinkController extends PageKeyController<PageLink> {
value.pageKey.textSearch = searchText;
notifyListeners();
}
}
class TimePageLinkController extends PageKeyController<TimePageLink> {
TimePageLinkController({int pageSize = 20, String? searchText}) : super(TimePageLink(pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC)));
TimePageLinkController({int pageSize = 20, String? searchText})
: super(TimePageLink(
pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC)));
@override
TimePageLink nextPageKey(TimePageLink pageKey) => pageKey.nextPageLink();
@@ -190,29 +180,27 @@ class TimePageLinkController extends PageKeyController<TimePageLink> {
value.pageKey.textSearch = searchText;
notifyListeners();
}
}
abstract class BaseEntitiesWidget<T, P> extends TbContextWidget with EntitiesBase<T, P> {
abstract class BaseEntitiesWidget<T, P> extends TbContextWidget
with EntitiesBase<T, P> {
final bool searchMode;
final PageKeyController<P> pageKeyController;
BaseEntitiesWidget(TbContext tbContext, this.pageKeyController, {this.searchMode = false}):
super(tbContext);
BaseEntitiesWidget(TbContext tbContext, this.pageKeyController,
{this.searchMode = false})
: super(tbContext);
@override
Widget? buildHeading(BuildContext context) => searchMode ? Text('Search results', style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 16,
height: 24 / 16
)) : null;
Widget? buildHeading(BuildContext context) => searchMode
? Text('Search results',
style: TextStyle(
color: Color(0xFFAFAFAF), fontSize: 16, height: 24 / 16))
: null;
}
abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget<T, P>> {
abstract class BaseEntitiesState<T, P>
extends TbContextState<BaseEntitiesWidget<T, P>> {
late final PagingController<P, T> pagingController;
Completer<void>? _refreshCompleter;
bool _dataLoading = false;
@@ -224,7 +212,8 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
@override
void initState() {
super.initState();
pagingController = PagingController(firstPageKey: widget.pageKeyController.value.pageKey);
pagingController =
PagingController(firstPageKey: widget.pageKeyController.value.pageKey);
widget.pageKeyController.addListener(_didChangePageKeyValue);
pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
@@ -315,18 +304,14 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.wait([
widget.onRefresh(),
_refresh()
]),
child: pagedViewBuilder(context)
);
onRefresh: () => Future.wait([widget.onRefresh(), _refresh()]),
child: pagedViewBuilder(context));
}
Widget pagedViewBuilder(BuildContext context);
Widget firstPageProgressIndicatorBuilder(BuildContext context) {
return Stack( children: [
return Stack(children: [
Positioned(
top: 20,
left: 0,
@@ -338,7 +323,7 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
)
]);
}
Widget newPageProgressIndicatorBuilder(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
@@ -348,7 +333,7 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
child: Center(child: RefreshProgressIndicator()),
);
}
Widget noItemsFoundIndicatorBuilder(BuildContext context) {
return FirstPageExceptionIndicator(
title: widget.noItemsFoundText,
@@ -356,7 +341,6 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
onTryAgain: widget.searchMode ? null : () => pagingController.refresh(),
);
}
}
class FirstPageExceptionIndicator extends StatelessWidget {

View File

@@ -6,14 +6,11 @@ import 'entities_base.dart';
import 'entity_grid_card.dart';
mixin EntitiesGridStateBase on StatefulWidget {
@override
_EntitiesGridState createState() => _EntitiesGridState();
}
class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
_EntitiesGridState() : super();
@override
@@ -24,9 +21,7 @@ class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
if (heading != null) {
slivers.add(SliverPadding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
sliver: SliverToBoxAdapter(
child: heading
)));
sliver: SliverToBoxAdapter(child: heading)));
}
slivers.add(SliverPadding(
padding: EdgeInsets.all(16),
@@ -44,19 +39,17 @@ class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
),
builderDelegate: PagedChildBuilderDelegate<T>(
itemBuilder: (context, item, index) => EntityGridCard<T>(
item,
key: widget.getKey(item),
entityCardWidgetBuilder: widget.buildEntityGridCard,
onEntityTap: widget.onEntityTap,
settings: widget.entityGridCardSettings(item),
),
firstPageProgressIndicatorBuilder: firstPageProgressIndicatorBuilder,
newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder,
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder
)
)));
return CustomScrollView(
slivers: slivers
);
item,
key: widget.getKey(item),
entityCardWidgetBuilder: widget.buildEntityGridCard,
onEntityTap: widget.onEntityTap,
settings: widget.entityGridCardSettings(item),
),
firstPageProgressIndicatorBuilder:
firstPageProgressIndicatorBuilder,
newPageProgressIndicatorBuilder:
newPageProgressIndicatorBuilder,
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder))));
return CustomScrollView(slivers: slivers);
}
}

View File

@@ -6,14 +6,11 @@ import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'entity_list_card.dart';
mixin EntitiesListStateBase on StatefulWidget {
@override
_EntitiesListState createState() => _EntitiesListState();
}
class _EntitiesListState<T,P> extends BaseEntitiesState<T, P> {
class _EntitiesListState<T, P> extends BaseEntitiesState<T, P> {
_EntitiesListState() : super();
@override
@@ -23,9 +20,7 @@ class _EntitiesListState<T,P> extends BaseEntitiesState<T, P> {
if (heading != null) {
slivers.add(SliverPadding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
sliver: SliverToBoxAdapter(
child: heading
)));
sliver: SliverToBoxAdapter(child: heading)));
}
slivers.add(SliverPadding(
padding: EdgeInsets.all(16),
@@ -34,19 +29,16 @@ class _EntitiesListState<T,P> extends BaseEntitiesState<T, P> {
separatorBuilder: (context, index) => SizedBox(height: 8),
builderDelegate: PagedChildBuilderDelegate<T>(
itemBuilder: (context, item, index) => EntityListCard<T>(
item,
key: widget.getKey(item),
entityCardWidgetBuilder: widget.buildEntityListCard,
onEntityTap: widget.onEntityTap,
settings: widget.entityListCardSettings(item),
),
firstPageProgressIndicatorBuilder: firstPageProgressIndicatorBuilder,
newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder,
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder
)
)));
return CustomScrollView(
slivers: slivers
);
item,
key: widget.getKey(item),
entityCardWidgetBuilder: widget.buildEntityListCard,
onEntityTap: widget.onEntityTap,
),
firstPageProgressIndicatorBuilder:
firstPageProgressIndicatorBuilder,
newPageProgressIndicatorBuilder:
newPageProgressIndicatorBuilder,
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder))));
return CustomScrollView(slivers: slivers);
}
}

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:fading_edge_scrollview/fading_edge_scrollview.dart';
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/core/entity/entities_base.dart';
@@ -11,14 +10,15 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'entity_list_card.dart';
class EntitiesListWidgetController {
final List<_EntitiesListWidgetState> states = [];
void _registerEntitiesWidgetState(_EntitiesListWidgetState entitiesListWidgetState) {
void _registerEntitiesWidgetState(
_EntitiesListWidgetState entitiesListWidgetState) {
states.add(entitiesListWidgetState);
}
void _unregisterEntitiesWidgetState(_EntitiesListWidgetState entitiesListWidgetState) {
void _unregisterEntitiesWidgetState(
_EntitiesListWidgetState entitiesListWidgetState) {
states.remove(entitiesListWidgetState);
}
@@ -29,45 +29,48 @@ class EntitiesListWidgetController {
void dispose() {
states.clear();
}
}
abstract class EntitiesListPageLinkWidget<T> extends EntitiesListWidget<T, PageLink> {
EntitiesListPageLinkWidget(TbContext tbContext, {EntitiesListWidgetController? controller}) : super(tbContext, controller: controller);
abstract class EntitiesListPageLinkWidget<T>
extends EntitiesListWidget<T, PageLink> {
EntitiesListPageLinkWidget(TbContext tbContext,
{EntitiesListWidgetController? controller})
: super(tbContext, controller: controller);
@override
PageKeyController<PageLink> createPageKeyController() => PageLinkController(pageSize: 5);
PageKeyController<PageLink> createPageKeyController() =>
PageLinkController(pageSize: 5);
}
abstract class EntitiesListWidget<T, P> extends TbContextWidget with EntitiesBase<T,P> {
abstract class EntitiesListWidget<T, P> extends TbContextWidget
with EntitiesBase<T, P> {
final EntitiesListWidgetController? _controller;
EntitiesListWidget(TbContext tbContext, {EntitiesListWidgetController? controller}):
_controller = controller,
super(tbContext);
EntitiesListWidget(TbContext tbContext,
{EntitiesListWidgetController? controller})
: _controller = controller,
super(tbContext);
@override
_EntitiesListWidgetState createState() => _EntitiesListWidgetState(_controller);
_EntitiesListWidgetState createState() =>
_EntitiesListWidgetState(_controller);
PageKeyController<P> createPageKeyController();
void onViewAll();
}
class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,P>> {
class _EntitiesListWidgetState<T, P>
extends TbContextState<EntitiesListWidget<T, P>> {
final EntitiesListWidgetController? _controller;
late final PageKeyController<P> _pageKeyController;
final StreamController<PageData<T>?> _entitiesStreamController = StreamController.broadcast();
final StreamController<PageData<T>?> _entitiesStreamController =
StreamController.broadcast();
_EntitiesListWidgetState(EntitiesListWidgetController? controller):
_controller = controller;
_EntitiesListWidgetState(EntitiesListWidgetController? controller)
: _controller = controller;
@override
void initState() {
@@ -76,7 +79,7 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
if (_controller != null) {
_controller!._registerEntitiesWidgetState(this);
}
_refresh();
_refresh();
}
@override
@@ -121,17 +124,15 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
builder: (context, snapshot) {
var title = widget.title;
if (snapshot.hasData) {
var data = snapshot.data;
title += ' (${data!.totalElements})';
var data = snapshot.data;
title += ' (${data!.totalElements})';
}
return Text(title,
style: TextStyle(
color: Color(0xFF282828),
fontSize: 16,
fontWeight: FontWeight.normal,
height: 1.5
)
);
style: TextStyle(
color: Color(0xFF282828),
fontSize: 16,
fontWeight: FontWeight.normal,
height: 1.5));
},
),
Spacer(),
@@ -141,73 +142,62 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero),
child: Text('View all')
)
child: Text('View all'))
],
),
),
Container(
height: 64,
child: StreamBuilder<PageData<T>?>(
stream: _entitiesStreamController.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
var data = snapshot.data!;
if (data.data.isEmpty) {
return _buildNoEntitiesFound(); //return Text('Loaded');
stream: _entitiesStreamController.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
var data = snapshot.data!;
if (data.data.isEmpty) {
return _buildNoEntitiesFound(); //return Text('Loaded');
} else {
return _buildEntitiesView(context, data.data);
}
} else {
return _buildEntitiesView(context, data.data);
return Center(
child: RefreshProgressIndicator(
valueColor: AlwaysStoppedAnimation(
Theme.of(tbContext.currentState!.context)
.colorScheme
.primary),
));
}
} else {
return Center(
child: RefreshProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
)
);
}
}
),
}),
)
],
)
)
),
))),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(25),
blurRadius: 10.0,
offset: Offset(0, 4)
),
offset: Offset(0, 4)),
BoxShadow(
color: Colors.black.withAlpha(18),
blurRadius: 30.0,
offset: Offset(0, 10)
),
offset: Offset(0, 10)),
],
)
);
));
}
Widget _buildNoEntitiesFound() {
return Container(
decoration: BoxDecoration(
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFDEDEDE),
style: BorderStyle.solid,
width: 1
),
borderRadius: BorderRadius.circular(4)
),
child: Center(
child:
Text(widget.noItemsFoundText,
color: Color(0xFFDEDEDE), style: BorderStyle.solid, width: 1),
borderRadius: BorderRadius.circular(4)),
child: Center(
child: Text(widget.noItemsFoundText,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 14,
)
),
),
)),
),
);
}
@@ -219,13 +209,11 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
child: ListView(
scrollDirection: Axis.horizontal,
controller: ScrollController(),
children: entities.map((entity) => EntityListCard<T>(
entity,
entityCardWidgetBuilder: widget.buildEntityListWidgetCard,
onEntityTap: widget.onEntityTap,
settings: widget.entityListCardSettings(entity),
listWidgetCard: true
)).toList()
));
children: entities
.map((entity) => EntityListCard<T>(entity,
entityCardWidgetBuilder: widget.buildEntityListWidgetCard,
onEntityTap: widget.onEntityTap,
listWidgetCard: true))
.toList()));
}
}

View File

@@ -1,6 +1,4 @@
import 'package:flutter/cupertino.dart';
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/widgets/tb_app_bar.dart';
@@ -8,18 +6,11 @@ import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget {
final labelTextStyle =
TextStyle(color: Color(0xFF757575), fontSize: 14, height: 20 / 14);
final labelTextStyle = TextStyle(
color: Color(0xFF757575),
fontSize: 14,
height: 20 / 14
);
final valueTextStyle = TextStyle(
color: Color(0xFF282828),
fontSize: 14,
height: 20 / 14
);
final valueTextStyle =
TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14);
final String _defaultTitle;
final String _entityId;
@@ -29,19 +20,19 @@ abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget {
final double? _appBarElevation;
EntityDetailsPage(TbContext tbContext,
{required String defaultTitle,
required String entityId,
String? subTitle,
bool showLoadingIndicator = true,
bool hideAppBar = false,
double? appBarElevation}):
this._defaultTitle = defaultTitle,
this._entityId = entityId,
this._subTitle = subTitle,
this._showLoadingIndicator = showLoadingIndicator,
this._hideAppBar = hideAppBar,
this._appBarElevation = appBarElevation,
super(tbContext);
{required String defaultTitle,
required String entityId,
String? subTitle,
bool showLoadingIndicator = true,
bool hideAppBar = false,
double? appBarElevation})
: this._defaultTitle = defaultTitle,
this._entityId = entityId,
this._subTitle = subTitle,
this._showLoadingIndicator = showLoadingIndicator,
this._hideAppBar = hideAppBar,
this._appBarElevation = appBarElevation,
super(tbContext);
@override
_EntityDetailsPageState createState() => _EntityDetailsPageState();
@@ -53,11 +44,10 @@ abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget {
}
Widget buildEntityDetails(BuildContext context, T entity);
}
class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDetailsPage<T>> {
class _EntityDetailsPageState<T extends BaseData>
extends TbPageState<EntityDetailsPage<T>> {
late Future<T?> entityFuture;
late ValueNotifier<String> titleValue;
@@ -70,7 +60,7 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
titleValue = ValueNotifier(widget._defaultTitle);
entityFuture.then((value) {
if (value is HasName) {
titleValue.value = (value as HasName).getName();
titleValue.value = (value as HasName).getName();
}
});
} else {
@@ -82,36 +72,43 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: widget._hideAppBar ? null : TbAppBar(
tbContext,
showLoadingIndicator: widget._showLoadingIndicator,
elevation: widget._appBarElevation,
title: ValueListenableBuilder<String>(
valueListenable: titleValue,
builder: (context, title, _widget) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title,
style: widget._subTitle != null ? Theme.of(context).primaryTextTheme.headline6!.copyWith(
fontSize: 16
) : null
)
),
if (widget._subTitle != null) Text(widget._subTitle!, style: TextStyle(
color: Theme.of(context).primaryTextTheme.headline6!.color!.withAlpha((0.38 * 255).ceil()),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
);
},
),
),
appBar: widget._hideAppBar
? null
: TbAppBar(
tbContext,
showLoadingIndicator: widget._showLoadingIndicator,
elevation: widget._appBarElevation,
title: ValueListenableBuilder<String>(
valueListenable: titleValue,
builder: (context, title, _widget) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title,
style: widget._subTitle != null
? Theme.of(context)
.primaryTextTheme
.headline6!
.copyWith(fontSize: 16)
: null)),
if (widget._subTitle != null)
Text(widget._subTitle!,
style: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headline6!
.color!
.withAlpha((0.38 * 255).ceil()),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12))
]);
},
),
),
body: FutureBuilder<T?>(
future: entityFuture,
builder: (context, snapshot) {
@@ -123,7 +120,8 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
return Center(child: Text('Requested entity does not exists.'));
}
} else {
return Center(child: TbProgressIndicator(
return Center(
child: TbProgressIndicator(
size: 50.0,
));
}
@@ -131,21 +129,24 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
),
);
}
}
abstract class ContactBasedDetailsPage<T extends ContactBased> extends EntityDetailsPage<T> {
abstract class ContactBasedDetailsPage<T extends ContactBased>
extends EntityDetailsPage<T> {
ContactBasedDetailsPage(TbContext tbContext,
{ required String defaultTitle,
required String entityId,
String? subTitle,
bool showLoadingIndicator = true,
bool hideAppBar = false,
double? appBarElevation}):
super(tbContext, defaultTitle: defaultTitle, entityId: entityId,
subTitle: subTitle, showLoadingIndicator: showLoadingIndicator,
hideAppBar: hideAppBar, appBarElevation: appBarElevation);
{required String defaultTitle,
required String entityId,
String? subTitle,
bool showLoadingIndicator = true,
bool hideAppBar = false,
double? appBarElevation})
: super(tbContext,
defaultTitle: defaultTitle,
entityId: entityId,
subTitle: subTitle,
showLoadingIndicator: showLoadingIndicator,
hideAppBar: hideAppBar,
appBarElevation: appBarElevation);
@override
Widget buildEntityDetails(BuildContext context, T contact) {
@@ -201,9 +202,6 @@ abstract class ContactBasedDetailsPage<T extends ContactBased> extends EntityDet
SizedBox(height: 16),
Text('Email', style: labelTextStyle),
Text(contact.email ?? '', style: valueTextStyle),
]
)
);
]));
}
}

View File

@@ -1,7 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
import 'entities_base.dart';
@@ -11,10 +8,12 @@ class EntityGridCard<T> extends StatelessWidget {
final EntityCardWidgetBuilder<T> _entityCardWidgetBuilder;
final EntityCardSettings _settings;
EntityGridCard(T entity, {Key? key, EntityTapFunction<T>? onEntityTap,
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
required EntityCardSettings settings}):
this._entity = entity,
EntityGridCard(T entity,
{Key? key,
EntityTapFunction<T>? onEntityTap,
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
required EntityCardSettings settings})
: this._entity = entity,
this._onEntityTap = onEntityTap,
this._entityCardWidgetBuilder = entityCardWidgetBuilder,
this._settings = settings,
@@ -22,35 +21,31 @@ class EntityGridCard<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
return
GestureDetector(
behavior: HitTestBehavior.opaque,
child:
Container(
child: Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
child: _entityCardWidgetBuilder(context, _entity)
),
decoration: _settings.dropShadow ? BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha((255 * 0.05).ceil()),
blurRadius: 6.0,
offset: Offset(0, 4)
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
child: Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
child: _entityCardWidgetBuilder(context, _entity)),
decoration: _settings.dropShadow
? BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha((255 * 0.05).ceil()),
blurRadius: 6.0,
offset: Offset(0, 4))
],
)
],
) : null,
),
onTap: () {
if (_onEntityTap != null) {
_onEntityTap!(_entity);
}
: null,
),
onTap: () {
if (_onEntityTap != null) {
_onEntityTap!(_entity);
}
);
});
}
}

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'entities_base.dart';
@@ -9,58 +7,51 @@ class EntityListCard<T> extends StatelessWidget {
final T _entity;
final EntityTapFunction<T>? _onEntityTap;
final EntityCardWidgetBuilder<T> _entityCardWidgetBuilder;
final EntityCardSettings _settings;
EntityListCard(T entity, {Key? key, EntityTapFunction<T>? onEntityTap,
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
required EntityCardSettings settings,
bool listWidgetCard = false}):
this._entity = entity,
EntityListCard(T entity,
{Key? key,
EntityTapFunction<T>? onEntityTap,
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
bool listWidgetCard = false})
: this._entity = entity,
this._onEntityTap = onEntityTap,
this._entityCardWidgetBuilder = entityCardWidgetBuilder,
this._settings = settings,
this._listWidgetCard = listWidgetCard,
super(key: key);
@override
Widget build(BuildContext context) {
return
GestureDetector(
behavior: HitTestBehavior.opaque,
child:
Container(
margin: _listWidgetCard ? EdgeInsets.only(right: 8) : EdgeInsets.zero,
child: Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
margin: _listWidgetCard ? EdgeInsets.only(right: 8) : EdgeInsets.zero,
child: Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
child: _entityCardWidgetBuilder(context, _entity)),
decoration: _listWidgetCard
? BoxDecoration(
border: Border.all(
color: Color(0xFFDEDEDE),
style: BorderStyle.solid,
width: 1),
borderRadius: BorderRadius.circular(4))
: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha((255 * 0.05).ceil()),
blurRadius: 6.0,
offset: Offset(0, 4)),
],
),
elevation: 0,
child: _entityCardWidgetBuilder(context, _entity)
),
decoration: _listWidgetCard ? BoxDecoration(
border: Border.all(
color: Color(0xFFDEDEDE),
style: BorderStyle.solid,
width: 1
),
borderRadius: BorderRadius.circular(4)
) : BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha((255 * 0.05).ceil()),
blurRadius: 6.0,
offset: Offset(0, 4)
),
],
),
),
onTap: () {
if (_onEntityTap != null) {
_onEntityTap!(_entity);
}
),
onTap: () {
if (_onEntityTap != null) {
_onEntityTap!(_entity);
}
);
});
}
}

View File

@@ -1,20 +1,17 @@
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/widgets/tb_progress_indicator.dart';
class ThingsboardInitApp extends TbPageWidget {
ThingsboardInitApp(TbContext tbContext, {Key? key}) : super(tbContext, key: key);
ThingsboardInitApp(TbContext tbContext, {Key? key})
: super(tbContext, key: key);
@override
_ThingsboardInitAppState createState() => _ThingsboardInitAppState();
}
class _ThingsboardInitAppState extends TbPageState<ThingsboardInitApp> {
@override
void initState() {
super.initState();
@@ -26,10 +23,7 @@ class _ThingsboardInitAppState extends TbPageState<ThingsboardInitApp> {
return Container(
alignment: Alignment.center,
color: Colors.white,
child: TbProgressIndicator(
size: 50.0
),
child: TbProgressIndicator(size: 50.0),
);
}
}

View File

@@ -7,8 +7,8 @@ import 'package:thingsboard_app/core/context/tb_context.dart';
import 'init_app.dart';
class InitRoutes extends TbRoutes {
late var initHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var initHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return ThingsboardInitApp(tbContext);
});
@@ -18,5 +18,4 @@ class InitRoutes extends TbRoutes {
void doRegisterRoutes(router) {
router.define("/", handler: initHandler);
}
}