Merge branch 'master' of github.com:thingsboard/flutter_thingsboard_app
This commit is contained in:
20
README.md
20
README.md
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
6
android/app/src/main/res/xml/provider_paths.xml
Normal file
6
android/app/src/main/res/xml/provider_paths.xml
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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!);
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class LoginPageBackground extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.expand(
|
||||
child: CustomPaint(
|
||||
painter: _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor),
|
||||
)
|
||||
);
|
||||
painter:
|
||||
_LoginPageBackgroundPainter(color: Theme.of(context).primaryColor),
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _LoginPageBackgroundPainter extends CustomPainter {
|
||||
|
||||
final Color color;
|
||||
|
||||
const _LoginPageBackgroundPainter({required this.color});
|
||||
|
||||
@@ -1,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;
|
||||
}
|
||||
}
|
||||
|
||||
504
lib/core/auth/login/two_factor_authentication_page.dart
Normal file
504
lib/core/auth/login/two_factor_authentication_page.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
import 'package:thingsboard_app/constants/app_constants.dart';
|
||||
|
||||
abstract class AppSecretProvider {
|
||||
|
||||
Future<String> getAppSecret();
|
||||
|
||||
factory AppSecretProvider.local() => _LocalAppSecretProvider();
|
||||
|
||||
}
|
||||
|
||||
/// Not for production (only for debugging)
|
||||
class _LocalAppSecretProvider implements AppSecretProvider {
|
||||
|
||||
@override
|
||||
Future<String> getAppSecret() async {
|
||||
return ThingsboardAppConstants.thingsboardOAuth2AppSecret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,39 +19,42 @@ class TbOAuth2AuthenticateResult {
|
||||
TbOAuth2AuthenticateResult.failed(this.error);
|
||||
|
||||
bool get success => error == null;
|
||||
|
||||
}
|
||||
|
||||
class TbOAuth2Client {
|
||||
|
||||
final TbContext _tbContext;
|
||||
final AppSecretProvider _appSecretProvider;
|
||||
|
||||
TbOAuth2Client(
|
||||
{ required TbContext tbContext,
|
||||
required AppSecretProvider appSecretProvider} ):
|
||||
_tbContext = tbContext,
|
||||
_appSecretProvider = appSecretProvider;
|
||||
{required TbContext tbContext,
|
||||
required AppSecretProvider appSecretProvider})
|
||||
: _tbContext = tbContext,
|
||||
_appSecretProvider = appSecretProvider;
|
||||
|
||||
Future<TbOAuth2AuthenticateResult> authenticate(String oauth2Url) async {
|
||||
final appSecret = await _appSecretProvider.getAppSecret();
|
||||
final pkgName = _tbContext.packageName;
|
||||
final jwt = JWT(
|
||||
{
|
||||
'callbackUrlScheme': ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme
|
||||
'callbackUrlScheme':
|
||||
ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme
|
||||
},
|
||||
issuer: pkgName,
|
||||
);
|
||||
final key = SecretKey(appSecret);
|
||||
final appToken = jwt.sign(key, algorithm: _HMACBase64Algorithm.HS512, expiresIn: Duration(minutes: 2));
|
||||
var url = Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + oauth2Url);
|
||||
final params = Map<String,String>.from(url.queryParameters);
|
||||
final appToken = jwt.sign(key,
|
||||
algorithm: _HMACBase64Algorithm.HS512, expiresIn: Duration(minutes: 2));
|
||||
var url =
|
||||
Uri.parse(ThingsboardAppConstants.thingsBoardApiEndpoint + oauth2Url);
|
||||
final params = Map<String, String>.from(url.queryParameters);
|
||||
params['pkg'] = pkgName;
|
||||
params['appToken'] = appToken;
|
||||
url = url.replace(queryParameters: params);
|
||||
final result = await TbWebAuth.authenticate(
|
||||
url: url.toString(),
|
||||
callbackUrlScheme: ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme, saveHistory: false);
|
||||
callbackUrlScheme:
|
||||
ThingsboardAppConstants.thingsboardOAuth2CallbackUrlScheme,
|
||||
saveHistory: false);
|
||||
final resultUri = Uri.parse(result);
|
||||
final error = resultUri.queryParameters['error'];
|
||||
if (error != null) {
|
||||
@@ -62,14 +65,14 @@ class TbOAuth2Client {
|
||||
if (accessToken != null && refreshToken != null) {
|
||||
return TbOAuth2AuthenticateResult.success(accessToken, refreshToken);
|
||||
} else {
|
||||
return TbOAuth2AuthenticateResult.failed('No authentication credentials in response.');
|
||||
return TbOAuth2AuthenticateResult.failed(
|
||||
'No authentication credentials in response.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _HMACBase64Algorithm extends JWTAlgorithm {
|
||||
|
||||
static const HS512 = _HMACBase64Algorithm('HS512');
|
||||
|
||||
final String _name;
|
||||
|
||||
@@ -19,13 +19,18 @@ class _OnAppLifecycleResumeObserver extends WidgetsBindingObserver {
|
||||
class TbWebAuth {
|
||||
static const MethodChannel _channel = const MethodChannel('tb_web_auth');
|
||||
|
||||
static final _OnAppLifecycleResumeObserver _resumedObserver = _OnAppLifecycleResumeObserver(() {
|
||||
static final _OnAppLifecycleResumeObserver _resumedObserver =
|
||||
_OnAppLifecycleResumeObserver(() {
|
||||
_cleanUpDanglingCalls();
|
||||
});
|
||||
|
||||
static Future<String> authenticate({required String url, required String callbackUrlScheme, bool? saveHistory}) async {
|
||||
WidgetsBinding.instance?.removeObserver(_resumedObserver); // safety measure so we never add this observer twice
|
||||
WidgetsBinding.instance?.addObserver(_resumedObserver);
|
||||
static Future<String> authenticate(
|
||||
{required String url,
|
||||
required String callbackUrlScheme,
|
||||
bool? saveHistory}) async {
|
||||
WidgetsBinding.instance.removeObserver(
|
||||
_resumedObserver); // safety measure so we never add this observer twice
|
||||
WidgetsBinding.instance.addObserver(_resumedObserver);
|
||||
return await _channel.invokeMethod('authenticate', <String, dynamic>{
|
||||
'url': url,
|
||||
'callbackUrlScheme': callbackUrlScheme,
|
||||
@@ -35,6 +40,6 @@ class TbWebAuth {
|
||||
|
||||
static Future<void> _cleanUpDanglingCalls() async {
|
||||
await _channel.invokeMethod('cleanUpDanglingCalls');
|
||||
WidgetsBinding.instance?.removeObserver(_resumedObserver);
|
||||
WidgetsBinding.instance.removeObserver(_resumedObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,7 @@ import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
import 'package:thingsboard_app/utils/services/tb_app_storage.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
|
||||
enum NotificationType {
|
||||
info,
|
||||
warn,
|
||||
success,
|
||||
error
|
||||
}
|
||||
enum NotificationType { info, warn, success, error }
|
||||
|
||||
class TbLogOutput extends LogOutput {
|
||||
@override
|
||||
@@ -45,18 +40,14 @@ class TbLogsFilter extends LogFilter {
|
||||
class TbLogger {
|
||||
final _logger = Logger(
|
||||
filter: TbLogsFilter(),
|
||||
printer: PrefixPrinter(
|
||||
PrettyPrinter(
|
||||
methodCount: 0,
|
||||
errorMethodCount: 8,
|
||||
lineLength: 200,
|
||||
colors: false,
|
||||
printEmojis: true,
|
||||
printTime: false
|
||||
)
|
||||
),
|
||||
output: TbLogOutput()
|
||||
);
|
||||
printer: PrefixPrinter(PrettyPrinter(
|
||||
methodCount: 0,
|
||||
errorMethodCount: 8,
|
||||
lineLength: 200,
|
||||
colors: false,
|
||||
printEmojis: true,
|
||||
printTime: false)),
|
||||
output: TbLogOutput());
|
||||
|
||||
void verbose(dynamic message, [dynamic error, StackTrace? stackTrace]) {
|
||||
_logger.v(message, error, stackTrace);
|
||||
@@ -83,11 +74,15 @@ class TbLogger {
|
||||
}
|
||||
}
|
||||
|
||||
typedef OpenDashboardCallback = void Function(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar});
|
||||
typedef OpenDashboardCallback = void Function(String dashboardId,
|
||||
{String? dashboardTitle, String? state, bool? hideToolbar});
|
||||
|
||||
abstract class TbMainDashboardHolder {
|
||||
|
||||
Future<void> navigateToDashboard(String dashboardId, {String? dashboardTitle, String? state, bool? hideToolbar, bool animate = true});
|
||||
Future<void> navigateToDashboard(String dashboardId,
|
||||
{String? dashboardTitle,
|
||||
String? state,
|
||||
bool? hideToolbar,
|
||||
bool animate = true});
|
||||
|
||||
Future<bool> openMain({bool animate});
|
||||
|
||||
@@ -100,7 +95,6 @@ abstract class TbMainDashboardHolder {
|
||||
bool isDashboardOpen();
|
||||
|
||||
Future<bool> dashboardGoBack();
|
||||
|
||||
}
|
||||
|
||||
class TbContext {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
|
||||
abstract class RefreshableWidget extends Widget {
|
||||
refresh();
|
||||
}
|
||||
|
||||
abstract class TbContextStatelessWidget extends StatelessWidget with HasTbContext {
|
||||
abstract class TbContextStatelessWidget extends StatelessWidget
|
||||
with HasTbContext {
|
||||
TbContextStatelessWidget(TbContext tbContext, {Key? key}) : super(key: key) {
|
||||
setTbContext(tbContext);
|
||||
}
|
||||
@@ -18,8 +18,8 @@ abstract class TbContextWidget extends StatefulWidget with HasTbContext {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TbContextState<T extends TbContextWidget> extends State<T> with HasTbContext {
|
||||
|
||||
abstract class TbContextState<T extends TbContextWidget> extends State<T>
|
||||
with HasTbContext {
|
||||
final bool handleLoading;
|
||||
bool closeMainFirst = false;
|
||||
|
||||
@@ -35,25 +35,23 @@ abstract class TbContextState<T extends TbContextWidget> extends State<T> with H
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mixin TbMainState {
|
||||
|
||||
bool canNavigate(String path);
|
||||
|
||||
navigateToPath(String path);
|
||||
|
||||
bool isHomePage();
|
||||
|
||||
}
|
||||
|
||||
abstract class TbPageWidget extends TbContextWidget {
|
||||
TbPageWidget(TbContext tbContext, {Key? key}) : super(tbContext, key: key);
|
||||
}
|
||||
|
||||
abstract class TbPageState<W extends TbPageWidget> extends TbContextState<W> with RouteAware {
|
||||
TbPageState({bool handleUserLoaded = false}): super(handleLoading: true);
|
||||
abstract class TbPageState<W extends TbPageWidget> extends TbContextState<W>
|
||||
with RouteAware {
|
||||
TbPageState({bool handleUserLoaded = false}) : super(handleLoading: true);
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -77,25 +75,20 @@ abstract class TbPageState<W extends TbPageWidget> extends TbContextState<W> wit
|
||||
hideNotification();
|
||||
setupCurrentState(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TextContextWidget extends TbContextWidget {
|
||||
|
||||
final String text;
|
||||
|
||||
TextContextWidget(TbContext tbContext, this.text) : super(tbContext);
|
||||
|
||||
@override
|
||||
_TextContextWidgetState createState() => _TextContextWidgetState();
|
||||
|
||||
}
|
||||
|
||||
class _TextContextWidgetState extends TbContextState<TextContextWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(body: Center(child: Text(widget.text)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,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,
|
||||
|
||||
@@ -6,14 +6,11 @@ import 'entities_base.dart';
|
||||
import 'entity_grid_card.dart';
|
||||
|
||||
mixin EntitiesGridStateBase on StatefulWidget {
|
||||
|
||||
@override
|
||||
_EntitiesGridState createState() => _EntitiesGridState();
|
||||
|
||||
}
|
||||
|
||||
class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
|
||||
|
||||
_EntitiesGridState() : super();
|
||||
|
||||
@override
|
||||
@@ -24,9 +21,7 @@ class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
|
||||
if (heading != null) {
|
||||
slivers.add(SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: heading
|
||||
)));
|
||||
sliver: SliverToBoxAdapter(child: heading)));
|
||||
}
|
||||
slivers.add(SliverPadding(
|
||||
padding: EdgeInsets.all(16),
|
||||
@@ -44,19 +39,17 @@ class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
|
||||
),
|
||||
builderDelegate: PagedChildBuilderDelegate<T>(
|
||||
itemBuilder: (context, item, index) => EntityGridCard<T>(
|
||||
item,
|
||||
key: widget.getKey(item),
|
||||
entityCardWidgetBuilder: widget.buildEntityGridCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
settings: widget.entityGridCardSettings(item),
|
||||
),
|
||||
firstPageProgressIndicatorBuilder: firstPageProgressIndicatorBuilder,
|
||||
newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder,
|
||||
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder
|
||||
)
|
||||
)));
|
||||
return CustomScrollView(
|
||||
slivers: slivers
|
||||
);
|
||||
item,
|
||||
key: widget.getKey(item),
|
||||
entityCardWidgetBuilder: widget.buildEntityGridCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
settings: widget.entityGridCardSettings(item),
|
||||
),
|
||||
firstPageProgressIndicatorBuilder:
|
||||
firstPageProgressIndicatorBuilder,
|
||||
newPageProgressIndicatorBuilder:
|
||||
newPageProgressIndicatorBuilder,
|
||||
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder))));
|
||||
return CustomScrollView(slivers: slivers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,11 @@ import 'package:thingsboard_app/core/entity/entities_base.dart';
|
||||
import 'entity_list_card.dart';
|
||||
|
||||
mixin EntitiesListStateBase on StatefulWidget {
|
||||
|
||||
@override
|
||||
_EntitiesListState createState() => _EntitiesListState();
|
||||
|
||||
}
|
||||
|
||||
class _EntitiesListState<T,P> extends BaseEntitiesState<T, P> {
|
||||
|
||||
class _EntitiesListState<T, P> extends BaseEntitiesState<T, P> {
|
||||
_EntitiesListState() : super();
|
||||
|
||||
@override
|
||||
@@ -23,9 +20,7 @@ class _EntitiesListState<T,P> extends BaseEntitiesState<T, P> {
|
||||
if (heading != null) {
|
||||
slivers.add(SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: heading
|
||||
)));
|
||||
sliver: SliverToBoxAdapter(child: heading)));
|
||||
}
|
||||
slivers.add(SliverPadding(
|
||||
padding: EdgeInsets.all(16),
|
||||
@@ -34,19 +29,16 @@ class _EntitiesListState<T,P> extends BaseEntitiesState<T, P> {
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
builderDelegate: PagedChildBuilderDelegate<T>(
|
||||
itemBuilder: (context, item, index) => EntityListCard<T>(
|
||||
item,
|
||||
key: widget.getKey(item),
|
||||
entityCardWidgetBuilder: widget.buildEntityListCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
settings: widget.entityListCardSettings(item),
|
||||
),
|
||||
firstPageProgressIndicatorBuilder: firstPageProgressIndicatorBuilder,
|
||||
newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder,
|
||||
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder
|
||||
)
|
||||
)));
|
||||
return CustomScrollView(
|
||||
slivers: slivers
|
||||
);
|
||||
item,
|
||||
key: widget.getKey(item),
|
||||
entityCardWidgetBuilder: widget.buildEntityListCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
),
|
||||
firstPageProgressIndicatorBuilder:
|
||||
firstPageProgressIndicatorBuilder,
|
||||
newPageProgressIndicatorBuilder:
|
||||
newPageProgressIndicatorBuilder,
|
||||
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder))));
|
||||
return CustomScrollView(slivers: slivers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:fading_edge_scrollview/fading_edge_scrollview.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/core/entity/entities_base.dart';
|
||||
@@ -11,14 +10,15 @@ import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
import 'entity_list_card.dart';
|
||||
|
||||
class EntitiesListWidgetController {
|
||||
|
||||
final List<_EntitiesListWidgetState> states = [];
|
||||
|
||||
void _registerEntitiesWidgetState(_EntitiesListWidgetState entitiesListWidgetState) {
|
||||
void _registerEntitiesWidgetState(
|
||||
_EntitiesListWidgetState entitiesListWidgetState) {
|
||||
states.add(entitiesListWidgetState);
|
||||
}
|
||||
|
||||
void _unregisterEntitiesWidgetState(_EntitiesListWidgetState entitiesListWidgetState) {
|
||||
void _unregisterEntitiesWidgetState(
|
||||
_EntitiesListWidgetState entitiesListWidgetState) {
|
||||
states.remove(entitiesListWidgetState);
|
||||
}
|
||||
|
||||
@@ -29,45 +29,48 @@ class EntitiesListWidgetController {
|
||||
void dispose() {
|
||||
states.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class EntitiesListPageLinkWidget<T> extends EntitiesListWidget<T, PageLink> {
|
||||
|
||||
EntitiesListPageLinkWidget(TbContext tbContext, {EntitiesListWidgetController? controller}) : super(tbContext, controller: controller);
|
||||
abstract class EntitiesListPageLinkWidget<T>
|
||||
extends EntitiesListWidget<T, PageLink> {
|
||||
EntitiesListPageLinkWidget(TbContext tbContext,
|
||||
{EntitiesListWidgetController? controller})
|
||||
: super(tbContext, controller: controller);
|
||||
|
||||
@override
|
||||
PageKeyController<PageLink> createPageKeyController() => PageLinkController(pageSize: 5);
|
||||
|
||||
PageKeyController<PageLink> createPageKeyController() =>
|
||||
PageLinkController(pageSize: 5);
|
||||
}
|
||||
|
||||
abstract class EntitiesListWidget<T, P> extends TbContextWidget with EntitiesBase<T,P> {
|
||||
|
||||
abstract class EntitiesListWidget<T, P> extends TbContextWidget
|
||||
with EntitiesBase<T, P> {
|
||||
final EntitiesListWidgetController? _controller;
|
||||
|
||||
EntitiesListWidget(TbContext tbContext, {EntitiesListWidgetController? controller}):
|
||||
_controller = controller,
|
||||
super(tbContext);
|
||||
EntitiesListWidget(TbContext tbContext,
|
||||
{EntitiesListWidgetController? controller})
|
||||
: _controller = controller,
|
||||
super(tbContext);
|
||||
|
||||
@override
|
||||
_EntitiesListWidgetState createState() => _EntitiesListWidgetState(_controller);
|
||||
_EntitiesListWidgetState createState() =>
|
||||
_EntitiesListWidgetState(_controller);
|
||||
|
||||
PageKeyController<P> createPageKeyController();
|
||||
|
||||
void onViewAll();
|
||||
|
||||
}
|
||||
|
||||
class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,P>> {
|
||||
|
||||
class _EntitiesListWidgetState<T, P>
|
||||
extends TbContextState<EntitiesListWidget<T, P>> {
|
||||
final EntitiesListWidgetController? _controller;
|
||||
|
||||
late final PageKeyController<P> _pageKeyController;
|
||||
|
||||
final StreamController<PageData<T>?> _entitiesStreamController = StreamController.broadcast();
|
||||
final StreamController<PageData<T>?> _entitiesStreamController =
|
||||
StreamController.broadcast();
|
||||
|
||||
_EntitiesListWidgetState(EntitiesListWidgetController? controller):
|
||||
_controller = controller;
|
||||
_EntitiesListWidgetState(EntitiesListWidgetController? controller)
|
||||
: _controller = controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -76,7 +79,7 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
|
||||
if (_controller != null) {
|
||||
_controller!._registerEntitiesWidgetState(this);
|
||||
}
|
||||
_refresh();
|
||||
_refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -121,17 +124,15 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
|
||||
builder: (context, snapshot) {
|
||||
var title = widget.title;
|
||||
if (snapshot.hasData) {
|
||||
var data = snapshot.data;
|
||||
title += ' (${data!.totalElements})';
|
||||
var data = snapshot.data;
|
||||
title += ' (${data!.totalElements})';
|
||||
}
|
||||
return Text(title,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF282828),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 1.5
|
||||
)
|
||||
);
|
||||
style: TextStyle(
|
||||
color: Color(0xFF282828),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 1.5));
|
||||
},
|
||||
),
|
||||
Spacer(),
|
||||
@@ -141,73 +142,62 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero),
|
||||
child: Text('View all')
|
||||
)
|
||||
child: Text('View all'))
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 64,
|
||||
child: StreamBuilder<PageData<T>?>(
|
||||
stream: _entitiesStreamController.stream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var data = snapshot.data!;
|
||||
if (data.data.isEmpty) {
|
||||
return _buildNoEntitiesFound(); //return Text('Loaded');
|
||||
stream: _entitiesStreamController.stream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var data = snapshot.data!;
|
||||
if (data.data.isEmpty) {
|
||||
return _buildNoEntitiesFound(); //return Text('Loaded');
|
||||
} else {
|
||||
return _buildEntitiesView(context, data.data);
|
||||
}
|
||||
} else {
|
||||
return _buildEntitiesView(context, data.data);
|
||||
return Center(
|
||||
child: RefreshProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
Theme.of(tbContext.currentState!.context)
|
||||
.colorScheme
|
||||
.primary),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Center(
|
||||
child: RefreshProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(Theme.of(tbContext.currentState!.context).colorScheme.primary),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
}),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
))),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(25),
|
||||
blurRadius: 10.0,
|
||||
offset: Offset(0, 4)
|
||||
),
|
||||
offset: Offset(0, 4)),
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(18),
|
||||
blurRadius: 30.0,
|
||||
offset: Offset(0, 10)
|
||||
),
|
||||
offset: Offset(0, 10)),
|
||||
],
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildNoEntitiesFound() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Color(0xFFDEDEDE),
|
||||
style: BorderStyle.solid,
|
||||
width: 1
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4)
|
||||
),
|
||||
child: Center(
|
||||
child:
|
||||
Text(widget.noItemsFoundText,
|
||||
color: Color(0xFFDEDEDE), style: BorderStyle.solid, width: 1),
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
child: Center(
|
||||
child: Text(widget.noItemsFoundText,
|
||||
style: TextStyle(
|
||||
color: Color(0xFFAFAFAF),
|
||||
fontSize: 14,
|
||||
)
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,13 +209,11 @@ class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: ScrollController(),
|
||||
children: entities.map((entity) => EntityListCard<T>(
|
||||
entity,
|
||||
entityCardWidgetBuilder: widget.buildEntityListWidgetCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
settings: widget.entityListCardSettings(entity),
|
||||
listWidgetCard: true
|
||||
)).toList()
|
||||
));
|
||||
children: entities
|
||||
.map((entity) => EntityListCard<T>(entity,
|
||||
entityCardWidgetBuilder: widget.buildEntityListWidgetCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
listWidgetCard: true))
|
||||
.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
@@ -8,18 +6,11 @@ import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
|
||||
abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget {
|
||||
final labelTextStyle =
|
||||
TextStyle(color: Color(0xFF757575), fontSize: 14, height: 20 / 14);
|
||||
|
||||
final labelTextStyle = TextStyle(
|
||||
color: Color(0xFF757575),
|
||||
fontSize: 14,
|
||||
height: 20 / 14
|
||||
);
|
||||
|
||||
final valueTextStyle = TextStyle(
|
||||
color: Color(0xFF282828),
|
||||
fontSize: 14,
|
||||
height: 20 / 14
|
||||
);
|
||||
final valueTextStyle =
|
||||
TextStyle(color: Color(0xFF282828), fontSize: 14, height: 20 / 14);
|
||||
|
||||
final String _defaultTitle;
|
||||
final String _entityId;
|
||||
@@ -29,19 +20,19 @@ abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget {
|
||||
final double? _appBarElevation;
|
||||
|
||||
EntityDetailsPage(TbContext tbContext,
|
||||
{required String defaultTitle,
|
||||
required String entityId,
|
||||
String? subTitle,
|
||||
bool showLoadingIndicator = true,
|
||||
bool hideAppBar = false,
|
||||
double? appBarElevation}):
|
||||
this._defaultTitle = defaultTitle,
|
||||
this._entityId = entityId,
|
||||
this._subTitle = subTitle,
|
||||
this._showLoadingIndicator = showLoadingIndicator,
|
||||
this._hideAppBar = hideAppBar,
|
||||
this._appBarElevation = appBarElevation,
|
||||
super(tbContext);
|
||||
{required String defaultTitle,
|
||||
required String entityId,
|
||||
String? subTitle,
|
||||
bool showLoadingIndicator = true,
|
||||
bool hideAppBar = false,
|
||||
double? appBarElevation})
|
||||
: this._defaultTitle = defaultTitle,
|
||||
this._entityId = entityId,
|
||||
this._subTitle = subTitle,
|
||||
this._showLoadingIndicator = showLoadingIndicator,
|
||||
this._hideAppBar = hideAppBar,
|
||||
this._appBarElevation = appBarElevation,
|
||||
super(tbContext);
|
||||
|
||||
@override
|
||||
_EntityDetailsPageState createState() => _EntityDetailsPageState();
|
||||
@@ -53,11 +44,10 @@ abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget {
|
||||
}
|
||||
|
||||
Widget buildEntityDetails(BuildContext context, T entity);
|
||||
|
||||
}
|
||||
|
||||
class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDetailsPage<T>> {
|
||||
|
||||
class _EntityDetailsPageState<T extends BaseData>
|
||||
extends TbPageState<EntityDetailsPage<T>> {
|
||||
late Future<T?> entityFuture;
|
||||
late ValueNotifier<String> titleValue;
|
||||
|
||||
@@ -70,7 +60,7 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
|
||||
titleValue = ValueNotifier(widget._defaultTitle);
|
||||
entityFuture.then((value) {
|
||||
if (value is HasName) {
|
||||
titleValue.value = (value as HasName).getName();
|
||||
titleValue.value = (value as HasName).getName();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -82,36 +72,43 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: widget._hideAppBar ? null : TbAppBar(
|
||||
tbContext,
|
||||
showLoadingIndicator: widget._showLoadingIndicator,
|
||||
elevation: widget._appBarElevation,
|
||||
title: ValueListenableBuilder<String>(
|
||||
valueListenable: titleValue,
|
||||
builder: (context, title, _widget) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(title,
|
||||
style: widget._subTitle != null ? Theme.of(context).primaryTextTheme.headline6!.copyWith(
|
||||
fontSize: 16
|
||||
) : null
|
||||
)
|
||||
),
|
||||
if (widget._subTitle != null) Text(widget._subTitle!, style: TextStyle(
|
||||
color: Theme.of(context).primaryTextTheme.headline6!.color!.withAlpha((0.38 * 255).ceil()),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 16 / 12
|
||||
))
|
||||
]
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
appBar: widget._hideAppBar
|
||||
? null
|
||||
: TbAppBar(
|
||||
tbContext,
|
||||
showLoadingIndicator: widget._showLoadingIndicator,
|
||||
elevation: widget._appBarElevation,
|
||||
title: ValueListenableBuilder<String>(
|
||||
valueListenable: titleValue,
|
||||
builder: (context, title, _widget) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(title,
|
||||
style: widget._subTitle != null
|
||||
? Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headline6!
|
||||
.copyWith(fontSize: 16)
|
||||
: null)),
|
||||
if (widget._subTitle != null)
|
||||
Text(widget._subTitle!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headline6!
|
||||
.color!
|
||||
.withAlpha((0.38 * 255).ceil()),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 16 / 12))
|
||||
]);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: FutureBuilder<T?>(
|
||||
future: entityFuture,
|
||||
builder: (context, snapshot) {
|
||||
@@ -123,7 +120,8 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
|
||||
return Center(child: Text('Requested entity does not exists.'));
|
||||
}
|
||||
} else {
|
||||
return Center(child: TbProgressIndicator(
|
||||
return Center(
|
||||
child: TbProgressIndicator(
|
||||
size: 50.0,
|
||||
));
|
||||
}
|
||||
@@ -131,21 +129,24 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class ContactBasedDetailsPage<T extends ContactBased> extends EntityDetailsPage<T> {
|
||||
|
||||
abstract class ContactBasedDetailsPage<T extends ContactBased>
|
||||
extends EntityDetailsPage<T> {
|
||||
ContactBasedDetailsPage(TbContext tbContext,
|
||||
{ required String defaultTitle,
|
||||
required String entityId,
|
||||
String? subTitle,
|
||||
bool showLoadingIndicator = true,
|
||||
bool hideAppBar = false,
|
||||
double? appBarElevation}):
|
||||
super(tbContext, defaultTitle: defaultTitle, entityId: entityId,
|
||||
subTitle: subTitle, showLoadingIndicator: showLoadingIndicator,
|
||||
hideAppBar: hideAppBar, appBarElevation: appBarElevation);
|
||||
{required String defaultTitle,
|
||||
required String entityId,
|
||||
String? subTitle,
|
||||
bool showLoadingIndicator = true,
|
||||
bool hideAppBar = false,
|
||||
double? appBarElevation})
|
||||
: super(tbContext,
|
||||
defaultTitle: defaultTitle,
|
||||
entityId: entityId,
|
||||
subTitle: subTitle,
|
||||
showLoadingIndicator: showLoadingIndicator,
|
||||
hideAppBar: hideAppBar,
|
||||
appBarElevation: appBarElevation);
|
||||
|
||||
@override
|
||||
Widget buildEntityDetails(BuildContext context, T contact) {
|
||||
@@ -201,9 +202,6 @@ abstract class ContactBasedDetailsPage<T extends ContactBased> extends EntityDet
|
||||
SizedBox(height: 16),
|
||||
Text('Email', style: labelTextStyle),
|
||||
Text(contact.email ?? '', style: valueTextStyle),
|
||||
]
|
||||
)
|
||||
);
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
|
||||
import 'entities_base.dart';
|
||||
|
||||
@@ -11,10 +8,12 @@ class EntityGridCard<T> extends StatelessWidget {
|
||||
final EntityCardWidgetBuilder<T> _entityCardWidgetBuilder;
|
||||
final EntityCardSettings _settings;
|
||||
|
||||
EntityGridCard(T entity, {Key? key, EntityTapFunction<T>? onEntityTap,
|
||||
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
|
||||
required EntityCardSettings settings}):
|
||||
this._entity = entity,
|
||||
EntityGridCard(T entity,
|
||||
{Key? key,
|
||||
EntityTapFunction<T>? onEntityTap,
|
||||
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
|
||||
required EntityCardSettings settings})
|
||||
: this._entity = entity,
|
||||
this._onEntityTap = onEntityTap,
|
||||
this._entityCardWidgetBuilder = entityCardWidgetBuilder,
|
||||
this._settings = settings,
|
||||
@@ -22,35 +21,31 @@ class EntityGridCard<T> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child:
|
||||
Container(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
elevation: 0,
|
||||
child: _entityCardWidgetBuilder(context, _entity)
|
||||
),
|
||||
decoration: _settings.dropShadow ? BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha((255 * 0.05).ceil()),
|
||||
blurRadius: 6.0,
|
||||
offset: Offset(0, 4)
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
elevation: 0,
|
||||
child: _entityCardWidgetBuilder(context, _entity)),
|
||||
decoration: _settings.dropShadow
|
||||
? BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha((255 * 0.05).ceil()),
|
||||
blurRadius: 6.0,
|
||||
offset: Offset(0, 4))
|
||||
],
|
||||
)
|
||||
],
|
||||
) : null,
|
||||
),
|
||||
onTap: () {
|
||||
if (_onEntityTap != null) {
|
||||
_onEntityTap!(_entity);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
onTap: () {
|
||||
if (_onEntityTap != null) {
|
||||
_onEntityTap!(_entity);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'entities_base.dart';
|
||||
|
||||
@@ -9,58 +7,51 @@ class EntityListCard<T> extends StatelessWidget {
|
||||
final T _entity;
|
||||
final EntityTapFunction<T>? _onEntityTap;
|
||||
final EntityCardWidgetBuilder<T> _entityCardWidgetBuilder;
|
||||
final EntityCardSettings _settings;
|
||||
|
||||
EntityListCard(T entity, {Key? key, EntityTapFunction<T>? onEntityTap,
|
||||
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
|
||||
required EntityCardSettings settings,
|
||||
bool listWidgetCard = false}):
|
||||
this._entity = entity,
|
||||
EntityListCard(T entity,
|
||||
{Key? key,
|
||||
EntityTapFunction<T>? onEntityTap,
|
||||
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
|
||||
bool listWidgetCard = false})
|
||||
: this._entity = entity,
|
||||
this._onEntityTap = onEntityTap,
|
||||
this._entityCardWidgetBuilder = entityCardWidgetBuilder,
|
||||
this._settings = settings,
|
||||
this._listWidgetCard = listWidgetCard,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child:
|
||||
Container(
|
||||
margin: _listWidgetCard ? EdgeInsets.only(right: 8) : EdgeInsets.zero,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
margin: _listWidgetCard ? EdgeInsets.only(right: 8) : EdgeInsets.zero,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
elevation: 0,
|
||||
child: _entityCardWidgetBuilder(context, _entity)),
|
||||
decoration: _listWidgetCard
|
||||
? BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Color(0xFFDEDEDE),
|
||||
style: BorderStyle.solid,
|
||||
width: 1),
|
||||
borderRadius: BorderRadius.circular(4))
|
||||
: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha((255 * 0.05).ceil()),
|
||||
blurRadius: 6.0,
|
||||
offset: Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
elevation: 0,
|
||||
child: _entityCardWidgetBuilder(context, _entity)
|
||||
),
|
||||
decoration: _listWidgetCard ? BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Color(0xFFDEDEDE),
|
||||
style: BorderStyle.solid,
|
||||
width: 1
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4)
|
||||
) : BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha((255 * 0.05).ceil()),
|
||||
blurRadius: 6.0,
|
||||
offset: Offset(0, 4)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (_onEntityTap != null) {
|
||||
_onEntityTap!(_entity);
|
||||
}
|
||||
),
|
||||
onTap: () {
|
||||
if (_onEntityTap != null) {
|
||||
_onEntityTap!(_entity);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
|
||||
|
||||
class ThingsboardInitApp extends TbPageWidget {
|
||||
|
||||
ThingsboardInitApp(TbContext tbContext, {Key? key}) : super(tbContext, key: key);
|
||||
ThingsboardInitApp(TbContext tbContext, {Key? key})
|
||||
: super(tbContext, key: key);
|
||||
|
||||
@override
|
||||
_ThingsboardInitAppState createState() => _ThingsboardInitAppState();
|
||||
|
||||
}
|
||||
|
||||
class _ThingsboardInitAppState extends TbPageState<ThingsboardInitApp> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -26,10 +23,7 @@ class _ThingsboardInitAppState extends TbPageState<ThingsboardInitApp> {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
color: Colors.white,
|
||||
child: TbProgressIndicator(
|
||||
size: 50.0
|
||||
),
|
||||
child: TbProgressIndicator(size: 50.0),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'init_app.dart';
|
||||
|
||||
class InitRoutes extends TbRoutes {
|
||||
|
||||
late var initHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
late var initHandler = Handler(
|
||||
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return ThingsboardInitApp(tbContext);
|
||||
});
|
||||
|
||||
@@ -18,5 +18,4 @@ class InitRoutes extends TbRoutes {
|
||||
void doRegisterRoutes(router) {
|
||||
router.define("/", handler: initHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
66
lib/generated/intl/messages_all.dart
Normal file
66
lib/generated/intl/messages_all.dart
Normal 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);
|
||||
}
|
||||
173
lib/generated/intl/messages_en.dart
Normal file
173
lib/generated/intl/messages_en.dart
Normal 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")
|
||||
};
|
||||
}
|
||||
107
lib/generated/intl/messages_zh.dart
Normal file
107
lib/generated/intl/messages_zh.dart
Normal 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
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
112
lib/l10n/intl_en.arb
Normal 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
90
lib/l10n/intl_zh.arb
Normal 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": "再试一次"
|
||||
}
|
||||
@@ -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),
|
||||
)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
)
|
||||
);
|
||||
]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
],
|
||||
))
|
||||
])))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
//}
|
||||
//),
|
||||
);
|
||||
//}
|
||||
//),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
))),
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
])
|
||||
],
|
||||
)))
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user