Initial commit
This commit is contained in:
44
lib/config/routes/router.dart
Normal file
44
lib/config/routes/router.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/auth/login/login_page.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/init/init_app.dart';
|
||||
import 'package:thingsboard_app/modules/device/devices_page.dart';
|
||||
import 'package:thingsboard_app/modules/home/home_page.dart';
|
||||
import 'package:thingsboard_app/modules/profile/profile_page.dart';
|
||||
|
||||
class ThingsboardAppRouter {
|
||||
final router = FluroRouter();
|
||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||
late final _tbContext = TbContext(router, routeObserver);
|
||||
|
||||
late var initHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return ThingsboardInitApp(tbContext);
|
||||
});
|
||||
|
||||
late var loginHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return LoginPage(_tbContext);
|
||||
});
|
||||
|
||||
late var homeHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return HomePage(_tbContext);
|
||||
});
|
||||
|
||||
late var profileHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return ProfilePage(_tbContext);
|
||||
});
|
||||
|
||||
late var devicesHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||
return DevicesPage(_tbContext);
|
||||
});
|
||||
|
||||
ThingsboardAppRouter() {
|
||||
router.define("/", handler: initHandler);
|
||||
router.define("/login", handler: loginHandler);
|
||||
router.define("/home", handler: homeHandler);
|
||||
router.define("/profile", handler: profileHandler);
|
||||
router.define("/devices", handler: devicesHandler);
|
||||
}
|
||||
|
||||
TbContext get tbContext => _tbContext;
|
||||
}
|
||||
46
lib/config/themes/tb_theme.dart
Normal file
46
lib/config/themes/tb_theme.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const int _tbPrimaryColor = 0xFF305680;
|
||||
const int _tbSecondaryColor = 0xFF527dad;
|
||||
const int _tbDarkPrimaryColor = 0xFF9fa8da;
|
||||
|
||||
const tbMatIndigo = MaterialColor(
|
||||
_tbPrimaryColor,
|
||||
<int, Color>{
|
||||
50: Color(0xFFE8EAF6),
|
||||
100: Color(0xFFC5CAE9),
|
||||
200: Color(0xFF9FA8DA),
|
||||
300: Color(0xFF7986CB),
|
||||
400: Color(0xFF5C6BC0),
|
||||
500: Color(_tbPrimaryColor),
|
||||
600: Color(_tbSecondaryColor),
|
||||
700: Color(0xFF303F9F),
|
||||
800: Color(0xFF283593),
|
||||
900: Color(0xFF1A237E),
|
||||
},);
|
||||
|
||||
const tbDarkMatIndigo = MaterialColor(
|
||||
_tbPrimaryColor,
|
||||
<int, Color>{
|
||||
50: Color(0xFFE8EAF6),
|
||||
100: Color(0xFFC5CAE9),
|
||||
200: Color(0xFF9FA8DA),
|
||||
300: Color(0xFF7986CB),
|
||||
400: Color(0xFF5C6BC0),
|
||||
500: Color(_tbDarkPrimaryColor),
|
||||
600: Color(_tbSecondaryColor),
|
||||
700: Color(0xFF303F9F),
|
||||
800: Color(_tbPrimaryColor),
|
||||
900: Color(0xFF1A237E),
|
||||
},);
|
||||
|
||||
ThemeData tbTheme = ThemeData(
|
||||
primarySwatch: tbMatIndigo,
|
||||
accentColor: Colors.deepOrange
|
||||
);
|
||||
|
||||
ThemeData tbDarkTheme = ThemeData(
|
||||
primarySwatch: tbDarkMatIndigo,
|
||||
accentColor: Colors.deepOrange,
|
||||
brightness: Brightness.dark
|
||||
);
|
||||
3
lib/constants/api_path.dart
Normal file
3
lib/constants/api_path.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
const thingsBoardApiEndpoint = 'https://demo.thingsboard.io';
|
||||
const username = 'ikulikov82@gmail.com';
|
||||
const password = 'qwerty';
|
||||
0
lib/constants/app_constants.dart
Normal file
0
lib/constants/app_constants.dart
Normal file
3
lib/constants/assets_path.dart
Normal file
3
lib/constants/assets_path.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
abstract class ThingsboardImage {
|
||||
static final thingsBoardLogoBlue = 'assets/images/thingsboard_logo_blue.svg';
|
||||
}
|
||||
147
lib/core/auth/login/login_page.dart
Normal file
147
lib/core/auth/login/login_page.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'dart:ui';
|
||||
|
||||
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_client/thingsboard_client.dart';
|
||||
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
|
||||
class LoginPage extends TbPageWidget<LoginPage, _LoginPageState> {
|
||||
|
||||
LoginPage(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
_LoginPageState createState() => _LoginPageState();
|
||||
|
||||
}
|
||||
|
||||
class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
|
||||
|
||||
final usernameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
usernameController.dispose();
|
||||
passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Login to ThingsBoard'),
|
||||
),
|
||||
body: ValueListenableBuilder(
|
||||
valueListenable: loadingNotifier,
|
||||
builder: (BuildContext context, bool loading, child) {
|
||||
List<Widget> children = [
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 60.0),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 150,
|
||||
child: SvgPicture.asset(ThingsboardImage.thingsBoardLogoBlue,
|
||||
semanticsLabel: 'ThingsBoard Logo')
|
||||
)
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
//padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: TextField(
|
||||
enabled: !loading,
|
||||
controller: usernameController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Username (email)',
|
||||
hintText: 'Enter valid email id as abc@gmail.com'),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 15.0, right: 15.0, top: 15, bottom: 0),
|
||||
//padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: TextField(
|
||||
enabled: !loading,
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Password',
|
||||
hintText: 'Enter secure password'),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: loading ? null : () {
|
||||
//TODO FORGOT PASSWORD SCREEN GOES HERE
|
||||
},
|
||||
child: Text(
|
||||
'Forgot Password?',
|
||||
style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, fontSize: 15),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 50,
|
||||
width: 250,
|
||||
decoration: BoxDecoration(
|
||||
color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(4)),
|
||||
child: TextButton(
|
||||
onPressed: loading ? null : () {
|
||||
tbContext.tbClient.login(
|
||||
LoginRequest(usernameController.text, passwordController.text));
|
||||
},
|
||||
child: Text(
|
||||
'Login',
|
||||
style: TextStyle(color: Colors.white, fontSize: 25),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 130,
|
||||
),
|
||||
Text('New User? Create Account')
|
||||
]
|
||||
)
|
||||
)
|
||||
];
|
||||
if (loading) {
|
||||
children.add(
|
||||
SizedBox.expand(
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
|
||||
child: Container(
|
||||
decoration: new BoxDecoration(
|
||||
color: Colors.grey.shade200.withOpacity(0.2)
|
||||
),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
//children.add(Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
return Stack(
|
||||
children: children,
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
202
lib/core/context/tb_context.dart
Normal file
202
lib/core/context/tb_context.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluro/fluro.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
import 'package:thingsboard_app/utils/services/tb_secure_storage.dart';
|
||||
import 'package:thingsboard_app/constants/api_path.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
|
||||
enum NotificationType {
|
||||
info,
|
||||
warn,
|
||||
success,
|
||||
error
|
||||
}
|
||||
|
||||
class TbContext {
|
||||
bool _initialized = false;
|
||||
bool isUserLoaded = false;
|
||||
bool isAuthenticated = false;
|
||||
final _isLoadingNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
GlobalKey<ScaffoldMessengerState> messengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
late ThingsboardClient tbClient;
|
||||
|
||||
final FluroRouter router;
|
||||
final RouteObserver<PageRoute> routeObserver;
|
||||
|
||||
TbContextState? currentState;
|
||||
|
||||
TbContext(this.router, this.routeObserver);
|
||||
|
||||
void init() {
|
||||
assert(() {
|
||||
if (_initialized) {
|
||||
throw StateError('TbContext already initialized!');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
_initialized = true;
|
||||
tbClient = ThingsboardClient(thingsBoardApiEndpoint,
|
||||
storage: TbSecureStorage(),
|
||||
onUserLoaded: onUserLoaded,
|
||||
onError: onError,
|
||||
onLoadStarted: onLoadStarted,
|
||||
onLoadFinished: onLoadFinished,
|
||||
computeFunc: <Q, R>(callback, message) => compute(callback, message));
|
||||
tbClient.init().onError((error, stackTrace) {
|
||||
print('Error: $error');
|
||||
print('Stack: $stackTrace');
|
||||
});
|
||||
}
|
||||
|
||||
void onError(ThingsboardError error) {
|
||||
print('onError: error=$error');
|
||||
showErrorNotification(error.message!);
|
||||
}
|
||||
|
||||
void showErrorNotification(String message, {Duration? duration}) {
|
||||
showNotification(message, NotificationType.error, duration: duration);
|
||||
}
|
||||
|
||||
void showInfoNotification(String message, {Duration? duration}) {
|
||||
showNotification(message, NotificationType.info, duration: duration);
|
||||
}
|
||||
|
||||
void showWarnNotification(String message, {Duration? duration}) {
|
||||
showNotification(message, NotificationType.warn, duration: duration);
|
||||
}
|
||||
|
||||
void showSuccessNotification(String message, {Duration? duration}) {
|
||||
showNotification(message, NotificationType.success, duration: duration);
|
||||
}
|
||||
|
||||
void showNotification(String message, NotificationType type, {Duration? duration}) {
|
||||
duration ??= const Duration(days: 1);
|
||||
Color backgroundColor;
|
||||
var textColor = Color(0xFFFFFFFF);
|
||||
switch(type) {
|
||||
case NotificationType.info:
|
||||
backgroundColor = Color(0xFF323232);
|
||||
break;
|
||||
case NotificationType.warn:
|
||||
backgroundColor = Color(0xFFdc6d1b);
|
||||
break;
|
||||
case NotificationType.success:
|
||||
backgroundColor = Color(0xFF008000);
|
||||
break;
|
||||
case NotificationType.error:
|
||||
backgroundColor = Color(0xFF800000);
|
||||
break;
|
||||
}
|
||||
final snackBar = SnackBar(
|
||||
duration: duration,
|
||||
backgroundColor: backgroundColor,
|
||||
content: Text(message,
|
||||
style: TextStyle(
|
||||
color: textColor
|
||||
),
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'Close',
|
||||
textColor: textColor,
|
||||
onPressed: () {
|
||||
messengerKey.currentState!.hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss);
|
||||
},
|
||||
),
|
||||
);
|
||||
messengerKey.currentState!.removeCurrentSnackBar();
|
||||
messengerKey.currentState!.showSnackBar(snackBar);
|
||||
}
|
||||
|
||||
void hideNotification() {
|
||||
messengerKey.currentState!.removeCurrentSnackBar();
|
||||
}
|
||||
|
||||
void onLoadStarted() {
|
||||
print('ON LOAD STARTED!');
|
||||
_isLoadingNotifier.value = true;
|
||||
}
|
||||
|
||||
void onLoadFinished() {
|
||||
print('ON LOAD FINISHED!');
|
||||
_isLoadingNotifier.value = false;
|
||||
}
|
||||
|
||||
Future<void> onUserLoaded() async {
|
||||
try {
|
||||
print('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}');
|
||||
isUserLoaded = true;
|
||||
isAuthenticated = tbClient.isAuthenticated();
|
||||
if (tbClient.isAuthenticated()) {
|
||||
print('authUser: ${tbClient.getAuthUser()}');
|
||||
}
|
||||
updateRouteState();
|
||||
} catch (e, s) {
|
||||
print('Error: $e');
|
||||
print('Stack: $s');
|
||||
}
|
||||
}
|
||||
|
||||
void updateRouteState() {
|
||||
if (currentState != null) {
|
||||
if (tbClient.isAuthenticated()) {
|
||||
navigateTo('/home', replace: true);
|
||||
} else {
|
||||
navigateTo('/login', replace: true, clearStack: true, transition: TransitionType.inFromTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void navigateTo(String path, {bool replace = false, bool clearStack = false, TransitionType? transition}) {
|
||||
if (currentState != null) {
|
||||
if (transition == null) {
|
||||
transition = TransitionType.inFromRight;
|
||||
}
|
||||
hideNotification();
|
||||
router.navigateTo(currentState!.context, path, transition: transition, replace: replace, clearStack: clearStack);
|
||||
}
|
||||
}
|
||||
|
||||
void pop() {
|
||||
if (currentState != null) {
|
||||
router.pop(currentState!.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mixin HasTbContext {
|
||||
late final TbContext _tbContext;
|
||||
|
||||
void setTbContext(TbContext tbContext) {
|
||||
_tbContext = tbContext;
|
||||
}
|
||||
|
||||
void setupTbContext(TbContextState currentState) {
|
||||
_tbContext = currentState.widget.tbContext;
|
||||
}
|
||||
|
||||
void setupCurrentState(TbContextState currentState) {
|
||||
_tbContext.currentState = currentState;
|
||||
}
|
||||
|
||||
ValueNotifier<bool> get loadingNotifier => _tbContext._isLoadingNotifier;
|
||||
TbContext get tbContext => _tbContext;
|
||||
|
||||
void navigateTo(String path, {bool replace = false}) => _tbContext.navigateTo(path, replace: replace);
|
||||
|
||||
void pop() => _tbContext.pop();
|
||||
|
||||
void hideNotification() => _tbContext.hideNotification();
|
||||
|
||||
void showErrorNotification(String message, {Duration? duration}) => _tbContext.showErrorNotification(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 showSuccessNotification(String message, {Duration? duration}) => _tbContext.showSuccessNotification(message, duration: duration);
|
||||
|
||||
}
|
||||
68
lib/core/context/tb_context_widget.dart
Normal file
68
lib/core/context/tb_context_widget.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
|
||||
abstract class TbContextStatelessWidget extends StatelessWidget with HasTbContext {
|
||||
TbContextStatelessWidget(TbContext tbContext, {Key? key}) : super(key: key) {
|
||||
setTbContext(tbContext);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TbContextWidget<W extends TbContextWidget<W,S>, S extends TbContextState<W,S>> extends StatefulWidget with HasTbContext {
|
||||
TbContextWidget(TbContext tbContext, {Key? key}) : super(key: key) {
|
||||
setTbContext(tbContext);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TbContextState<W extends TbContextWidget<W,S>, S extends TbContextState<W,S>> extends State<W> with HasTbContext {
|
||||
|
||||
final bool handleLoading;
|
||||
|
||||
TbContextState({this.handleLoading = false});
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setupTbContext(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void updateState() {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TbPageWidget<W extends TbPageWidget<W,S>, S extends TbPageState<W,S>> extends TbContextWidget<W,S> {
|
||||
TbPageWidget(TbContext tbContext, {Key? key}) : super(tbContext, key: key);
|
||||
}
|
||||
|
||||
abstract class TbPageState<W extends TbPageWidget<W,S>, S extends TbPageState<W,S>> extends TbContextState<W,S> with RouteAware {
|
||||
TbPageState({bool handleUserLoaded = false}): super(handleLoading: true);
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
tbContext.routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tbContext.routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
setupCurrentState(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
tbContext.hideNotification();
|
||||
setupCurrentState(this);
|
||||
}
|
||||
|
||||
}
|
||||
34
lib/core/init/init_app.dart
Normal file
34
lib/core/init/init_app.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
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 ThingsboardInitApp extends TbPageWidget<ThingsboardInitApp, _ThingsboardInitAppState> {
|
||||
|
||||
ThingsboardInitApp(TbContext tbContext, {Key? key}) : super(tbContext, key: key);
|
||||
|
||||
@override
|
||||
_ThingsboardInitAppState createState() => _ThingsboardInitAppState();
|
||||
|
||||
}
|
||||
|
||||
class _ThingsboardInitAppState extends TbPageState<ThingsboardInitApp, _ThingsboardInitAppState> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
tbContext.init();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('ThingsBoard Init'),
|
||||
),
|
||||
body: Center(
|
||||
child: CircularProgressIndicator()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/main.dart
Normal file
38
lib/main.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thingsboard_app/config/routes/router.dart';
|
||||
import 'config/themes/tb_theme.dart';
|
||||
|
||||
final appRouter = ThingsboardAppRouter();
|
||||
|
||||
void main() {
|
||||
runApp(ThingsboardApp());
|
||||
}
|
||||
|
||||
class ThingsboardApp extends StatefulWidget {
|
||||
|
||||
ThingsboardApp({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ThingsboardAppState createState() => ThingsboardAppState();
|
||||
|
||||
}
|
||||
|
||||
class ThingsboardAppState extends State<ThingsboardApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
scaffoldMessengerKey: appRouter.tbContext.messengerKey,
|
||||
title: 'ThingsBoard',
|
||||
theme: tbTheme,
|
||||
darkTheme: tbDarkTheme,
|
||||
onGenerateRoute: appRouter.router.generator,
|
||||
navigatorObservers: [appRouter.routeObserver],
|
||||
);
|
||||
}
|
||||
}
|
||||
328
lib/modules/device/devices_page.dart
Normal file
328
lib/modules/device/devices_page.dart
Normal file
@@ -0,0 +1,328 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
|
||||
class DeviceInfoCard extends StatelessWidget {
|
||||
|
||||
final DeviceInfo device;
|
||||
final void Function(DeviceInfo device)? onDetails;
|
||||
|
||||
DeviceInfoCard(this.device, {this.onDetails});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
child: ListTile(
|
||||
title: Text('${device.name}'),
|
||||
subtitle: Text('${device.type}'),
|
||||
trailing: IconButton(
|
||||
icon: Icon(Icons.navigate_next),
|
||||
onPressed: () {
|
||||
if (onDetails != null) {
|
||||
onDetails!(device);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FirstPageExceptionIndicator extends StatelessWidget {
|
||||
const FirstPageExceptionIndicator({
|
||||
required this.title,
|
||||
this.message,
|
||||
this.onTryAgain,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final String? message;
|
||||
final VoidCallback? onTryAgain;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final message = this.message;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
if (message != null)
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (message != null)
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onTryAgain != null)
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
if (onTryAgain != null)
|
||||
SizedBox(
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onTryAgain,
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: const Text(
|
||||
'Try Again',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DevicesPage extends TbPageWidget<DevicesPage, _DevicesPageState> {
|
||||
|
||||
DevicesPage(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
_DevicesPageState createState() => _DevicesPageState();
|
||||
|
||||
}
|
||||
|
||||
class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
|
||||
|
||||
final _searchModeNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
final PagingController<PageLink, DeviceInfo> _pagingController = PagingController(firstPageKey: PageLink(10));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool dataLoading = false;
|
||||
bool scheduleRefresh = false;
|
||||
|
||||
void refresh() {
|
||||
if (dataLoading) {
|
||||
scheduleRefresh = true;
|
||||
} else {
|
||||
_pagingController.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(PageLink pageKey) async {
|
||||
dataLoading = true;
|
||||
try {
|
||||
hideNotification();
|
||||
final pageData = await tbContext.tbClient.getDeviceService().getTenantDeviceInfos(pageKey);
|
||||
final isLastPage = !pageData.hasNext;
|
||||
if (isLastPage) {
|
||||
_pagingController.appendLastPage(pageData.data);
|
||||
} else {
|
||||
final nextPageKey = pageKey.nextPageLink();
|
||||
_pagingController.appendPage(pageData.data, nextPageKey);
|
||||
}
|
||||
} catch (error) {
|
||||
_pagingController.error = error;
|
||||
} finally {
|
||||
dataLoading = false;
|
||||
if (scheduleRefresh) {
|
||||
scheduleRefresh = false;
|
||||
_pagingController.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
title: const Text('Devices'),
|
||||
searchModeNotifier: _searchModeNotifier,
|
||||
searchHint: 'Search devices',
|
||||
onSearch: (String searchText) {
|
||||
_pagingController.firstPageKey.textSearch = searchText;
|
||||
refresh();
|
||||
},
|
||||
),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return PagedListView(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<DeviceInfo>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return DeviceInfoCard(
|
||||
item,
|
||||
onDetails: (device) {
|
||||
print('open details: $device');
|
||||
},
|
||||
);
|
||||
},
|
||||
noMoreItemsIndicatorBuilder: (context) => FirstPageExceptionIndicator(
|
||||
title: 'No more devices'
|
||||
),
|
||||
noItemsFoundIndicatorBuilder: (context) => FirstPageExceptionIndicator(
|
||||
title: 'No devices found',
|
||||
message: 'The list is currently empty.',
|
||||
onTryAgain: () => refresh(),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
shape: CircularNotchedRectangle(),
|
||||
notchMargin: 4.0,
|
||||
child: new Row(
|
||||
children: <Widget>[
|
||||
IconButton(icon: Icon(Icons.refresh), onPressed: () {
|
||||
refresh();
|
||||
},),
|
||||
Spacer(),
|
||||
IconButton(icon: Icon(Icons.search), onPressed: () {
|
||||
_searchModeNotifier.value = true;
|
||||
}),
|
||||
_simplePopup(),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add), onPressed: () {},),
|
||||
|
||||
/* SpeedDial(
|
||||
animatedIcon: AnimatedIcons.menu_close,
|
||||
animatedIconTheme: IconThemeData(size: 22),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Colors.white,
|
||||
visible: true,
|
||||
curve: Curves.bounceIn,
|
||||
children: [
|
||||
// FAB 1
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.refresh),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Colors.white,
|
||||
onTap: () {
|
||||
refresh();
|
||||
/* setState(() {
|
||||
var rng = Random();
|
||||
var pageSize = 1 + rng.nextInt(9);
|
||||
futureDevices = tbContext.tbClient.getDeviceService().getTenantDeviceInfos(PageLink(pageSize));
|
||||
}); */
|
||||
},
|
||||
label: 'Refresh',
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0),
|
||||
),
|
||||
// FAB 2
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.logout),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Colors.white,
|
||||
onTap: () {
|
||||
tbContext.tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
|
||||
},
|
||||
label: 'Logout',
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0),
|
||||
)
|
||||
],
|
||||
)*/
|
||||
);
|
||||
}
|
||||
|
||||
Widget _simplePopup() => PopupMenuButton<int>(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Text("First"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.work),
|
||||
title: Text('Second'),
|
||||
)
|
||||
),
|
||||
],
|
||||
icon: Icon(Icons.settings),
|
||||
);
|
||||
|
||||
SpeedDial speedDial(context) => SpeedDial(
|
||||
animatedIcon: AnimatedIcons.menu_close,
|
||||
animatedIconTheme: IconThemeData(size: 22),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Colors.white,
|
||||
visible: true,
|
||||
curve: Curves.bounceIn,
|
||||
children: [
|
||||
// FAB 1
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.refresh),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Colors.white,
|
||||
onTap: () {
|
||||
refresh();
|
||||
/* setState(() {
|
||||
var rng = Random();
|
||||
var pageSize = 1 + rng.nextInt(9);
|
||||
futureDevices = tbContext.tbClient.getDeviceService().getTenantDeviceInfos(PageLink(pageSize));
|
||||
}); */
|
||||
},
|
||||
label: 'Refresh',
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0),
|
||||
),
|
||||
// FAB 2
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.logout),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Colors.white,
|
||||
onTap: () {
|
||||
tbContext.tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
|
||||
},
|
||||
label: 'Logout',
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
48
lib/modules/home/home_page.dart
Normal file
48
lib/modules/home/home_page.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
|
||||
class HomePage extends TbPageWidget<HomePage, _HomePageState> {
|
||||
|
||||
HomePage(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
_HomePageState createState() => _HomePageState();
|
||||
|
||||
}
|
||||
|
||||
class _HomePageState extends TbPageState<HomePage, _HomePageState> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
title: const Text('ThingsBoard'),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Center(child:
|
||||
Column(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
child: Text('Devices'),
|
||||
onPressed: () {
|
||||
navigateTo('/devices');
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/modules/profile/profile_page.dart
Normal file
39
lib/modules/profile/profile_page.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
|
||||
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
|
||||
class ProfilePage extends TbPageWidget<ProfilePage, _ProfilePageState> {
|
||||
|
||||
ProfilePage(TbContext tbContext) : super(tbContext);
|
||||
|
||||
@override
|
||||
_ProfilePageState createState() => _ProfilePageState();
|
||||
|
||||
}
|
||||
|
||||
class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: TbAppBar(
|
||||
tbContext,
|
||||
title: const Text('Profile'),
|
||||
showProfile: false,
|
||||
showLogout: true,
|
||||
),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Center(child: const Text('TODO: Implement!'));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib/utils/services/tb_secure_storage.dart
Normal file
23
lib/utils/services/tb_secure_storage.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
|
||||
class TbSecureStorage implements TbStorage {
|
||||
|
||||
final flutterStorage = FlutterSecureStorage();
|
||||
|
||||
@override
|
||||
Future<void> deleteItem(String key) async {
|
||||
return await flutterStorage.delete(key: key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getItem(String key) async {
|
||||
return await flutterStorage.read(key: key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setItem(String key, String value) async {
|
||||
return await flutterStorage.write(key: key, value: value);
|
||||
}
|
||||
|
||||
}
|
||||
149
lib/widgets/tb_app_bar.dart
Normal file
149
lib/widgets/tb_app_bar.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_app/config/themes/tb_theme.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context.dart';
|
||||
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
|
||||
class TbAppBar extends TbContextWidget<TbAppBar, _TbAppBarState> implements PreferredSizeWidget {
|
||||
|
||||
final Widget? title;
|
||||
final bool? showProfile;
|
||||
final bool? showLogout;
|
||||
final ValueNotifier<bool>? searchModeNotifier;
|
||||
final String? searchHint;
|
||||
final void Function(String searchText)? onSearch;
|
||||
final void Function()? onSearchClosed;
|
||||
|
||||
@override
|
||||
final Size preferredSize;
|
||||
|
||||
TbAppBar(TbContext tbContext, {this.title, this.showProfile = true, this.showLogout = false, this.searchModeNotifier, this.searchHint, this.onSearch, this.onSearchClosed}) :
|
||||
preferredSize = Size.fromHeight(kToolbarHeight + 4),
|
||||
super(tbContext);
|
||||
|
||||
@override
|
||||
_TbAppBarState createState() => _TbAppBarState();
|
||||
|
||||
}
|
||||
|
||||
class _TbAppBarState extends TbContextState<TbAppBar, _TbAppBarState> {
|
||||
|
||||
final TextEditingController _filter = new TextEditingController();
|
||||
final _textUpdates = StreamController<String>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.searchModeNotifier != null) {
|
||||
_textUpdates.add('');
|
||||
_filter.addListener(() {
|
||||
_textUpdates.add(_filter.text);
|
||||
});
|
||||
_textUpdates.stream.debounce(const Duration(milliseconds: 150)).distinct().forEach((element) => widget.onSearch!(element));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_filter.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> children = <Widget>[];
|
||||
if (widget.searchModeNotifier != null) {
|
||||
children.add(ValueListenableBuilder(
|
||||
valueListenable: widget.searchModeNotifier!,
|
||||
builder: (context, bool searchMode, child) {
|
||||
if (searchMode) {
|
||||
return buildSearchBar();
|
||||
} else {
|
||||
return buildDefaultBar();
|
||||
}
|
||||
}
|
||||
));
|
||||
} else {
|
||||
children.add(buildDefaultBar());
|
||||
}
|
||||
children.add(
|
||||
ValueListenableBuilder(
|
||||
valueListenable: loadingNotifier,
|
||||
builder: (context, bool loading, child) {
|
||||
if (loading) {
|
||||
return LinearProgressIndicator();
|
||||
} else {
|
||||
return Container(height: 4);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
AppBar buildSearchBar() {
|
||||
return AppBar(
|
||||
centerTitle: true,
|
||||
title: Theme(
|
||||
data: tbDarkTheme,
|
||||
child: TextField(
|
||||
controller: _filter,
|
||||
cursorColor: Colors.white,
|
||||
decoration: new InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(left: 15, bottom: 11, top: 15, right: 15),
|
||||
suffixIcon: new Icon(Icons.search, color: Colors.white),
|
||||
hintText: widget.searchHint ?? 'Search...',
|
||||
),
|
||||
)
|
||||
),
|
||||
leading: new IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
_filter.text = '';
|
||||
widget.searchModeNotifier!.value = false;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AppBar buildDefaultBar() {
|
||||
List<Widget> actions = <Widget>[];
|
||||
if (widget.showProfile!) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(
|
||||
Icons.account_circle,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
navigateTo('/profile');
|
||||
},
|
||||
));
|
||||
}
|
||||
if (widget.showLogout!) {
|
||||
actions.add(
|
||||
TextButton(
|
||||
child: const Text('Logout',
|
||||
style: TextStyle(
|
||||
color: Colors.white
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
tbContext.tbClient.logout(
|
||||
requestConfig: RequestConfig(ignoreErrors: true));
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return AppBar(
|
||||
title: widget.title,
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user