Add flutter 3+ support. Update dependencies. Fix code style and format issues.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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!);
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
)
|
||||
);
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user