Initial commit

This commit is contained in:
Igor Kulikov
2021-04-23 19:35:13 +03:00
parent 422cfb3b0b
commit 2212d9db7c
81 changed files with 2873 additions and 26 deletions

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

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

View File

@@ -0,0 +1,3 @@
const thingsBoardApiEndpoint = 'https://demo.thingsboard.io';
const username = 'ikulikov82@gmail.com';
const password = 'qwerty';

View File

View File

@@ -0,0 +1,3 @@
abstract class ThingsboardImage {
static final thingsBoardLogoBlue = 'assets/images/thingsboard_logo_blue.svg';
}

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

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

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

View 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
View 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],
);
}
}

View 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),
)
],
);
}

View 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');
},
)
],
)
);
}),
);
}
}

View 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!'));
}),
);
}
}

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