Merge branch 'master' of github.com:thingsboard/flutter_thingsboard_app

This commit is contained in:
Igor Kulikov
2022-08-17 17:20:01 +03:00
102 changed files with 5414 additions and 3210 deletions

View File

@@ -1,14 +1,16 @@
# Flutter ThingsBoard Mobile Application
## [ThingsBoard Mobile Application](https://thingsboard.io/products/mobile/) is an open-source project based on [Flutter](https://flutter.dev/)
Powered by [ThingsBoard](https://thingsboard.io) IoT Platform
## Getting Started
Build your own IoT mobile application **with minimum coding efforts**
This project is a starting point for a ThingsBoard Mobile application.
## Resources
A few resources to get you started if this is your first Flutter project:
- [Getting started](https://thingsboard.io/docs/mobile/getting-started/) - learn how to set up and run your first IoT mobile app
- [Customize your app](https://thingsboard.io/docs/mobile/customization/) - learn how to customize the app
- [Publish your app](https://thingsboard.io/docs/mobile/release/) - learn how to publish app to Google Play or App Store
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
## Live demo app
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
To be familiar with common app features try out our ThingsBoard Live mobile application available on Google Play and App Store
- [Get it on Google Play](https://play.google.com/store/apps/details?id=org.thingsboard.demo.app&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
- [Download on the App Store](https://apps.apple.com/us/app/thingsboard-live/id1594355695?itsct=apps_box_badge&itscg=30200)

View File

@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 31
compileSdkVersion 33
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -36,7 +36,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "org.thingsboard.app"
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View File

@@ -17,6 +17,7 @@
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -45,7 +46,9 @@
</intent-filter>
</activity>
<activity android:name=".TbWebCallbackActivity" >
<activity
android:name=".TbWebCallbackActivity"
android:exported="true" >
<intent-filter android:label="tb_web_auth">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -62,5 +65,15 @@
/>
<meta-data android:name="io.flutter.network-policy"
android:resource="@xml/network_security_config"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.flutter_inappwebview.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external_files"
path="." />
</paths>

View File

@@ -1,6 +1,5 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/auth/auth_routes.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/init/init_routes.dart';
@@ -20,15 +19,12 @@ class ThingsboardAppRouter {
late final _tbContext = TbContext(router);
ThingsboardAppRouter() {
router.notFoundHandler = Handler(handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
router.notFoundHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
var settings = context!.settings;
return Scaffold(
appBar: AppBar(
title: Text('Not Found')
),
body: Center(
child: Text('Route not defined: ${settings!.name}')
),
appBar: AppBar(title: Text('Not Found')),
body: Center(child: Text('Route not defined: ${settings!.name}')),
);
});
InitRoutes(_tbContext).registerRoutes();
@@ -49,7 +45,6 @@ class ThingsboardAppRouter {
}
abstract class TbRoutes {
final TbContext _tbContext;
TbRoutes(this._tbContext);
@@ -61,5 +56,4 @@ abstract class TbRoutes {
void doRegisterRoutes(FluroRouter router);
TbContext get tbContext => _tbContext;
}

View File

@@ -24,7 +24,8 @@ const tbMatIndigo = MaterialColor(
700: Color(0xFF303F9F),
800: Color(0xFF283593),
900: Color(0xFF1A237E),
},);
},
);
const tbDarkMatIndigo = MaterialColor(
_tbPrimaryColorValue,
@@ -39,44 +40,43 @@ const tbDarkMatIndigo = MaterialColor(
700: Color(0xFF303F9F),
800: _tbPrimaryColor,
900: Color(0xFF1A237E),
},);
},
);
final ThemeData theme = ThemeData(primarySwatch: tbMatIndigo);
ThemeData tbTheme = ThemeData(
primarySwatch: tbMatIndigo,
accentColor: Colors.deepOrange,
colorScheme: theme.colorScheme.copyWith(secondary: Colors.deepOrange),
scaffoldBackgroundColor: Color(0xFFFAFAFA),
textTheme: tbTypography.black,
primaryTextTheme: tbTypography.black,
typography: tbTypography,
appBarTheme: AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: _tbTextColor,
/* titleTextStyle: TextStyle(
backgroundColor: Colors.white,
foregroundColor: _tbTextColor,
/* titleTextStyle: TextStyle(
color: _tbTextColor
),
toolbarTextStyle: TextStyle(
color: _tbTextColor
), */
iconTheme: IconThemeData(
color: _tbTextColor
)
),
iconTheme: IconThemeData(color: _tbTextColor)),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Colors.white,
selectedItemColor: _tbPrimaryColor,
unselectedItemColor: _tbPrimaryColor.withAlpha((255 * 0.38).ceil()),
showSelectedLabels: true,
showUnselectedLabels: true
),
backgroundColor: Colors.white,
selectedItemColor: _tbPrimaryColor,
unselectedItemColor: _tbPrimaryColor.withAlpha((255 * 0.38).ceil()),
showSelectedLabels: true,
showUnselectedLabels: true),
pageTransitionsTheme: PageTransitionsTheme(builders: {
TargetPlatform.iOS: FadeOpenPageTransitionsBuilder(),
TargetPlatform.android: FadeOpenPageTransitionsBuilder(),
})
);
}));
final ThemeData darkTheme =
ThemeData(primarySwatch: tbDarkMatIndigo, brightness: Brightness.dark);
ThemeData tbDarkTheme = ThemeData(
primarySwatch: tbDarkMatIndigo,
accentColor: Colors.deepOrange,
brightness: Brightness.dark
);
colorScheme: darkTheme.colorScheme.copyWith(secondary: Colors.deepOrange),
brightness: Brightness.dark);

View File

@@ -1,16 +1,17 @@
abstract class ThingsboardImage {
static final thingsBoardWithTitle = 'assets/images/thingsboard_with_title.svg';
static final thingsBoardWithTitle =
'assets/images/thingsboard_with_title.svg';
static final thingsboard = 'assets/images/thingsboard.svg';
static final thingsboardOuter = 'assets/images/thingsboard_outer.svg';
static final thingsboardCenter = 'assets/images/thingsboard_center.svg';
static final dashboardPlaceholder = 'assets/images/dashboard-placeholder.svg';
static final deviceProfilePlaceholder = 'assets/images/device-profile-placeholder.svg';
static final deviceProfilePlaceholder =
'assets/images/device-profile-placeholder.svg';
static final oauth2Logos = <String,String>{
static final oauth2Logos = <String, String>{
'google-logo': 'assets/images/google-logo.svg',
'github-logo': 'assets/images/github-logo.svg',
'facebook-logo': 'assets/images/facebook-logo.svg',
'apple-logo': 'assets/images/apple-logo.svg'
};
}

View File

@@ -3,26 +3,34 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/auth/login/reset_password_request_page.dart';
import 'package:thingsboard_app/core/auth/login/two_factor_authentication_page.dart';
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);
});
late var twoFactorAuthenticationHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return TwoFactorAuthenticationPage(tbContext);
});
AuthRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/login", handler: loginHandler);
router.define("/login/resetPasswordRequest", handler: resetPasswordRequestHandler);
router.define("/login/resetPasswordRequest",
handler: resetPasswordRequestHandler);
router.define("/login/mfa", handler: twoFactorAuthenticationHandler);
}
}

View File

@@ -1,8 +1,7 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/scheduler.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';
@@ -10,29 +9,27 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart
import 'package:thingsboard_app/constants/assets_path.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
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);
@@ -42,6 +39,11 @@ class _LoginPageState extends TbPageState<LoginPage> {
@override
void initState() {
super.initState();
if (tbClient.isPreVerificationToken()) {
SchedulerBinding.instance.addPostFrameCallback((_) {
navigateTo('/login/mfa');
});
}
}
@override
@@ -54,174 +56,191 @@ 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:
'${S.of(context).logoDefaultValue}')
]),
SizedBox(height: 32),
Row(children: [
Text('${S.of(context).loginNotification}',
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('${S.of(context).OR}'),
),
Flexible(child: Divider())
],
)),
FormBuilder(
key: _loginFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
FormBuilderTextField(
name: 'username',
keyboardType:
TextInputType.emailAddress,
validator:
FormBuilderValidators.compose([
FormBuilderValidators.required(
errorText:
'${S.of(context).emailRequireText}'),
FormBuilderValidators.email(
errorText:
'${S.of(context).emailInvalidText}')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText:
'${S.of(context).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:
'${S.of(context).passwordRequireText}')
]),
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:
'${S.of(context).password}'),
);
})
],
)),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
_forgotPassword();
},
child: Text(
'${S.of(context).passwordForgotText}',
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('${S.of(context).login}'),
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 +250,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 +280,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 +305,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 +318,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,24 +1,23 @@
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';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/generated/l10n.dart';
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 +25,77 @@ 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'),
title: Text('${S.of(context).passwordReset}'),
),
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(
'${S.of(context).passwordResetText}',
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:
'${S.of(context).emailRequireText}'),
FormBuilderValidators.email(
errorText:
'${S.of(context).emailInvalidText}')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: '${S.of(context).email} *'),
),
Spacer(),
ElevatedButton(
child: Text(
'${S.of(context).requestPasswordReset}'),
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 {
@@ -114,8 +108,9 @@ class _ResetPasswordRequestPageState extends TbPageState<ResetPasswordRequestPag
await Future.delayed(Duration(milliseconds: 300));
await tbClient.sendResetPasswordLink(email);
_isLoadingNotifier.value = false;
showSuccessNotification('Password reset link was successfully sent!');
} catch(e) {
showSuccessNotification(
'${S.of(context).passwordResetLinkSuccessfullySentNotification}');
} catch (e) {
_isLoadingNotifier.value = false;
}
}

View File

@@ -0,0 +1,504 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:alt_sms_autofill/alt_sms_autofill.dart';
import 'package:thingsboard_app/core/auth/login/login_page_background.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
import 'package:collection/collection.dart';
typedef ProviderDescFunction = String Function(
BuildContext context, String? contact);
typedef TextFunction = String Function(BuildContext context);
class TwoFactorAuthProviderLoginData {
TextFunction nameFunction;
ProviderDescFunction descFunction;
TextFunction placeholderFunction;
String icon;
TwoFactorAuthProviderLoginData(
{required this.nameFunction,
required this.descFunction,
required this.placeholderFunction,
required this.icon});
}
final Map<TwoFaProviderType, TwoFactorAuthProviderLoginData>
twoFactorAuthProvidersLoginData = {
TwoFaProviderType.TOTP: TwoFactorAuthProviderLoginData(
nameFunction: (context) => S.of(context).mfaProviderTopt,
descFunction: (context, contact) => S.of(context).totpAuthDescription,
placeholderFunction: (context) => S.of(context).toptAuthPlaceholder,
icon: 'cellphone-key'),
TwoFaProviderType.SMS: TwoFactorAuthProviderLoginData(
nameFunction: (context) => S.of(context).mfaProviderSms,
descFunction: (context, contact) =>
S.of(context).smsAuthDescription(contact ?? ''),
placeholderFunction: (context) => S.of(context).smsAuthPlaceholder,
icon: 'message-reply-text-outline'),
TwoFaProviderType.EMAIL: TwoFactorAuthProviderLoginData(
nameFunction: (context) => S.of(context).mfaProviderEmail,
descFunction: (context, contact) =>
S.of(context).emailAuthDescription(contact ?? ''),
placeholderFunction: (context) => S.of(context).emailAuthPlaceholder,
icon: 'email-outline'),
TwoFaProviderType.BACKUP_CODE: TwoFactorAuthProviderLoginData(
nameFunction: (context) => S.of(context).mfaProviderBackupCode,
descFunction: (context, contact) =>
S.of(context).backupCodeAuthDescription,
placeholderFunction: (context) => S.of(context).backupCodeAuthPlaceholder,
icon: 'lock-outline')
};
class TwoFactorAuthenticationPage extends TbPageWidget {
TwoFactorAuthenticationPage(TbContext tbContext) : super(tbContext);
@override
_TwoFactorAuthenticationPageState createState() =>
_TwoFactorAuthenticationPageState();
}
class _TwoFactorAuthenticationPageState
extends TbPageState<TwoFactorAuthenticationPage> {
static RegExp smsCodeRegExp = new RegExp(r"(\d{6})");
final _twoFactorAuthFormKey = GlobalKey<FormBuilderState>();
ValueNotifier<TwoFaProviderType?> _selectedProvider =
ValueNotifier<TwoFaProviderType?>(null);
TwoFaProviderType? _prevProvider;
int? _minVerificationPeriod;
List<TwoFaProviderType> _allowProviders = [];
ValueNotifier<bool> _disableSendButton = ValueNotifier<bool>(false);
ValueNotifier<bool> _showResendAction = ValueNotifier<bool>(false);
ValueNotifier<bool> _hideResendButton = ValueNotifier<bool>(true);
Timer? _timer;
Timer? _tooManyRequestsTimer;
ValueNotifier<int> _countDownTime = ValueNotifier<int>(0);
bool _listenForSms = false;
@override
void initState() {
super.initState();
var providersInfo = tbContext.twoFactorAuthProviders;
TwoFaProviderType.values.forEach((provider) {
var providerConfig =
providersInfo!.firstWhereOrNull((config) => config.type == provider);
if (providerConfig != null) {
if (providerConfig.isDefault) {
_minVerificationPeriod =
providerConfig.minVerificationCodeSendPeriod ?? 30;
_selectedProvider.value = providerConfig.type;
}
_allowProviders.add(providerConfig.type);
}
});
if (this._selectedProvider.value != TwoFaProviderType.TOTP) {
_sendCode();
_showResendAction.value = true;
if (this._selectedProvider.value == TwoFaProviderType.SMS) {
_startListenForSmsCode();
}
}
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
_updatedTime();
});
}
@override
void dispose() {
if (_timer != null) {
_timer!.cancel();
}
if (_tooManyRequestsTimer != null) {
_tooManyRequestsTimer!.cancel();
}
_cancelSmsCodeListen();
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return await _goBack();
},
child: Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
body: Stack(children: [
LoginPageBackground(),
SizedBox.expand(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: TbAppBar(
tbContext,
title: Text('${S.of(context).verifyYourIdentity}'),
),
body: Stack(children: [
SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(24),
child: ValueListenableBuilder<TwoFaProviderType?>(
valueListenable: _selectedProvider,
builder: (context, providerType, _widget) {
if (providerType == null) {
var children = <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 16),
child: Text(
'${S.of(context).selectWayToVerify}',
style: TextStyle(
color: Colors.black87,
fontSize: 16,
height: 24 / 16)))
];
_allowProviders.forEach((type) {
var providerData =
twoFactorAuthProvidersLoginData[
type]!;
Widget? icon;
var iconData = MdiIcons.fromString(
providerData.icon);
if (iconData != null) {
icon = Icon(iconData,
size: 24,
color:
Theme.of(context).primaryColor);
} else {
icon = Icon(Icons.login,
size: 24,
color:
Theme.of(context).primaryColor);
}
children.add(Container(
padding:
EdgeInsets.symmetric(vertical: 8),
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: EdgeInsets.all(16),
alignment:
Alignment.centerLeft),
onPressed: () async =>
await _selectProvider(type),
icon: icon,
label: Text(providerData
.nameFunction(context)))));
});
return ListView(
padding:
EdgeInsets.symmetric(vertical: 8),
children: children,
);
} else {
var providerConfig = tbContext
.twoFactorAuthProviders
?.firstWhereOrNull((config) =>
config.type == providerType);
if (providerConfig == null) {
return SizedBox.shrink();
}
var providerDescription =
twoFactorAuthProvidersLoginData[
providerType]!
.descFunction;
return FormBuilder(
key: _twoFactorAuthFormKey,
autovalidateMode:
AutovalidateMode.disabled,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
Text(
providerDescription(context,
providerConfig.contact),
textAlign: TextAlign.start,
style: TextStyle(
color: Color(0xFF7F7F7F),
fontSize: 14,
height: 24 / 14),
),
SizedBox(height: 16),
_buildVerificationCodeField(
context, providerType),
Spacer(),
ValueListenableBuilder<bool>(
valueListenable:
_disableSendButton,
builder: (context,
disableSendButton,
_widget) {
return ElevatedButton(
child: Text(
'${S.of(context).continueText}'),
style: ElevatedButton
.styleFrom(
padding: EdgeInsets
.symmetric(
vertical:
16)),
onPressed: disableSendButton
? null
: () =>
_sendVerificationCode(
context));
}),
SizedBox(height: 16),
SizedBox(
height: 48,
child: Row(
mainAxisSize:
MainAxisSize.max,
children: [
ValueListenableBuilder<
bool>(
valueListenable:
_showResendAction,
builder: (context,
showResendActionValue,
_widget) {
if (showResendActionValue) {
return Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
ValueListenableBuilder<
int>(
valueListenable:
_countDownTime,
builder: (context,
countDown,
_widget) {
if (countDown >
0) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Text(
S.of(context).resendCodeWait(countDown),
textAlign: TextAlign.center,
style: TextStyle(color: Color(0xFF7F7F7F), fontSize: 12, height: 24 / 12),
),
);
} else {
return SizedBox.shrink();
}
}),
ValueListenableBuilder<
bool>(
valueListenable:
_hideResendButton,
builder: (context,
hideResendButton,
_widget) {
if (!hideResendButton) {
return TextButton(
child: Text('${S.of(context).resendCode}'),
style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)),
onPressed: () {
_sendCode();
},
);
} else {
return SizedBox.shrink();
}
})
]));
} else {
return SizedBox
.shrink();
}
}),
if (_allowProviders
.length >
1)
Expanded(
child: TextButton(
child: Text(
'${S.of(context).tryAnotherWay}'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets
.symmetric(
vertical:
16)),
onPressed:
() async {
await _selectProvider(
null);
},
))
]))
]));
}
})))
]),
),
)
])));
}
FormBuilderTextField _buildVerificationCodeField(
BuildContext context, TwoFaProviderType providerType) {
int maxLengthInput = 6;
TextInputType keyboardType = TextInputType.number;
String pattern = '[0-9]*';
if (providerType == TwoFaProviderType.BACKUP_CODE) {
maxLengthInput = 8;
pattern = '[0-9abcdef]*';
keyboardType = TextInputType.text;
}
List<FormFieldValidator<String>> validators = [
FormBuilderValidators.required(
errorText: '${S.of(context).verificationCodeInvalid}'),
FormBuilderValidators.equalLength(maxLengthInput,
errorText: '${S.of(context).verificationCodeInvalid}'),
FormBuilderValidators.match(pattern,
errorText: '${S.of(context).verificationCodeInvalid}')
];
var providerFormData = twoFactorAuthProvidersLoginData[providerType]!;
return FormBuilderTextField(
name: 'verificationCode',
autofocus: true,
maxLength: maxLengthInput,
keyboardType: keyboardType,
validator: FormBuilderValidators.compose(validators),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: providerFormData.placeholderFunction(context)));
}
Future<void> _startListenForSmsCode() async {
_listenForSms = true;
_listenForSmsCode();
}
Future<void> _listenForSmsCode() async {
String? comingSms;
try {
comingSms = await AltSmsAutofill().listenForSms;
} catch (e) {
_listenForSms = false;
comingSms = null;
}
if (comingSms != null) {
RegExpMatch? match = smsCodeRegExp.firstMatch(comingSms);
if (match != null) {
String? codeStr = match.group(1);
if (codeStr != null) {
_twoFactorAuthFormKey.currentState
?.patchValue({'verificationCode': codeStr});
}
}
}
if (_listenForSms) {
_listenForSmsCode();
}
}
Future<void> _cancelSmsCodeListen() async {
_listenForSms = false;
AltSmsAutofill().unregisterListener();
}
Future<void> _sendVerificationCode(BuildContext context) async {
FocusScope.of(context).unfocus();
if (_twoFactorAuthFormKey.currentState?.saveAndValidate() ?? false) {
var formValue = _twoFactorAuthFormKey.currentState!.value;
String verificationCode = formValue['verificationCode'];
try {
await tbClient.checkTwoFaVerificationCode(
_selectedProvider.value!, verificationCode,
requestConfig: RequestConfig(ignoreErrors: true));
} catch (e) {
if (e is ThingsboardError) {
if (e.status == 400) {
_twoFactorAuthFormKey.currentState!.fields['verificationCode']!
.invalidate(S.of(context).verificationCodeIncorrect);
} else if (e.status == 429) {
_twoFactorAuthFormKey.currentState!.fields['verificationCode']!
.invalidate(S.of(context).verificationCodeManyRequest);
_disableSendButton.value = true;
if (_tooManyRequestsTimer != null) {
_tooManyRequestsTimer!.cancel();
}
_tooManyRequestsTimer = Timer(Duration(seconds: 5), () {
_twoFactorAuthFormKey.currentState!.fields['verificationCode']!
.validate();
_disableSendButton.value = false;
});
} else {
showErrorNotification(e.message ?? 'Code verification failed!');
}
} else {
showErrorNotification('Code verification failed!');
}
}
}
}
Future<void> _selectProvider(TwoFaProviderType? type) async {
_prevProvider = type == null ? _selectedProvider.value : null;
_selectedProvider.value = type;
_showResendAction.value = false;
await _cancelSmsCodeListen();
if (type != null) {
var providersInfo = tbContext.twoFactorAuthProviders;
var providerConfig =
providersInfo!.firstWhereOrNull((config) => config.type == type)!;
if (type != TwoFaProviderType.TOTP &&
type != TwoFaProviderType.BACKUP_CODE) {
_sendCode();
_showResendAction.value = true;
_minVerificationPeriod =
providerConfig.minVerificationCodeSendPeriod ?? 30;
if (type == TwoFaProviderType.SMS) {
_startListenForSmsCode();
}
}
}
}
Future<void> _sendCode() async {
_hideResendButton.value = true;
_countDownTime.value = 0;
try {
await tbContext.tbClient
.getTwoFactorAuthService()
.requestTwoFaVerificationCode(_selectedProvider.value!,
requestConfig: RequestConfig(ignoreErrors: true));
} catch (e) {
} finally {
_countDownTime.value = _minVerificationPeriod!;
}
}
Future<bool> _goBack() async {
if (_prevProvider != null) {
await _selectProvider(_prevProvider);
_prevProvider = null;
} else {
tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
}
return false;
}
void _updatedTime() {
if (_countDownTime.value > 0) {
_countDownTime.value--;
if (_countDownTime.value == 0) {
_hideResendButton.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 {
@@ -110,6 +104,7 @@ class TbContext {
final ValueNotifier<bool> _isAuthenticated = ValueNotifier(false);
PlatformType? _oauth2PlatformType;
List<OAuth2ClientInfo>? oauth2ClientInfos;
List<TwoFaProviderInfo>? twoFactorAuthProviders;
User? userDetails;
HomeDashboardInfo? homeDashboard;
final _isLoadingNotifier = ValueNotifier<bool>(false);
@@ -121,7 +116,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 +143,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 +200,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 +222,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);
},
),
);
@@ -259,12 +257,13 @@ class TbContext {
try {
log.debug('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}');
isUserLoaded = true;
if (tbClient.isAuthenticated()) {
if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) {
log.debug('authUser: ${tbClient.getAuthUser()}');
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();
@@ -274,39 +273,59 @@ class TbContext {
}
}
} else {
if (tbClient.isPreVerificationToken()) {
log.debug('authUser: ${tbClient.getAuthUser()}');
twoFactorAuthProviders = await tbClient
.getTwoFactorAuthService()
.getAvailableLoginTwoFaProviders();
} else {
twoFactorAuthProviders = null;
}
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 isAuthenticated =>
_isAuthenticated.value && !tbClient.isPreVerificationToken();
bool get hasOAuthClients => oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty;
bool get hasOAuthClients =>
oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty;
Future<void> updateRouteState() async {
if (currentState != null) {
if (tbClient.isAuthenticated()) {
if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) {
var defaultDashboardId = _defaultDashboardId();
if (defaultDashboardId != null) {
bool fullscreen = _userForceFullscreen();
@@ -318,14 +337,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 +363,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 +382,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 +402,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 +437,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 +462,7 @@ class TbContext {
builder: (BuildContext context) {
return dialog;
},
fullscreenDialog: true
));
fullscreenDialog: true));
}
void pop<T>([T? result, BuildContext? context]) async {
@@ -428,7 +473,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 +486,7 @@ class TbContext {
return true;
}
if (_mainDashboardHolder != null) {
return await _mainDashboardHolder!.dashboardGoBack();
return await _mainDashboardHolder!.dashboardGoBack();
}
return true;
}
@@ -456,18 +501,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 +529,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 +565,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,11 +1,11 @@
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';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_app/utils/utils.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
@@ -31,7 +31,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 +40,6 @@ class EntityCardSettings {
}
mixin EntitiesBase<T, P> on HasTbContext {
final entityDateFormat = DateFormat('yyyy-MM-dd');
String get title;
@@ -55,15 +55,15 @@ mixin EntitiesBase<T, P> on HasTbContext {
Key? getKey(T entity) => null;
Widget buildEntityListCard(BuildContext context, T entity) {
return Text('Not implemented!');
return Text('${S.of(context).notImplemented}');
}
Widget buildEntityListWidgetCard(BuildContext context, T entity) {
return Text('Not implemented!');
return Text('${S.of(context).notImplemented}');
}
Widget buildEntityGridCard(BuildContext context, T entity) {
return Text('Not implemented!');
return Text('${S.of(context).notImplemented}');
}
double? gridChildAspectRatio() => null;
@@ -73,11 +73,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 +87,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 +102,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 +142,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 +166,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 +181,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 +213,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 +305,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 +324,7 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
)
]);
}
Widget newPageProgressIndicatorBuilder(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
@@ -348,15 +334,14 @@ abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget
child: Center(child: RefreshProgressIndicator()),
);
}
Widget noItemsFoundIndicatorBuilder(BuildContext context) {
return FirstPageExceptionIndicator(
title: widget.noItemsFoundText,
message: 'The list is currently empty.',
message: '${S.of(context).listIsEmptyText}',
onTryAgain: widget.searchMode ? null : () => pagingController.refresh(),
);
}
}
class FirstPageExceptionIndicator extends StatelessWidget {
@@ -407,8 +392,8 @@ class FirstPageExceptionIndicator extends StatelessWidget {
Icons.refresh,
color: Colors.white,
),
label: const Text(
'Try Again',
label: Text(
'${S.of(context).tryAgain}',
style: TextStyle(
fontSize: 16,
color: Colors.white,

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);
}
}

View File

@@ -0,0 +1,66 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that looks up messages for specific locales by
// delegating to the appropriate library.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:implementation_imports, file_names, unnecessary_new
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
// ignore_for_file:argument_type_not_assignable, invalid_assignment
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
// ignore_for_file:comment_references
import 'dart:async';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_en.dart' as messages_en;
import 'messages_zh.dart' as messages_zh;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new Future.value(null),
'zh': () => new Future.value(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'en':
return messages_en.messages;
case 'zh':
return messages_zh.messages;
default:
return null;
}
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) async {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new Future.value(false);
}
var lib = _deferredLibraries[availableLocale];
await (lib == null ? new Future.value(false) : lib());
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new Future.value(true);
}
bool _messagesExistFor(String locale) {
try {
return _findExact(locale) != null;
} catch (e) {
return false;
}
}
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale =
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}

View File

@@ -0,0 +1,173 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a en locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';
static String m0(contact) =>
"A security code has been sent to your email address at ${contact}.";
static String m1(time) =>
"Resend code in ${Intl.plural(time, one: '1 second', other: '${time} seconds')}";
static String m2(contact) =>
"A security code has been sent to your phone at ${contact}.";
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"No": MessageLookupByLibrary.simpleMessage("No"),
"OR": MessageLookupByLibrary.simpleMessage("OR"),
"Yes": MessageLookupByLibrary.simpleMessage("Yes"),
"actionData": MessageLookupByLibrary.simpleMessage("Action data"),
"active": MessageLookupByLibrary.simpleMessage("Active"),
"address": MessageLookupByLibrary.simpleMessage("Address"),
"address2": MessageLookupByLibrary.simpleMessage("Address 2"),
"alarmAcknowledgeText": MessageLookupByLibrary.simpleMessage(
"Are you sure you want to acknowledge Alarm?"),
"alarmAcknowledgeTitle":
MessageLookupByLibrary.simpleMessage("Acknowledge Alarm"),
"alarmClearText": MessageLookupByLibrary.simpleMessage(
"Are you sure you want to clear Alarm?"),
"alarmClearTitle": MessageLookupByLibrary.simpleMessage("Clear Alarm"),
"alarms": MessageLookupByLibrary.simpleMessage("Alarms"),
"allDevices": MessageLookupByLibrary.simpleMessage("All devices"),
"appTitle": MessageLookupByLibrary.simpleMessage("Thingsboard"),
"assetName": MessageLookupByLibrary.simpleMessage("Asset name"),
"assets": MessageLookupByLibrary.simpleMessage("Assets"),
"assignedToCustomer":
MessageLookupByLibrary.simpleMessage("Assigned to customer"),
"auditLogDetails":
MessageLookupByLibrary.simpleMessage("Audit log details"),
"auditLogs": MessageLookupByLibrary.simpleMessage("Audit Logs"),
"backupCodeAuthDescription": MessageLookupByLibrary.simpleMessage(
"Please enter one of your backup codes."),
"backupCodeAuthPlaceholder":
MessageLookupByLibrary.simpleMessage("Backup code"),
"changePassword":
MessageLookupByLibrary.simpleMessage("Change Password"),
"city": MessageLookupByLibrary.simpleMessage("City"),
"continueText": MessageLookupByLibrary.simpleMessage("Continue"),
"country": MessageLookupByLibrary.simpleMessage("Country"),
"currentPassword":
MessageLookupByLibrary.simpleMessage("currentPassword"),
"currentPasswordRequireText": MessageLookupByLibrary.simpleMessage(
"Current password is required."),
"currentPasswordStar":
MessageLookupByLibrary.simpleMessage("Current password *"),
"customer": MessageLookupByLibrary.simpleMessage("Customer"),
"customers": MessageLookupByLibrary.simpleMessage("Customers"),
"devices": MessageLookupByLibrary.simpleMessage("Devices"),
"email": MessageLookupByLibrary.simpleMessage("Email"),
"emailAuthDescription": m0,
"emailAuthPlaceholder":
MessageLookupByLibrary.simpleMessage("Email code"),
"emailInvalidText":
MessageLookupByLibrary.simpleMessage("Invalid email format."),
"emailRequireText":
MessageLookupByLibrary.simpleMessage("Email is required."),
"emailStar": MessageLookupByLibrary.simpleMessage("Email *"),
"entityType": MessageLookupByLibrary.simpleMessage("Entity Type"),
"failureDetails":
MessageLookupByLibrary.simpleMessage("Failure details"),
"firstName": MessageLookupByLibrary.simpleMessage("firstName"),
"firstNameUpper": MessageLookupByLibrary.simpleMessage("First Name"),
"home": MessageLookupByLibrary.simpleMessage("Home"),
"inactive": MessageLookupByLibrary.simpleMessage("Inactive"),
"label": MessageLookupByLibrary.simpleMessage("Label"),
"lastName": MessageLookupByLibrary.simpleMessage("lastName"),
"lastNameUpper": MessageLookupByLibrary.simpleMessage("Last Name"),
"listIsEmptyText": MessageLookupByLibrary.simpleMessage(
"The list is currently empty."),
"login": MessageLookupByLibrary.simpleMessage("Log In"),
"loginNotification":
MessageLookupByLibrary.simpleMessage("Login to your account"),
"logoDefaultValue":
MessageLookupByLibrary.simpleMessage("Thingsboard Logo"),
"logout": MessageLookupByLibrary.simpleMessage("Log Out"),
"mfaProviderBackupCode":
MessageLookupByLibrary.simpleMessage("Backup code"),
"mfaProviderEmail": MessageLookupByLibrary.simpleMessage("Email"),
"mfaProviderSms": MessageLookupByLibrary.simpleMessage("SMS"),
"mfaProviderTopt":
MessageLookupByLibrary.simpleMessage("Authenticator app"),
"more": MessageLookupByLibrary.simpleMessage("More"),
"newPassword": MessageLookupByLibrary.simpleMessage("newPassword"),
"newPassword2": MessageLookupByLibrary.simpleMessage("newPassword2"),
"newPassword2RequireText": MessageLookupByLibrary.simpleMessage(
"New password again is required."),
"newPassword2Star":
MessageLookupByLibrary.simpleMessage("New password again *"),
"newPasswordRequireText":
MessageLookupByLibrary.simpleMessage("New password is required."),
"newPasswordStar":
MessageLookupByLibrary.simpleMessage("New password *"),
"notImplemented":
MessageLookupByLibrary.simpleMessage("Not implemented!"),
"password": MessageLookupByLibrary.simpleMessage("Password"),
"passwordErrorNotification": MessageLookupByLibrary.simpleMessage(
"Entered passwords must be same!"),
"passwordForgotText":
MessageLookupByLibrary.simpleMessage("Forgot Password?"),
"passwordRequireText":
MessageLookupByLibrary.simpleMessage("Password is required."),
"passwordReset": MessageLookupByLibrary.simpleMessage("Reset password"),
"passwordResetLinkSuccessfullySentNotification":
MessageLookupByLibrary.simpleMessage(
"Password reset link was successfully sent!"),
"passwordResetText": MessageLookupByLibrary.simpleMessage(
"Enter the email associated with your account and we\'ll send an email with password reset link"),
"passwordSuccessNotification": MessageLookupByLibrary.simpleMessage(
"Password successfully changed"),
"phone": MessageLookupByLibrary.simpleMessage("Phone"),
"postalCode": MessageLookupByLibrary.simpleMessage("Zip / Postal Code"),
"profileSuccessNotification": MessageLookupByLibrary.simpleMessage(
"Profile successfully updated"),
"requestPasswordReset":
MessageLookupByLibrary.simpleMessage("Request password reset"),
"resendCode": MessageLookupByLibrary.simpleMessage("Resend code"),
"resendCodeWait": m1,
"selectWayToVerify":
MessageLookupByLibrary.simpleMessage("Select a way to verify"),
"smsAuthDescription": m2,
"smsAuthPlaceholder": MessageLookupByLibrary.simpleMessage("SMS code"),
"stateOrProvince":
MessageLookupByLibrary.simpleMessage("State / Province"),
"systemAdministrator":
MessageLookupByLibrary.simpleMessage("System Administrator"),
"tenantAdministrator":
MessageLookupByLibrary.simpleMessage("Tenant Administrator"),
"title": MessageLookupByLibrary.simpleMessage("Title"),
"toptAuthPlaceholder": MessageLookupByLibrary.simpleMessage("Code"),
"totpAuthDescription": MessageLookupByLibrary.simpleMessage(
"Please enter the security code from your authenticator app."),
"tryAgain": MessageLookupByLibrary.simpleMessage("Try Again"),
"tryAnotherWay":
MessageLookupByLibrary.simpleMessage("Try another way"),
"type": MessageLookupByLibrary.simpleMessage("Type"),
"username": MessageLookupByLibrary.simpleMessage("username"),
"verificationCodeIncorrect": MessageLookupByLibrary.simpleMessage(
"Verification code is incorrect"),
"verificationCodeInvalid": MessageLookupByLibrary.simpleMessage(
"Invalid verification code format"),
"verificationCodeManyRequest": MessageLookupByLibrary.simpleMessage(
"Too many requests check verification code"),
"verifyYourIdentity":
MessageLookupByLibrary.simpleMessage("Verify your identity")
};
}

View File

@@ -0,0 +1,107 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a zh locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'zh';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"No": MessageLookupByLibrary.simpleMessage(""),
"OR": MessageLookupByLibrary.simpleMessage(""),
"Yes": MessageLookupByLibrary.simpleMessage(""),
"actionData": MessageLookupByLibrary.simpleMessage("动作数据"),
"active": MessageLookupByLibrary.simpleMessage("激活"),
"address": MessageLookupByLibrary.simpleMessage("地址"),
"address2": MessageLookupByLibrary.simpleMessage("地址 2"),
"alarmAcknowledgeText":
MessageLookupByLibrary.simpleMessage("你确定要确认告警吗?"),
"alarmAcknowledgeTitle": MessageLookupByLibrary.simpleMessage("确认告警"),
"alarmClearText": MessageLookupByLibrary.simpleMessage("你确定要清除告警吗?"),
"alarmClearTitle": MessageLookupByLibrary.simpleMessage("清除告警"),
"alarms": MessageLookupByLibrary.simpleMessage("告警"),
"allDevices": MessageLookupByLibrary.simpleMessage("所有设备"),
"appTitle": MessageLookupByLibrary.simpleMessage("Thingsboard"),
"assetName": MessageLookupByLibrary.simpleMessage("资产名"),
"assignedToCustomer": MessageLookupByLibrary.simpleMessage("分配给客户"),
"auditLogDetails": MessageLookupByLibrary.simpleMessage("审计日志详情"),
"auditLogs": MessageLookupByLibrary.simpleMessage("审计报告"),
"changePassword": MessageLookupByLibrary.simpleMessage("修改密码"),
"city": MessageLookupByLibrary.simpleMessage("城市"),
"country": MessageLookupByLibrary.simpleMessage("国家"),
"currentPassword": MessageLookupByLibrary.simpleMessage("当前密码"),
"currentPasswordRequireText":
MessageLookupByLibrary.simpleMessage("输入当前密码"),
"currentPasswordStar": MessageLookupByLibrary.simpleMessage("当前密码 *"),
"customer": MessageLookupByLibrary.simpleMessage("客户"),
"customers": MessageLookupByLibrary.simpleMessage("客户"),
"devices": MessageLookupByLibrary.simpleMessage("设备"),
"email": MessageLookupByLibrary.simpleMessage("Email"),
"emailInvalidText": MessageLookupByLibrary.simpleMessage("Email格式错误"),
"emailRequireText": MessageLookupByLibrary.simpleMessage("输入Email"),
"emailStar": MessageLookupByLibrary.simpleMessage("Email *"),
"entityType": MessageLookupByLibrary.simpleMessage("实体类型"),
"failureDetails": MessageLookupByLibrary.simpleMessage("失败详情"),
"firstName": MessageLookupByLibrary.simpleMessage(""),
"firstNameUpper": MessageLookupByLibrary.simpleMessage(""),
"home": MessageLookupByLibrary.simpleMessage("主页"),
"inactive": MessageLookupByLibrary.simpleMessage("失活"),
"label": MessageLookupByLibrary.simpleMessage("标签"),
"lastName": MessageLookupByLibrary.simpleMessage(""),
"lastNameUpper": MessageLookupByLibrary.simpleMessage(""),
"listIsEmptyText": MessageLookupByLibrary.simpleMessage("列表当前为空"),
"login": MessageLookupByLibrary.simpleMessage("登录"),
"loginNotification": MessageLookupByLibrary.simpleMessage("登录你的账号"),
"logoDefaultValue":
MessageLookupByLibrary.simpleMessage("Thingsboard Logo"),
"logout": MessageLookupByLibrary.simpleMessage("登出"),
"more": MessageLookupByLibrary.simpleMessage("更多"),
"newPassword": MessageLookupByLibrary.simpleMessage("新密码"),
"newPassword2": MessageLookupByLibrary.simpleMessage("新密码2"),
"newPassword2RequireText":
MessageLookupByLibrary.simpleMessage("再次输入新密码"),
"newPassword2Star": MessageLookupByLibrary.simpleMessage("再次输入新密码 *"),
"newPasswordRequireText": MessageLookupByLibrary.simpleMessage("输入新密码"),
"newPasswordStar": MessageLookupByLibrary.simpleMessage("新密码 *"),
"notImplemented": MessageLookupByLibrary.simpleMessage("未实现!"),
"password": MessageLookupByLibrary.simpleMessage("密码"),
"passwordErrorNotification":
MessageLookupByLibrary.simpleMessage("输入的密码必须相同"),
"passwordForgotText": MessageLookupByLibrary.simpleMessage("忘记密码?"),
"passwordRequireText": MessageLookupByLibrary.simpleMessage("输入密码"),
"passwordReset": MessageLookupByLibrary.simpleMessage("重置密码"),
"passwordResetLinkSuccessfullySentNotification":
MessageLookupByLibrary.simpleMessage("密码重置链接已发送"),
"passwordResetText": MessageLookupByLibrary.simpleMessage(
"输入和账号关联的Email我们将发送一个密码重置链接到的Email"),
"passwordSuccessNotification":
MessageLookupByLibrary.simpleMessage("密码修改成功"),
"phone": MessageLookupByLibrary.simpleMessage("电话"),
"postalCode": MessageLookupByLibrary.simpleMessage("邮编"),
"profileSuccessNotification":
MessageLookupByLibrary.simpleMessage("配置更新成功"),
"requestPasswordReset": MessageLookupByLibrary.simpleMessage("要求重置密码"),
"stateOrProvince": MessageLookupByLibrary.simpleMessage("州 / 省"),
"systemAdministrator": MessageLookupByLibrary.simpleMessage("系统管理员"),
"tenantAdministrator": MessageLookupByLibrary.simpleMessage("租户管理员"),
"title": MessageLookupByLibrary.simpleMessage("标题"),
"tryAgain": MessageLookupByLibrary.simpleMessage("再试一次"),
"type": MessageLookupByLibrary.simpleMessage("类型"),
"username": MessageLookupByLibrary.simpleMessage("用户名")
};
}

1019
lib/generated/l10n.dart Normal file

File diff suppressed because it is too large Load Diff

112
lib/l10n/intl_en.arb Normal file
View File

@@ -0,0 +1,112 @@
{
"appTitle": "Thingsboard",
"home": "Home",
"alarms": "Alarms",
"devices": "Devices",
"more": "More",
"customers": "Customers",
"assets": "Assets",
"auditLogs": "Audit Logs",
"logout": "Log Out",
"login": "Log In",
"logoDefaultValue": "Thingsboard Logo",
"loginNotification": "Login to your account",
"email": "Email",
"emailRequireText": "Email is required.",
"emailInvalidText": "Invalid email format.",
"username": "username",
"password": "Password",
"passwordRequireText": "Password is required.",
"passwordForgotText": "Forgot Password?",
"passwordReset": "Reset password",
"passwordResetText": "Enter the email associated with your account and we'll send an email with password reset link",
"requestPasswordReset": "Request password reset",
"passwordResetLinkSuccessfullySentNotification": "Password reset link was successfully sent!",
"OR": "OR",
"No": "No",
"Yes": "Yes",
"title": "Title",
"country": "Country",
"city": "City",
"stateOrProvince": "State / Province",
"postalCode": "Zip / Postal Code",
"address": "Address",
"address2": "Address 2",
"phone": "Phone",
"alarmClearTitle": "Clear Alarm",
"alarmClearText": "Are you sure you want to clear Alarm?",
"alarmAcknowledgeTitle": "Acknowledge Alarm",
"alarmAcknowledgeText": "Are you sure you want to acknowledge Alarm?",
"assetName": "Asset name",
"type": "Type",
"label": "Label",
"assignedToCustomer": "Assigned to customer",
"auditLogDetails": "Audit log details",
"entityType": "Entity Type",
"actionData": "Action data",
"failureDetails": "Failure details",
"allDevices": "All devices",
"active": "Active",
"inactive": "Inactive",
"systemAdministrator": "System Administrator",
"tenantAdministrator": "Tenant Administrator",
"customer": "Customer",
"changePassword": "Change Password",
"currentPassword": "currentPassword",
"currentPasswordRequireText": "Current password is required.",
"currentPasswordStar": "Current password *",
"newPassword": "newPassword",
"newPasswordRequireText": "New password is required.",
"newPasswordStar": "New password *",
"newPassword2": "newPassword2",
"newPassword2RequireText": "New password again is required.",
"newPassword2Star": "New password again *",
"passwordErrorNotification": "Entered passwords must be same!",
"emailStar": "Email *",
"firstName": "firstName",
"firstNameUpper": "First Name",
"lastName": "lastName",
"lastNameUpper": "Last Name",
"profileSuccessNotification": "Profile successfully updated",
"passwordSuccessNotification": "Password successfully changed",
"notImplemented": "Not implemented!",
"listIsEmptyText": "The list is currently empty.",
"tryAgain": "Try Again",
"verifyYourIdentity": "Verify your identity",
"continueText": "Continue",
"resendCode": "Resend code",
"resendCodeWait": "Resend code in {time,plural, =1{1 second}other{{time} seconds}}",
"totpAuthDescription": "Please enter the security code from your authenticator app.",
"smsAuthDescription": "A security code has been sent to your phone at {contact}.",
"emailAuthDescription": "A security code has been sent to your email address at {contact}.",
"backupCodeAuthDescription": "Please enter one of your backup codes.",
"verificationCodeInvalid": "Invalid verification code format",
"toptAuthPlaceholder": "Code",
"smsAuthPlaceholder": "SMS code",
"emailAuthPlaceholder": "Email code",
"backupCodeAuthPlaceholder": "Backup code",
"verificationCodeIncorrect": "Verification code is incorrect",
"verificationCodeManyRequest": "Too many requests check verification code",
"tryAnotherWay": "Try another way",
"selectWayToVerify": "Select a way to verify",
"mfaProviderTopt": "Authenticator app",
"mfaProviderSms": "SMS",
"mfaProviderEmail": "Email",
"mfaProviderBackupCode": "Backup code"
}

90
lib/l10n/intl_zh.arb Normal file
View File

@@ -0,0 +1,90 @@
{
"appTitle": "Thingsboard",
"home": "主页",
"alarms": "告警",
"devices": "设备",
"more": "更多",
"customers": "客户",
"asserts": "资产",
"auditLogs": "审计报告",
"logout": "登出",
"login": "登录",
"logoDefaultValue": "Thingsboard Logo",
"loginNotification": "登录你的账号",
"email": "Email",
"emailRequireText": "输入Email",
"emailInvalidText": "Email格式错误",
"username": "用户名",
"password": "密码",
"passwordRequireText": "输入密码",
"passwordForgotText": "忘记密码?",
"passwordReset": "重置密码",
"passwordResetText": "输入和账号关联的Email我们将发送一个密码重置链接到的Email",
"requestPasswordReset": "要求重置密码",
"passwordResetLinkSuccessfullySentNotification": "密码重置链接已发送",
"OR": "或",
"No": "否",
"Yes": "是",
"title": "标题",
"country": "国家",
"city": "城市",
"stateOrProvince": "州 / 省",
"postalCode": "邮编",
"address": "地址",
"address2": "地址 2",
"phone": "电话",
"alarmClearTitle": "清除告警",
"alarmClearText": "你确定要清除告警吗?",
"alarmAcknowledgeTitle": "确认告警",
"alarmAcknowledgeText": "你确定要确认告警吗?",
"assetName": "资产名",
"type": "类型",
"label": "标签",
"assignedToCustomer": "分配给客户",
"auditLogDetails": "审计日志详情",
"entityType": "实体类型",
"actionData": "动作数据",
"failureDetails": "失败详情",
"allDevices": "所有设备",
"active": "激活",
"inactive": "失活",
"systemAdministrator": "系统管理员",
"tenantAdministrator": "租户管理员",
"customer": "客户",
"changePassword": "修改密码",
"currentPassword": "当前密码",
"currentPasswordRequireText": "输入当前密码",
"currentPasswordStar": "当前密码 *",
"newPassword": "新密码",
"newPasswordRequireText": "输入新密码",
"newPasswordStar": "新密码 *",
"newPassword2": "新密码2",
"newPassword2RequireText": "再次输入新密码",
"newPassword2Star": "再次输入新密码 *",
"passwordErrorNotification": "输入的密码必须相同",
"emailStar": "Email *",
"firstName": "名",
"firstNameUpper": "名",
"lastName": "姓",
"lastNameUpper": "姓",
"profileSuccessNotification": "配置更新成功",
"passwordSuccessNotification": "密码修改成功",
"notImplemented": "未实现!",
"listIsEmptyText": "列表当前为空",
"tryAgain": "再试一次"
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
@@ -10,11 +10,11 @@ import 'package:thingsboard_app/modules/dashboard/main_dashboard_page.dart';
import 'package:thingsboard_app/widgets/two_page_view.dart';
import 'config/themes/tb_theme.dart';
import 'generated/l10n.dart';
final appRouter = ThingsboardAppRouter();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// await FlutterDownloader.initialize();
// await Permission.storage.request();
@@ -27,18 +27,18 @@ void main() async {
}
class ThingsboardApp extends StatefulWidget {
ThingsboardApp({Key? key}) : super(key: key);
@override
ThingsboardAppState createState() => ThingsboardAppState();
}
class ThingsboardAppState extends State<ThingsboardApp> with TickerProviderStateMixin implements TbMainDashboardHolder {
class ThingsboardAppState extends State<ThingsboardApp>
with TickerProviderStateMixin
implements TbMainDashboardHolder {
final TwoPageViewController _mainPageViewController = TwoPageViewController();
final MainDashboardPageController _mainDashboardPageController = MainDashboardPageController();
final MainDashboardPageController _mainDashboardPageController =
MainDashboardPageController();
final GlobalKey mainAppKey = GlobalKey();
final GlobalKey dashboardKey = GlobalKey();
@@ -50,8 +50,13 @@ class ThingsboardAppState extends State<ThingsboardApp> with TickerProviderState
}
@override
Future<void> navigateToDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar, bool animate = true}) async {
await _mainDashboardPageController.openDashboard(dashboardId, dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar);
Future<void> navigateToDashboard(String dashboardId,
{String? dashboardTitle,
String? state,
bool? hideToolbar,
bool animate = true}) async {
await _mainDashboardPageController.openDashboard(dashboardId,
dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar);
_openDashboard(animate: animate);
}
@@ -121,40 +126,57 @@ class ThingsboardAppState extends State<ThingsboardApp> with TickerProviderState
return res;
}
@override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
statusBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.light
));
systemNavigationBarIconBrightness: Brightness.light));
return MaterialApp(
title: 'ThingsBoard',
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
onGenerateTitle: (BuildContext context) => S.of(context).appTitle,
themeMode: ThemeMode.light,
home: TwoPageView(
controller: _mainPageViewController,
first: MaterialApp(
key: mainAppKey,
scaffoldMessengerKey: appRouter.tbContext.messengerKey,
title: 'ThingsBoard',
theme: tbTheme,
themeMode: ThemeMode.light,
darkTheme: tbDarkTheme,
onGenerateRoute: appRouter.router.generator,
navigatorObservers: [appRouter.tbContext.routeObserver],
),
second: MaterialApp(
key: dashboardKey,
// scaffoldMessengerKey: appRouter.tbContext.messengerKey,
title: 'ThingsBoard',
theme: tbTheme,
themeMode: ThemeMode.light,
darkTheme: tbDarkTheme,
home: MainDashboardPage(appRouter.tbContext, controller: _mainDashboardPageController),
)
)
);
controller: _mainPageViewController,
first: MaterialApp(
key: mainAppKey,
scaffoldMessengerKey: appRouter.tbContext.messengerKey,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
onGenerateTitle: (BuildContext context) => S.of(context).appTitle,
theme: tbTheme,
themeMode: ThemeMode.light,
darkTheme: tbDarkTheme,
onGenerateRoute: appRouter.router.generator,
navigatorObservers: [appRouter.tbContext.routeObserver],
),
second: MaterialApp(
key: dashboardKey,
// scaffoldMessengerKey: appRouter.tbContext.messengerKey,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
onGenerateTitle: (BuildContext context) => S.of(context).appTitle,
theme: tbTheme,
themeMode: ThemeMode.light,
darkTheme: tbDarkTheme,
home: MainDashboardPage(appRouter.tbContext,
controller: _mainDashboardPageController),
)));
}
}

View File

@@ -6,8 +6,8 @@ import 'package:thingsboard_app/modules/alarm/alarms_page.dart';
import 'package:thingsboard_app/modules/main/main_page.dart';
class AlarmRoutes extends TbRoutes {
late var alarmsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var alarmsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
if (searchMode) {
return AlarmsPage(tbContext, searchMode: true);
@@ -22,5 +22,4 @@ class AlarmRoutes extends TbRoutes {
void doRegisterRoutes(router) {
router.define("/alarms", handler: alarmsHandler);
}
}

View File

@@ -1,14 +1,13 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_app/utils/utils.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
const Map<AlarmSeverity, Color> alarmSeverityColors = {
AlarmSeverity.CRITICAL: Color(0xFFFF0000),
AlarmSeverity.MAJOR: Color(0xFFFFA500),
@@ -33,7 +32,6 @@ const Map<AlarmStatus, String> alarmStatusTranslations = {
};
mixin AlarmsBase on EntitiesBase<AlarmInfo, AlarmQuery> {
@override
String get title => 'Alarms';
@@ -49,11 +47,14 @@ mixin AlarmsBase on EntitiesBase<AlarmInfo, AlarmQuery> {
void onEntityTap(AlarmInfo alarm) {
String? dashboardId = alarm.details?['dashboardId'];
if (dashboardId != null) {
var state = Utils.createDashboardEntityState(alarm.originator, entityName: alarm.originatorName);
navigateToDashboard(dashboardId, dashboardTitle: alarm.originatorName, state: state);
var state = Utils.createDashboardEntityState(alarm.originator,
entityName: alarm.originatorName);
navigateToDashboard(dashboardId,
dashboardTitle: alarm.originatorName, state: state);
} else {
if (tbClient.isTenantAdmin()) {
showWarnNotification('Mobile dashboard should be configured in device profile alarm rules!');
showWarnNotification(
'Mobile dashboard should be configured in device profile alarm rules!');
}
}
}
@@ -69,8 +70,11 @@ mixin AlarmsBase on EntitiesBase<AlarmInfo, AlarmQuery> {
}
class AlarmQueryController extends PageKeyController<AlarmQuery> {
AlarmQueryController({int pageSize = 20, String? searchText}) : super(AlarmQuery(TimePageLink(pageSize, 0, searchText, SortOrder('createdTime', Direction.DESC)), fetchOriginator: true));
AlarmQueryController({int pageSize = 20, String? searchText})
: super(AlarmQuery(
TimePageLink(pageSize, 0, searchText,
SortOrder('createdTime', Direction.DESC)),
fetchOriginator: true));
@override
AlarmQuery nextPageKey(AlarmQuery pageKey) {
@@ -83,28 +87,24 @@ class AlarmQueryController extends PageKeyController<AlarmQuery> {
query.pageLink.textSearch = searchText;
notifyListeners();
}
}
class AlarmCard extends TbContextWidget {
final AlarmInfo alarm;
AlarmCard(TbContext tbContext, {required this.alarm}) : super(tbContext);
@override
_AlarmCardState createState() => _AlarmCardState(alarm);
}
class _AlarmCardState extends TbContextState<AlarmCard> {
bool loading = false;
AlarmInfo alarm;
final entityDateFormat = DateFormat('yyyy-MM-dd');
_AlarmCardState(this.alarm): super();
_AlarmCardState(this.alarm) : super();
@override
void initState() {
@@ -121,161 +121,178 @@ class _AlarmCardState extends TbContextState<AlarmCard> {
@override
Widget build(BuildContext context) {
if (this.loading) {
return Container( height: 134, alignment: Alignment.center, child: RefreshProgressIndicator());
return Container(
height: 134,
alignment: Alignment.center,
child: RefreshProgressIndicator());
} else {
bool hasDashboard = alarm.details?['dashboardId'] != null;
return Stack(
children: [
Positioned.fill(
child: Container(
alignment: Alignment.centerLeft,
child: Container(
width: 4,
decoration: BoxDecoration(
color: alarmSeverityColors[alarm.severity]!,
borderRadius: BorderRadius.only(topLeft: Radius.circular(4), bottomLeft: Radius.circular(4))
),
)
)
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(width: 4),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
child: AutoSizeText(alarm.type,
maxLines: 2,
minFontSize: 8,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14)
)
),
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(alarm.createdTime!)),
child: Container(
alignment: Alignment.centerLeft,
child: Container(
width: 4,
decoration: BoxDecoration(
color: alarmSeverityColors[alarm.severity]!,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
bottomLeft: Radius.circular(4))),
))),
Row(mainAxisSize: MainAxisSize.max, children: [
SizedBox(width: 4),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
child: AutoSizeText(alarm.type,
maxLines: 2,
minFontSize: 8,
overflow:
TextOverflow.ellipsis,
style: TextStyle(
color: Color(0xFF282828),
fontWeight:
FontWeight.w500,
fontSize: 14,
height: 20 / 14))),
Text(
entityDateFormat.format(DateTime
.fromMillisecondsSinceEpoch(
alarm.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight: FontWeight.normal,
fontSize: 12,
height: 16 / 12))
]),
SizedBox(height: 4),
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
child: Text(
alarm.originatorName != null
? alarm.originatorName!
: '',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight: FontWeight.normal,
fontWeight:
FontWeight.normal,
fontSize: 12,
height: 16 / 12)
)
]
),
SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
child: Text(alarm.originatorName != null ? alarm.originatorName! : '',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight: FontWeight.normal,
fontSize: 12,
height: 16 / 12)
)
),
Text(alarmSeverityTranslations[alarm.severity]!,
style: TextStyle(
color: alarmSeverityColors[alarm.severity]!,
fontWeight: FontWeight.w500,
fontSize: 12,
height: 16 / 12)
)
]
),
SizedBox(height: 12)],
)
),
SizedBox(width: 16),
if (hasDashboard) Icon(Icons.chevron_right, color: Color(0xFFACACAC)),
if (hasDashboard) SizedBox(width: 16),
]
),
Divider(height: 1),
SizedBox(height: 8),
height: 16 / 12))),
Text(
alarmSeverityTranslations[
alarm.severity]!,
style: TextStyle(
color: alarmSeverityColors[
alarm.severity]!,
fontWeight: FontWeight.w500,
fontSize: 12,
height: 16 / 12))
]),
SizedBox(height: 12)
],
)),
SizedBox(width: 16),
if (hasDashboard)
Icon(Icons.chevron_right,
color: Color(0xFFACACAC)),
if (hasDashboard) SizedBox(width: 16),
]),
Divider(height: 1),
SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Text(
alarmStatusTranslations[alarm.status]!,
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.normal,
fontSize: 14,
height: 20 / 14))),
SizedBox(height: 32),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Text(alarmStatusTranslations[alarm.status]!,
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.normal,
fontSize: 14,
height: 20 / 14)
)
),
SizedBox(height: 32),
Row(
children: [
if ([AlarmStatus.CLEARED_UNACK, AlarmStatus.ACTIVE_UNACK].contains(alarm.status))
CircleAvatar(
radius: 16,
backgroundColor: Color(0xffF0F4F9),
child: IconButton(icon: Icon(Icons.done, size: 18), padding: EdgeInsets.all(7.0), onPressed: () => _ackAlarm(alarm))
),
if ([AlarmStatus.ACTIVE_UNACK, AlarmStatus.ACTIVE_ACK].contains(alarm.status))
Row(
children: [
SizedBox(width: 4),
CircleAvatar(
radius: 16,
backgroundColor: Color(0xffF0F4F9),
child: IconButton(icon: Icon(Icons.clear, size: 18), padding: EdgeInsets.all(7.0), onPressed: () => _clearAlarm(alarm))
)
]
)
],
),
SizedBox(width: 8)
if ([
AlarmStatus.CLEARED_UNACK,
AlarmStatus.ACTIVE_UNACK
].contains(alarm.status))
CircleAvatar(
radius: 16,
backgroundColor: Color(0xffF0F4F9),
child: IconButton(
icon: Icon(Icons.done, size: 18),
padding: EdgeInsets.all(7.0),
onPressed: () =>
_ackAlarm(alarm, context))),
if ([
AlarmStatus.ACTIVE_UNACK,
AlarmStatus.ACTIVE_ACK
].contains(alarm.status))
Row(children: [
SizedBox(width: 4),
CircleAvatar(
radius: 16,
backgroundColor: Color(0xffF0F4F9),
child: IconButton(
icon: Icon(Icons.clear, size: 18),
padding: EdgeInsets.all(7.0),
onPressed: () =>
_clearAlarm(alarm, context)))
])
],
),
SizedBox(height: 8)
]
)
)
]
)
SizedBox(width: 8)
],
),
SizedBox(height: 8)
]))
])
],
);
}
}
_clearAlarm(AlarmInfo alarm) async {
var res = await confirm(title: 'Clear Alarm', message: 'Are you sure you want to clear Alarm?', cancel: 'No', ok: 'Yes');
_clearAlarm(AlarmInfo alarm, BuildContext context) async {
var res = await confirm(
title: '${S.of(context).alarmClearTitle}',
message: '${S.of(context).alarmClearText}',
cancel: '${S.of(context).No}',
ok: '${S.of(context).Yes}');
if (res != null && res) {
setState(() {
loading = true;
});
await tbClient.getAlarmService().clearAlarm(alarm.id!.id!);
var newAlarm = await tbClient.getAlarmService().getAlarmInfo(
alarm.id!.id!);
var newAlarm =
await tbClient.getAlarmService().getAlarmInfo(alarm.id!.id!);
setState(() {
loading = false;
this.alarm = newAlarm!;
@@ -283,20 +300,23 @@ class _AlarmCardState extends TbContextState<AlarmCard> {
}
}
_ackAlarm(AlarmInfo alarm) async {
var res = await confirm(title: 'Acknowledge Alarm', message: 'Are you sure you want to acknowledge Alarm?', cancel: 'No', ok: 'Yes');
_ackAlarm(AlarmInfo alarm, BuildContext context) async {
var res = await confirm(
title: '${S.of(context).alarmAcknowledgeTitle}',
message: '${S.of(context).alarmAcknowledgeText}',
cancel: '${S.of(context).No}',
ok: '${S.of(context).Yes}');
if (res != null && res) {
setState(() {
loading = true;
});
await tbClient.getAlarmService().ackAlarm(alarm.id!.id!);
var newAlarm = await tbClient.getAlarmService().getAlarmInfo(
alarm.id!.id!);
var newAlarm =
await tbClient.getAlarmService().getAlarmInfo(alarm.id!.id!);
setState(() {
loading = false;
this.alarm = newAlarm!;
});
}
}
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/core/entity/entities_list.dart';
@@ -6,9 +5,10 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'alarms_base.dart';
class AlarmsList extends BaseEntitiesWidget<AlarmInfo, AlarmQuery> with AlarmsBase, EntitiesListStateBase {
AlarmsList(TbContext tbContext, PageKeyController<AlarmQuery> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
class AlarmsList extends BaseEntitiesWidget<AlarmInfo, AlarmQuery>
with AlarmsBase, EntitiesListStateBase {
AlarmsList(
TbContext tbContext, PageKeyController<AlarmQuery> pageKeyController,
{searchMode = false})
: super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -7,18 +7,16 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'alarms_list.dart';
class AlarmsPage extends TbContextWidget {
final bool searchMode;
AlarmsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext);
@override
_AlarmsPageState createState() => _AlarmsPageState();
}
class _AlarmsPageState extends TbContextState<AlarmsPage> with AutomaticKeepAliveClientMixin<AlarmsPage> {
class _AlarmsPageState extends TbContextState<AlarmsPage>
with AutomaticKeepAliveClientMixin<AlarmsPage> {
final AlarmQueryController _alarmQueryController = AlarmQueryController();
@override
@@ -29,32 +27,26 @@ class _AlarmsPageState extends TbContextState<AlarmsPage> with AutomaticKeepAliv
@override
Widget build(BuildContext context) {
super.build(context);
var alarmsList = AlarmsList(tbContext, _alarmQueryController, searchMode: widget.searchMode);
var alarmsList = AlarmsList(tbContext, _alarmQueryController,
searchMode: widget.searchMode);
PreferredSizeWidget appBar;
if (widget.searchMode) {
appBar = TbAppSearchBar(
tbContext,
onSearch: (searchText) => _alarmQueryController.onSearchText(searchText),
onSearch: (searchText) =>
_alarmQueryController.onSearchText(searchText),
);
} else {
appBar = TbAppBar(
tbContext,
title: Text(alarmsList.title),
actions: [
IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/alarms?search=true');
},
)
]);
appBar = TbAppBar(tbContext, title: Text(alarmsList.title), actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
navigateTo('/alarms?search=true');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: alarmsList
);
return Scaffold(appBar: appBar, body: alarmsList);
}
@override
@@ -62,5 +54,4 @@ class _AlarmsPageState extends TbContextState<AlarmsPage> with AutomaticKeepAliv
_alarmQueryController.dispose();
super.dispose();
}
}

View File

@@ -2,14 +2,15 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entity_details_page.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AssetDetailsPage extends EntityDetailsPage<AssetInfo> {
AssetDetailsPage(TbContext tbContext, String assetId):
super(tbContext,
entityId: assetId,
defaultTitle: 'Asset', subTitle: 'Asset details');
AssetDetailsPage(TbContext tbContext, String assetId)
: super(tbContext,
entityId: assetId,
defaultTitle: 'Asset',
subTitle: 'Asset details');
@override
Future<AssetInfo?> fetchEntity(String assetId) {
@@ -24,20 +25,18 @@ class AssetDetailsPage extends EntityDetailsPage<AssetInfo> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text('Asset name', style: labelTextStyle),
Text('${S.of(context).assetName}', style: labelTextStyle),
Text(asset.name, style: valueTextStyle),
SizedBox(height: 16),
Text('Type', style: labelTextStyle),
Text('${S.of(context).type}', style: labelTextStyle),
Text(asset.type, style: valueTextStyle),
SizedBox(height: 16),
Text('Label', style: labelTextStyle),
Text('${S.of(context).label}', style: labelTextStyle),
Text(asset.label ?? '', style: valueTextStyle),
SizedBox(height: 16),
Text('Assigned to customer', style: labelTextStyle),
Text('${S.of(context).assignedToCustomer}',
style: labelTextStyle),
Text(asset.customerTitle ?? '', style: valueTextStyle),
]
)
);
]));
}
}

View File

@@ -7,13 +7,14 @@ import 'package:thingsboard_app/modules/asset/assets_page.dart';
import 'asset_details_page.dart';
class AssetRoutes extends TbRoutes {
late var assetsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var assetsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
return AssetsPage(tbContext, searchMode: searchMode);
});
late var assetDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var assetDetailsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return AssetDetailsPage(tbContext, params["id"][0]);
});
@@ -24,5 +25,4 @@ class AssetRoutes extends TbRoutes {
router.define("/assets", handler: assetsHandler);
router.define("/asset/:id", handler: assetDetailsHandler);
}
}

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin AssetsBase on EntitiesBase<AssetInfo, PageLink> {
@override
String get title => 'Assets';
@@ -16,7 +14,9 @@ mixin AssetsBase on EntitiesBase<AssetInfo, PageLink> {
if (tbClient.isTenantAdmin()) {
return tbClient.getAssetService().getTenantAssetInfos(pageLink);
} else {
return tbClient.getAssetService().getCustomerAssetInfos(tbClient.getAuthUser()!.customerId, pageLink);
return tbClient
.getAssetService()
.getCustomerAssetInfos(tbClient.getAuthUser()!.customerId!, pageLink);
}
}
@@ -41,115 +41,91 @@ mixin AssetsBase on EntitiesBase<AssetInfo, PageLink> {
}
Widget _buildCard(context, AssetInfo asset) {
return Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
fit: FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${asset.name}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14
))
),
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(asset.createdTime!)),
return Row(mainAxisSize: MainAxisSize.max, children: [
Flexible(
fit: FlexFit.tight,
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${asset.name}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
),
SizedBox(height: 4),
Text('${asset.type}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
),
SizedBox(width: 16),
Icon(Icons.chevron_right, color: Color(0xFFACACAC)),
SizedBox(width: 16)
],
),
)
)
]
);
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14))),
Text(
entityDateFormat.format(
DateTime.fromMillisecondsSinceEpoch(
asset.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12))
]),
SizedBox(height: 4),
Text('${asset.type}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33))
],
)),
SizedBox(width: 16),
Icon(Icons.chevron_right, color: Color(0xFFACACAC)),
SizedBox(width: 16)
],
),
))
]);
}
Widget _buildListWidgetCard(BuildContext context, AssetInfo asset) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
fit: FlexFit.loose,
child:
Container(
padding: EdgeInsets.symmetric(vertical: 9, horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
fit: FlexFit.loose,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${asset.name}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.7
))
),
Text('${asset.type}',
return Row(mainAxisSize: MainAxisSize.min, children: [
Flexible(
fit: FlexFit.loose,
child: Container(
padding: EdgeInsets.symmetric(vertical: 9, horizontal: 16),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Flexible(
fit: FlexFit.loose,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${asset.name}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
)
]
)
)
)
]
);
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.7))),
Text('${asset.type}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33))
],
))
])))
]);
}
}

View File

@@ -5,9 +5,9 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'assets_base.dart';
class AssetsList extends BaseEntitiesWidget<AssetInfo, PageLink> with AssetsBase, EntitiesListStateBase {
AssetsList(TbContext tbContext, PageKeyController<PageLink> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
class AssetsList extends BaseEntitiesWidget<AssetInfo, PageLink>
with AssetsBase, EntitiesListStateBase {
AssetsList(TbContext tbContext, PageKeyController<PageLink> pageKeyController,
{searchMode = false})
: super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -3,13 +3,14 @@ import 'package:thingsboard_app/core/entity/entities_list_widget.dart';
import 'package:thingsboard_app/modules/asset/assets_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AssetsListWidget extends EntitiesListPageLinkWidget<AssetInfo> with AssetsBase {
AssetsListWidget(TbContext tbContext, {EntitiesListWidgetController? controller}): super(tbContext, controller: controller);
class AssetsListWidget extends EntitiesListPageLinkWidget<AssetInfo>
with AssetsBase {
AssetsListWidget(TbContext tbContext,
{EntitiesListWidgetController? controller})
: super(tbContext, controller: controller);
@override
void onViewAll() {
navigateTo('/assets');
}
}

View File

@@ -7,23 +7,21 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'assets_list.dart';
class AssetsPage extends TbPageWidget {
final bool searchMode;
AssetsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext);
@override
_AssetsPageState createState() => _AssetsPageState();
}
class _AssetsPageState extends TbPageState<AssetsPage> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
var assetsList = AssetsList(tbContext, _pageLinkController, searchMode: widget.searchMode);
var assetsList = AssetsList(tbContext, _pageLinkController,
searchMode: widget.searchMode);
PreferredSizeWidget appBar;
if (widget.searchMode) {
appBar = TbAppSearchBar(
@@ -31,24 +29,16 @@ class _AssetsPageState extends TbPageState<AssetsPage> {
onSearch: (searchText) => _pageLinkController.onSearchText(searchText),
);
} else {
appBar = TbAppBar(
tbContext,
title: Text(assetsList.title),
actions: [
IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/assets?search=true');
},
)
]);
appBar = TbAppBar(tbContext, title: Text(assetsList.title), actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
navigateTo('/assets?search=true');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: assetsList
);
return Scaffold(appBar: appBar, body: assetsList);
}
@override
@@ -56,5 +46,4 @@ class _AssetsPageState extends TbPageState<AssetsPage> {
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -4,34 +4,26 @@ import 'package:flutter/material.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_app/modules/audit_log/audit_logs_base.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AuditLogDetailsPage extends TbContextWidget {
final AuditLog auditLog;
AuditLogDetailsPage(TbContext tbContext, this.auditLog) : super(tbContext);
@override
_AuditLogDetailsPageState createState() => _AuditLogDetailsPageState();
}
class _AuditLogDetailsPageState extends TbContextState<AuditLogDetailsPage> {
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 JsonEncoder encoder = new JsonEncoder.withIndent(' ');
@@ -39,51 +31,52 @@ class _AuditLogDetailsPageState extends TbContextState<AuditLogDetailsPage> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: TbAppBar(
tbContext,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.auditLog.entityName != null)
Text(widget.auditLog.entityName!, style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
height: 20 / 16
)),
Text('Audit log details', style: TextStyle(
color: Theme.of(context).primaryTextTheme.headline6!.color!.withAlpha((0.38 * 255).ceil()),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
)
),
appBar: TbAppBar(tbContext,
title:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
if (widget.auditLog.entityName != null)
Text(widget.auditLog.entityName!,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
height: 20 / 16)),
Text('${S.of(context).auditLogDetails}',
style: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headline6!
.color!
.withAlpha((0.38 * 255).ceil()),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12))
])),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text('Entity Type', style: labelTextStyle),
Text(entityTypeTranslations[widget.auditLog.entityId.entityType]!, style: valueTextStyle),
SizedBox(height: 16),
Text('Type', style: labelTextStyle),
Text(actionTypeTranslations[widget.auditLog.actionType]!, style: valueTextStyle),
SizedBox(height: 16),
Flexible(
fit: FlexFit.loose,
child: buildBorderedText('Action data', encoder.convert(widget.auditLog.actionData))
),
if (widget.auditLog.actionStatus == ActionStatus.FAILURE)
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text('${S.of(context).entityType}', style: labelTextStyle),
Text(entityTypeTranslations[widget.auditLog.entityId.entityType]!,
style: valueTextStyle),
SizedBox(height: 16),
Text('${S.of(context).type}', style: labelTextStyle),
Text(actionTypeTranslations[widget.auditLog.actionType]!,
style: valueTextStyle),
SizedBox(height: 16),
if (widget.auditLog.actionStatus == ActionStatus.FAILURE)
Flexible(
fit: FlexFit.loose,
child: buildBorderedText('Failure details', widget.auditLog.actionFailureDetails!)
)
]
),
fit: FlexFit.loose,
child: buildBorderedText('${S.of(context).actionData}',
encoder.convert(widget.auditLog.actionData))),
if (widget.auditLog.actionStatus == ActionStatus.FAILURE)
SizedBox(height: 16),
if (widget.auditLog.actionStatus == ActionStatus.FAILURE)
Flexible(
fit: FlexFit.loose,
child: buildBorderedText('${S.of(context).failureDetails}',
widget.auditLog.actionFailureDetails!))
]),
),
);
}
@@ -96,8 +89,7 @@ class _AuditLogDetailsPageState extends TbContextState<AuditLogDetailsPage> {
padding: EdgeInsets.fromLTRB(16, 18, 48, 18),
margin: EdgeInsets.only(top: 6),
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFDEDEDE), width: 1),
border: Border.all(color: Color(0xFFDEDEDE), width: 1),
borderRadius: BorderRadius.circular(4),
shape: BoxShape.rectangle,
),
@@ -105,10 +97,7 @@ class _AuditLogDetailsPageState extends TbContextState<AuditLogDetailsPage> {
child: Text(
content,
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
height: 20 / 14
),
color: Color(0xFF282828), fontSize: 14, height: 20 / 14),
),
),
),
@@ -120,11 +109,11 @@ class _AuditLogDetailsPageState extends TbContextState<AuditLogDetailsPage> {
color: Colors.white,
child: Text(
title,
style: TextStyle(color: Color(0xFF757575), fontSize: 12, height: 14 / 12),
style: TextStyle(
color: Color(0xFF757575), fontSize: 12, height: 14 / 12),
),
)),
],
);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
@@ -46,7 +45,6 @@ const Map<ActionStatus, String> actionStatusTranslations = {
};
mixin AuditLogsBase on EntitiesBase<AuditLog, TimePageLink> {
@override
String get title => 'Audit Logs';
@@ -59,8 +57,7 @@ mixin AuditLogsBase on EntitiesBase<AuditLog, TimePageLink> {
}
@override
void onEntityTap(AuditLog auditLog) {
}
void onEntityTap(AuditLog auditLog) {}
@override
Widget buildEntityListCard(BuildContext context, AuditLog auditLog) {
@@ -73,21 +70,18 @@ mixin AuditLogsBase on EntitiesBase<AuditLog, TimePageLink> {
}
class AuditLogCard extends TbContextWidget {
final AuditLog auditLog;
AuditLogCard(TbContext tbContext, {required this.auditLog}) : super(tbContext);
AuditLogCard(TbContext tbContext, {required this.auditLog})
: super(tbContext);
@override
_AuditLogCardState createState() => _AuditLogCardState();
}
class _AuditLogCardState extends TbContextState<AuditLogCard> {
final entityDateFormat = DateFormat('yyyy-MM-dd');
@override
void initState() {
super.initState();
@@ -100,131 +94,141 @@ class _AuditLogCardState extends TbContextState<AuditLogCard> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: Container(
alignment: Alignment.centerLeft,
child: Container(
width: 4,
decoration: BoxDecoration(
color: widget.auditLog.actionStatus == ActionStatus.SUCCESS ? Color(0xFF008A00) : Color(0xFFFF0000),
borderRadius: BorderRadius.only(topLeft: Radius.circular(4), bottomLeft: Radius.circular(4))
),
)
)
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(width: 4),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
return Stack(
children: [
Positioned.fill(
child: Container(
alignment: Alignment.centerLeft,
child: Container(
width: 4,
decoration: BoxDecoration(
color:
widget.auditLog.actionStatus == ActionStatus.SUCCESS
? Color(0xFF008A00)
: Color(0xFFFF0000),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
bottomLeft: Radius.circular(4))),
))),
Row(mainAxisSize: MainAxisSize.max, children: [
SizedBox(width: 4),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
child: AutoSizeText(widget.auditLog.entityName ?? '',
maxLines: 2,
minFontSize: 8,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14)
)
),
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(widget.auditLog.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight: FontWeight.normal,
fontSize: 12,
height: 16 / 12)
)
]
),
SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
child: Text(entityTypeTranslations[widget.auditLog.entityId.entityType]!,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight: FontWeight.normal,
fontSize: 12,
height: 16 / 12)
)
),
Text(actionStatusTranslations[widget.auditLog.actionStatus]!,
style: TextStyle(
color: widget.auditLog.actionStatus == ActionStatus.SUCCESS ? Color(0xFF008A00) : Color(0xFFFF0000),
fontWeight: FontWeight.w500,
fontSize: 12,
height: 16 / 12)
)
]
),
SizedBox(height: 12)],
)
),
SizedBox(width: 16)
]
),
SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Text(actionTypeTranslations[widget.auditLog.actionType]!,
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.normal,
fontSize: 14,
height: 20 / 14)
)
),
SizedBox(height: 32),
CircleAvatar(
radius: 16,
backgroundColor: Color(0xffF0F4F9),
child: IconButton(icon: Icon(Icons.code, size: 18), padding: EdgeInsets.all(7.0), onPressed: () => _auditLogDetails(widget.auditLog))
),
SizedBox(width: 8)
],
),
SizedBox(height: 8)
]
)
)
]
)
],
);
Flexible(
fit: FlexFit.tight,
child: AutoSizeText(
widget.auditLog.entityName ??
'',
maxLines: 2,
minFontSize: 8,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14))),
Text(
entityDateFormat.format(DateTime
.fromMillisecondsSinceEpoch(
widget.auditLog
.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight: FontWeight.normal,
fontSize: 12,
height: 16 / 12))
]),
SizedBox(height: 4),
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
child: Text(
entityTypeTranslations[widget
.auditLog
.entityId
.entityType]!,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight:
FontWeight.normal,
fontSize: 12,
height: 16 / 12))),
Text(
actionStatusTranslations[
widget.auditLog.actionStatus]!,
style: TextStyle(
color: widget.auditLog
.actionStatus ==
ActionStatus.SUCCESS
? Color(0xFF008A00)
: Color(0xFFFF0000),
fontWeight: FontWeight.w500,
fontSize: 12,
height: 16 / 12))
]),
SizedBox(height: 12)
],
)),
SizedBox(width: 16)
]),
SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 16),
Flexible(
fit: FlexFit.tight,
child: Text(
actionTypeTranslations[
widget.auditLog.actionType]!,
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.normal,
fontSize: 14,
height: 20 / 14))),
SizedBox(height: 32),
CircleAvatar(
radius: 16,
backgroundColor: Color(0xffF0F4F9),
child: IconButton(
icon: Icon(Icons.code, size: 18),
padding: EdgeInsets.all(7.0),
onPressed: () =>
_auditLogDetails(widget.auditLog))),
SizedBox(width: 8)
],
),
SizedBox(height: 8)
]))
])
],
);
}
_auditLogDetails(AuditLog auditLog) {
tbContext.showFullScreenDialog(new AuditLogDetailsPage(tbContext, auditLog));
tbContext
.showFullScreenDialog(new AuditLogDetailsPage(tbContext, auditLog));
}
}

View File

@@ -4,8 +4,10 @@ import 'package:thingsboard_app/core/entity/entities_list.dart';
import 'package:thingsboard_app/modules/audit_log/audit_logs_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AuditLogsList extends BaseEntitiesWidget<AuditLog, TimePageLink> with AuditLogsBase, EntitiesListStateBase {
AuditLogsList(TbContext tbContext, PageKeyController<TimePageLink> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
class AuditLogsList extends BaseEntitiesWidget<AuditLog, TimePageLink>
with AuditLogsBase, EntitiesListStateBase {
AuditLogsList(
TbContext tbContext, PageKeyController<TimePageLink> pageKeyController,
{searchMode = false})
: super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -6,48 +6,41 @@ import 'package:thingsboard_app/modules/audit_log/audit_logs_list.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class AuditLogsPage extends TbPageWidget {
final bool searchMode;
AuditLogsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext);
AuditLogsPage(TbContext tbContext, {this.searchMode = false})
: super(tbContext);
@override
_AuditLogsPageState createState() => _AuditLogsPageState();
}
class _AuditLogsPageState extends TbPageState<AuditLogsPage> {
final TimePageLinkController _timePageLinkController = TimePageLinkController();
final TimePageLinkController _timePageLinkController =
TimePageLinkController();
@override
Widget build(BuildContext context) {
var auditLogsList = AuditLogsList(tbContext, _timePageLinkController, searchMode: widget.searchMode);
var auditLogsList = AuditLogsList(tbContext, _timePageLinkController,
searchMode: widget.searchMode);
PreferredSizeWidget appBar;
if (widget.searchMode) {
appBar = TbAppSearchBar(
tbContext,
onSearch: (searchText) => _timePageLinkController.onSearchText(searchText),
onSearch: (searchText) =>
_timePageLinkController.onSearchText(searchText),
);
} else {
appBar = TbAppBar(
tbContext,
title: Text(auditLogsList.title),
actions: [
IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/auditLogs?search=true');
},
)
]);
appBar = TbAppBar(tbContext, title: Text(auditLogsList.title), actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
navigateTo('/auditLogs?search=true');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: auditLogsList
);
return Scaffold(appBar: appBar, body: auditLogsList);
}
@override
@@ -55,5 +48,4 @@ class _AuditLogsPageState extends TbPageState<AuditLogsPage> {
_timePageLinkController.dispose();
super.dispose();
}
}

View File

@@ -5,8 +5,8 @@ import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/audit_log/audit_logs_page.dart';
class AuditLogsRoutes extends TbRoutes {
late var auditLogsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var auditLogsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
return AuditLogsPage(tbContext, searchMode: searchMode);
});
@@ -17,5 +17,4 @@ class AuditLogsRoutes extends TbRoutes {
void doRegisterRoutes(router) {
router.define("/auditLogs", handler: auditLogsHandler);
}
}

View File

@@ -3,13 +3,14 @@ import 'package:thingsboard_app/core/entity/entity_details_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class CustomerDetailsPage extends ContactBasedDetailsPage<Customer> {
CustomerDetailsPage(TbContext tbContext, String customerId):
super(tbContext, entityId: customerId, defaultTitle: 'Customer', subTitle: 'Customer details');
CustomerDetailsPage(TbContext tbContext, String customerId)
: super(tbContext,
entityId: customerId,
defaultTitle: 'Customer',
subTitle: 'Customer details');
@override
Future<Customer?> fetchEntity(String customerId) {
return tbClient.getCustomerService().getCustomer(customerId);
}
}

View File

@@ -6,13 +6,14 @@ import 'customer_details_page.dart';
import 'customers_page.dart';
class CustomerRoutes extends TbRoutes {
late var customersHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var customersHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
return CustomersPage(tbContext, searchMode: searchMode);
});
late var customerDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var customerDetailsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return CustomerDetailsPage(tbContext, params["id"][0]);
});
@@ -23,5 +24,4 @@ class CustomerRoutes extends TbRoutes {
router.define("/customers", handler: customersHandler);
router.define("/customer/:id", handler: customerDetailsHandler);
}
}

View File

@@ -1,8 +1,7 @@
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin CustomersBase on EntitiesBase<Customer, PageLink> {
mixin CustomersBase on EntitiesBase<Customer, PageLink> {
@override
String get title => 'Customers';
@@ -18,5 +17,4 @@ mixin CustomersBase on EntitiesBase<Customer, PageLink> {
void onEntityTap(Customer customer) {
navigateTo('/customer/${customer.id!.id}');
}
}

View File

@@ -5,8 +5,10 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'customers_base.dart';
class CustomersList extends BaseEntitiesWidget<Customer, PageLink> with CustomersBase, ContactBasedBase, EntitiesListStateBase {
CustomersList(TbContext tbContext, PageKeyController<PageLink> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
class CustomersList extends BaseEntitiesWidget<Customer, PageLink>
with CustomersBase, ContactBasedBase, EntitiesListStateBase {
CustomersList(
TbContext tbContext, PageKeyController<PageLink> pageKeyController,
{searchMode = false})
: super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -6,23 +6,22 @@ import 'package:thingsboard_app/modules/customer/customers_list.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class CustomersPage extends TbPageWidget {
final bool searchMode;
CustomersPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext);
CustomersPage(TbContext tbContext, {this.searchMode = false})
: super(tbContext);
@override
_CustomersPageState createState() => _CustomersPageState();
}
class _CustomersPageState extends TbPageState<CustomersPage> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
var customersList = CustomersList(tbContext, _pageLinkController, searchMode: widget.searchMode);
var customersList = CustomersList(tbContext, _pageLinkController,
searchMode: widget.searchMode);
PreferredSizeWidget appBar;
if (widget.searchMode) {
appBar = TbAppSearchBar(
@@ -30,24 +29,16 @@ class _CustomersPageState extends TbPageState<CustomersPage> {
onSearch: (searchText) => _pageLinkController.onSearchText(searchText),
);
} else {
appBar = TbAppBar(
tbContext,
title: Text(customersList.title),
actions: [
IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/customers?search=true');
},
)
]);
appBar = TbAppBar(tbContext, title: Text(customersList.title), actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
navigateTo('/customers?search=true');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: customersList
);
return Scaffold(appBar: appBar, body: customersList);
}
@override
@@ -55,5 +46,4 @@ class _CustomersPageState extends TbPageState<CustomersPage> {
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -3,7 +3,6 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:thingsboard_app/constants/app_constants.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
@@ -11,10 +10,9 @@ import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
import 'package:thingsboard_app/widgets/two_value_listenable_builder.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
class DashboardController {
final ValueNotifier<bool> canGoBack = ValueNotifier(false);
final ValueNotifier<bool> hasRightLayout = ValueNotifier(false);
final ValueNotifier<bool> rightLayoutOpened = ValueNotifier(false);
@@ -22,8 +20,10 @@ class DashboardController {
final _DashboardState dashboardState;
DashboardController(this.dashboardState);
Future<void> openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async {
return await dashboardState._openDashboard(dashboardId, state: state, hideToolbar: hideToolbar, fullscreen: fullscreen);
Future<void> openDashboard(String dashboardId,
{String? state, bool? hideToolbar, bool fullscreen = false}) async {
return await dashboardState._openDashboard(dashboardId,
state: state, hideToolbar: hideToolbar, fullscreen: fullscreen);
}
Future<bool> goBack() async {
@@ -59,22 +59,26 @@ class DashboardController {
hasRightLayout.dispose();
rightLayoutOpened.dispose();
}
}
typedef DashboardTitleCallback = void Function(String title);
typedef DashboardControllerCallback = void Function(DashboardController controller);
typedef DashboardControllerCallback = void Function(
DashboardController controller);
class Dashboard extends TbContextWidget {
final bool? _home;
final bool _activeByDefault;
final DashboardTitleCallback? _titleCallback;
final DashboardControllerCallback? _controllerCallback;
Dashboard(TbContext tbContext, {Key? key, bool? home, bool activeByDefault = true, DashboardTitleCallback? titleCallback, DashboardControllerCallback? controllerCallback}):
this._home = home,
Dashboard(TbContext tbContext,
{Key? key,
bool? home,
bool activeByDefault = true,
DashboardTitleCallback? titleCallback,
DashboardControllerCallback? controllerCallback})
: this._home = home,
this._activeByDefault = activeByDefault,
this._titleCallback = titleCallback,
this._controllerCallback = controllerCallback,
@@ -82,12 +86,11 @@ class Dashboard extends TbContextWidget {
@override
_DashboardState createState() => _DashboardState();
}
class _DashboardState extends TbContextState<Dashboard> {
final Completer<InAppWebViewController> _controller = Completer<InAppWebViewController>();
final Completer<InAppWebViewController> _controller =
Completer<InAppWebViewController>();
bool webViewLoading = true;
final ValueNotifier<bool> dashboardLoading = ValueNotifier(true);
@@ -98,8 +101,6 @@ class _DashboardState extends TbContextState<Dashboard> {
late final DashboardController _dashboardController;
bool _fullscreen = false;
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true,
@@ -110,13 +111,10 @@ class _DashboardState extends TbContextState<Dashboard> {
// useOnDownloadStart: true
),
android: AndroidInAppWebViewOptions(
useHybridComposition: true,
thirdPartyCookiesEnabled: true
),
useHybridComposition: true, thirdPartyCookiesEnabled: true),
ios: IOSInAppWebViewOptions(
allowsInlineMediaPlayback: true,
allowsBackForwardNavigationGestures: false
));
allowsInlineMediaPlayback: true,
allowsBackForwardNavigationGestures: false));
late Uri _initialUrl;
@@ -137,7 +135,8 @@ class _DashboardState extends TbContextState<Dashboard> {
void _onAuthenticated() async {
if (tbContext.isAuthenticated) {
if (!readyState.value) {
_initialUrl = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + '?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}');
_initialUrl = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint +
'?accessToken=${tbClient.getJwtToken()!}&refreshToken=${tbClient.getRefreshToken()!}');
readyState.value = true;
} else {
var windowMessage = <String, dynamic>{
@@ -193,8 +192,8 @@ class _DashboardState extends TbContextState<Dashboard> {
}
}
Future<void> _openDashboard(String dashboardId, {String? state, bool? hideToolbar, bool fullscreen = false}) async {
_fullscreen = fullscreen;
Future<void> _openDashboard(String dashboardId,
{String? state, bool? hideToolbar, bool fullscreen = false}) async {
dashboardLoading.value = true;
InAppWebViewController? controller;
if (!UniversalPlatform.isWeb) {
@@ -202,9 +201,7 @@ class _DashboardState extends TbContextState<Dashboard> {
}
var windowMessage = <String, dynamic>{
'type': 'openDashboardMessage',
'data': <String, dynamic>{
'dashboardId': dashboardId
}
'data': <String, dynamic>{'dashboardId': dashboardId}
};
if (state != null) {
windowMessage['data']['state'] = state;
@@ -217,18 +214,17 @@ class _DashboardState extends TbContextState<Dashboard> {
}
var webMessage = WebMessage(data: jsonEncode(windowMessage));
if (!UniversalPlatform.isWeb) {
await controller!.postWebMessage(
message: webMessage, targetOrigin: Uri.parse('*'));
await controller!
.postWebMessage(message: webMessage, targetOrigin: Uri.parse('*'));
}
}
Future<void> _toggleRightLayout() async {
var controller = await _controller.future;
var windowMessage = <String, dynamic>{
'type': 'toggleDashboardLayout'
};
var windowMessage = <String, dynamic>{'type': 'toggleDashboardLayout'};
var webMessage = WebMessage(data: jsonEncode(windowMessage));
await controller.postWebMessage(message: webMessage, targetOrigin: Uri.parse('*'));
await controller.postWebMessage(
message: webMessage, targetOrigin: Uri.parse('*'));
}
Future<void> tryLocalNavigation(String? path) async {
@@ -244,7 +240,8 @@ class _DashboardState extends TbContextState<Dashboard> {
'customers',
'auditLogs'
].contains(parts[0])) {
if ((parts[0] == 'dashboard' || parts[0] == 'dashboards') && parts.length > 1) {
if ((parts[0] == 'dashboard' || parts[0] == 'dashboards') &&
parts.length > 1) {
var dashboardId = parts[1];
await navigateToDashboard(dashboardId);
} else if (parts[0] != 'dashboard') {
@@ -261,147 +258,176 @@ class _DashboardState extends TbContextState<Dashboard> {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (widget._home == true && !tbContext.isHomePage()) {
return true;
}
if (readyState.value) {
return await _goBack();
onWillPop: () async {
if (widget._home == true && !tbContext.isHomePage()) {
return true;
}
if (readyState.value) {
return await _goBack();
} else {
return true;
}
},
child: ValueListenableBuilder(
valueListenable: readyState,
builder: (BuildContext context, bool ready, child) {
if (!ready) {
return SizedBox.shrink();
} else {
return true;
}
},
child:
ValueListenableBuilder(
valueListenable: readyState,
builder: (BuildContext context, bool ready, child) {
if (!ready) {
return SizedBox.shrink();
} else {
return Stack(
children: [
UniversalPlatform.isWeb ? Center(child: Text('Not implemented!')) :
InAppWebView(
key: webViewKey,
initialUrlRequest: URLRequest(url: _initialUrl),
initialOptions: options,
onWebViewCreated: (webViewController) {
log.debug("onWebViewCreated");
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardLoadedHandler", callback: (args) async {
bool hasRightLayout = args[0];
bool rightLayoutOpened = args[1];
log.debug("Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened");
_dashboardController.onHasRightLayout(hasRightLayout);
_dashboardController.onRightLayoutOpened(rightLayoutOpened);
dashboardLoading.value = false;
});
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardLayoutHandler", callback: (args) async {
bool rightLayoutOpened = args[0];
log.debug("Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened");
_dashboardController.onRightLayoutOpened(rightLayoutOpened);
});
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardStateNameHandler", callback: (args) async {
log.debug("Invoked tbMobileDashboardStateNameHandler: $args");
if (args.isNotEmpty && args[0] is String) {
if (widget._titleCallback != null) {
widget._titleCallback!(args[0]);
}
}
});
webViewController.addJavaScriptHandler(handlerName: "tbMobileNavigationHandler", callback: (args) async {
log.debug("Invoked tbMobileNavigationHandler: $args");
if (args.length > 0) {
String? path = args[0];
Map<String, dynamic>? params;
if (args.length > 1) {
params = args[1];
}
log.debug("path: $path");
log.debug("params: $params");
tryLocalNavigation(path);
}
});
webViewController.addJavaScriptHandler(handlerName: "tbMobileHandler", callback: (args) async {
log.debug("Invoked tbMobileHandler: $args");
return await widgetActionHandler.handleWidgetMobileAction(args, webViewController);
});
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
var uri = navigationAction.request.url!;
var uriString = uri.toString();
log.debug('shouldOverrideUrlLoading $uriString');
if (Platform.isAndroid || Platform.isIOS && navigationAction.iosWKNavigationType == IOSWKNavigationType.LINK_ACTIVATED) {
if (uriString.startsWith(ThingsboardAppConstants.thingsBoardApiEndpoint)) {
var target = uriString.substring(ThingsboardAppConstants.thingsBoardApiEndpoint.length);
if (!target.startsWith("?accessToken")) {
if (target.startsWith("/")) {
target = target.substring(1);
return Stack(children: [
UniversalPlatform.isWeb
? Center(child: Text('Not implemented!'))
: InAppWebView(
key: webViewKey,
initialUrlRequest: URLRequest(url: _initialUrl),
initialOptions: options,
onWebViewCreated: (webViewController) {
log.debug("onWebViewCreated");
webViewController.addJavaScriptHandler(
handlerName: "tbMobileDashboardLoadedHandler",
callback: (args) async {
bool hasRightLayout = args[0];
bool rightLayoutOpened = args[1];
log.debug(
"Invoked tbMobileDashboardLoadedHandler: hasRightLayout: $hasRightLayout, rightLayoutOpened: $rightLayoutOpened");
_dashboardController
.onHasRightLayout(hasRightLayout);
_dashboardController
.onRightLayoutOpened(rightLayoutOpened);
dashboardLoading.value = false;
});
webViewController.addJavaScriptHandler(
handlerName: "tbMobileDashboardLayoutHandler",
callback: (args) async {
bool rightLayoutOpened = args[0];
log.debug(
"Invoked tbMobileDashboardLayoutHandler: rightLayoutOpened: $rightLayoutOpened");
_dashboardController
.onRightLayoutOpened(rightLayoutOpened);
});
webViewController.addJavaScriptHandler(
handlerName:
"tbMobileDashboardStateNameHandler",
callback: (args) async {
log.debug(
"Invoked tbMobileDashboardStateNameHandler: $args");
if (args.isNotEmpty && args[0] is String) {
if (widget._titleCallback != null) {
widget._titleCallback!(args[0]);
}
await tryLocalNavigation(target);
return NavigationActionPolicy.CANCEL;
}
} else if (await canLaunch(uriString)) {
await launch(
uriString,
);
});
webViewController.addJavaScriptHandler(
handlerName: "tbMobileNavigationHandler",
callback: (args) async {
log.debug(
"Invoked tbMobileNavigationHandler: $args");
if (args.length > 0) {
String? path = args[0];
Map<String, dynamic>? params;
if (args.length > 1) {
params = args[1];
}
log.debug("path: $path");
log.debug("params: $params");
tryLocalNavigation(path);
}
});
webViewController.addJavaScriptHandler(
handlerName: "tbMobileHandler",
callback: (args) async {
log.debug("Invoked tbMobileHandler: $args");
return await widgetActionHandler
.handleWidgetMobileAction(
args, webViewController);
});
},
shouldOverrideUrlLoading:
(controller, navigationAction) async {
var uri = navigationAction.request.url!;
var uriString = uri.toString();
log.debug('shouldOverrideUrlLoading $uriString');
if (Platform.isAndroid ||
Platform.isIOS &&
navigationAction.iosWKNavigationType ==
IOSWKNavigationType.LINK_ACTIVATED) {
if (uriString.startsWith(ThingsboardAppConstants
.thingsBoardApiEndpoint)) {
var target = uriString.substring(
ThingsboardAppConstants
.thingsBoardApiEndpoint.length);
if (!target.startsWith("?accessToken")) {
if (target.startsWith("/")) {
target = target.substring(1);
}
await tryLocalNavigation(target);
return NavigationActionPolicy.CANCEL;
}
} else if (await canLaunchUrlString(uriString)) {
await launchUrlString(
uriString,
);
return NavigationActionPolicy.CANCEL;
}
return Platform.isIOS ? NavigationActionPolicy.ALLOW : NavigationActionPolicy.CANCEL;
},
onUpdateVisitedHistory: (controller, url, androidIsReload) async {
log.debug('onUpdateVisitedHistory: $url');
_dashboardController.onHistoryUpdated(controller.canGoBack());
},
onConsoleMessage: (controller, consoleMessage) {
log.debug('[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}');
},
onLoadStart: (controller, url) async {
log.debug('onLoadStart: $url');
},
onLoadStop: (controller, url) async {
log.debug('onLoadStop: $url');
if (webViewLoading) {
webViewLoading = false;
_controller.complete(controller);
}
},
androidOnPermissionRequest: (controller, origin, resources) async {
log.debug('androidOnPermissionRequest origin: $origin, resources: $resources');
return PermissionRequestResponse(
resources: resources,
action: PermissionRequestResponseAction.GRANT);
},
),
if (!UniversalPlatform.isWeb)
TwoValueListenableBuilder(
firstValueListenable: dashboardLoading,
secondValueListenable: dashboardActive,
builder: (BuildContext context, bool loading, bool active, child) {
if (!loading && active) {
return SizedBox.shrink();
} else {
var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window);
var bottomPadding = data.padding.top;
if (widget._home != true) {
bottomPadding += kToolbarHeight;
}
return Container(
padding: EdgeInsets.only(bottom: bottomPadding),
alignment: Alignment.center,
color: Colors.white,
child: TbProgressIndicator(
size: 50.0
),
);
}
}
)
]
);
}
}
)
);
}
return Platform.isIOS
? NavigationActionPolicy.ALLOW
: NavigationActionPolicy.CANCEL;
},
onUpdateVisitedHistory:
(controller, url, androidIsReload) async {
log.debug('onUpdateVisitedHistory: $url');
_dashboardController
.onHistoryUpdated(controller.canGoBack());
},
onConsoleMessage: (controller, consoleMessage) {
log.debug(
'[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}');
},
onLoadStart: (controller, url) async {
log.debug('onLoadStart: $url');
},
onLoadStop: (controller, url) async {
log.debug('onLoadStop: $url');
if (webViewLoading) {
webViewLoading = false;
_controller.complete(controller);
}
},
androidOnPermissionRequest:
(controller, origin, resources) async {
log.debug(
'androidOnPermissionRequest origin: $origin, resources: $resources');
return PermissionRequestResponse(
resources: resources,
action: PermissionRequestResponseAction.GRANT);
},
),
if (!UniversalPlatform.isWeb)
TwoValueListenableBuilder(
firstValueListenable: dashboardLoading,
secondValueListenable: dashboardActive,
builder: (BuildContext context, bool loading,
bool active, child) {
if (!loading && active) {
return SizedBox.shrink();
} else {
var data = MediaQueryData.fromWindow(
WidgetsBinding.instance.window);
var bottomPadding = data.padding.top;
if (widget._home != true) {
bottomPadding += kToolbarHeight;
}
return Container(
padding: EdgeInsets.only(bottom: bottomPadding),
alignment: Alignment.center,
color: Colors.white,
child: TbProgressIndicator(size: 50.0),
);
}
})
]);
}
}));
}
}

View File

@@ -1,31 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class DashboardPage extends TbPageWidget {
final String? _dashboardTitle;
final String? _dashboardId;
final String? _state;
final bool? _fullscreen;
// final String? _dashboardId;
// final String? _state;
// final bool? _fullscreen;
DashboardPage(TbContext tbContext, {String? dashboardId, bool? fullscreen, String? dashboardTitle, String? state}):
_dashboardId = dashboardId,
_fullscreen = fullscreen,
DashboardPage(TbContext tbContext,
{String? dashboardId,
bool? fullscreen,
String? dashboardTitle,
String? state})
:
// _dashboardId = dashboardId,
// _fullscreen = fullscreen,
_dashboardTitle = dashboardTitle,
_state = state,
// _state = state,
super(tbContext);
@override
_DashboardPageState createState() => _DashboardPageState();
}
class _DashboardPageState extends TbPageState<DashboardPage> {
late ValueNotifier<String> dashboardTitleValue;
@override
@@ -37,27 +37,26 @@ class _DashboardPageState extends TbPageState<DashboardPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TbAppBar(
tbContext,
showLoadingIndicator: false,
elevation: 0,
title: ValueListenableBuilder<String>(
valueListenable: dashboardTitleValue,
builder: (context, title, widget) {
return FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title)
);
},
appBar: TbAppBar(
tbContext,
showLoadingIndicator: false,
elevation: 0,
title: ValueListenableBuilder<String>(
valueListenable: dashboardTitleValue,
builder: (context, title, widget) {
return FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title));
},
),
),
),
body: Text('Deprecated') //Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state,
//fullscreen: widget._fullscreen, titleCallback: (title) {
body: Text(
'Deprecated') //Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state,
//fullscreen: widget._fullscreen, titleCallback: (title) {
//dashboardTitleValue.value = title;
//}
//),
);
//}
//),
);
}
}

View File

@@ -8,20 +8,25 @@ import 'package:thingsboard_app/modules/dashboard/fullscreen_dashboard_page.dart
import 'dashboard_page.dart';
class DashboardRoutes extends TbRoutes {
late var dashboardsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var dashboardsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return DashboardsPage(tbContext);
});
late var dashboardDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
late var dashboardDetailsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, List<String>> params) {
var fullscreen = params['fullscreen']?.first == 'true';
var dashboardTitle = params['title']?.first;
var state = params['state']?.first;
return DashboardPage(tbContext, dashboardId: params["id"]![0], fullscreen: fullscreen,
dashboardTitle: dashboardTitle, state: state);
return DashboardPage(tbContext,
dashboardId: params["id"]![0],
fullscreen: fullscreen,
dashboardTitle: dashboardTitle,
state: state);
});
late var fullscreenDashboardHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var fullscreenDashboardHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return FullscreenDashboardPage(tbContext, params["id"]![0]);
});
@@ -31,7 +36,7 @@ class DashboardRoutes extends TbRoutes {
void doRegisterRoutes(router) {
router.define("/dashboards", handler: dashboardsHandler);
router.define("/dashboard/:id", handler: dashboardDetailsHandler);
router.define("/fullscreenDashboard/:id", handler: fullscreenDashboardHandler);
router.define("/fullscreenDashboard/:id",
handler: fullscreenDashboardHandler);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:thingsboard_app/constants/assets_path.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
@@ -10,7 +9,6 @@ import 'package:thingsboard_app/utils/utils.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin DashboardsBase on EntitiesBase<DashboardInfo, PageLink> {
@override
String get title => 'Dashboards';
@@ -20,9 +18,13 @@ mixin DashboardsBase on EntitiesBase<DashboardInfo, PageLink> {
@override
Future<PageData<DashboardInfo>> fetchEntities(PageLink pageLink) {
if (tbClient.isTenantAdmin()) {
return tbClient.getDashboardService().getTenantDashboards(pageLink, mobile: true);
return tbClient
.getDashboardService()
.getTenantDashboards(pageLink, mobile: true);
} else {
return tbClient.getDashboardService().getCustomerDashboards(tbClient.getAuthUser()!.customerId, pageLink, mobile: true);
return tbClient.getDashboardService().getCustomerDashboards(
tbClient.getAuthUser()!.customerId!, pageLink,
mobile: true);
}
}
@@ -39,34 +41,37 @@ mixin DashboardsBase on EntitiesBase<DashboardInfo, PageLink> {
}
@override
Widget buildEntityListWidgetCard(BuildContext context, DashboardInfo dashboard) {
Widget buildEntityListWidgetCard(
BuildContext context, DashboardInfo dashboard) {
return _buildEntityListCard(context, dashboard, true);
}
@override
EntityCardSettings entityGridCardSettings(DashboardInfo dashboard) => EntityCardSettings(dropShadow: true); //dashboard.image != null);
EntityCardSettings entityGridCardSettings(DashboardInfo dashboard) =>
EntityCardSettings(dropShadow: true); //dashboard.image != null);
@override
Widget buildEntityGridCard(BuildContext context, DashboardInfo dashboard) {
return DashboardGridCard(tbContext, dashboard: dashboard);
}
Widget _buildEntityListCard(BuildContext context, DashboardInfo dashboard, bool listWidgetCard) {
Widget _buildEntityListCard(
BuildContext context, DashboardInfo dashboard, bool listWidgetCard) {
return Row(
mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: listWidgetCard ? FlexFit.loose : FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: listWidgetCard ? 9 : 10, horizontal: 16),
child: Container(
padding: EdgeInsets.symmetric(
vertical: listWidgetCard ? 9 : 10, horizontal: 16),
child: Row(
mainAxisSize: listWidgetCard ? MainAxisSize.min : MainAxisSize.max,
mainAxisSize:
listWidgetCard ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: listWidgetCard ? FlexFit.loose : FlexFit.tight,
child:
Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
@@ -77,37 +82,35 @@ mixin DashboardsBase on EntitiesBase<DashboardInfo, PageLink> {
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.7
))
),
height: 1.7))),
Text('${_dashboardDetailsText(dashboard)}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
height: 1.33))
],
)
),
(!listWidgetCard ? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(dashboard.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
) : Container())
)),
(!listWidgetCard
? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
entityDateFormat.format(
DateTime.fromMillisecondsSinceEpoch(
dashboard.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33))
],
)
: Container())
],
),
)
)
]
);
))
]);
}
String _dashboardDetailsText(DashboardInfo dashboard) {
@@ -124,23 +127,20 @@ mixin DashboardsBase on EntitiesBase<DashboardInfo, PageLink> {
bool _isPublicDashboard(DashboardInfo dashboard) {
return dashboard.assignedCustomers.any((element) => element.isPublic);
}
}
class DashboardGridCard extends TbContextWidget {
final DashboardInfo dashboard;
DashboardGridCard(TbContext tbContext, {required this.dashboard}) : super(tbContext);
DashboardGridCard(TbContext tbContext, {required this.dashboard})
: super(tbContext);
@override
_DashboardGridCardState createState() => _DashboardGridCardState();
}
class _DashboardGridCardState extends TbContextState<DashboardGridCard> {
_DashboardGridCardState(): super();
_DashboardGridCardState() : super();
@override
void initState() {
@@ -164,48 +164,37 @@ class _DashboardGridCardState extends TbContextState<DashboardGridCard> {
colorBlendMode: BlendMode.overlay,
semanticsLabel: 'Dashboard');
}
return
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Column(
children: [
Expanded(
child: Stack (
children: [
SizedBox.expand(
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.cover,
child: image
)
)
]
)
),
Divider(height: 1),
Container(
height: 44,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child:
Center(
child: AutoSizeText(widget.dashboard.title,
textAlign: TextAlign.center,
maxLines: 1,
minFontSize: 12,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
),
)
)
),
)
],
)
);
return ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Column(
children: [
Expanded(
child: Stack(children: [
SizedBox.expand(
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.cover,
child: image))
])),
Divider(height: 1),
Container(
height: 44,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child: Center(
child: AutoSizeText(
widget.dashboard.title,
textAlign: TextAlign.center,
maxLines: 1,
minFontSize: 12,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14),
))),
)
],
));
}
}

View File

@@ -8,16 +8,13 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'dashboards_base.dart';
class DashboardsGridWidget extends TbContextWidget {
DashboardsGridWidget(TbContext tbContext) : super(tbContext);
@override
_DashboardsGridWidgetState createState() => _DashboardsGridWidgetState();
}
class _DashboardsGridWidgetState extends TbContextState<DashboardsGridWidget> {
final PageLinkController _pageLinkController = PageLinkController();
@override
@@ -30,13 +27,11 @@ class _DashboardsGridWidgetState extends TbContextState<DashboardsGridWidget> {
_pageLinkController.dispose();
super.dispose();
}
}
class DashboardsGrid extends BaseEntitiesWidget<DashboardInfo, PageLink> with DashboardsBase, EntitiesGridStateBase {
DashboardsGrid(TbContext tbContext, PageKeyController<PageLink> pageKeyController) : super(tbContext, pageKeyController);
class DashboardsGrid extends BaseEntitiesWidget<DashboardInfo, PageLink>
with DashboardsBase, EntitiesGridStateBase {
DashboardsGrid(
TbContext tbContext, PageKeyController<PageLink> pageKeyController)
: super(tbContext, pageKeyController);
}

View File

@@ -5,9 +5,9 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'dashboards_base.dart';
class DashboardsList extends BaseEntitiesWidget<DashboardInfo, PageLink> with DashboardsBase, EntitiesListStateBase {
DashboardsList(TbContext tbContext, PageKeyController<PageLink> pageKeyController) : super(tbContext, pageKeyController);
class DashboardsList extends BaseEntitiesWidget<DashboardInfo, PageLink>
with DashboardsBase, EntitiesListStateBase {
DashboardsList(
TbContext tbContext, PageKeyController<PageLink> pageKeyController)
: super(tbContext, pageKeyController);
}

View File

@@ -3,14 +3,14 @@ import 'package:thingsboard_app/core/entity/entities_list_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboards_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DashboardsListWidget extends EntitiesListPageLinkWidget<DashboardInfo> with DashboardsBase {
DashboardsListWidget(TbContext tbContext, {EntitiesListWidgetController? controller}): super(tbContext, controller: controller);
class DashboardsListWidget extends EntitiesListPageLinkWidget<DashboardInfo>
with DashboardsBase {
DashboardsListWidget(TbContext tbContext,
{EntitiesListWidgetController? controller})
: super(tbContext, controller: controller);
@override
void onViewAll() {
navigateTo('/dashboards');
}
}

View File

@@ -6,30 +6,22 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'dashboards_grid.dart';
class DashboardsPage extends TbPageWidget {
DashboardsPage(TbContext tbContext) : super(tbContext);
@override
_DashboardsPageState createState() => _DashboardsPageState();
}
class _DashboardsPageState extends TbPageState<DashboardsPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TbAppBar(
tbContext,
title: Text('Dashboards')
),
body: DashboardsGridWidget(tbContext)
);
appBar: TbAppBar(tbContext, title: Text('Dashboards')),
body: DashboardsGridWidget(tbContext));
}
@override
void dispose() {
super.dispose();
}
}

View File

@@ -1,26 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class FullscreenDashboardPage extends TbPageWidget {
final String fullscreenDashboardId;
final String? _dashboardTitle;
FullscreenDashboardPage(TbContext tbContext, this.fullscreenDashboardId, {String? dashboardTitle}):
_dashboardTitle = dashboardTitle,
FullscreenDashboardPage(TbContext tbContext, this.fullscreenDashboardId,
{String? dashboardTitle})
: _dashboardTitle = dashboardTitle,
super(tbContext);
@override
_FullscreenDashboardPageState createState() => _FullscreenDashboardPageState();
_FullscreenDashboardPageState createState() =>
_FullscreenDashboardPageState();
}
class _FullscreenDashboardPageState extends TbPageState<FullscreenDashboardPage> {
class _FullscreenDashboardPageState
extends TbPageState<FullscreenDashboardPage> {
late ValueNotifier<String> dashboardTitleValue;
final ValueNotifier<bool> showBackValue = ValueNotifier(false);
@@ -47,13 +46,12 @@ class _FullscreenDashboardPageState extends TbPageState<FullscreenDashboardPage>
child: ValueListenableBuilder<bool>(
valueListenable: showBackValue,
builder: (context, canGoBack, widget) {
return TbAppBar(
tbContext,
leading: canGoBack ? BackButton(
onPressed: () {
maybePop();
}
) : null,
return TbAppBar(tbContext,
leading: canGoBack
? BackButton(onPressed: () {
maybePop();
})
: null,
showLoadingIndicator: false,
elevation: 1,
shadowColor: Colors.transparent,
@@ -63,29 +61,25 @@ class _FullscreenDashboardPageState extends TbPageState<FullscreenDashboardPage>
return FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title)
);
child: Text(title));
},
),
actions: [
IconButton(icon: Icon(Icons.settings), onPressed: () => navigateTo('/profile?fullscreen=true'))
]
);
}
),
IconButton(
icon: Icon(Icons.settings),
onPressed: () =>
navigateTo('/profile?fullscreen=true'))
]);
}),
),
body: Dashboard(
tbContext,
titleCallback: (title) {
dashboardTitleValue.value = title;
},
controllerCallback: (controller) {
controller.canGoBack.addListener(() {
_onCanGoBack(controller.canGoBack.value);
});
controller.openDashboard(widget.fullscreenDashboardId, fullscreen: true);
}
)
);
body: Dashboard(tbContext, titleCallback: (title) {
dashboardTitleValue.value = title;
}, controllerCallback: (controller) {
controller.canGoBack.addListener(() {
_onCanGoBack(controller.canGoBack.value);
});
controller.openDashboard(widget.fullscreenDashboardId,
fullscreen: true);
}));
}
}

View File

@@ -1,12 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class MainDashboardPageController {
DashboardController? _dashboardController;
_MainDashboardPageState? _mainDashboardPageState;
@@ -26,11 +24,13 @@ class MainDashboardPageController {
}
}
Future<void> openDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar}) async {
Future<void> openDashboard(String dashboardId,
{String? dashboardTitle, String? state, bool? hideToolbar}) async {
if (dashboardTitle != null) {
_mainDashboardPageState?._updateTitle(dashboardTitle);
}
await _dashboardController?.openDashboard(dashboardId, state: state, hideToolbar: hideToolbar);
await _dashboardController?.openDashboard(dashboardId,
state: state, hideToolbar: hideToolbar);
}
Future<void> activateDashboard() async {
@@ -40,28 +40,24 @@ class MainDashboardPageController {
Future<void> deactivateDashboard() async {
await _dashboardController?.deactivateDashboard();
}
}
class MainDashboardPage extends TbContextWidget {
final String? _dashboardTitle;
final MainDashboardPageController? _controller;
MainDashboardPage(TbContext tbContext,
{MainDashboardPageController? controller,
String? dashboardTitle}):
_controller = controller,
{MainDashboardPageController? controller, String? dashboardTitle})
: _controller = controller,
_dashboardTitle = dashboardTitle,
super(tbContext);
@override
_MainDashboardPageState createState() => _MainDashboardPageState();
}
class _MainDashboardPageState extends TbContextState<MainDashboardPage> with TickerProviderStateMixin {
class _MainDashboardPageState extends TbContextState<MainDashboardPage>
with TickerProviderStateMixin {
late ValueNotifier<String> dashboardTitleValue;
final ValueNotifier<bool> hasRightLayout = ValueNotifier(false);
DashboardController? _dashboardController;
@@ -76,9 +72,7 @@ class _MainDashboardPageState extends TbContextState<MainDashboardPage> with Tic
duration: Duration(milliseconds: 200),
);
rightLayoutMenuAnimation = CurvedAnimation(
curve: Curves.linear,
parent: rightLayoutMenuController
);
curve: Curves.linear, parent: rightLayoutMenuController);
if (widget._controller != null) {
widget._controller!._setMainDashboardPageState(this);
}
@@ -99,68 +93,57 @@ class _MainDashboardPageState extends TbContextState<MainDashboardPage> with Tic
Widget build(BuildContext context) {
return Scaffold(
appBar: TbAppBar(
tbContext,
leading: BackButton(
onPressed: () {
maybePop();
}
),
showLoadingIndicator: false,
elevation: 1,
shadowColor: Colors.transparent,
title: ValueListenableBuilder<String>(
valueListenable: dashboardTitleValue,
builder: (context, title, widget) {
return FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title)
);
},
),
actions: [
ValueListenableBuilder<bool>(
tbContext,
leading: BackButton(onPressed: () {
maybePop();
}),
showLoadingIndicator: false,
elevation: 1,
shadowColor: Colors.transparent,
title: ValueListenableBuilder<String>(
valueListenable: dashboardTitleValue,
builder: (context, title, widget) {
return FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title));
},
),
actions: [
ValueListenableBuilder<bool>(
valueListenable: hasRightLayout,
builder: (context, _hasRightLayout, widget) {
if (_hasRightLayout) {
return IconButton(
onPressed: () => _dashboardController?.toggleRightLayout(),
onPressed: () =>
_dashboardController?.toggleRightLayout(),
icon: AnimatedIcon(
progress: rightLayoutMenuAnimation,
icon: AnimatedIcons.menu_close
)
);
progress: rightLayoutMenuAnimation,
icon: AnimatedIcons.menu_close));
} else {
return SizedBox.shrink();
}
}
)
],
})
],
),
body: Dashboard(
tbContext,
activeByDefault: false,
body: Dashboard(tbContext, activeByDefault: false,
titleCallback: (title) {
dashboardTitleValue.value = title;
},
controllerCallback: (controller) {
_dashboardController = controller;
if (widget._controller != null) {
widget._controller!._setDashboardController(controller);
controller.hasRightLayout.addListener(() {
hasRightLayout.value = controller.hasRightLayout.value;
});
controller.rightLayoutOpened.addListener(() {
if(controller.rightLayoutOpened.value) {
rightLayoutMenuController.forward();
} else {
rightLayoutMenuController.reverse();
}
});
dashboardTitleValue.value = title;
}, controllerCallback: (controller) {
_dashboardController = controller;
if (widget._controller != null) {
widget._controller!._setDashboardController(controller);
controller.hasRightLayout.addListener(() {
hasRightLayout.value = controller.hasRightLayout.value;
});
controller.rightLayoutOpened.addListener(() {
if (controller.rightLayoutOpened.value) {
rightLayoutMenuController.forward();
} else {
rightLayoutMenuController.reverse();
}
}
)
);
});
}
}));
}
}

View File

@@ -1,15 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entity_details_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DeviceDetailsPage extends EntityDetailsPage<DeviceInfo> {
DeviceDetailsPage(TbContext tbContext, String deviceId):
super(tbContext,
entityId: deviceId,
defaultTitle: 'Device');
DeviceDetailsPage(TbContext tbContext, String deviceId)
: super(tbContext, entityId: deviceId, defaultTitle: 'Device');
@override
Future<DeviceInfo?> fetchEntity(String deviceId) {
@@ -23,5 +19,4 @@ class DeviceDetailsPage extends EntityDetailsPage<DeviceInfo> {
subtitle: Text('${device.type}'),
);
}
}

View File

@@ -2,19 +2,18 @@ import 'dart:async';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:thingsboard_app/constants/assets_path.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_app/utils/services/device_profile_cache.dart';
import 'package:thingsboard_app/utils/services/entity_query_api.dart';
import 'package:thingsboard_app/utils/utils.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin DeviceProfilesBase on EntitiesBase<DeviceProfileInfo, PageLink> {
final RefreshDeviceCounts refreshDeviceCounts = RefreshDeviceCounts();
@override
@@ -48,7 +47,8 @@ mixin DeviceProfilesBase on EntitiesBase<DeviceProfileInfo, PageLink> {
}
@override
Widget buildEntityGridCard(BuildContext context, DeviceProfileInfo deviceProfile) {
Widget buildEntityGridCard(
BuildContext context, DeviceProfileInfo deviceProfile) {
return DeviceProfileCard(tbContext, deviceProfile);
}
@@ -56,7 +56,6 @@ mixin DeviceProfilesBase on EntitiesBase<DeviceProfileInfo, PageLink> {
double? gridChildAspectRatio() {
return 156 / 200;
}
}
class RefreshDeviceCounts {
@@ -64,20 +63,20 @@ class RefreshDeviceCounts {
}
class AllDevicesCard extends TbContextWidget {
final RefreshDeviceCounts refreshDeviceCounts;
AllDevicesCard(TbContext tbContext, this.refreshDeviceCounts) : super(tbContext);
AllDevicesCard(TbContext tbContext, this.refreshDeviceCounts)
: super(tbContext);
@override
_AllDevicesCardState createState() => _AllDevicesCardState();
}
class _AllDevicesCardState extends TbContextState<AllDevicesCard> {
final StreamController<int?> _activeDevicesCount = StreamController.broadcast();
final StreamController<int?> _inactiveDevicesCount = StreamController.broadcast();
final StreamController<int?> _activeDevicesCount =
StreamController.broadcast();
final StreamController<int?> _inactiveDevicesCount =
StreamController.broadcast();
@override
void initState() {
@@ -103,9 +102,12 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard> {
Future<void> _countDevices() {
_activeDevicesCount.add(null);
_inactiveDevicesCount.add(null);
Future<int> activeDevicesCount = EntityQueryApi.countDevices(tbClient, active: true);
Future<int> inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, active: false);
Future<List<int>> countsFuture = Future.wait([activeDevicesCount, inactiveDevicesCount]);
Future<int> activeDevicesCount =
EntityQueryApi.countDevices(tbClient, active: true);
Future<int> inactiveDevicesCount =
EntityQueryApi.countDevices(tbClient, active: false);
Future<List<int>> countsFuture =
Future.wait([activeDevicesCount, inactiveDevicesCount]);
countsFuture.then((counts) {
if (this.mounted) {
_activeDevicesCount.add(counts[0]);
@@ -117,142 +119,147 @@ class _AllDevicesCardState extends TbContextState<AllDevicesCard> {
@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: Column(
children: [
Padding(padding: EdgeInsets.fromLTRB(16, 12, 16, 15),
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
child: Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
child: Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 15),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('All devices',
Text('${S.of(context).allDevices}',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
)
),
height: 20 / 14)),
Icon(Icons.arrow_forward, size: 18)
],
)
),
Divider(height: 1),
Padding(padding: EdgeInsets.all(0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(fit: FlexFit.tight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: StreamBuilder<int?>(
stream: _activeDevicesCount.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, true, deviceCount);
} else {
return Center(child:
Container(height: 20, width: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
strokeWidth: 2.5)));
}
},
)
),
onTap: () {
navigateTo('/deviceList?active=true');
}
),
),
// SizedBox(width: 4),
Container(width: 1,
height: 40,
child: VerticalDivider(width: 1)
),
Flexible(fit: FlexFit.tight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: StreamBuilder<int?>(
stream: _inactiveDevicesCount.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, false, deviceCount);
} else {
return Center(child:
Container(height: 20, width: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
strokeWidth: 2.5)));
}
},
)
),
onTap: () {
navigateTo('/deviceList?active=false');
}
),
)
],
)
)
],
)
),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha((255 * 0.05).ceil()),
blurRadius: 6.0,
offset: Offset(0, 4)
)
],
),
)),
Divider(height: 1),
Padding(
padding: EdgeInsets.all(0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
fit: FlexFit.tight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: StreamBuilder<int?>(
stream: _activeDevicesCount.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(
context, true, deviceCount);
} else {
return Center(
child: Container(
height: 20,
width: 20,
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation(
Theme.of(tbContext
.currentState!
.context)
.colorScheme
.primary),
strokeWidth: 2.5)));
}
},
)),
onTap: () {
navigateTo('/deviceList?active=true');
}),
),
// SizedBox(width: 4),
Container(
width: 1,
height: 40,
child: VerticalDivider(width: 1)),
Flexible(
fit: FlexFit.tight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
child: StreamBuilder<int?>(
stream: _inactiveDevicesCount.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(
context, false, deviceCount);
} else {
return Center(
child: Container(
height: 20,
width: 20,
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation(
Theme.of(tbContext
.currentState!
.context)
.colorScheme
.primary),
strokeWidth: 2.5)));
}
},
)),
onTap: () {
navigateTo('/deviceList?active=false');
}),
)
],
))
],
)),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha((255 * 0.05).ceil()),
blurRadius: 6.0,
offset: Offset(0, 4))
],
),
onTap: () {
navigateTo('/deviceList');
}
);
),
onTap: () {
navigateTo('/deviceList');
});
}
}
class DeviceProfileCard extends TbContextWidget {
final DeviceProfileInfo deviceProfile;
DeviceProfileCard(TbContext tbContext, this.deviceProfile) : super(tbContext);
@override
_DeviceProfileCardState createState() => _DeviceProfileCardState();
}
class _DeviceProfileCardState extends TbContextState<DeviceProfileCard> {
late Future<int> activeDevicesCount;
late Future<int> inactiveDevicesCount;
@@ -269,8 +276,10 @@ class _DeviceProfileCardState extends TbContextState<DeviceProfileCard> {
}
_countDevices() {
activeDevicesCount = EntityQueryApi.countDevices(tbClient, deviceType: widget.deviceProfile.name, active: true);
inactiveDevicesCount = EntityQueryApi.countDevices(tbClient, deviceType: widget.deviceProfile.name, active: false);
activeDevicesCount = EntityQueryApi.countDevices(tbClient,
deviceType: widget.deviceProfile.name, active: true);
inactiveDevicesCount = EntityQueryApi.countDevices(tbClient,
deviceType: widget.deviceProfile.name, active: false);
}
@override
@@ -292,99 +301,95 @@ class _DeviceProfileCardState extends TbContextState<DeviceProfileCard> {
imageFit = BoxFit.cover;
padding = 0;
}
return
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Column(
children: [
Expanded(
child: Stack (
children: [
SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(padding),
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: imageFit,
child: image
)
)
)
]
)
),
Container(
height: 44,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child: Center(
child: AutoSizeText(entity.name,
textAlign: TextAlign.center,
maxLines: 1,
minFontSize: 12,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
),
)
)
)
),
Divider(height: 1),
GestureDetector(
behavior: HitTestBehavior.opaque,
child: FutureBuilder<int>(
future: activeDevicesCount,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, true, deviceCount);
} else {
return Container(height: 40,
child: Center(
child: Container(
height: 20, width: 20,
child:
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
strokeWidth: 2.5))));
}
},
),
onTap: () {
navigateTo('/deviceList?active=true&deviceType=${entity.name}');
}
),
Divider(height: 1),
GestureDetector(
behavior: HitTestBehavior.opaque,
child: FutureBuilder<int>(
future: inactiveDevicesCount,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, false, deviceCount);
} else {
return Container(height: 40,
child: Center(
child: Container(
height: 20, width: 20,
child:
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
strokeWidth: 2.5))));
}
},
),
onTap: () {
navigateTo('/deviceList?active=false&deviceType=${entity.name}');
}
)
]
)
);
return ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Column(children: [
Expanded(
child: Stack(children: [
SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(padding),
child: FittedBox(
clipBehavior: Clip.hardEdge,
fit: imageFit,
child: image)))
])),
Container(
height: 44,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child: Center(
child: AutoSizeText(
entity.name,
textAlign: TextAlign.center,
maxLines: 1,
minFontSize: 12,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14),
)))),
Divider(height: 1),
GestureDetector(
behavior: HitTestBehavior.opaque,
child: FutureBuilder<int>(
future: activeDevicesCount,
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, true, deviceCount);
} else {
return Container(
height: 40,
child: Center(
child: Container(
height: 20,
width: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(
tbContext.currentState!.context)
.colorScheme
.primary),
strokeWidth: 2.5))));
}
},
),
onTap: () {
navigateTo('/deviceList?active=true&deviceType=${entity.name}');
}),
Divider(height: 1),
GestureDetector(
behavior: HitTestBehavior.opaque,
child: FutureBuilder<int>(
future: inactiveDevicesCount,
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
var deviceCount = snapshot.data!;
return _buildDeviceCount(context, false, deviceCount);
} else {
return Container(
height: 40,
child: Center(
child: Container(
height: 20,
width: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(
tbContext.currentState!.context)
.colorScheme
.primary),
strokeWidth: 2.5))));
}
},
),
onTap: () {
navigateTo(
'/deviceList?active=false&deviceType=${entity.name}');
})
]));
}
}
@@ -402,26 +407,30 @@ Widget _buildDeviceCount(BuildContext context, bool active, int count) {
Stack(
children: [
Icon(Icons.devices_other, size: 16, color: color),
if (!active) CustomPaint(
size: Size.square(16),
painter: StrikeThroughPainter(color: color, offset: 2),
)
if (!active)
CustomPaint(
size: Size.square(16),
painter: StrikeThroughPainter(color: color, offset: 2),
)
],
),
SizedBox(width: 8.67),
Text(active ? 'Active' : 'Inactive', style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
height: 16 / 12,
color: color
)),
SizedBox(width: 8.67),
Text(count.toString(), style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
height: 16 / 12,
color: color
))
SizedBox(width: 8.67),
Text(
active
? '${S.of(context).active}'
: '${S.of(context).inactive}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
height: 16 / 12,
color: color)),
SizedBox(width: 8.67),
Text(count.toString(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
height: 16 / 12,
color: color))
],
),
Icon(Icons.chevron_right, size: 16, color: Color(0xFFACACAC))
@@ -431,7 +440,6 @@ Widget _buildDeviceCount(BuildContext context, bool active, int count) {
}
class StrikeThroughPainter extends CustomPainter {
final Color color;
final double offset;
@@ -441,7 +449,8 @@ class StrikeThroughPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
paint.strokeWidth = 1.5;
canvas.drawLine(Offset(offset, offset), Offset(size.width - offset, size.height - offset), paint);
canvas.drawLine(Offset(offset, offset),
Offset(size.width - offset, size.height - offset), paint);
paint.color = Colors.white;
canvas.drawLine(Offset(2, 0), Offset(size.width + 2, size.height), paint);
}
@@ -450,5 +459,4 @@ class StrikeThroughPainter extends CustomPainter {
bool shouldRepaint(covariant StrikeThroughPainter oldDelegate) {
return color != oldDelegate.color;
}
}

View File

@@ -5,8 +5,9 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'device_profiles_base.dart';
class DeviceProfilesGrid extends BaseEntitiesWidget<DeviceProfileInfo, PageLink> with DeviceProfilesBase, EntitiesGridStateBase {
DeviceProfilesGrid(TbContext tbContext, PageKeyController<PageLink> pageKeyController) : super(tbContext, pageKeyController);
class DeviceProfilesGrid extends BaseEntitiesWidget<DeviceProfileInfo, PageLink>
with DeviceProfilesBase, EntitiesGridStateBase {
DeviceProfilesGrid(
TbContext tbContext, PageKeyController<PageLink> pageKeyController)
: super(tbContext, pageKeyController);
}

View File

@@ -9,24 +9,28 @@ import 'device_details_page.dart';
import 'devices_list_page.dart';
class DeviceRoutes extends TbRoutes {
late var devicesHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var devicesHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return MainPage(tbContext, path: '/devices');
});
late var devicesPageHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var devicesPageHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return DevicesPage(tbContext);
});
late var deviceListHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var deviceListHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
var deviceType = params['deviceType']?.first;
String? activeStr = params['active']?.first;
bool? active = activeStr != null ? activeStr == 'true' : null;
return DevicesListPage(tbContext, searchMode: searchMode, deviceType: deviceType, active: active);
return DevicesListPage(tbContext,
searchMode: searchMode, deviceType: deviceType, active: active);
});
late var deviceDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var deviceDetailsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return DeviceDetailsPage(tbContext, params["id"][0]);
});
@@ -39,5 +43,4 @@ class DeviceRoutes extends TbRoutes {
router.define("/deviceList", handler: deviceListHandler);
router.define("/device/:id", handler: deviceDetailsHandler);
}
}

View File

@@ -1,20 +1,19 @@
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';
import 'package:thingsboard_app/constants/assets_path.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_app/utils/services/device_profile_cache.dart';
import 'package:thingsboard_app/utils/services/entity_query_api.dart';
import 'package:thingsboard_app/utils/utils.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin DevicesBase on EntitiesBase<EntityData, EntityDataQuery> {
@override
String get title => 'Devices';
@@ -28,14 +27,19 @@ mixin DevicesBase on EntitiesBase<EntityData, EntityDataQuery> {
@override
void onEntityTap(EntityData device) async {
var profile = await DeviceProfileCache.getDeviceProfileInfo(tbClient, device.field('type')!, device.entityId.id!);
var profile = await DeviceProfileCache.getDeviceProfileInfo(
tbClient, device.field('type')!, device.entityId.id!);
if (profile.defaultDashboardId != null) {
var dashboardId = profile.defaultDashboardId!.id!;
var state = Utils.createDashboardEntityState(device.entityId, entityName: device.field('name')!, entityLabel: device.field('label')!);
navigateToDashboard(dashboardId, dashboardTitle: device.field('name'), state: state);
var state = Utils.createDashboardEntityState(device.entityId,
entityName: device.field('name')!,
entityLabel: device.field('label')!);
navigateToDashboard(dashboardId,
dashboardTitle: device.field('name'), state: state);
} else {
if (tbClient.isTenantAdmin()) {
showWarnNotification('Mobile dashboard should be configured in device profile!');
showWarnNotification(
'Mobile dashboard should be configured in device profile!');
}
}
}
@@ -57,42 +61,51 @@ mixin DevicesBase on EntitiesBase<EntityData, EntityDataQuery> {
bool displayCardImage(bool listWidgetCard) => listWidgetCard;
Widget _buildEntityListCard(BuildContext context, EntityData device, bool listWidgetCard) {
return DeviceCard(tbContext, device: device, listWidgetCard: listWidgetCard, displayImage: displayCardImage(listWidgetCard));
Widget _buildEntityListCard(
BuildContext context, EntityData device, bool listWidgetCard) {
return DeviceCard(tbContext,
device: device,
listWidgetCard: listWidgetCard,
displayImage: displayCardImage(listWidgetCard));
}
}
class DeviceQueryController extends PageKeyController<EntityDataQuery> {
DeviceQueryController({int pageSize = 20, String? searchText, String? deviceType, bool? active}):
super(EntityQueryApi.createDefaultDeviceQuery(pageSize: pageSize, searchText: searchText, deviceType: deviceType, active: active));
DeviceQueryController(
{int pageSize = 20, String? searchText, String? deviceType, bool? active})
: super(EntityQueryApi.createDefaultDeviceQuery(
pageSize: pageSize,
searchText: searchText,
deviceType: deviceType,
active: active));
@override
EntityDataQuery nextPageKey(EntityDataQuery deviceQuery) => deviceQuery.next();
EntityDataQuery nextPageKey(EntityDataQuery deviceQuery) =>
deviceQuery.next();
onSearchText(String searchText) {
value.pageKey.pageLink.page = 0;
value.pageKey.pageLink.textSearch = searchText;
notifyListeners();
}
}
class DeviceCard extends TbContextWidget {
final EntityData device;
final bool listWidgetCard;
final bool displayImage;
DeviceCard(TbContext tbContext, {required this.device, this.listWidgetCard = false, this.displayImage = false}) : super(tbContext);
DeviceCard(TbContext tbContext,
{required this.device,
this.listWidgetCard = false,
this.displayImage = false})
: super(tbContext);
@override
_DeviceCardState createState() => _DeviceCardState();
}
class _DeviceCardState extends TbContextState<DeviceCard> {
final entityDateFormat = DateFormat('yyyy-MM-dd');
late Future<DeviceProfileInfo> deviceProfileFuture;
@@ -129,250 +142,256 @@ class _DeviceCardState extends TbContextState<DeviceCard> {
}
Widget buildCard(BuildContext context) {
return Stack(
children: [
Positioned.fill(
return Stack(children: [
Positioned.fill(
child: Container(
alignment: Alignment.centerLeft,
child: Container(
alignment: Alignment.centerLeft,
child: Container(
width: 4,
decoration: BoxDecoration(
color: widget.device.attribute('active') == 'true' ? Color(0xFF008A00) : Color(0xFFAFAFAF),
borderRadius: BorderRadius.only(topLeft: Radius.circular(4), bottomLeft: Radius.circular(4))
),
)
)
),
FutureBuilder<DeviceProfileInfo>(
width: 4,
decoration: BoxDecoration(
color: widget.device.attribute('active') == 'true'
? Color(0xFF008A00)
: Color(0xFFAFAFAF),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
bottomLeft: Radius.circular(4))),
))),
FutureBuilder<DeviceProfileInfo>(
future: deviceProfileFuture,
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
var profile = snapshot.data!;
bool hasDashboard = profile.defaultDashboardId != null;
Widget image;
BoxFit imageFit;
if (profile.image != null) {
image = Utils.imageFromBase64(profile.image!);
imageFit = BoxFit.contain;
} else {
image = SvgPicture.asset(
ThingsboardImage.deviceProfilePlaceholder,
color: Theme.of(context).primaryColor,
colorBlendMode: BlendMode.overlay,
semanticsLabel: 'Device');
imageFit = BoxFit.cover;
}
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 20),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (widget.displayImage)
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(4))),
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(4)),
child: Stack(
children: [
Positioned.fill(
child: FittedBox(
fit: imageFit,
child: image,
))
],
))),
SizedBox(width: 12),
Flexible(
fit: FlexFit.tight,
child: Column(children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment:
Alignment.centerLeft,
child: Text(
'${widget.device.field('name')!}',
style: TextStyle(
color: Color(
0xFF282828),
fontSize: 14,
fontWeight:
FontWeight
.w500,
height:
20 / 14)))),
SizedBox(width: 12),
Text(
entityDateFormat.format(DateTime
.fromMillisecondsSinceEpoch(
widget.device
.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight:
FontWeight.normal,
height: 16 / 12))
]),
SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'${widget.device.field('type')!}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight:
FontWeight.normal,
height: 16 / 12)),
Text(
widget.device.attribute(
'active') ==
'true'
? '${S.of(context).active}'
: '${S.of(context).inactive}',
style: TextStyle(
color: widget.device
.attribute(
'active') ==
'true'
? Color(0xFF008A00)
: Color(0xFFAFAFAF),
fontSize: 12,
height: 16 / 12,
fontWeight: FontWeight.normal,
))
],
)
])),
SizedBox(width: 16),
if (hasDashboard)
Icon(Icons.chevron_right,
color: Color(0xFFACACAC)),
if (hasDashboard) SizedBox(width: 16),
]),
SizedBox(height: 12)
],
))
]);
} else {
return Container(
height: 64,
child: Center(
child: RefreshProgressIndicator(
valueColor: AlwaysStoppedAnimation(
Theme.of(tbContext.currentState!.context)
.colorScheme
.primary))));
}
})
]);
}
Widget buildListWidgetCard(BuildContext context) {
return Row(mainAxisSize: MainAxisSize.min, children: [
if (widget.displayImage)
Container(
width: 58,
height: 58,
decoration: BoxDecoration(
// color: Color(0xFFEEEEEE),
borderRadius: BorderRadius.horizontal(left: Radius.circular(4))),
child: FutureBuilder<DeviceProfileInfo>(
future: deviceProfileFuture,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
var profile = snapshot.data!;
bool hasDashboard = profile.defaultDashboardId != null;
Widget image;
BoxFit imageFit;
if (profile.image != null) {
image = Utils.imageFromBase64(profile.image!);
imageFit = BoxFit.contain;
} else {
image = SvgPicture.asset(ThingsboardImage.deviceProfilePlaceholder,
image = SvgPicture.asset(
ThingsboardImage.deviceProfilePlaceholder,
color: Theme.of(context).primaryColor,
colorBlendMode: BlendMode.overlay,
semanticsLabel: 'Device');
imageFit = BoxFit.cover;
}
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 20),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (widget.displayImage) Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4))
),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(4)),
child: Stack(
children: [
Positioned.fill(
child: FittedBox(
fit: imageFit,
child: image,
)
)
],
)
)
),
SizedBox(width: 12),
Flexible(
fit: FlexFit.tight,
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text('${widget.device.field('name')!}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14
))
)
),
SizedBox(width: 12),
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(widget.device.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
),
SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${widget.device.field('type')!}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
)),
Text(widget.device.attribute('active') == 'true' ? 'Active' : 'Inactive',
style: TextStyle(
color: widget.device.attribute('active') == 'true' ? Color(0xFF008A00) : Color(0xFFAFAFAF),
fontSize: 12,
height: 16 / 12,
fontWeight: FontWeight.normal,
))
],
)
]
)
),
SizedBox(width: 16),
if (hasDashboard) Icon(Icons.chevron_right, color: Color(0xFFACACAC)),
if (hasDashboard) SizedBox(width: 16),
]
),
SizedBox(height: 12)
],
)
)
]
);
return ClipRRect(
borderRadius:
BorderRadius.horizontal(left: Radius.circular(4)),
child: Stack(
children: [
Positioned.fill(
child: FittedBox(
fit: imageFit,
child: image,
))
],
));
} else {
return Container(
height: 64,
child: Center(
child: RefreshProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary)
)
)
);
return Center(
child: RefreshProgressIndicator(
valueColor: AlwaysStoppedAnimation(
Theme.of(tbContext.currentState!.context)
.colorScheme
.primary)));
}
}
)
]
);
}
Widget buildListWidgetCard(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.displayImage) Container(
width: 58,
height: 58,
decoration: BoxDecoration(
// color: Color(0xFFEEEEEE),
borderRadius: BorderRadius.horizontal(left: Radius.circular(4))
),
child: FutureBuilder<DeviceProfileInfo>(
future: deviceProfileFuture,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
var profile = snapshot.data!;
Widget image;
BoxFit imageFit;
if (profile.image != null) {
image = Utils.imageFromBase64(profile.image!);
imageFit = BoxFit.contain;
} else {
image = SvgPicture.asset(ThingsboardImage.deviceProfilePlaceholder,
color: Theme.of(context).primaryColor,
colorBlendMode: BlendMode.overlay,
semanticsLabel: 'Device');
imageFit = BoxFit.cover;
}
return ClipRRect(
borderRadius: BorderRadius.horizontal(left: Radius.circular(4)),
child: Stack(
children: [
Positioned.fill(
child: FittedBox(
fit: imageFit,
child: image,
)
)
],
)
);
} else {
return Center(child: RefreshProgressIndicator(
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary)
));
}
},
),
},
),
Flexible(
fit: FlexFit.loose,
child:
Container(
padding: EdgeInsets.symmetric(vertical: 9, horizontal: 16),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${widget.device.field('name')!}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14
))
)
]
),
SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${widget.device.field('type')!}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
)),
]
)
],
)
)
)
]
);
),
Flexible(
fit: FlexFit.loose,
child: Container(
padding: EdgeInsets.symmetric(vertical: 9, horizontal: 16),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${widget.device.field('name')!}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14)))
]),
SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${widget.device.field('type')!}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12)),
])
],
)))
]);
}
}

View File

@@ -4,15 +4,15 @@ import 'package:thingsboard_app/core/entity/entities_list.dart';
import 'package:thingsboard_app/modules/device/devices_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DevicesList extends BaseEntitiesWidget<EntityData, EntityDataQuery> with DevicesBase, EntitiesListStateBase {
class DevicesList extends BaseEntitiesWidget<EntityData, EntityDataQuery>
with DevicesBase, EntitiesListStateBase {
final bool displayDeviceImage;
DevicesList(TbContext tbContext, PageKeyController<EntityDataQuery> pageKeyController, {searchMode = false, this.displayDeviceImage = false}):
super(tbContext, pageKeyController, searchMode: searchMode);
DevicesList(
TbContext tbContext, PageKeyController<EntityDataQuery> pageKeyController,
{searchMode = false, this.displayDeviceImage = false})
: super(tbContext, pageKeyController, searchMode: searchMode);
@override
bool displayCardImage(bool listWidgetCard) => displayDeviceImage;
}

View File

@@ -1,92 +1,94 @@
import 'package:flutter/material.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_app/modules/device/devices_base.dart';
import 'package:thingsboard_app/modules/device/devices_list.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class DevicesListPage extends TbPageWidget {
final String? deviceType;
final bool? active;
final bool searchMode;
DevicesListPage(TbContext tbContext, {this.deviceType, this.active, this.searchMode = false}) : super(tbContext);
DevicesListPage(TbContext tbContext,
{this.deviceType, this.active, this.searchMode = false})
: super(tbContext);
@override
_DevicesListPageState createState() => _DevicesListPageState();
}
class _DevicesListPageState extends TbPageState<DevicesListPage> {
late final DeviceQueryController _deviceQueryController;
@override
void initState() {
super.initState();
_deviceQueryController = DeviceQueryController(deviceType: widget.deviceType, active: widget.active);
_deviceQueryController = DeviceQueryController(
deviceType: widget.deviceType, active: widget.active);
}
@override
Widget build(BuildContext context) {
var devicesList = DevicesList(tbContext, _deviceQueryController, searchMode: widget.searchMode, displayDeviceImage: widget.deviceType == null);
var devicesList = DevicesList(tbContext, _deviceQueryController,
searchMode: widget.searchMode,
displayDeviceImage: widget.deviceType == null);
PreferredSizeWidget appBar;
if (widget.searchMode) {
appBar = TbAppSearchBar(
tbContext,
onSearch: (searchText) => _deviceQueryController.onSearchText(searchText),
onSearch: (searchText) =>
_deviceQueryController.onSearchText(searchText),
);
} else {
String titleText = widget.deviceType != null ? widget.deviceType! : 'All devices';
String titleText = widget.deviceType != null
? widget.deviceType!
: '${S.of(context).allDevices}';
String? subTitleText;
if (widget.active != null) {
subTitleText = widget.active == true ? 'Active' : 'Inactive';
subTitleText = widget.active == true
? '${S.of(context).active}'
: '${S.of(context).inactive}';
}
Column title = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(titleText, style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: subTitleText != null ? 16 : 20,
height: subTitleText != null ? 20 / 16 : 24 / 20
)),
if (subTitleText != null)
Text(subTitleText, style: TextStyle(
color: Theme.of(context).primaryTextTheme.headline6!.color!.withAlpha((0.38 * 255).ceil()),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
);
Column title =
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(titleText,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: subTitleText != null ? 16 : 20,
height: subTitleText != null ? 20 / 16 : 24 / 20)),
if (subTitleText != null)
Text(subTitleText,
style: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headline6!
.color!
.withAlpha((0.38 * 255).ceil()),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12))
]);
appBar = TbAppBar(
tbContext,
title: title,
actions: [
IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
List<String> params = [];
params.add('search=true');
if (widget.deviceType != null) {
params.add('deviceType=${widget.deviceType}');
}
if (widget.active != null) {
params.add('active=${widget.active}');
}
navigateTo('/deviceList?${params.join('&')}');
},
)
]);
appBar = TbAppBar(tbContext, title: title, actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
List<String> params = [];
params.add('search=true');
if (widget.deviceType != null) {
params.add('deviceType=${widget.deviceType}');
}
if (widget.active != null) {
params.add('active=${widget.active}');
}
navigateTo('/deviceList?${params.join('&')}');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: devicesList
);
return Scaffold(appBar: appBar, body: devicesList);
}
@override
@@ -94,5 +96,4 @@ class _DevicesListPageState extends TbPageState<DevicesListPage> {
_deviceQueryController.dispose();
super.dispose();
}
}

View File

@@ -4,9 +4,11 @@ import 'package:thingsboard_app/core/entity/entities_list_widget.dart';
import 'package:thingsboard_app/modules/device/devices_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DevicesListWidget extends EntitiesListWidget<EntityData, EntityDataQuery> with DevicesBase {
DevicesListWidget(TbContext tbContext, {EntitiesListWidgetController? controller}): super(tbContext, controller: controller);
class DevicesListWidget extends EntitiesListWidget<EntityData, EntityDataQuery>
with DevicesBase {
DevicesListWidget(TbContext tbContext,
{EntitiesListWidgetController? controller})
: super(tbContext, controller: controller);
@override
void onViewAll() {
@@ -14,6 +16,6 @@ class DevicesListWidget extends EntitiesListWidget<EntityData, EntityDataQuery>
}
@override
PageKeyController<EntityDataQuery> createPageKeyController() => DeviceQueryController(pageSize: 5);
PageKeyController<EntityDataQuery> createPageKeyController() =>
DeviceQueryController(pageSize: 5);
}

View File

@@ -6,16 +6,14 @@ import 'package:thingsboard_app/modules/device/device_profiles_grid.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class DevicesMainPage extends TbContextWidget {
DevicesMainPage(TbContext tbContext) : super(tbContext);
@override
_DevicesMainPageState createState() => _DevicesMainPageState();
}
class _DevicesMainPageState extends TbContextState<DevicesMainPage> with AutomaticKeepAliveClientMixin<DevicesMainPage> {
class _DevicesMainPageState extends TbContextState<DevicesMainPage>
with AutomaticKeepAliveClientMixin<DevicesMainPage> {
final PageLinkController _pageLinkController = PageLinkController();
@override
@@ -28,12 +26,8 @@ class _DevicesMainPageState extends TbContextState<DevicesMainPage> with Automat
super.build(context);
var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController);
return Scaffold(
appBar: TbAppBar(
tbContext,
title: Text(deviceProfilesList.title)
),
body: deviceProfilesList
);
appBar: TbAppBar(tbContext, title: Text(deviceProfilesList.title)),
body: deviceProfilesList);
}
@override
@@ -41,5 +35,4 @@ class _DevicesMainPageState extends TbContextState<DevicesMainPage> with Automat
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -6,28 +6,21 @@ import 'package:thingsboard_app/modules/device/device_profiles_grid.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
class DevicesPage extends TbPageWidget {
DevicesPage(TbContext tbContext) : super(tbContext);
@override
_DevicesPageState createState() => _DevicesPageState();
}
class _DevicesPageState extends TbPageState<DevicesPage> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
var deviceProfilesList = DeviceProfilesGrid(tbContext, _pageLinkController);
return Scaffold(
appBar: TbAppBar(
tbContext,
title: Text(deviceProfilesList.title)
),
body: deviceProfilesList
);
appBar: TbAppBar(tbContext, title: Text(deviceProfilesList.title)),
body: deviceProfilesList);
}
@override
@@ -35,5 +28,4 @@ class _DevicesPageState extends TbPageState<DevicesPage> {
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -1,26 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:thingsboard_app/constants/assets_path.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard.dart' as dashboardUi;
import 'package:thingsboard_app/modules/dashboard/dashboard.dart'
as dashboardUi;
import 'package:thingsboard_app/modules/dashboard/dashboards_grid.dart';
import 'package:thingsboard_app/modules/tenant/tenants_widget.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class HomePage extends TbContextWidget {
HomePage(TbContext tbContext) : super(tbContext);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends TbContextState<HomePage> with AutomaticKeepAliveClientMixin<HomePage> {
class _HomePageState extends TbContextState<HomePage>
with AutomaticKeepAliveClientMixin<HomePage> {
@override
void initState() {
super.initState();
@@ -50,33 +48,29 @@ class _HomePageState extends TbContextState<HomePage> with AutomaticKeepAliveCli
height: 24,
child: SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle,
color: Theme.of(context).primaryColor,
semanticsLabel: 'ThingsBoard Logo')
)
),
semanticsLabel: 'ThingsBoard Logo'))),
actions: [
if (tbClient.isSystemAdmin()) IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/tenants?search=true');
},
)
if (tbClient.isSystemAdmin())
IconButton(
icon: Icon(Icons.search),
onPressed: () {
navigateTo('/tenants?search=true');
},
)
],
),
body: Builder(
builder: (context) {
if (dashboardState) {
return _buildDashboardHome(context, homeDashboard!);
} else {
return _buildDefaultHome(context);
}
}
),
body: Builder(builder: (context) {
if (dashboardState) {
return _buildDashboardHome(context, homeDashboard!);
} else {
return _buildDefaultHome(context);
}
}),
);
}
Widget _buildDashboardHome(BuildContext context, HomeDashboardInfo dashboard) {
Widget _buildDashboardHome(
BuildContext context, HomeDashboardInfo dashboard) {
return HomeDashboard(tbContext, dashboard);
}
@@ -91,31 +85,24 @@ class _HomePageState extends TbContextState<HomePage> with AutomaticKeepAliveCli
Widget _buildSysAdminHome(BuildContext context) {
return TenantsWidget(tbContext);
}
}
class HomeDashboard extends TbContextWidget {
final HomeDashboardInfo dashboard;
HomeDashboard(TbContext tbContext, this.dashboard) : super(tbContext);
@override
_HomeDashboardState createState() => _HomeDashboardState();
}
class _HomeDashboardState extends TbContextState<HomeDashboard> {
@override
Widget build(BuildContext context) {
return dashboardUi.Dashboard(tbContext,
home: true,
controllerCallback: (controller) {
controller.openDashboard(widget.dashboard.dashboardId!.id!,
hideToolbar: widget.dashboard.hideDashboardToolbar);
}
);
return dashboardUi.Dashboard(tbContext, home: true,
controllerCallback: (controller) {
controller.openDashboard(widget.dashboard.dashboardId!.id!,
hideToolbar: widget.dashboard.hideDashboardToolbar);
});
}
}

View File

@@ -5,8 +5,8 @@ import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/main/main_page.dart';
class HomeRoutes extends TbRoutes {
late var homeHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var homeHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return MainPage(tbContext, path: '/home');
});
@@ -16,5 +16,4 @@ class HomeRoutes extends TbRoutes {
void doRegisterRoutes(router) {
router.define("/home", handler: homeHandler);
}
}

View File

@@ -1,8 +1,7 @@
import 'package:flutter/gestures.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/generated/l10n.dart';
import 'package:thingsboard_app/modules/alarm/alarms_page.dart';
import 'package:thingsboard_app/modules/device/devices_main_page.dart';
import 'package:thingsboard_app/modules/home/home_page.dart';
@@ -11,21 +10,22 @@ import 'package:thingsboard_client/thingsboard_client.dart';
class TbMainNavigationItem {
final Widget page;
final String title;
String title;
final Icon icon;
final String path;
TbMainNavigationItem({
required this.page,
required this.title,
required this.icon,
required this.path
});
TbMainNavigationItem(
{required this.page,
required this.title,
required this.icon,
required this.path});
static Map<Authority, Set<String>> mainPageStateMap = {
Authority.SYS_ADMIN: Set.unmodifiable(['/home', '/more']),
Authority.TENANT_ADMIN: Set.unmodifiable(['/home', '/alarms', '/devices', '/more']),
Authority.CUSTOMER_USER: Set.unmodifiable(['/home', '/alarms', '/devices', '/more']),
Authority.TENANT_ADMIN:
Set.unmodifiable(['/home', '/alarms', '/devices', '/more']),
Authority.CUSTOMER_USER:
Set.unmodifiable(['/home', '/alarms', '/devices', '/more']),
};
static bool isMainPageState(TbContext tbContext, String path) {
@@ -44,61 +44,78 @@ class TbMainNavigationItem {
page: HomePage(tbContext),
title: 'Home',
icon: Icon(Icons.home),
path: '/home'
)
path: '/home')
];
switch(tbContext.tbClient.getAuthUser()!.authority) {
switch (tbContext.tbClient.getAuthUser()!.authority) {
case Authority.SYS_ADMIN:
break;
case Authority.TENANT_ADMIN:
case Authority.CUSTOMER_USER:
items.addAll([
TbMainNavigationItem(
page: AlarmsPage(tbContext),
title: 'Alarms',
icon: Icon(Icons.notifications),
path: '/alarms'
),
TbMainNavigationItem(
page: DevicesMainPage(tbContext),
title: 'Devices',
icon: Icon(Icons.devices_other),
path: '/devices'
)
]);
break;
items.addAll([
TbMainNavigationItem(
page: AlarmsPage(tbContext),
title: 'Alarms',
icon: Icon(Icons.notifications),
path: '/alarms'),
TbMainNavigationItem(
page: DevicesMainPage(tbContext),
title: 'Devices',
icon: Icon(Icons.devices_other),
path: '/devices')
]);
break;
case Authority.REFRESH_TOKEN:
break;
case Authority.ANONYMOUS:
break;
case Authority.PRE_VERIFICATION_TOKEN:
break;
}
items.add(TbMainNavigationItem(
page: MorePage(tbContext),
title: 'More',
icon: Icon(Icons.menu),
path: '/more'
));
path: '/more'));
return items;
} else {
return [];
}
}
static void changeItemsTitleIntl(
List<TbMainNavigationItem> items, BuildContext context) {
for (var item in items) {
switch (item.path) {
case '/home':
item.title = '${S.of(context).home}';
break;
case '/alarms':
item.title = '${S.of(context).alarms}';
break;
case '/devices':
item.title = '${S.of(context).devices}';
break;
case '/more':
item.title = '${S.of(context).more}';
break;
}
}
}
}
class MainPage extends TbPageWidget {
final String _path;
MainPage(TbContext tbContext, {required String path}):
_path = path, super(tbContext);
MainPage(TbContext tbContext, {required String path})
: _path = path,
super(tbContext);
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends TbPageState<MainPage> with TbMainState, TickerProviderStateMixin {
class _MainPageState extends TbPageState<MainPage>
with TbMainState, TickerProviderStateMixin {
late ValueNotifier<int> _currentIndexNotifier;
late final List<TbMainNavigationItem> _tabItems;
late TabController _tabController;
@@ -108,7 +125,8 @@ class _MainPageState extends TbPageState<MainPage> with TbMainState, TickerProvi
super.initState();
_tabItems = TbMainNavigationItem.getItems(tbContext);
int currentIndex = _indexFromPath(widget._path);
_tabController = TabController(initialIndex: currentIndex, length: _tabItems.length, vsync: this);
_tabController = TabController(
initialIndex: currentIndex, length: _tabItems.length, vsync: this);
_currentIndexNotifier = ValueNotifier(currentIndex);
_tabController.animation!.addListener(_onTabAnimation);
}
@@ -119,7 +137,7 @@ class _MainPageState extends TbPageState<MainPage> with TbMainState, TickerProvi
super.dispose();
}
_onTabAnimation () {
_onTabAnimation() {
var value = _tabController.animation!.value;
var targetIndex;
if (value >= _tabController.previousIndex) {
@@ -132,6 +150,7 @@ class _MainPageState extends TbPageState<MainPage> with TbMainState, TickerProvi
@override
Widget build(BuildContext context) {
TbMainNavigationItem.changeItemsTitleIntl(_tabItems, context);
return WillPopScope(
onWillPop: () async {
if (_tabController.index > 0) {
@@ -142,24 +161,24 @@ class _MainPageState extends TbPageState<MainPage> with TbMainState, TickerProvi
},
child: Scaffold(
body: TabBarView(
physics: tbContext.homeDashboard != null ? NeverScrollableScrollPhysics() : null,
physics: tbContext.homeDashboard != null
? NeverScrollableScrollPhysics()
: null,
controller: _tabController,
children: _tabItems.map((item) => item.page).toList(),
),
bottomNavigationBar: ValueListenableBuilder<int>(
valueListenable: _currentIndexNotifier,
builder: (context, index, child) => BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: index,
onTap: (int index) => _setIndex(index) /*_currentIndex = index*/,
items: _tabItems.map((item) => BottomNavigationBarItem(
icon: item.icon,
label: item.title
)).toList()
),
)
)
);
valueListenable: _currentIndexNotifier,
builder: (context, index, child) => BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: index,
onTap: (int index) =>
_setIndex(index) /*_currentIndex = index*/,
items: _tabItems
.map((item) => BottomNavigationBarItem(
icon: item.icon, label: item.title))
.toList()),
)));
}
int _indexFromPath(String path) {
@@ -175,7 +194,7 @@ class _MainPageState extends TbPageState<MainPage> with TbMainState, TickerProvi
navigateToPath(String path) {
int targetIndex = _indexFromPath(path);
_setIndex(targetIndex);
}
}
@override
bool isHomePage() {
@@ -185,5 +204,4 @@ class _MainPageState extends TbPageState<MainPage> with TbMainState, TickerProvi
_setIndex(int index) {
_tabController.index = index;
}
}

View File

@@ -1,132 +1,113 @@
import 'package:flutter/material.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class MorePage extends TbContextWidget {
MorePage(TbContext tbContext) : super(tbContext);
@override
_MorePageState createState() => _MorePageState();
}
class _MorePageState extends TbContextState<MorePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Container(
padding: EdgeInsets.fromLTRB(16, 40, 16, 20),
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.account_circle, size: 48, color: Color(0xFFAFAFAF)),
Spacer(),
IconButton(icon: Icon(Icons.settings, color: Color(0xFFAFAFAF)), onPressed: () async {
await navigateTo('/profile');
setState(() {});
})
],
),
SizedBox(height: 22),
Text(_getUserDisplayName(),
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.w500,
fontSize: 20,
height: 23 / 20
)
),
SizedBox(height: 2),
Text(_getAuthorityName(),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight: FontWeight.normal,
fontSize: 14,
height: 16 / 14
)
),
SizedBox(height: 24),
Divider(color: Color(0xFFEDEDED)),
SizedBox(height: 8),
buildMoreMenuItems(context),
SizedBox(height: 8),
Divider(color: Color(0xFFEDEDED)),
SizedBox(height: 8),
GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.account_circle,
size: 48, color: Color(0xFFAFAFAF)),
Spacer(),
IconButton(
icon: Icon(Icons.settings, color: Color(0xFFAFAFAF)),
onPressed: () async {
await navigateTo('/profile');
setState(() {});
})
],
),
SizedBox(height: 22),
Text(_getUserDisplayName(),
style: TextStyle(
color: Color(0xFF282828),
fontWeight: FontWeight.w500,
fontSize: 20,
height: 23 / 20)),
SizedBox(height: 2),
Text(_getAuthorityName(context),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontWeight: FontWeight.normal,
fontSize: 14,
height: 16 / 14)),
SizedBox(height: 24),
Divider(color: Color(0xFFEDEDED)),
SizedBox(height: 8),
buildMoreMenuItems(context),
SizedBox(height: 8),
Divider(color: Color(0xFFEDEDED)),
SizedBox(height: 8),
GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
height: 48,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 18),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Icon(Icons.logout, color: Color(0xFFE04B2F)),
SizedBox(width: 34),
Text('Log out',
style: TextStyle(
color: Color(0xFFE04B2F),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
))
]
)
)
),
padding:
EdgeInsets.symmetric(vertical: 0, horizontal: 18),
child: Row(mainAxisSize: MainAxisSize.max, children: [
Icon(Icons.logout, color: Color(0xFFE04B2F)),
SizedBox(width: 34),
Text('${S.of(context).logout}',
style: TextStyle(
color: Color(0xFFE04B2F),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14))
]))),
onTap: () {
tbClient.logout(
requestConfig: RequestConfig(ignoreErrors: true));
}
)
],
),
)
);
})
],
),
));
}
Widget buildMoreMenuItems(BuildContext context) {
List<Widget> items = MoreMenuItem.getItems(tbContext).map((menuItem) {
List<Widget> items =
MoreMenuItem.getItems(tbContext, context).map((menuItem) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 18),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Icon(menuItem.icon, color: Color(0xFF282828)),
SizedBox(width: 34),
Text(menuItem.title,
style: TextStyle(
color: Color(0xFF282828),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14
))
]
)
)
),
onTap: () {
navigateTo(menuItem.path);
}
);
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 18),
child: Row(mainAxisSize: MainAxisSize.max, children: [
Icon(menuItem.icon, color: Color(0xFF282828)),
SizedBox(width: 34),
Text(menuItem.title,
style: TextStyle(
color: Color(0xFF282828),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500,
fontSize: 14,
height: 20 / 14))
]))),
onTap: () {
navigateTo(menuItem.path);
});
}).toList();
return Column(
children: items
);
return Column(children: items);
}
String _getUserDisplayName() {
@@ -151,26 +132,27 @@ class _MorePageState extends TbContextState<MorePage> {
return name;
}
String _getAuthorityName() {
String _getAuthorityName(BuildContext context) {
var user = tbContext.userDetails;
var name = '';
if (user != null) {
var authority = user.authority;
switch(authority) {
switch (authority) {
case Authority.SYS_ADMIN:
name = 'System Administrator';
name = '${S.of(context).systemAdministrator}';
break;
case Authority.TENANT_ADMIN:
name = 'Tenant Administrator';
name = '${S.of(context).tenantAdministrator}';
break;
case Authority.CUSTOMER_USER:
name = 'Customer';
name = '${S.of(context).customer}';
break;
default:
break;
}
}
return name;
}
}
class MoreMenuItem {
@@ -178,13 +160,10 @@ class MoreMenuItem {
final IconData icon;
final String path;
MoreMenuItem({
required this.title,
required this.icon,
required this.path
});
MoreMenuItem({required this.title, required this.icon, required this.path});
static List<MoreMenuItem> getItems(TbContext tbContext) {
static List<MoreMenuItem> getItems(
TbContext tbContext, BuildContext context) {
if (tbContext.isAuthenticated) {
List<MoreMenuItem> items = [];
switch (tbContext.tbClient.getAuthUser()!.authority) {
@@ -193,35 +172,33 @@ class MoreMenuItem {
case Authority.TENANT_ADMIN:
items.addAll([
MoreMenuItem(
title: 'Customers',
title: '${S.of(context).customers}',
icon: Icons.supervisor_account,
path: '/customers'
),
path: '/customers'),
MoreMenuItem(
title: 'Assets',
title: '${S.of(context).assets}',
icon: Icons.domain,
path: '/assets'
),
path: '/assets'),
MoreMenuItem(
title: 'Audit Logs',
title: '${S.of(context).auditLogs}',
icon: Icons.track_changes,
path: '/auditLogs'
)
path: '/auditLogs')
]);
break;
case Authority.CUSTOMER_USER:
items.addAll([
MoreMenuItem(
title: 'Assets',
title: '${S.of(context).assets}',
icon: Icons.domain,
path: '/assets'
)
path: '/assets')
]);
break;
case Authority.REFRESH_TOKEN:
break;
case Authority.ANONYMOUS:
break;
case Authority.PRE_VERIFICATION_TOKEN:
break;
}
return items;
} else {

View File

@@ -3,20 +3,18 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/generated/l10n.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
class ChangePasswordPage extends TbContextWidget {
ChangePasswordPage(TbContext tbContext) : super(tbContext);
@override
_ChangePasswordPageState createState() => _ChangePasswordPageState();
}
class _ChangePasswordPageState extends TbContextState<ChangePasswordPage> {
final _isLoadingNotifier = ValueNotifier<bool>(false);
final _showCurrentPasswordNotifier = ValueNotifier<bool>(false);
@@ -31,7 +29,7 @@ class _ChangePasswordPageState extends TbContextState<ChangePasswordPage> {
backgroundColor: Colors.white,
appBar: TbAppBar(
tbContext,
title: const Text('Change Password'),
title: Text('${S.of(context).changePassword}'),
),
body: Stack(
children: [
@@ -39,96 +37,111 @@ class _ChangePasswordPageState extends TbContextState<ChangePasswordPage> {
child: Padding(
padding: EdgeInsets.all(16),
child: SingleChildScrollView(
child: FormBuilder(
key: _changePasswordFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
ValueListenableBuilder(
child: FormBuilder(
key: _changePasswordFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
ValueListenableBuilder(
valueListenable: _showCurrentPasswordNotifier,
builder: (BuildContext context, bool showPassword, child) {
builder: (BuildContext context, bool showPassword,
child) {
return FormBuilderTextField(
name: 'currentPassword',
obscureText: !showPassword,
autofocus: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Current password is required.')
FormBuilderValidators.required(
errorText:
'${S.of(context).currentPasswordRequireText}')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
icon: Icon(showPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () {
_showCurrentPasswordNotifier.value = !_showCurrentPasswordNotifier.value;
_showCurrentPasswordNotifier.value =
!_showCurrentPasswordNotifier
.value;
},
),
border: OutlineInputBorder(),
labelText: 'Current password *'
),
labelText:
'${S.of(context).currentPasswordStar}'),
);
}
),
SizedBox(height: 24),
ValueListenableBuilder(
valueListenable: _showNewPasswordNotifier,
builder: (BuildContext context, bool showPassword, child) {
return FormBuilderTextField(
name: 'newPassword',
obscureText: !showPassword,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'New password is required.')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
_showNewPasswordNotifier.value = !_showNewPasswordNotifier.value;
},
),
border: OutlineInputBorder(),
labelText: 'New password *'
),
);
}
),
SizedBox(height: 24),
ValueListenableBuilder(
valueListenable: _showNewPassword2Notifier,
builder: (BuildContext context, bool showPassword, child) {
return FormBuilderTextField(
name: 'newPassword2',
obscureText: !showPassword,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'New password again is required.')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
_showNewPassword2Notifier.value = !_showNewPassword2Notifier.value;
},
),
border: OutlineInputBorder(),
labelText: 'New password again *'
),
);
}
),
SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(padding: EdgeInsets.all(16),
alignment: Alignment.centerLeft),
onPressed: () {
_changePassword();
},
child: Center(child: Text('Change Password'))
)
]
),
)
)
),
}),
SizedBox(height: 24),
ValueListenableBuilder(
valueListenable: _showNewPasswordNotifier,
builder: (BuildContext context, bool showPassword,
child) {
return FormBuilderTextField(
name: 'newPassword',
obscureText: !showPassword,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(
errorText:
'${S.of(context).newPasswordRequireText}')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () {
_showNewPasswordNotifier.value =
!_showNewPasswordNotifier.value;
},
),
border: OutlineInputBorder(),
labelText:
'${S.of(context).newPasswordStar}'),
);
}),
SizedBox(height: 24),
ValueListenableBuilder(
valueListenable: _showNewPassword2Notifier,
builder: (BuildContext context, bool showPassword,
child) {
return FormBuilderTextField(
name: 'newPassword2',
obscureText: !showPassword,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(
errorText:
'${S.of(context).newPassword2RequireText}')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () {
_showNewPassword2Notifier.value =
!_showNewPassword2Notifier.value;
},
),
border: OutlineInputBorder(),
labelText:
'${S.of(context).newPassword2Star}'),
);
}),
SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.all(16),
alignment: Alignment.centerLeft),
onPressed: () {
_changePassword();
},
child: Center(
child:
Text('${S.of(context).changePassword}')))
]),
))),
),
ValueListenableBuilder<bool>(
valueListenable: _isLoadingNotifier,
@@ -136,18 +149,15 @@ class _ChangePasswordPageState extends TbContextState<ChangePasswordPage> {
if (loading) {
return SizedBox.expand(
child: Container(
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
)
);
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
));
} else {
return SizedBox.shrink();
}
}
)
})
],
)
);
));
}
Future<void> _changePassword() async {
@@ -158,18 +168,17 @@ class _ChangePasswordPageState extends TbContextState<ChangePasswordPage> {
String newPassword = formValue['newPassword'];
String newPassword2 = formValue['newPassword2'];
if (newPassword != newPassword2) {
showErrorNotification('Entered passwords must be same!');
showErrorNotification('${S.of(context).passwordErrorNotification}');
} else {
_isLoadingNotifier.value = true;
try {
await Future.delayed(Duration(milliseconds: 300));
await tbClient.changePassword(currentPassword, newPassword);
pop(true);
} catch(e) {
} catch (e) {
_isLoadingNotifier.value = false;
}
}
}
}
}

View File

@@ -1,7 +1,7 @@
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/generated/l10n.dart';
import 'package:thingsboard_app/modules/profile/change_password_page.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
@@ -11,18 +11,17 @@ import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class ProfilePage extends TbPageWidget {
final bool _fullscreen;
ProfilePage(TbContext tbContext, {bool fullscreen = false}) : _fullscreen = fullscreen, super(tbContext);
ProfilePage(TbContext tbContext, {bool fullscreen = false})
: _fullscreen = fullscreen,
super(tbContext);
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends TbPageState<ProfilePage> {
final _isLoadingNotifier = ValueNotifier<bool>(true);
final _profileFormKey = GlobalKey<FormBuilderState>();
@@ -49,21 +48,16 @@ class _ProfilePageState extends TbPageState<ProfilePage> {
title: const Text('Profile'),
actions: [
IconButton(
icon: Icon(
Icons.check
),
icon: Icon(Icons.check),
onPressed: () {
_saveProfile();
}
),
if (widget._fullscreen) IconButton(
icon: Icon(
Icons.logout
),
onPressed: () {
tbClient.logout();
}
)
}),
if (widget._fullscreen)
IconButton(
icon: Icon(Icons.logout),
onPressed: () {
tbClient.logout();
})
],
),
body: Stack(
@@ -72,73 +66,70 @@ class _ProfilePageState extends TbPageState<ProfilePage> {
child: Padding(
padding: EdgeInsets.all(16),
child: SingleChildScrollView(
child: FormBuilder(
key: _profileFormKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
FormBuilderTextField(
name: 'email',
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Email is required.'),
FormBuilderValidators.email(context, errorText: 'Invalid email format.')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email *'
),
),
SizedBox(height: 24),
FormBuilderTextField(
name: 'firstName',
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'First Name'
),
),
SizedBox(height: 24),
FormBuilderTextField(
name: 'lastName',
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Last Name'
),
),
SizedBox(height: 24),
OutlinedButton(
style: OutlinedButton.styleFrom(padding: EdgeInsets.all(16),
alignment: Alignment.centerLeft),
onPressed: () {
_changePassword();
},
child: Center(child: Text('Change Password'))
)
]
),
)
)
),
child: FormBuilder(
key: _profileFormKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
FormBuilderTextField(
name: 'email',
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(
errorText:
'${S.of(context).emailRequireText}'),
FormBuilderValidators.email(
errorText:
'${S.of(context).emailInvalidText}')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: '${S.of(context).emailStar}'),
),
SizedBox(height: 24),
FormBuilderTextField(
name: 'firstName',
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: '${S.of(context).firstNameUpper}'),
),
SizedBox(height: 24),
FormBuilderTextField(
name: 'lastName',
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: '${S.of(context).lastNameUpper}'),
),
SizedBox(height: 24),
OutlinedButton(
style: OutlinedButton.styleFrom(
padding: EdgeInsets.all(16),
alignment: Alignment.centerLeft),
onPressed: () {
_changePassword();
},
child: Center(
child:
Text('${S.of(context).changePassword}')))
]),
))),
),
ValueListenableBuilder<bool>(
valueListenable: _isLoadingNotifier,
builder: (BuildContext context, bool loading, child) {
if (loading) {
return SizedBox.expand(
if (loading) {
return SizedBox.expand(
child: Container(
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
)
);
} else {
return SizedBox.shrink();
}
}
)
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
));
} else {
return SizedBox.shrink();
}
})
],
)
);
));
}
Future<void> _loadUser() async {
@@ -170,15 +161,20 @@ class _ProfilePageState extends TbPageState<ProfilePage> {
_setUser();
await Future.delayed(Duration(milliseconds: 300));
_isLoadingNotifier.value = false;
showSuccessNotification('Profile successfully updated', duration: Duration(milliseconds: 1500));
showSuccessNotification('${S.of(context).profileSuccessNotification}',
duration: Duration(milliseconds: 1500));
showSuccessNotification('${S.of(context).profileSuccessNotification}',
duration: Duration(milliseconds: 1500));
}
}
}
_changePassword() async {
var res = await tbContext.showFullScreenDialog<bool>(new ChangePasswordPage(tbContext));
var res = await tbContext
.showFullScreenDialog<bool>(new ChangePasswordPage(tbContext));
if (res == true) {
showSuccessNotification('Password successfully changed', duration: Duration(milliseconds: 1500));
showSuccessNotification('${S.of(context).passwordSuccessNotification}',
duration: Duration(milliseconds: 1500));
}
}
}

View File

@@ -6,8 +6,8 @@ import 'package:thingsboard_app/core/context/tb_context.dart';
import 'profile_page.dart';
class ProfileRoutes extends TbRoutes {
late var profileHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var profileHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var fullscreen = params['fullscreen']?.first == 'true';
return ProfilePage(tbContext, fullscreen: fullscreen);
});
@@ -18,5 +18,4 @@ class ProfileRoutes extends TbRoutes {
void doRegisterRoutes(router) {
router.define("/profile", handler: profileHandler);
}
}

View File

@@ -3,13 +3,14 @@ import 'package:thingsboard_app/core/entity/entity_details_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class TenantDetailsPage extends ContactBasedDetailsPage<Tenant> {
TenantDetailsPage(TbContext tbContext, String tenantId):
super(tbContext, entityId: tenantId, defaultTitle: 'Tenant', subTitle: 'Tenant details');
TenantDetailsPage(TbContext tbContext, String tenantId)
: super(tbContext,
entityId: tenantId,
defaultTitle: 'Tenant',
subTitle: 'Tenant details');
@override
Future<Tenant?> fetchEntity(String tenantId) {
return tbClient.getTenantService().getTenant(tenantId);
}
}

View File

@@ -6,13 +6,14 @@ import 'tenant_details_page.dart';
import 'tenants_page.dart';
class TenantRoutes extends TbRoutes {
late var tenantsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var tenantsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
var searchMode = params['search']?.first == 'true';
return TenantsPage(tbContext, searchMode: searchMode);
});
late var tenantDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var tenantDetailsHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return TenantDetailsPage(tbContext, params["id"][0]);
});
@@ -23,5 +24,4 @@ class TenantRoutes extends TbRoutes {
router.define("/tenants", handler: tenantsHandler);
router.define("/tenant/:id", handler: tenantDetailsHandler);
}
}

View File

@@ -1,8 +1,7 @@
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
mixin TenantsBase on EntitiesBase<Tenant, PageLink> {
mixin TenantsBase on EntitiesBase<Tenant, PageLink> {
@override
String get title => 'Tenants';
@@ -18,5 +17,4 @@ mixin TenantsBase on EntitiesBase<Tenant, PageLink> {
void onEntityTap(Tenant tenant) {
navigateTo('/tenant/${tenant.id!.id}');
}
}

View File

@@ -5,8 +5,10 @@ import 'package:thingsboard_client/thingsboard_client.dart';
import 'tenants_base.dart';
class TenantsList extends BaseEntitiesWidget<Tenant, PageLink> with TenantsBase, ContactBasedBase, EntitiesListStateBase {
TenantsList(TbContext tbContext, PageKeyController<PageLink> pageKeyController, {searchMode = false}) : super(tbContext, pageKeyController, searchMode: searchMode);
class TenantsList extends BaseEntitiesWidget<Tenant, PageLink>
with TenantsBase, ContactBasedBase, EntitiesListStateBase {
TenantsList(
TbContext tbContext, PageKeyController<PageLink> pageKeyController,
{searchMode = false})
: super(tbContext, pageKeyController, searchMode: searchMode);
}

View File

@@ -7,23 +7,22 @@ import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'tenants_list.dart';
class TenantsPage extends TbPageWidget {
final bool searchMode;
TenantsPage(TbContext tbContext, {this.searchMode = false}) : super(tbContext);
TenantsPage(TbContext tbContext, {this.searchMode = false})
: super(tbContext);
@override
_TenantsPageState createState() => _TenantsPageState();
}
class _TenantsPageState extends TbPageState<TenantsPage> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
var tenantsList = TenantsList(tbContext, _pageLinkController, searchMode: widget.searchMode);
var tenantsList = TenantsList(tbContext, _pageLinkController,
searchMode: widget.searchMode);
PreferredSizeWidget appBar;
if (widget.searchMode) {
appBar = TbAppSearchBar(
@@ -31,24 +30,16 @@ class _TenantsPageState extends TbPageState<TenantsPage> {
onSearch: (searchText) => _pageLinkController.onSearchText(searchText),
);
} else {
appBar = TbAppBar(
tbContext,
title: Text(tenantsList.title),
actions: [
IconButton(
icon: Icon(
Icons.search
),
onPressed: () {
navigateTo('/tenants?search=true');
},
)
]);
appBar = TbAppBar(tbContext, title: Text(tenantsList.title), actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
navigateTo('/tenants?search=true');
},
)
]);
}
return Scaffold(
appBar: appBar,
body: tenantsList
);
return Scaffold(appBar: appBar, body: tenantsList);
}
@override
@@ -56,5 +47,4 @@ class _TenantsPageState extends TbPageState<TenantsPage> {
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -6,21 +6,18 @@ import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'tenants_list.dart';
class TenantsWidget extends TbContextWidget {
TenantsWidget(TbContext tbContext) : super(tbContext);
@override
_TenantsWidgetState createState() => _TenantsWidgetState();
}
class _TenantsWidgetState extends TbContextState<TenantsWidget> {
final PageLinkController _pageLinkController = PageLinkController();
@override
Widget build(BuildContext context) {
return TenantsList(tbContext, _pageLinkController);
return TenantsList(tbContext, _pageLinkController);
}
@override
@@ -28,5 +25,4 @@ class _TenantsWidgetState extends TbContextState<TenantsWidget> {
_pageLinkController.dispose();
super.dispose();
}
}

View File

@@ -4,7 +4,6 @@ import 'package:thingsboard_client/thingsboard_client.dart';
TbStorage createAppStorage() => TbSecureStorage();
class TbSecureStorage implements TbStorage {
final flutterStorage = FlutterSecureStorage();
@override
@@ -21,5 +20,4 @@ class TbSecureStorage implements TbStorage {
Future<void> setItem(String key, String value) async {
return await flutterStorage.write(key: key, value: value);
}
}

View File

@@ -1,11 +1,10 @@
import 'package:thingsboard_client/thingsboard_client.dart';
import 'dart:html';
import 'package:universal_html/html.dart' as html;
TbStorage createAppStorage() => TbWebLocalStorage();
class TbWebLocalStorage implements TbStorage {
final Storage _localStorage = window.localStorage;
final html.Storage _localStorage = html.window.localStorage;
@override
Future<void> deleteItem(String key) async {
@@ -21,5 +20,4 @@ class TbWebLocalStorage implements TbStorage {
Future<void> setItem(String key, String value) async {
_localStorage[key] = value;
}
}

View File

@@ -1,25 +1,29 @@
import 'package:thingsboard_client/thingsboard_client.dart';
abstract class DeviceProfileCache {
static final _cache = Map<String, DeviceProfileInfo>();
static Future<DeviceProfileInfo> getDeviceProfileInfo(ThingsboardClient tbClient, String name, String deviceId) async {
static Future<DeviceProfileInfo> getDeviceProfileInfo(
ThingsboardClient tbClient, String name, String deviceId) async {
var deviceProfile = _cache[name];
if (deviceProfile == null) {
var device = await tbClient.getDeviceService().getDevice(deviceId);
deviceProfile = await tbClient.getDeviceProfileService().getDeviceProfileInfo(device!.deviceProfileId!.id!);
deviceProfile = await tbClient
.getDeviceProfileService()
.getDeviceProfileInfo(device!.deviceProfileId!.id!);
_cache[name] = deviceProfile!;
}
return deviceProfile;
}
static Future<PageData<DeviceProfileInfo>> getDeviceProfileInfos(ThingsboardClient tbClient, PageLink pageLink) async {
var deviceProfileInfos = await tbClient.getDeviceProfileService().getDeviceProfileInfos(pageLink);
static Future<PageData<DeviceProfileInfo>> getDeviceProfileInfos(
ThingsboardClient tbClient, PageLink pageLink) async {
var deviceProfileInfos = await tbClient
.getDeviceProfileService()
.getDeviceProfileInfos(pageLink);
deviceProfileInfos.data.forEach((deviceProfile) {
_cache[deviceProfile.name] = deviceProfile;
});
return deviceProfileInfos;
}
}

View File

@@ -1,7 +1,6 @@
import 'package:thingsboard_client/thingsboard_client.dart';
abstract class EntityQueryApi {
static final activeDeviceKeyFilter = KeyFilter(
key: EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active'),
valueType: EntityKeyValueType.BOOLEAN,
@@ -27,36 +26,54 @@ abstract class EntityQueryApi {
EntityKey(type: EntityKeyType.ATTRIBUTE, key: 'active')
];
static Future<int> countDevices(ThingsboardClient tbClient, {String? deviceType, bool? active}) {
static Future<int> countDevices(ThingsboardClient tbClient,
{String? deviceType, bool? active}) {
EntityFilter deviceFilter;
if (deviceType != null) {
deviceFilter = DeviceTypeFilter(deviceType: deviceType, deviceNameFilter: '');
deviceFilter =
DeviceTypeFilter(deviceType: deviceType, deviceNameFilter: '');
} else {
deviceFilter = EntityTypeFilter(entityType: EntityType.DEVICE);
}
EntityCountQuery deviceCountQuery = EntityCountQuery(entityFilter: deviceFilter);
EntityCountQuery deviceCountQuery =
EntityCountQuery(entityFilter: deviceFilter);
if (active != null) {
deviceCountQuery.keyFilters = [active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter];
deviceCountQuery.keyFilters = [
active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter
];
}
return tbClient.getEntityQueryService().countEntitiesByQuery(deviceCountQuery);
return tbClient
.getEntityQueryService()
.countEntitiesByQuery(deviceCountQuery);
}
static EntityDataQuery createDefaultDeviceQuery({int pageSize = 20, String? searchText, String? deviceType, bool? active}) {
static EntityDataQuery createDefaultDeviceQuery(
{int pageSize = 20,
String? searchText,
String? deviceType,
bool? active}) {
EntityFilter deviceFilter;
List<KeyFilter>? keyFilters;
if (deviceType != null) {
deviceFilter = DeviceTypeFilter(deviceType: deviceType, deviceNameFilter: '');
deviceFilter =
DeviceTypeFilter(deviceType: deviceType, deviceNameFilter: '');
} else {
deviceFilter = EntityTypeFilter(entityType: EntityType.DEVICE);
}
if (active != null) {
keyFilters = [active ? activeDeviceKeyFilter : inactiveDeviceKeyFilter];
}
return EntityDataQuery(entityFilter: deviceFilter, keyFilters: keyFilters,
entityFields: defaultDeviceFields, latestValues: defaultDeviceAttributes, pageLink: EntityDataPageLink(pageSize: pageSize,
return EntityDataQuery(
entityFilter: deviceFilter,
keyFilters: keyFilters,
entityFields: defaultDeviceFields,
latestValues: defaultDeviceAttributes,
pageLink: EntityDataPageLink(
pageSize: pageSize,
textSearch: searchText,
sortOrder: EntityDataSortOrder(key: EntityKey(type: EntityKeyType.ENTITY_FIELD, key: 'createdTime'),
sortOrder: EntityDataSortOrder(
key: EntityKey(
type: EntityKeyType.ENTITY_FIELD, key: 'createdTime'),
direction: EntityDataSortOrderDirection.DESC)));
}
}

View File

@@ -1,3 +1,3 @@
export '_tb_app_storage.dart'
if (dart.library.io) 'tb_secure_storage.dart'
if (dart.library.html) 'tb_web_local_storage.dart';
if (dart.library.io) '_tb_secure_storage.dart'
if (dart.library.html) '_tb_web_local_storage.dart';

View File

@@ -8,7 +8,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
class WidgetMobileActionResult<T extends MobileActionResult> {
T? result;
@@ -16,11 +16,17 @@ class WidgetMobileActionResult<T extends MobileActionResult> {
String? error;
bool hasError = false;
WidgetMobileActionResult.errorResult(this.error): hasError = true, hasResult = false;
WidgetMobileActionResult.errorResult(this.error)
: hasError = true,
hasResult = false;
WidgetMobileActionResult.successResult(this.result): hasError = false, hasResult = true;
WidgetMobileActionResult.successResult(this.result)
: hasError = false,
hasResult = true;
WidgetMobileActionResult.emptyResult(): hasError = false, hasResult = false;
WidgetMobileActionResult.emptyResult()
: hasError = false,
hasResult = false;
Map<String, dynamic> toJson() {
var json = <String, dynamic>{};
@@ -33,7 +39,6 @@ class WidgetMobileActionResult<T extends MobileActionResult> {
}
class MobileActionResult {
MobileActionResult();
factory MobileActionResult.launched(bool launched) {
@@ -123,24 +128,27 @@ enum WidgetMobileActionType {
}
WidgetMobileActionType widgetMobileActionTypeFromString(String value) {
return WidgetMobileActionType.values.firstWhere((e)=>e.toString().split('.')[1].toUpperCase()==value.toUpperCase(), orElse: () => WidgetMobileActionType.unknown);
return WidgetMobileActionType.values.firstWhere(
(e) => e.toString().split('.')[1].toUpperCase() == value.toUpperCase(),
orElse: () => WidgetMobileActionType.unknown);
}
class WidgetActionHandler with HasTbContext {
WidgetActionHandler(TbContext tbContext) {
setTbContext(tbContext);
}
Future<Map<String, dynamic>> handleWidgetMobileAction(List<dynamic> args, InAppWebViewController controller) async {
Future<Map<String, dynamic>> handleWidgetMobileAction(
List<dynamic> args, InAppWebViewController controller) async {
var result = await _handleWidgetMobileAction(args, controller);
return result.toJson();
}
Future<WidgetMobileActionResult> _handleWidgetMobileAction(List<dynamic> args, InAppWebViewController controller) async {
Future<WidgetMobileActionResult> _handleWidgetMobileAction(
List<dynamic> args, InAppWebViewController controller) async {
if (args.isNotEmpty && args[0] is String) {
var actionType = widgetMobileActionTypeFromString(args[0]);
switch(actionType) {
switch (actionType) {
case WidgetMobileActionType.takePictureFromGallery:
return await _takePicture(ImageSource.gallery);
case WidgetMobileActionType.takePhoto:
@@ -158,24 +166,26 @@ class WidgetActionHandler with HasTbContext {
case WidgetMobileActionType.takeScreenshot:
return await _takeScreenshot(controller);
case WidgetMobileActionType.unknown:
return WidgetMobileActionResult.errorResult('Unknown actionType: ${args[0]}');
return WidgetMobileActionResult.errorResult(
'Unknown actionType: ${args[0]}');
}
} else {
return WidgetMobileActionResult.errorResult('actionType is not provided.');
return WidgetMobileActionResult.errorResult(
'actionType is not provided.');
}
}
Future<WidgetMobileActionResult> _takePicture(ImageSource source) async {
try {
final picker = ImagePicker();
final pickedFile = await picker.getImage(source: source);
final pickedFile = await picker.pickImage(source: source);
if (pickedFile != null) {
var mimeType = lookupMimeType(pickedFile.path);
if (mimeType != null) {
var image = File(pickedFile.path);
List<int> imageBytes = await image.readAsBytes();
String imageUrl = UriData.fromBytes(imageBytes, mimeType: mimeType)
.toString();
String imageUrl =
UriData.fromBytes(imageBytes, mimeType: mimeType).toString();
return WidgetMobileActionResult.successResult(
MobileActionResult.image(imageUrl));
} else {
@@ -190,7 +200,8 @@ class WidgetActionHandler with HasTbContext {
}
}
Future<WidgetMobileActionResult> _launchMap(List<dynamic> args, bool directionElseLocation) async {
Future<WidgetMobileActionResult> _launchMap(
List<dynamic> args, bool directionElseLocation) async {
try {
num? lat;
num? lon;
@@ -213,9 +224,11 @@ class WidgetActionHandler with HasTbContext {
Future<WidgetMobileActionResult> _scanQrCode() async {
try {
Barcode? barcode = await tbContext.navigateTo('/qrCodeScan', transition: TransitionType.nativeModal);
Barcode? barcode = await tbContext.navigateTo('/qrCodeScan',
transition: TransitionType.nativeModal);
if (barcode != null && barcode.code != null) {
return WidgetMobileActionResult.successResult(MobileActionResult.qrCode(barcode.code!, describeEnum(barcode.format)));
return WidgetMobileActionResult.successResult(MobileActionResult.qrCode(
barcode.code!, describeEnum(barcode.format)));
} else {
return WidgetMobileActionResult.emptyResult();
}
@@ -261,19 +274,24 @@ class WidgetActionHandler with HasTbContext {
return WidgetMobileActionResult.errorResult(
'Location permissions are permanently denied, we cannot request permissions.');
}
var position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
return WidgetMobileActionResult.successResult(MobileActionResult.location(position.latitude, position.longitude));
var position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
return WidgetMobileActionResult.successResult(
MobileActionResult.location(position.latitude, position.longitude));
} catch (e) {
return _handleError(e);
}
}
Future<WidgetMobileActionResult> _takeScreenshot(InAppWebViewController controller) async {
Future<WidgetMobileActionResult> _takeScreenshot(
InAppWebViewController controller) async {
try {
List<int>? imageBytes = await controller.takeScreenshot();
if (imageBytes != null) {
String imageUrl = UriData.fromBytes(imageBytes, mimeType: 'image/png').toString();
return WidgetMobileActionResult.successResult(MobileActionResult.image(imageUrl));
String imageUrl =
UriData.fromBytes(imageBytes, mimeType: 'image/png').toString();
return WidgetMobileActionResult.successResult(
MobileActionResult.image(imageUrl));
} else {
return WidgetMobileActionResult.emptyResult();
}
@@ -283,8 +301,8 @@ class WidgetActionHandler with HasTbContext {
}
Future<MobileActionResult> _tryLaunch(String url) async {
if (await canLaunch(url)) {
await launch(url);
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
return MobileActionResult.launched(true);
} else {
log.error('Could not launch $url');
@@ -301,5 +319,4 @@ class WidgetActionHandler with HasTbContext {
}
return WidgetMobileActionResult.errorResult(error);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class FadeOpenPageTransitionsBuilder extends PageTransitionsBuilder {
/// Constructs a page transition animation that slides the page up.
@@ -7,12 +6,12 @@ class FadeOpenPageTransitionsBuilder extends PageTransitionsBuilder {
@override
Widget buildTransitions<T>(
PageRoute<T>? route,
BuildContext? context,
Animation<double> animation,
Animation<double>? secondaryAnimation,
Widget child,
) {
PageRoute<T>? route,
BuildContext? context,
Animation<double> animation,
Animation<double>? secondaryAnimation,
Widget child,
) {
return FadeOpenPageTransition(routeAnimation: animation, child: child);
}
}
@@ -20,9 +19,11 @@ class FadeOpenPageTransitionsBuilder extends PageTransitionsBuilder {
class FadeOpenPageTransition extends StatelessWidget {
FadeOpenPageTransition({
Key? key,
required Animation<double> routeAnimation, // The route's linear 0.0 - 1.0 animation.
required Animation<double>
routeAnimation, // The route's linear 0.0 - 1.0 animation.
required this.child,
}) : _positionAnimation = routeAnimation.drive(_leftRightTween.chain(_fastOutSlowInTween)),
}) : _positionAnimation =
routeAnimation.drive(_leftRightTween.chain(_fastOutSlowInTween)),
_opacityAnimation = routeAnimation.drive(_easeInTween),
super(key: key);
@@ -31,8 +32,10 @@ class FadeOpenPageTransition extends StatelessWidget {
begin: const Offset(0.5, 0.0),
end: Offset.zero,
);
static final Animatable<double> _fastOutSlowInTween = CurveTween(curve: Curves.fastOutSlowIn);
static final Animatable<double> _easeInTween = CurveTween(curve: Curves.easeIn);
static final Animatable<double> _fastOutSlowInTween =
CurveTween(curve: Curves.fastOutSlowIn);
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
final Animation<Offset> _positionAnimation;
final Animation<double> _opacityAnimation;

View File

@@ -2,22 +2,18 @@ import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
class QrCodeScannerPage extends TbPageWidget {
QrCodeScannerPage(TbContext tbContext) : super(tbContext);
@override
_QrCodeScannerPageState createState() => _QrCodeScannerPageState();
}
class _QrCodeScannerPageState extends TbPageState<QrCodeScannerPage> {
Timer? simulatedQrTimer;
QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
@@ -48,64 +44,63 @@ class _QrCodeScannerPageState extends TbPageState<QrCodeScannerPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
_buildQrView(context),
Positioned(
body: Stack(
children: [
_buildQrView(context),
Positioned(
bottom: 0,
left: 0,
right: 0,
height: kToolbarHeight,
child: Center(child: Text('Scan a code', style: TextStyle(color: Colors.white, fontSize: 20)))
child: Center(
child: Text('Scan a code',
style: TextStyle(color: Colors.white, fontSize: 20)))),
Positioned(
child: AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
iconTheme: IconThemeData(color: Colors.white),
elevation: 0,
actions: <Widget>[
IconButton(
icon: FutureBuilder(
future: controller?.getFlashStatus(),
builder: (context, snapshot) {
return Icon(snapshot.data == false
? Icons.flash_on
: Icons.flash_off);
}),
onPressed: () async {
await controller?.toggleFlash();
setState(() {});
},
tooltip: 'Toggle flash',
),
IconButton(
icon: FutureBuilder(
future: controller?.getCameraInfo(),
builder: (context, snapshot) {
return Icon(snapshot.data == CameraFacing.front
? Icons.camera_rear
: Icons.camera_front);
}),
onPressed: () async {
await controller?.flipCamera();
setState(() {});
},
tooltip: 'Toggle camera',
),
],
),
Positioned(
child:
AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
iconTheme: IconThemeData(
color: Colors.white
),
elevation: 0,
actions: <Widget>[
IconButton(
icon: FutureBuilder(
future: controller?.getFlashStatus(),
builder: (context, snapshot) {
return Icon(snapshot.data == false ? Icons.flash_on : Icons.flash_off);
}
),
onPressed: () async {
await controller?.toggleFlash();
setState(() {});
},
tooltip: 'Toggle flash',
),
IconButton(
icon: FutureBuilder(
future: controller?.getCameraInfo(),
builder: (context, snapshot) {
return Icon(snapshot.data == CameraFacing.front ? Icons.camera_rear : Icons.camera_front);
}
),
onPressed: () async {
await controller?.flipCamera();
setState(() {});
},
tooltip: 'Toggle camera',
),
],
),
)
],
)
);
)
],
));
}
Widget _buildQrView(BuildContext context) {
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
MediaQuery.of(context).size.height < 400)
? 150.0
: 300.0;
// To ensure the Scanner view is properly sizes after rotation

View File

@@ -5,8 +5,8 @@ import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/utils/ui/qr_code_scanner.dart';
class UiUtilsRoutes extends TbRoutes {
late var qrCodeScannerHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
late var qrCodeScannerHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return QrCodeScannerPage(tbContext);
});
@@ -16,5 +16,4 @@ class UiUtilsRoutes extends TbRoutes {
void doRegisterRoutes(router) {
router.define("/qrCodeScan", handler: qrCodeScannerHandler);
}
}

View File

@@ -5,13 +5,13 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
abstract class Utils {
static String createDashboardEntityState(EntityId entityId, {String? entityName, String? entityLabel}) {
var stateObj = [<String, dynamic>{
'params': <String, dynamic>{
'entityId': entityId.toJson()
static String createDashboardEntityState(EntityId entityId,
{String? entityName, String? entityLabel}) {
var stateObj = [
<String, dynamic>{
'params': <String, dynamic>{'entityId': entityId.toJson()}
}
}];
];
if (entityName != null) {
stateObj[0]['params']['entityName'] = entityName;
}
@@ -19,14 +19,13 @@ abstract class Utils {
stateObj[0]['params']['entityLabel'] = entityLabel;
}
var stateJson = json.encode(stateObj);
var encodedUri = Uri.encodeComponent(stateJson);
encodedUri = encodedUri.replaceAllMapped(RegExp(r'%([0-9A-F]{2})'), (match) {
var encodedUri = Uri.encodeComponent(stateJson);
encodedUri =
encodedUri.replaceAllMapped(RegExp(r'%([0-9A-F]{2})'), (match) {
var p1 = match.group(1)!;
return String.fromCharCode(int.parse(p1, radix: 16));
});
return Uri.encodeComponent(
base64.encode(utf8.encode(encodedUri))
);
return Uri.encodeComponent(base64.encode(utf8.encode(encodedUri)));
}
static String? contactToShortAddress(ContactBased contact) {
@@ -47,13 +46,21 @@ abstract class Utils {
}
}
static Widget imageFromBase64(String base64, {Color? color, double? width, double? height, String? semanticLabel}) {
static Widget imageFromBase64(String base64,
{Color? color, double? width, double? height, String? semanticLabel}) {
var uriData = UriData.parse(base64);
if (uriData.mimeType == 'image/svg+xml') {
return SvgPicture.memory(uriData.contentAsBytes(), color: color, width: width, height: height, semanticsLabel: semanticLabel);
return SvgPicture.memory(uriData.contentAsBytes(),
color: color,
width: width,
height: height,
semanticsLabel: semanticLabel);
} else {
return Image.memory(uriData.contentAsBytes(), color: color, width: width, height: height, semanticLabel: semanticLabel);
return Image.memory(uriData.contentAsBytes(),
color: color,
width: width,
height: height,
semanticLabel: semanticLabel);
}
}
}

View File

@@ -2,12 +2,10 @@ import 'dart:async';
import 'package:stream_transform/stream_transform.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';
class TbAppBar extends TbContextWidget implements PreferredSizeWidget {
final Widget? leading;
final Widget? title;
final List<Widget>? actions;
@@ -18,18 +16,22 @@ class TbAppBar extends TbContextWidget implements PreferredSizeWidget {
@override
final Size preferredSize;
TbAppBar(TbContext tbContext, {this.leading, this.title, this.actions, this.elevation = 8,
this.shadowColor, this.showLoadingIndicator = false}) :
preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)),
super(tbContext);
TbAppBar(TbContext tbContext,
{this.leading,
this.title,
this.actions,
this.elevation = 8,
this.shadowColor,
this.showLoadingIndicator = false})
: preferredSize =
Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)),
super(tbContext);
@override
_TbAppBarState createState() => _TbAppBarState();
}
class _TbAppBarState extends TbContextState<TbAppBar> {
@override
void initState() {
super.initState();
@@ -45,18 +47,15 @@ class _TbAppBarState extends TbContextState<TbAppBar> {
List<Widget> children = <Widget>[];
children.add(buildDefaultBar());
if (widget.showLoadingIndicator) {
children.add(
ValueListenableBuilder(
valueListenable: loadingNotifier,
builder: (context, bool loading, child) {
if (loading) {
return LinearProgressIndicator();
} else {
return Container(height: 4);
}
}
)
);
children.add(ValueListenableBuilder(
valueListenable: loadingNotifier,
builder: (context, bool loading, child) {
if (loading) {
return LinearProgressIndicator();
} else {
return Container(height: 4);
}
}));
}
return Column(
children: children,
@@ -75,7 +74,6 @@ class _TbAppBarState extends TbContextState<TbAppBar> {
}
class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget {
final double? elevation;
final Color? shadowColor;
final bool showLoadingIndicator;
@@ -85,9 +83,14 @@ class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget {
@override
final Size preferredSize;
TbAppSearchBar(TbContext tbContext, {this.elevation = 8,
this.shadowColor, this.showLoadingIndicator = false, this.searchHint, this.onSearch}) :
preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)),
TbAppSearchBar(TbContext tbContext,
{this.elevation = 8,
this.shadowColor,
this.showLoadingIndicator = false,
this.searchHint,
this.onSearch})
: preferredSize =
Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)),
super(tbContext);
@override
@@ -95,7 +98,6 @@ class TbAppSearchBar extends TbContextWidget implements PreferredSizeWidget {
}
class _TbAppSearchBarState extends TbContextState<TbAppSearchBar> {
final TextEditingController _filter = new TextEditingController();
final _textUpdates = StreamController<String>();
@@ -106,7 +108,11 @@ class _TbAppSearchBarState extends TbContextState<TbAppSearchBar> {
_filter.addListener(() {
_textUpdates.add(_filter.text);
});
_textUpdates.stream.skip(1).debounce(const Duration(milliseconds: 150)).distinct().forEach((element) => widget.onSearch!(element));
_textUpdates.stream
.skip(1)
.debounce(const Duration(milliseconds: 150))
.distinct()
.forEach((element) => widget.onSearch!(element));
}
@override
@@ -120,18 +126,15 @@ class _TbAppSearchBarState extends TbContextState<TbAppSearchBar> {
List<Widget> children = <Widget>[];
children.add(buildSearchBar());
if (widget.showLoadingIndicator) {
children.add(
ValueListenableBuilder(
valueListenable: loadingNotifier,
builder: (context, bool loading, child) {
if (loading) {
return LinearProgressIndicator();
} else {
return Container(height: 4);
}
}
)
);
children.add(ValueListenableBuilder(
valueListenable: loadingNotifier,
builder: (context, bool loading, child) {
if (loading) {
return LinearProgressIndicator();
} else {
return Container(height: 4);
}
}));
}
return Column(
children: children,
@@ -140,40 +143,37 @@ class _TbAppSearchBarState extends TbContextState<TbAppSearchBar> {
AppBar buildSearchBar() {
return AppBar(
centerTitle: true,
elevation: widget.elevation ?? 8,
shadowColor: widget.shadowColor ?? Color(0xFFFFFFFF).withAlpha(150),
title: TextField(
controller: _filter,
autofocus: true,
// cursorColor: Colors.white,
decoration: new InputDecoration(
border: InputBorder.none,
hintStyle: TextStyle(
color: Color(0xFF282828).withAlpha((255 * 0.38).ceil()),
),
contentPadding: EdgeInsets.only(left: 15, bottom: 11, top: 15, right: 15),
hintText: widget.searchHint ?? 'Search',
)
),
actions: [
ValueListenableBuilder(valueListenable: _filter,
builder: (context, value, child) {
if (_filter.text.isNotEmpty) {
return IconButton(
icon: Icon(
Icons.clear
),
onPressed: () {
_filter.text = '';
},
);
} else {
return Container();
}
}
)
]
);
centerTitle: true,
elevation: widget.elevation ?? 8,
shadowColor: widget.shadowColor ?? Color(0xFFFFFFFF).withAlpha(150),
title: TextField(
controller: _filter,
autofocus: true,
// cursorColor: Colors.white,
decoration: new InputDecoration(
border: InputBorder.none,
hintStyle: TextStyle(
color: Color(0xFF282828).withAlpha((255 * 0.38).ceil()),
),
contentPadding:
EdgeInsets.only(left: 15, bottom: 11, top: 15, right: 15),
hintText: widget.searchHint ?? 'Search',
)),
actions: [
ValueListenableBuilder(
valueListenable: _filter,
builder: (context, value, child) {
if (_filter.text.isNotEmpty) {
return IconButton(
icon: Icon(Icons.clear),
onPressed: () {
_filter.text = '';
},
);
} else {
return Container();
}
})
]);
}
}

View File

@@ -1,12 +1,10 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:thingsboard_app/constants/assets_path.dart';
class TbProgressIndicator extends ProgressIndicator {
final double size;
const TbProgressIndicator({
@@ -16,22 +14,22 @@ class TbProgressIndicator extends ProgressIndicator {
String? semanticsLabel,
String? semanticsValue,
}) : super(
key: key,
value: null,
valueColor: valueColor,
semanticsLabel: semanticsLabel,
semanticsValue: semanticsValue,
);
key: key,
value: null,
valueColor: valueColor,
semanticsLabel: semanticsLabel,
semanticsValue: semanticsValue,
);
@override
_TbProgressIndicatorState createState() => _TbProgressIndicatorState();
Color _getValueColor(BuildContext context) => valueColor?.value ?? Theme.of(context).primaryColor;
Color _getValueColor(BuildContext context) =>
valueColor?.value ?? Theme.of(context).primaryColor;
}
class _TbProgressIndicatorState extends State<TbProgressIndicator> with SingleTickerProviderStateMixin {
class _TbProgressIndicatorState extends State<TbProgressIndicator>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late CurvedAnimation _rotation;
@@ -39,8 +37,10 @@ class _TbProgressIndicatorState extends State<TbProgressIndicator> with SingleTi
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this, upperBound: 1, animationBehavior: AnimationBehavior.preserve);
duration: const Duration(milliseconds: 1500),
vsync: this,
upperBound: 1,
animationBehavior: AnimationBehavior.preserve);
_rotation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
_controller.repeat();
}
@@ -48,8 +48,7 @@ class _TbProgressIndicatorState extends State<TbProgressIndicator> with SingleTi
@override
void didUpdateWidget(TbProgressIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (!_controller.isAnimating)
_controller.repeat();
if (!_controller.isAnimating) _controller.repeat();
}
@override
@@ -74,13 +73,10 @@ class _TbProgressIndicatorState extends State<TbProgressIndicator> with SingleTi
color: widget._getValueColor(context)),
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _rotation.value * pi * 2,
child: child
);
angle: _rotation.value * pi * 2, child: child);
},
)
],
);
}
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/widgets.dart';
import 'package:preload_page_view/preload_page_view.dart';
class TwoPageViewController {
_TwoPageViewState? _state;
setTransitionIndexedStackState(_TwoPageViewState state) {
@@ -24,7 +23,6 @@ class TwoPageViewController {
}
int? get index => _state?._selectedIndex;
}
class TwoPageView extends StatefulWidget {
@@ -33,21 +31,19 @@ class TwoPageView extends StatefulWidget {
final Duration duration;
final TwoPageViewController? controller;
const TwoPageView({
Key? key,
required this.first,
required this.second,
this.controller,
this.duration = const Duration(milliseconds: 250)
}) : super(key: key);
const TwoPageView(
{Key? key,
required this.first,
required this.second,
this.controller,
this.duration = const Duration(milliseconds: 250)})
: super(key: key);
@override
_TwoPageViewState createState() => _TwoPageViewState();
}
class _TwoPageViewState extends State<TwoPageView> {
late List<Widget> _pages;
bool _reverse = false;
int _selectedIndex = 0;
@@ -68,7 +64,8 @@ class _TwoPageViewState extends State<TwoPageView> {
_reverse = true;
});
}
await _pageController.animateToPage(_selectedIndex, duration: widget.duration, curve: Curves.fastOutSlowIn);
await _pageController.animateToPage(_selectedIndex,
duration: widget.duration, curve: Curves.fastOutSlowIn);
return true;
}
return false;
@@ -77,7 +74,8 @@ class _TwoPageViewState extends State<TwoPageView> {
Future<bool> _close(int index, {bool animate = true}) async {
if (_selectedIndex == index) {
_selectedIndex = index == 1 ? 0 : 1;
await _pageController.animateToPage(_selectedIndex, duration: widget.duration, curve: Curves.fastOutSlowIn);
await _pageController.animateToPage(_selectedIndex,
duration: widget.duration, curve: Curves.fastOutSlowIn);
if (index == 0) {
setState(() {
_reverse = false;
@@ -106,5 +104,4 @@ class _TwoPageViewState extends State<TwoPageView> {
controller: _pageController,
);
}
}

View File

@@ -2,14 +2,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
class TwoValueListenableBuilder<A, B> extends StatelessWidget {
TwoValueListenableBuilder(
{
Key? key,
required this.firstValueListenable,
required this.secondValueListenable,
required this.builder,
this.child,
}) : super(key: key);
TwoValueListenableBuilder({
Key? key,
required this.firstValueListenable,
required this.secondValueListenable,
required this.builder,
this.child,
}) : super(key: key);
final ValueListenable<A> firstValueListenable;
final ValueListenable<B> secondValueListenable;

Some files were not shown because too many files have changed in this diff Show More