Base pages implementation

This commit is contained in:
Igor Kulikov
2021-05-06 14:51:26 +03:00
parent 7bec80ef15
commit 64a7cdf167
80 changed files with 2878 additions and 380 deletions

View File

@@ -35,7 +35,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "org.thingsboard.app"
minSdkVersion 18
minSdkVersion 21
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@@ -4,4 +4,8 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application android:usesCleartextTraffic="true"/>
</manifest>

View File

@@ -1,6 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.thingsboard.app">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:requestLegacyExternalStorage="true"
android:label="ThingsBoard App"
android:icon="@mipmap/launcher_icon">
<activity
@@ -36,6 +41,7 @@
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
android:value="2"
/>
</application>
</manifest>

View File

@@ -2,11 +2,10 @@
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
android:gravity="center"
android:src="@mipmap/thingsboard" />
</item>
</layer-list>

View File

@@ -2,11 +2,10 @@
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
android:src="@mipmap/thingsboard" />
</item>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -4,4 +4,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
</manifest>

View File

@@ -0,0 +1 @@
include ':app'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ThingsBoard App</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>Explanation on why the camera access is needed.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>describe why your app needs permission for the photo library.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -41,5 +41,11 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>Explanation on why the camera access is needed.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>describe why your app needs permission for the photo library.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
</dict>
</plist>

View File

@@ -1,44 +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/auth/auth_routes.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';
import 'package:thingsboard_app/core/init/init_routes.dart';
import 'package:thingsboard_app/modules/asset/asset_routes.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard_routes.dart';
import 'package:thingsboard_app/modules/device/device_routes.dart';
import 'package:thingsboard_app/modules/home/home_routes.dart';
import 'package:thingsboard_app/modules/profile/profile_routes.dart';
import 'package:thingsboard_app/utils/ui_utils_routes.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);
});
late final _tbContext = TbContext(router);
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);
InitRoutes(_tbContext).registerRoutes();
AuthRoutes(_tbContext).registerRoutes();
UiUtilsRoutes(_tbContext).registerRoutes();
HomeRoutes(_tbContext).registerRoutes();
ProfileRoutes(_tbContext).registerRoutes();
AssetRoutes(_tbContext).registerRoutes();
DeviceRoutes(_tbContext).registerRoutes();
DashboardRoutes(_tbContext).registerRoutes();
}
TbContext get tbContext => _tbContext;
}
abstract class TbRoutes {
final TbContext _tbContext;
TbRoutes(this._tbContext);
void registerRoutes() {
doRegisterRoutes(_tbContext.router);
}
void doRegisterRoutes(FluroRouter router);
TbContext get tbContext => _tbContext;
}

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.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) {
return LoginPage(tbContext);
});
AuthRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/login", handler: loginHandler);
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:thingsboard_app/constants/assets_path.dart';
@@ -11,7 +12,12 @@ import 'package:thingsboard_app/core/context/tb_context_widget.dart';
class LoginPage extends TbPageWidget<LoginPage, _LoginPageState> {
LoginPage(TbContext tbContext) : super(tbContext);
LoginPage(TbContext tbContext) : super(tbContext) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.light
));
}
@override
_LoginPageState createState() => _LoginPageState();
@@ -101,7 +107,7 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(4)),
child: TextButton(
onPressed: loading ? null : () {
tbContext.tbClient.login(
tbClient.login(
LoginRequest(usernameController.text, passwordController.text));
},
child: Text(

View File

@@ -1,8 +1,13 @@
import 'dart:async';
import 'dart:io';
import 'package:device_info/device_info.dart';
import 'package:fluro/fluro.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import 'package:thingsboard_app/modules/main/main_page.dart';
import 'package:thingsboard_app/utils/services/widget_action_handler.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';
@@ -15,23 +20,97 @@ enum NotificationType {
error
}
class TbLogOutput extends LogOutput {
@override
void output(OutputEvent event) {
for (var line in event.lines) {
debugPrint(line);
}
}
}
class TbLogsFilter extends LogFilter {
@override
bool shouldLog(LogEvent event) {
if (kReleaseMode) {
return event.level.index >= Level.warning.index;
} else {
return true;
}
}
}
class TbLogger {
final _logger = Logger(
filter: TbLogsFilter(),
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);
}
void debug(dynamic message, [dynamic error, StackTrace? stackTrace]) {
_logger.d(message, error, stackTrace);
}
void info(dynamic message, [dynamic error, StackTrace? stackTrace]) {
_logger.i(message, error, stackTrace);
}
void warn(dynamic message, [dynamic error, StackTrace? stackTrace]) {
_logger.w(message, error, stackTrace);
}
void error(dynamic message, [dynamic error, StackTrace? stackTrace]) {
_logger.e(message, error, stackTrace);
}
void fatal(dynamic message, [dynamic error, StackTrace? stackTrace]) {
_logger.wtf(message, error, stackTrace);
}
}
class TbContext {
static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
bool _initialized = false;
bool isUserLoaded = false;
bool isAuthenticated = false;
User? userDetails;
HomeDashboardInfo? homeDashboard;
final _isLoadingNotifier = ValueNotifier<bool>(false);
final _log = TbLogger();
late final _widgetActionHandler;
late final AndroidDeviceInfo? _androidInfo;
late final IosDeviceInfo? _iosInfo;
GlobalKey<ScaffoldMessengerState> messengerKey = GlobalKey<ScaffoldMessengerState>();
late ThingsboardClient tbClient;
final FluroRouter router;
final RouteObserver<PageRoute> routeObserver;
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
TbContextState? currentState;
TbContext(this.router, this.routeObserver);
TbContext(this.router) {
_widgetActionHandler = WidgetActionHandler(this);
}
void init() {
TbLogger get log => _log;
WidgetActionHandler get widgetActionHandler => _widgetActionHandler;
Future<void> init() async {
assert(() {
if (_initialized) {
throw StateError('TbContext already initialized!');
@@ -46,15 +125,21 @@ class TbContext {
onLoadStarted: onLoadStarted,
onLoadFinished: onLoadFinished,
computeFunc: <Q, R>(callback, message) => compute(callback, message));
tbClient.init().onError((error, stackTrace) {
print('Error: $error');
print('Stack: $stackTrace');
});
try {
if (Platform.isAndroid) {
_androidInfo = await deviceInfoPlugin.androidInfo;
} else if (Platform.isIOS) {
_iosInfo = await deviceInfoPlugin.iosInfo;
}
await tbClient.init();
} catch (e, s) {
log.error('Failed to init tbContext: $e', e, s);
}
}
void onError(ThingsboardError error) {
print('onError: error=$error');
showErrorNotification(error.message!);
void onError(ThingsboardError tbError) {
log.error('onError', tbError, tbError.getStackTrace());
showErrorNotification(tbError.message!);
}
void showErrorNotification(String message, {Duration? duration}) {
@@ -116,53 +201,109 @@ class TbContext {
}
void onLoadStarted() {
print('ON LOAD STARTED!');
log.debug('On load started.');
_isLoadingNotifier.value = true;
}
void onLoadFinished() {
print('ON LOAD FINISHED!');
log.debug('On load finished.');
_isLoadingNotifier.value = false;
}
Future<void> onUserLoaded() async {
try {
print('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}');
log.debug('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}');
isUserLoaded = true;
isAuthenticated = tbClient.isAuthenticated();
if (tbClient.isAuthenticated()) {
print('authUser: ${tbClient.getAuthUser()}');
log.debug('authUser: ${tbClient.getAuthUser()}');
if (tbClient.getAuthUser()!.userId != null) {
try {
userDetails = await tbClient.getUserService().getUser(
tbClient.getAuthUser()!.userId!);
homeDashboard = await tbClient.getDashboardService().getHomeDashboardInfo();
} catch (e) {
tbClient.logout();
}
}
} else {
userDetails = null;
homeDashboard = null;
}
updateRouteState();
} catch (e, s) {
print('Error: $e');
print('Stack: $s');
log.error('Error: $e', e, s);
}
}
void updateRouteState() {
if (currentState != null) {
if (tbClient.isAuthenticated()) {
navigateTo('/home', replace: true);
var defaultDashboardId = _defaultDashboardId();
if (defaultDashboardId != null) {
bool fullscreen = _userForceFullscreen();
navigateTo('/dashboard/$defaultDashboardId?fullscreen=$fullscreen', replace: true, transition: TransitionType.fadeIn, transitionDuration: Duration(milliseconds: 750));
} else {
navigateTo('/home', replace: true, transition: TransitionType.fadeIn, transitionDuration: Duration(milliseconds: 750));
}
} else {
navigateTo('/login', replace: true, clearStack: true, transition: TransitionType.inFromTop);
navigateTo('/login', replace: true, clearStack: true, transition: TransitionType.fadeIn, transitionDuration: Duration(milliseconds: 750));
}
}
}
void navigateTo(String path, {bool replace = false, bool clearStack = false, TransitionType? transition}) {
String? _defaultDashboardId() {
if (userDetails != null && userDetails!.additionalInfo != null) {
return userDetails!.additionalInfo!['defaultDashboardId'];
}
return null;
}
bool _userForceFullscreen() {
return tbClient.getAuthUser()!.isPublic ||
(userDetails != null && userDetails!.additionalInfo != null &&
userDetails!.additionalInfo!['defaultDashboardFullscreen'] == true);
}
bool isPhysicalDevice() {
if (Platform.isAndroid) {
return _androidInfo!.isPhysicalDevice;
} else if (Platform.isIOS) {
return _iosInfo!.isPhysicalDevice;
} else {
return false;
}
}
Future<dynamic> navigateTo(String path, {bool replace = false, bool clearStack = false, TransitionType? transition, Duration? transitionDuration}) async {
if (currentState != null) {
if (transition == null) {
transition = TransitionType.inFromRight;
}
hideNotification();
router.navigateTo(currentState!.context, path, transition: transition, replace: replace, clearStack: clearStack);
if (currentState is TbMainState) {
var mainState = currentState as TbMainState;
if (mainState.canNavigate(path) && !replace) {
mainState.navigateToPath(path);
return;
}
}
if (TbMainNavigationItem.isMainPageState(this, path)) {
replace = true;
clearStack = true;
}
if (transition == null) {
if (replace) {
transition = TransitionType.fadeIn;
} else {
transition = TransitionType.inFromRight;
}
}
return await router.navigateTo(currentState!.context, path, transition: transition, transitionDuration: transitionDuration, replace: replace, clearStack: clearStack);
}
}
void pop() {
void pop<T>([T? result]) {
if (currentState != null) {
router.pop(currentState!.context);
router.pop<T>(currentState!.context, result);
}
}
}
@@ -174,20 +315,33 @@ mixin HasTbContext {
_tbContext = tbContext;
}
void setupTbContext(TbContextState currentState) {
_tbContext = currentState.widget.tbContext;
}
void setupCurrentState(TbContextState currentState) {
_tbContext.currentState = currentState;
}
ValueNotifier<bool> get loadingNotifier => _tbContext._isLoadingNotifier;
void setupTbContext(TbContextState currentState) {
_tbContext = currentState.widget.tbContext;
}
TbContext get tbContext => _tbContext;
void navigateTo(String path, {bool replace = false}) => _tbContext.navigateTo(path, replace: replace);
TbLogger get log => _tbContext.log;
void pop() => _tbContext.pop();
bool get isPhysicalDevice => _tbContext.isPhysicalDevice();
WidgetActionHandler get widgetActionHandler => _tbContext.widgetActionHandler;
ValueNotifier<bool> get loadingNotifier => _tbContext._isLoadingNotifier;
ThingsboardClient get tbClient => _tbContext.tbClient;
Future<void> initTbContext() async {
await _tbContext.init();
}
Future<dynamic> navigateTo(String path, {bool replace = false, bool clearStack = false}) => _tbContext.navigateTo(path, replace: replace, clearStack: clearStack);
void pop<T>([T? result]) => _tbContext.pop<T>(result);
void hideNotification() => _tbContext.hideNotification();
@@ -199,4 +353,12 @@ mixin HasTbContext {
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);
}
void unsubscribeRouteObserver(TbPageState pageState) {
_tbContext.routeObserver.unsubscribe(pageState);
}
}

View File

@@ -30,9 +30,14 @@ abstract class TbContextState<W extends TbContextWidget<W,S>, S extends TbContex
super.dispose();
}
void updateState() {
setState(() {});
}
}
mixin TbMainState {
bool canNavigate(String path);
navigateToPath(String path);
}
abstract class TbPageWidget<W extends TbPageWidget<W,S>, S extends TbPageState<W,S>> extends TbContextWidget<W,S> {
@@ -45,12 +50,12 @@ abstract class TbPageState<W extends TbPageWidget<W,S>, S extends TbPageState<W,
@override
void didChangeDependencies() {
super.didChangeDependencies();
tbContext.routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
subscribeRouteObserver(this);
}
@override
void dispose() {
tbContext.routeObserver.unsubscribe(this);
unsubscribeRouteObserver(this);
super.dispose();
}
@@ -61,7 +66,7 @@ abstract class TbPageState<W extends TbPageWidget<W,S>, S extends TbPageState<W,
@override
void didPopNext() {
tbContext.hideNotification();
hideNotification();
setupCurrentState(this);
}

View File

@@ -0,0 +1,89 @@
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_client/thingsboard_client.dart';
typedef EntityDetailsFunction<T extends BaseData> = Function(T entity);
typedef EntityCardWidgetBuilder<T extends BaseData> = Widget Function(BuildContext context, T entity, bool briefView);
mixin EntitiesBase<T extends BaseData> on HasTbContext {
final entityDateFormat = DateFormat('yyyy-MM-dd');
String get title;
String get noItemsFoundText;
Future<PageData<T>> fetchEntities(PageLink pageLink);
Widget buildEntityCard(BuildContext context, T entity, bool briefView);
void onEntityDetails(T entity);
}
class EntityCard<T extends BaseData> extends StatelessWidget {
final bool _briefView;
final T _entity;
final EntityDetailsFunction<T>? _onDetails;
final EntityCardWidgetBuilder<T> _entityCardWidgetBuilder;
EntityCard(T entity, {EntityDetailsFunction<T>? onDetails,
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
required bool briefView}):
this._entity = entity,
this._onDetails = onDetails,
this._entityCardWidgetBuilder = entityCardWidgetBuilder,
this._briefView = briefView;
@override
Widget build(BuildContext context) {
return
GestureDetector(
behavior: HitTestBehavior.opaque,
child:
Container(
height: 64,
margin: _briefView ? EdgeInsets.only(right: 8) : EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_briefView ? 4 : 6),
),
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(2),
child: _entityCardWidgetBuilder(context, _entity, _briefView)
)
),
decoration: _briefView ? BoxDecoration(
border: Border.all(
color: Color(0xFFDEDEDE),
style: BorderStyle.solid,
width: 1
),
borderRadius: BorderRadius.circular(4)
) : BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(25),
blurRadius: 10.0,
offset: Offset(0, 4)
),
BoxShadow(
color: Colors.black.withAlpha(18),
blurRadius: 30.0,
offset: Offset(0, 10)
),
],
),
),
onTap: () {
if (_onDetails != null) {
_onDetails!(_entity);
}
}
);
}
}

View File

@@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.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/widgets/tb_app_bar.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
abstract class EntitiesPage<T extends BaseData> extends TbContextWidget<EntitiesPage<T>, _EntitiesPageState<T>> with EntitiesBase<T> {
EntitiesPage(TbContext tbContext): super(tbContext);
String get searchHint;
String get noMoreItemsText;
@override
_EntitiesPageState createState() => _EntitiesPageState();
}
class _EntitiesPageState<T extends BaseData> extends TbContextState<EntitiesPage<T>, _EntitiesPageState<T>> {
final _searchModeNotifier = ValueNotifier<bool>(false);
final PagingController<PageLink, T> _pagingController = PagingController(firstPageKey: PageLink(10, 0, null, SortOrder('createdTime', Direction.DESC)));
@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 {
if (mounted) {
_dataLoading = true;
try {
hideNotification();
final pageData = await widget.fetchEntities(pageKey);
final isLastPage = !pageData.hasNext;
if (isLastPage) {
_pagingController.appendLastPage(pageData.data);
} else {
final nextPageKey = pageKey.nextPageLink();
_pagingController.appendPage(pageData.data, nextPageKey);
}
} catch (error) {
if (mounted) {
_pagingController.error = error;
}
} finally {
_dataLoading = false;
if (_scheduleRefresh) {
_scheduleRefresh = false;
if (mounted) {
_pagingController.refresh();
}
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TbAppBar(
tbContext,
title: Text(widget.title),
searchModeNotifier: _searchModeNotifier,
searchHint: widget.searchHint,
onSearch: (String searchText) {
_pagingController.firstPageKey.textSearch = searchText;
_pagingController.firstPageKey.page = 0;
_refresh();
},
),
body: RefreshIndicator(
onRefresh: () => Future.sync(
() => _refresh(),
),
child: PagedListView(
pagingController: _pagingController,
padding: EdgeInsets.all(0),
builderDelegate: PagedChildBuilderDelegate<T>(
itemBuilder: (context, item, index) => EntityCard<T>(
item,
entityCardWidgetBuilder: widget.buildEntityCard,
onDetails: widget.onEntityDetails,
briefView: false
),
firstPageProgressIndicatorBuilder: (context) {
return Stack( children: [
Positioned(
top: 20,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [RefreshProgressIndicator()],
),
)
]);
},
newPageProgressIndicatorBuilder: (context) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
bottom: 16,
),
child: Center(child: RefreshProgressIndicator()),
);
},
noItemsFoundIndicatorBuilder: (context) => FirstPageExceptionIndicator(
title: widget.noItemsFoundText,
message: 'The list is currently empty.',
onTryAgain: () => _refresh(),
)
)
)
)
/* bottomNavigationBar: BottomAppBar(
child: Row(
children: [
IconButton(icon: Icon(Icons.refresh), onPressed: () {
_refresh();
}),
Spacer(),
IconButton(icon: Icon(Icons.search), onPressed: () {
_searchModeNotifier.value = true;
})
]
)
) */
);
}
}
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,
),
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,215 @@
import 'dart:async';
import 'package:fading_edge_scrollview/fading_edge_scrollview.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_client/thingsboard_client.dart';
class EntitiesWidgetController {
final List<_EntitiesWidgetState> states = [];
void _registerEntitiesWidgetState(_EntitiesWidgetState entitiesWidgetState) {
states.add(entitiesWidgetState);
}
void _unregisterEntitiesWidgetState(_EntitiesWidgetState entitiesWidgetState) {
states.remove(entitiesWidgetState);
}
Future<void> refresh() {
return Future.wait(states.map((state) => state._refresh()));
}
void dispose() {
states.clear();
}
}
abstract class EntitiesWidget<T extends BaseData> extends TbContextWidget<EntitiesWidget<T>, _EntitiesWidgetState<T>> with EntitiesBase<T> {
final entityDateFormat = DateFormat('yyyy-MM-dd');
final EntitiesWidgetController? _controller;
EntitiesWidget(TbContext tbContext, {EntitiesWidgetController? controller}):
_controller = controller,
super(tbContext);
@override
_EntitiesWidgetState createState() => _EntitiesWidgetState(_controller);
void onViewAll();
}
class _EntitiesWidgetState<T extends BaseData> extends TbContextState<EntitiesWidget<T>, _EntitiesWidgetState<T>> {
final EntitiesWidgetController? _controller;
final StreamController<PageData<T>?> _entitiesStreamController = StreamController.broadcast();
_EntitiesWidgetState(EntitiesWidgetController? controller):
_controller = controller;
@override
void initState() {
super.initState();
if (_controller != null) {
_controller!._registerEntitiesWidgetState(this);
}
_refresh();
}
@override
void dispose() {
if (_controller != null) {
_controller!._unregisterEntitiesWidgetState(this);
}
_entitiesStreamController.close();
super.dispose();
}
Future<void> _refresh() {
_entitiesStreamController.add(null);
var entitiesFuture = widget.fetchEntities(PageLink(5, 0, null, SortOrder('createdTime', Direction.DESC)));
entitiesFuture.then((value) => _entitiesStreamController.add(value));
return entitiesFuture;
}
@override
Widget build(BuildContext context) {
return Container(
height: 120,
margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Container(
height: 24,
margin: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
StreamBuilder<PageData<T>?>(
stream: _entitiesStreamController.stream,
builder: (context, snapshot) {
var title = widget.title;
if (snapshot.hasData) {
var data = snapshot.data;
title += ' (${data!.totalElements})';
}
return Text(title,
style: TextStyle(
color: Color(0xFF282828),
fontSize: 16,
fontWeight: FontWeight.normal,
height: 1.5
)
);
},
),
Spacer(),
TextButton(
onPressed: () {
widget.onViewAll();
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero),
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');
} else {
return _buildEntitiesView(context, data.data);
}
} 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)
),
BoxShadow(
color: Colors.black.withAlpha(18),
blurRadius: 30.0,
offset: Offset(0, 10)
),
],
)
);
}
Widget _buildNoEntitiesFound() {
return Container(
decoration: BoxDecoration(
border: Border.all(
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,
)
),
),
);
}
Widget _buildEntitiesView(BuildContext context, List<T> entities) {
return FadingEdgeScrollView.fromScrollView(
gradientFractionOnStart: 0.2,
gradientFractionOnEnd: 0.2,
shouldDisposeScrollController: true,
child: ListView(
scrollDirection: Axis.horizontal,
controller: ScrollController(),
children: entities.map((entity) => EntityCard<T>(
entity,
entityCardWidgetBuilder: widget.buildEntityCard,
onDetails: widget.onEntityDetails,
briefView: true
)).toList()
));
}
}

View File

@@ -0,0 +1,97 @@
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';
import 'package:thingsboard_client/thingsboard_client.dart';
abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget<EntityDetailsPage<T>, _EntityDetailsPageState<T>> {
final String _defaultTitle;
final String _entityId;
final bool _showLoadingIndicator;
final bool _hideAppBar;
final double? _appBarElevation;
EntityDetailsPage(TbContext tbContext,
{required String defaultTitle,
required String entityId,
bool showLoadingIndicator = true,
bool hideAppBar = false,
double? appBarElevation}):
this._defaultTitle = defaultTitle,
this._entityId = entityId,
this._showLoadingIndicator = showLoadingIndicator,
this._hideAppBar = hideAppBar,
this._appBarElevation = appBarElevation,
super(tbContext);
@override
_EntityDetailsPageState createState() => _EntityDetailsPageState();
Future<T> fetchEntity(String id);
ValueNotifier<String>? detailsTitle() {
return null;
}
Widget buildEntityDetails(BuildContext context, T entity);
}
class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDetailsPage<T>, _EntityDetailsPageState<T>> {
late Future<T> entityFuture;
late ValueNotifier<String> titleValue;
@override
void initState() {
super.initState();
entityFuture = widget.fetchEntity(widget._entityId);
ValueNotifier<String>? detailsTitle = widget.detailsTitle();
if (detailsTitle == null) {
titleValue = ValueNotifier(widget._defaultTitle);
entityFuture.then((value) {
if (value is HasName) {
titleValue.value = (value as HasName).getName();
}
});
} else {
titleValue = detailsTitle;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget._hideAppBar ? null : TbAppBar(
tbContext,
showLoadingIndicator: widget._showLoadingIndicator,
elevation: widget._appBarElevation,
title: ValueListenableBuilder<String>(
valueListenable: titleValue,
builder: (context, title, widget) {
return FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title)
);
},
),
),
body: FutureBuilder<T>(
future: entityFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
var entity = snapshot.data!;
return widget.buildEntityDetails(context, entity);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}

View File

@@ -1,34 +1,71 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.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';
class ThingsboardInitApp extends TbPageWidget<ThingsboardInitApp, _ThingsboardInitAppState> {
ThingsboardInitApp(TbContext tbContext, {Key? key}) : super(tbContext, key: key);
ThingsboardInitApp(TbContext tbContext, {Key? key}) : super(tbContext, key: key) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.light
));
}
@override
_ThingsboardInitAppState createState() => _ThingsboardInitAppState();
}
class _ThingsboardInitAppState extends TbPageState<ThingsboardInitApp, _ThingsboardInitAppState> {
class _ThingsboardInitAppState extends TbPageState<ThingsboardInitApp, _ThingsboardInitAppState> with TickerProviderStateMixin {
late final AnimationController rotationController;
late final CurvedAnimation animation;
@override
void initState() {
rotationController = AnimationController(duration: Duration(milliseconds: 2000),
vsync: this, upperBound: 1, animationBehavior: AnimationBehavior.preserve);
animation = CurvedAnimation(parent: rotationController, curve: Curves.easeInOutCirc);
super.initState();
tbContext.init();
initTbContext();
rotationController.forward(from: 0.0);
rotationController.addListener(() {
if (rotationController.status == AnimationStatus.completed) {
rotationController.repeat();
}
});
}
@override
void dispose() {
rotationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ThingsBoard Init'),
return Container(
alignment: Alignment.center,
color: Colors.white,
child: AnimatedBuilder(
animation: animation,
child: Container(
height: 50.0,
width: 50.0,
child: Image.asset(ThingsboardImage.thingsboard),
),
body: Center(
child: CircularProgressIndicator()
)
);
builder: (BuildContext context, Widget? _widget) {
return Transform.rotate(
angle: animation.value * pi * 2,
child: _widget,
);
},
),
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
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) {
return ThingsboardInitApp(tbContext);
});
InitRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/", handler: initHandler);
}
}

View File

@@ -1,11 +1,22 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'config/themes/tb_theme.dart';
final appRouter = ThingsboardAppRouter();
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isAndroid) {
await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
}
runApp(ThingsboardApp());
}
@@ -32,7 +43,7 @@ class ThingsboardAppState extends State<ThingsboardApp> {
theme: tbTheme,
darkTheme: tbDarkTheme,
onGenerateRoute: appRouter.router.generator,
navigatorObservers: [appRouter.routeObserver],
navigatorObservers: [appRouter.tbContext.routeObserver],
);
}
}

View File

@@ -0,0 +1,27 @@
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 AssetDetailsPage extends EntityDetailsPage<AssetInfo> {
AssetDetailsPage(TbContext tbContext, String assetId):
super(tbContext,
entityId: assetId,
defaultTitle: 'Asset');
@override
Future<AssetInfo> fetchEntity(String assetId) {
return tbClient.getAssetService().getAssetInfo(assetId);
}
@override
Widget buildEntityDetails(BuildContext context, AssetInfo asset) {
return ListTile(
title: Text('${asset.name}'),
subtitle: Text('${asset.type}'),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
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) {
return AssetsPage(tbContext);
});
late var assetDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return AssetDetailsPage(tbContext, params["id"][0]);
});
AssetRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/assets", handler: assetsHandler);
router.define("/asset/:id", handler: assetDetailsHandler);
}
}

View File

@@ -0,0 +1,89 @@
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> {
@override
String get title => 'Assets';
@override
String get noItemsFoundText => 'No assets found';
@override
Future<PageData<AssetInfo>> fetchEntities(PageLink pageLink) {
if (tbClient.isTenantAdmin()) {
return tbClient.getAssetService().getTenantAssetInfos(pageLink);
} else {
return tbClient.getAssetService().getCustomerAssetInfos(tbClient.getAuthUser()!.customerId, pageLink);
}
}
@override
Widget buildEntityCard(BuildContext context, AssetInfo asset, bool briefView) {
return Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: briefView ? 9 : 10, horizontal: 16),
child: Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
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}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
),
(!briefView ? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(asset.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
) : Container())
],
),
)
)
]
);
}
@override
void onEntityDetails(AssetInfo asset) {
navigateTo('/asset/${asset.id!.id}');
}
}

View File

@@ -0,0 +1,16 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_page.dart';
import 'package:thingsboard_app/modules/asset/assets_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AssetsPage extends EntitiesPage<AssetInfo> with AssetsBase {
AssetsPage(TbContext tbContext) : super(tbContext);
@override
String get noMoreItemsText => 'No more assets';
@override
String get searchHint => 'Search assets';
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_widget.dart';
import 'package:thingsboard_app/modules/asset/assets_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class AssetsWidget extends EntitiesWidget<AssetInfo> with AssetsBase {
AssetsWidget(TbContext tbContext, {EntitiesWidgetController? controller}): super(tbContext, controller: controller);
@override
void onViewAll() {
navigateTo('/assets');
}
}

View File

@@ -0,0 +1,250 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:thingsboard_app/constants/api_path.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:url_launcher/url_launcher.dart';
typedef DashboardTitleCallback = void Function(String title);
class Dashboard extends TbContextWidget<Dashboard, _DashboardState> {
final String _dashboardId;
final String? _state;
final bool? _home;
final bool? _hideToolbar;
final bool _fullscreen;
final DashboardTitleCallback? _titleCallback;
Dashboard(TbContext tbContext, {required String dashboardId, required bool fullscreen,
DashboardTitleCallback? titleCallback, String? state, bool? home,
bool? hideToolbar}):
this._dashboardId = dashboardId,
this._fullscreen = fullscreen,
this._titleCallback = titleCallback,
this._state = state,
this._home = home,
this._hideToolbar = hideToolbar,
super(tbContext);
@override
_DashboardState createState() => _DashboardState();
}
class _DashboardState extends TbContextState<Dashboard, _DashboardState> {
final Completer<InAppWebViewController> _controller = Completer<InAppWebViewController>();
final ValueNotifier<bool> webViewLoading = ValueNotifier(true);
final GlobalKey webViewKey = GlobalKey();
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true,
mediaPlaybackRequiresUserGesture: false,
javaScriptEnabled: true,
),
android: AndroidInAppWebViewOptions(
useHybridComposition: false,
thirdPartyCookiesEnabled: true
),
ios: IOSInAppWebViewOptions(
allowsInlineMediaPlayback: true,
));
late String _dashboardUrl;
late String _currentDashboardId;
late String? _currentDashboardState;
@override
void initState() {
super.initState();
_dashboardUrl = thingsBoardApiEndpoint + '/dashboard/' + widget._dashboardId;
List<String> params = [];
if (widget._state != null) {
params.add('state=${widget._state}');
}
if (widget._home == true) {
params.add('embedded=true');
}
if (widget._hideToolbar == true) {
params.add('hideToolbar=true');
}
if (params.isNotEmpty) {
_dashboardUrl += '?${params.join('&')}';
}
_currentDashboardId = widget._dashboardId;
_currentDashboardState = widget._state;
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
var controller = await _controller.future;
if (await controller.canGoBack()) {
await controller.goBack();
return false;
}
return true;
},
child: Stack(
children: [
InAppWebView(
key: webViewKey,
initialUrlRequest: URLRequest(url: Uri.parse(_dashboardUrl)),
initialOptions: options,
onWebViewCreated: (webViewController) {
webViewController.addJavaScriptHandler(handlerName: "tbMobileDashboardStateNameHandler", callback: (args) async {
log.debug("Invoked tbMobileDashboardStateNameHandler: $args");
webViewLoading.value = false;
if (args.isNotEmpty && args[0] is String) {
if (widget._titleCallback != null) {
widget._titleCallback!(args[0]);
}
}
});
webViewController.addJavaScriptHandler(handlerName: "tbMobileHandler", callback: (args) async {
log.debug("Invoked tbMobileHandler: $args");
return await widgetActionHandler.handleWidgetMobileAction(args, webViewController);
});
_controller.complete(webViewController);
},
shouldOverrideUrlLoading: (controller, navigationAction) async {
var uri = navigationAction.request.url!;
var uriString = uri.toString();
log.debug('disallowing navigation to $uriString');
if (await canLaunch(uriString)) {
await launch(uriString);
}
return NavigationActionPolicy.CANCEL;
},
onUpdateVisitedHistory: (controller, url, androidIsReload) async {
if (url != null) {
String newStateId = url.pathSegments.last;
log.debug('onUpdateVisitedHistory: $newStateId');
if (newStateId == 'profile') {
webViewLoading.value = true;
await controller.goBack();
await navigateTo('/profile');
webViewLoading.value = false;
return;
} else if (newStateId == 'login') {
webViewLoading.value = true;
await controller.pauseTimers();
await controller.stopLoading();
await tbClient.logout();
return;
} else if (['devices', 'assets', 'dashboards'].contains(newStateId)) {
var controller = await _controller.future;
await controller.goBack();
navigateTo('/$newStateId');
return;
} else {
if (url.pathSegments.length > 1) {
var segmentName = url.pathSegments[url.pathSegments.length-2];
if (segmentName == 'dashboards' && widget._home != true) {
webViewLoading.value = true;
var targetPath = _createDashboardNavigationPath(newStateId, fullscreen: widget._fullscreen);
await navigateTo(targetPath, replace: true);
return;
} else if (segmentName == 'dashboard') {
_currentDashboardId = newStateId;
_currentDashboardState = url.queryParameters['state'];
return;
}
}
webViewLoading.value = true;
if (widget._home == true) {
await navigateTo('/home', replace: true);
} else {
var targetPath = _createDashboardNavigationPath(_currentDashboardId, state: _currentDashboardState, fullscreen: widget._fullscreen);
await navigateTo(targetPath, replace: true);
}
}
}
},
onConsoleMessage: (controller, consoleMessage) {
log.debug('[JavaScript console] ${consoleMessage.messageLevel}: ${consoleMessage.message}');
},
onLoadStart: (controller, url) async {
log.debug('onLoadStart: $url');
await _setTokens(controller.webStorage.localStorage);
},
onLoadStop: (controller, url) {
},
),
ValueListenableBuilder(
valueListenable: webViewLoading,
builder: (BuildContext context, bool loading, child) {
if (!loading) {
return SizedBox.shrink();
} else {
return Container(
decoration: BoxDecoration(color: Colors.white),
child: Center(
child: CircularProgressIndicator()
),
);
}
}
)
],
)
);
}
String _createDashboardNavigationPath(String dashboardId, {bool? fullscreen, String? state}) {
var targetPath = '/dashboard/$dashboardId';
List<String> params = [];
if (state != null) {
params.add('state=$state');
}
if (fullscreen != null) {
params.add('fullscreen=$fullscreen');
}
if (params.isNotEmpty) {
targetPath += '?${params.join('&')}';
}
return targetPath;
}
Future<void> _setTokens(Storage storage) async {
String jwtToken = tbClient.getJwtToken()!;
int jwtTokenExpiration = _getClientExpiration(jwtToken);
String refreshToken = tbClient.getRefreshToken()!;
int refreshTokenExpiration = _getClientExpiration(refreshToken);
await storage.setItem(key: 'jwt_token', value: jwtToken);
await storage.setItem(key: 'jwt_token_expiration', value: jwtTokenExpiration);
await storage.setItem(key: 'refresh_token', value: refreshToken);
await storage.setItem(key: 'refresh_token_expiration', value: refreshTokenExpiration);
}
/* String _setTokensJavaScript() {
String jwtToken = tbClient.getJwtToken()!;
int jwtTokenExpiration = _getClientExpiration(jwtToken);
String refreshToken = tbClient.getRefreshToken()!;
int refreshTokenExpiration = _getClientExpiration(refreshToken);
return "window.localStorage.setItem('jwt_token','$jwtToken');\n"+
"window.localStorage.setItem('jwt_token_expiration','$jwtTokenExpiration');\n"+
"window.localStorage.setItem('refresh_token','$refreshToken');\n"+
"window.localStorage.setItem('refresh_token_expiration','$refreshTokenExpiration');";
} */
int _getClientExpiration(String token) {
var decodedToken = JwtDecoder.decode(tbClient.getJwtToken()!);
int issuedAt = decodedToken['iat'];
int expTime = decodedToken['exp'];
int ttl = expTime - issuedAt;
int clientExpiration = DateTime.now().millisecondsSinceEpoch + ttl * 1000;
return clientExpiration;
}
}

View File

@@ -0,0 +1,62 @@
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<DashboardPage, _DashboardPageState> {
final String? _dashboardTitle;
final String _dashboardId;
final String? _state;
final bool _fullscreen;
DashboardPage(TbContext tbContext, {required String dashboardId, required bool fullscreen, String? dashboardTitle, String? state}):
_dashboardId = dashboardId,
_fullscreen = fullscreen,
_dashboardTitle = dashboardTitle,
_state = state,
super(tbContext);
@override
_DashboardPageState createState() => _DashboardPageState();
}
class _DashboardPageState extends TbPageState<DashboardPage, _DashboardPageState> {
late ValueNotifier<String> dashboardTitleValue;
@override
void initState() {
super.initState();
dashboardTitleValue = ValueNotifier(widget._dashboardTitle ?? 'Dashboard');
}
@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)
);
},
),
),
body: Dashboard(tbContext, dashboardId: widget._dashboardId, state: widget._state,
fullscreen: widget._fullscreen, titleCallback: (title) {
dashboardTitleValue.value = title;
}),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/main/main_page.dart';
import 'dashboard_page.dart';
class DashboardRoutes extends TbRoutes {
late var dashboardsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return MainPage(tbContext, path: '/dashboards');
});
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);
});
DashboardRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/dashboards", handler: dashboardsHandler);
router.define("/dashboard/:id", handler: dashboardDetailsHandler);
}
}

View File

@@ -0,0 +1,104 @@
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 DashboardsBase on EntitiesBase<DashboardInfo> {
@override
String get title => 'Dashboards';
@override
String get noItemsFoundText => 'No dashboards found';
@override
Future<PageData<DashboardInfo>> fetchEntities(PageLink pageLink) {
if (tbClient.isTenantAdmin()) {
return tbClient.getDashboardService().getTenantDashboards(pageLink);
} else {
return tbClient.getDashboardService().getCustomerDashboards(tbClient.getAuthUser()!.customerId, pageLink);
}
}
@override
Widget buildEntityCard(BuildContext context, DashboardInfo dashboard, bool briefView) {
return Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: briefView ? 9 : 10, horizontal: 16),
child: Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${dashboard.title}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.7
))
),
Text('${_dashboardDetailsText(dashboard)}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
),
(!briefView ? 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) {
if (tbClient.isTenantAdmin()) {
if (_isPublicDashboard(dashboard)) {
return 'Public';
} else {
return dashboard.assignedCustomers.map((e) => e.title).join(', ');
}
}
return '';
}
bool _isPublicDashboard(DashboardInfo dashboard) {
return dashboard.assignedCustomers.any((element) => element.isPublic);
}
@override
void onEntityDetails(DashboardInfo dashboard) {
navigateTo('/dashboard/${dashboard.id!.id}?title=${dashboard.title}');
}
}

View File

@@ -0,0 +1,18 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_page.dart';
import 'package:thingsboard_app/modules/dashboard/dashboards_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DashboardsPage extends EntitiesPage<DashboardInfo> with DashboardsBase {
DashboardsPage(TbContext tbContext) :
super(tbContext);
@override
String get noMoreItemsText => 'No more dashboards';
@override
String get searchHint => 'Search dashboards';
}

View File

@@ -0,0 +1,15 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboards_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DashboardsWidget extends EntitiesWidget<DashboardInfo> with DashboardsBase {
DashboardsWidget(TbContext tbContext, {EntitiesWidgetController? controller}): super(tbContext, controller: controller);
@override
void onViewAll() {
navigateTo('/dashboards');
}
}

View File

@@ -0,0 +1,27 @@
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');
@override
Future<DeviceInfo> fetchEntity(String deviceId) {
return tbClient.getDeviceService().getDeviceInfo(deviceId);
}
@override
Widget buildEntityDetails(BuildContext context, DeviceInfo device) {
return ListTile(
title: Text('${device.name}'),
subtitle: Text('${device.type}'),
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/modules/main/main_page.dart';
import 'device_details_page.dart';
import 'devices_page.dart';
class DeviceRoutes extends TbRoutes {
late var devicesHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return MainPage(tbContext, path: '/devices');
});
late var deviceDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return DeviceDetailsPage(tbContext, params["id"][0]);
});
DeviceRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/devices", handler: devicesHandler);
router.define("/device/:id", handler: deviceDetailsHandler);
}
}

View File

@@ -0,0 +1,98 @@
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 DevicesBase on EntitiesBase<DeviceInfo> {
@override
String get title => 'Devices';
@override
String get noItemsFoundText => 'No devices found';
@override
Future<PageData<DeviceInfo>> fetchEntities(PageLink pageLink) {
if (tbClient.isTenantAdmin()) {
return tbClient.getDeviceService().getTenantDeviceInfos(pageLink);
} else {
return tbClient.getDeviceService().getCustomerDeviceInfos(tbClient.getAuthUser()!.customerId, pageLink);
}
}
@override
void onEntityDetails(DeviceInfo device) {
navigateTo('/device/${device.id!.id}');
}
@override
Widget buildEntityCard(BuildContext context, DeviceInfo device, bool briefView) {
return Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Container(
width: briefView ? 58 : 60,
decoration: BoxDecoration(
color: Color(0xFFEEEEEE),
borderRadius: BorderRadius.horizontal(left: Radius.circular(briefView ? 4 : 6))
),
child: Center(
child: Icon(Icons.devices_other, color: Color(0xFFC2C2C2))
),
),
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Container(
padding: EdgeInsets.symmetric(vertical: briefView ? 9 : 10, horizontal: 16),
child: Row(
mainAxisSize: briefView ? MainAxisSize.min : MainAxisSize.max,
children: [
Flexible(
fit: briefView ? FlexFit.loose : FlexFit.tight,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${device.name}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.7
))
),
Text('${device.type}',
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
)
),
(!briefView ? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(device.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 1.33
))
],
) : Container())
],
),
)
)
]
);
}
}

View File

@@ -1,209 +1,21 @@
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_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_page.dart';
import 'package:thingsboard_app/modules/device/devices_base.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> {
class DevicesPage extends EntitiesPage<DeviceInfo> with DevicesBase {
DevicesPage(TbContext tbContext) : super(tbContext);
@override
_DevicesPageState createState() => _DevicesPageState();
String get noMoreItemsText => 'No more devices';
@override
String get searchHint => 'Search devices';
}
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(
/* bottomNavigationBar: BottomAppBar(
shape: CircularNotchedRectangle(),
notchMargin: 4.0,
child: new Row(
@@ -222,48 +34,6 @@ class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
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),
)
],
)*/
);
}
@@ -316,7 +86,7 @@ class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Colors.white,
onTap: () {
tbContext.tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true));
},
label: 'Logout',
labelStyle: TextStyle(
@@ -326,3 +96,4 @@ class _DevicesPageState extends TbPageState<DevicesPage, _DevicesPageState> {
],
);
}
*/

View File

@@ -0,0 +1,15 @@
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/entity/entities_widget.dart';
import 'package:thingsboard_app/modules/device/devices_base.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class DevicesWidget extends EntitiesWidget<DeviceInfo> with DevicesBase {
DevicesWidget(TbContext tbContext, {EntitiesWidgetController? controller}): super(tbContext, controller: controller);
@override
void onViewAll() {
navigateTo('/devices');
}
}

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/core/entity/entities_widget.dart';
import 'package:thingsboard_app/modules/asset/assets_widget.dart';
import 'package:thingsboard_app/modules/dashboard/dashboards_widget.dart';
import 'package:thingsboard_app/modules/device/devices_widget.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';
import 'package:thingsboard_client/thingsboard_client.dart';
import 'package:thingsboard_app/modules/dashboard/dashboard.dart' as dashboardUi;
class HomePage extends TbPageWidget<HomePage, _HomePageState> {
class HomePage extends TbContextWidget<HomePage, _HomePageState> {
HomePage(TbContext tbContext) : super(tbContext);
@@ -14,35 +20,85 @@ class HomePage extends TbPageWidget<HomePage, _HomePageState> {
}
class _HomePageState extends TbPageState<HomePage, _HomePageState> {
class _HomePageState extends TbContextState<HomePage, _HomePageState> {
final EntitiesWidgetController _entitiesWidgetController = EntitiesWidgetController();
@override
void initState() {
super.initState();
}
@override
void dispose() {
_entitiesWidgetController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var homeDashboard = tbContext.homeDashboard;
var dashboardState = homeDashboard != null;
return Scaffold(
appBar: TbAppBar(
tbContext,
title: const Text('ThingsBoard'),
showLoadingIndicator: !dashboardState,
elevation: dashboardState ? 0 : null,
title: const Text('Home'),
),
body: Builder(
builder: (BuildContext context) {
return Center(child:
Column(
children: [
ElevatedButton(
child: Text('Devices'),
onPressed: () {
navigateTo('/devices');
},
)
],
)
);
}),
builder: (context) {
if (dashboardState) {
return _buildDashboardHome(context, homeDashboard!);
} else {
return _buildDefaultHome(context);
}
}
),
);
}
Widget _buildDashboardHome(BuildContext context, HomeDashboardInfo dashboard) {
return dashboardUi.Dashboard(tbContext, dashboardId: dashboard.dashboardId!.id!,
fullscreen: false, home: true, hideToolbar: dashboard.hideDashboardToolbar);
}
Widget _buildDefaultHome(BuildContext context) {
return RefreshIndicator(
onRefresh: () => _entitiesWidgetController.refresh(),
child: ListView(
children: _buildUserHome(context)
)
);
}
List<Widget> _buildUserHome(BuildContext context) {
if (tbClient.isSystemAdmin()) {
return _buildSysAdminHome(context);
} else if (tbClient.isTenantAdmin()) {
return _buildTenantAdminHome(context);
} else {
return _buildCustomerUserHome(context);
}
}
List<Widget> _buildSysAdminHome(BuildContext context) {
return [Container(child: Text('TODO: Implement'))];
}
List<Widget> _buildTenantAdminHome(BuildContext context) {
return [
AssetsWidget(tbContext, controller: _entitiesWidgetController),
DevicesWidget(tbContext, controller: _entitiesWidgetController),
DashboardsWidget(tbContext, controller: _entitiesWidgetController)
];
}
List<Widget> _buildCustomerUserHome(BuildContext context) {
return [
AssetsWidget(tbContext, controller: _entitiesWidgetController),
DevicesWidget(tbContext, controller: _entitiesWidgetController),
DashboardsWidget(tbContext, controller: _entitiesWidgetController)
];
}
}

View File

@@ -0,0 +1,20 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
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) {
return MainPage(tbContext, path: '/home');
});
HomeRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/home", handler: homeHandler);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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/dashboards_page.dart';
import 'package:thingsboard_app/modules/device/devices_page.dart';
import 'package:thingsboard_app/modules/home/home_page.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
class TbMainNavigationItem {
final Widget page;
final String title;
final Icon icon;
final String 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', '/tenants', '/more']),
Authority.TENANT_ADMIN: Set.unmodifiable(['/home', '/devices', '/dashboards', '/more']),
Authority.CUSTOMER_USER: Set.unmodifiable(['/home', '/devices', '/dashboards', '/more']),
};
static bool isMainPageState(TbContext tbContext, String path) {
if (tbContext.isAuthenticated) {
return mainPageStateMap[tbContext.tbClient.getAuthUser()!.authority]!
.contains(path);
} else {
return false;
}
}
static List<TbMainNavigationItem> getItems(TbContext tbContext) {
if (tbContext.isAuthenticated) {
List<TbMainNavigationItem> items = [
TbMainNavigationItem(
page: HomePage(tbContext),
title: 'Home',
icon: Icon(Icons.home),
path: '/home'
)
];
switch(tbContext.tbClient.getAuthUser()!.authority) {
case Authority.SYS_ADMIN:
items.add(TbMainNavigationItem(
page: Scaffold(body: Center(child: Text('Tenants TODO'))),
title: 'Tenants',
icon: Icon(Icons.supervisor_account),
path: '/tenants'
));
break;
case Authority.TENANT_ADMIN:
case Authority.CUSTOMER_USER:
items.addAll([
TbMainNavigationItem(
page: DevicesPage(tbContext),
title: 'Devices',
icon: Icon(Icons.devices_other),
path: '/devices'
),
TbMainNavigationItem(
page: DashboardsPage(tbContext),
title: 'Dashboards',
icon: Icon(Icons.dashboard),
path: '/dashboards'
)
]);
break;
case Authority.REFRESH_TOKEN:
break;
case Authority.ANONYMOUS:
break;
}
items.add(TbMainNavigationItem(
page: Scaffold(body: Center(child: Text('TODO'))),
title: 'More',
icon: Icon(Icons.menu),
path: '/more'
));
return items;
} else {
return [];
}
}
}
class MainPage extends TbPageWidget<MainPage, _MainPageState> {
final String _path;
MainPage(TbContext tbContext, {required String path}):
_path = path, super(tbContext) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(tbContext.currentState!.context).colorScheme.primary,
systemNavigationBarIconBrightness: Brightness.dark
));
}
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends TbPageState<MainPage, _MainPageState> with TbMainState {
late int _currentIndex;
late final List<TbMainNavigationItem> _tabItems;
@override
void initState() {
super.initState();
_tabItems = TbMainNavigationItem.getItems(tbContext);
_currentIndex = _indexFromPath(widget._path);
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_currentIndex > 0) {
setState(() => _currentIndex = 0);
return false;
}
return true;
},
child: Scaffold(
/* body: IndexedStack(
index: _currentIndex,
children: _tabItems.map((item) => item.page).toList(),
),*/
body: _tabItems.elementAt(_currentIndex).page,
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
canvasColor: Theme.of(context).colorScheme.primary
),
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.white,
unselectedItemColor: Colors.white.withAlpha(97),
currentIndex: _currentIndex,
onTap: (int index) => setState(() => _currentIndex = index),
items: _tabItems.map((item) => BottomNavigationBarItem(
icon: item.icon,
label: item.title
)).toList()
)
)
),
);
}
int _indexFromPath(String path) {
return _tabItems.indexWhere((item) => item.path == path);
}
@override
bool canNavigate(String path) {
return _indexFromPath(path) > -1;
}
@override
navigateToPath(String path) {
int targetIndex = _indexFromPath(path);
if (_currentIndex != targetIndex) {
setState(() => _currentIndex = targetIndex);
}
}
}

View File

@@ -4,6 +4,7 @@ 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';
import 'package:thingsboard_client/thingsboard_client.dart';
class ProfilePage extends TbPageWidget<ProfilePage, _ProfilePageState> {
@@ -16,9 +17,12 @@ class ProfilePage extends TbPageWidget<ProfilePage, _ProfilePageState> {
class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
late Future<User> userFuture;
@override
void initState() {
super.initState();
userFuture = tbClient.getUserService().getUser(tbClient.getAuthUser()!.userId!);
}
@override
@@ -30,10 +34,20 @@ class _ProfilePageState extends TbPageState<ProfilePage, _ProfilePageState> {
showProfile: false,
showLogout: true,
),
body: Builder(
builder: (BuildContext context) {
return Center(child: const Text('TODO: Implement!'));
}),
body: FutureBuilder<User>(
future: userFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
var user = snapshot.data!;
return ListTile(
title: Text('${user.email}'),
subtitle: Text('${user.firstName} ${user.lastName}'),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
)
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
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) {
return ProfilePage(tbContext);
});
ProfileRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/profile", handler: profileHandler);
}
}

View File

@@ -0,0 +1,305 @@
import 'dart:io';
import 'package:fluro/fluro.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:geolocator/geolocator.dart';
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';
class WidgetMobileActionResult<T extends MobileActionResult> {
T? result;
bool hasResult = false;
String? error;
bool hasError = false;
WidgetMobileActionResult.errorResult(this.error): hasError = true, hasResult = false;
WidgetMobileActionResult.successResult(this.result): hasError = false, hasResult = true;
WidgetMobileActionResult.emptyResult(): hasError = false, hasResult = false;
Map<String, dynamic> toJson() {
var json = <String, dynamic>{};
json['hasError'] = hasError;
json['hasResult'] = hasResult;
json['error'] = error;
json['result'] = result?.toJson();
return json;
}
}
class MobileActionResult {
MobileActionResult();
factory MobileActionResult.launched(bool launched) {
return _LaunchResult(launched);
}
factory MobileActionResult.image(String imageUrl) {
return _ImageResult(imageUrl);
}
factory MobileActionResult.qrCode(String code, String format) {
return _QrCodeResult(code, format);
}
factory MobileActionResult.location(num latitude, num longitude) {
return _LocationResult(latitude, longitude);
}
Map<String, dynamic> toJson() {
var json = <String, dynamic>{};
return json;
}
}
class _LaunchResult extends MobileActionResult {
bool launched;
_LaunchResult(this.launched);
@override
Map<String, dynamic> toJson() {
var json = super.toJson();
json['launched'] = launched;
return json;
}
}
class _ImageResult extends MobileActionResult {
String imageUrl;
_ImageResult(this.imageUrl);
@override
Map<String, dynamic> toJson() {
var json = super.toJson();
json['imageUrl'] = imageUrl;
return json;
}
}
class _QrCodeResult extends MobileActionResult {
String code;
String format;
_QrCodeResult(this.code, this.format);
@override
Map<String, dynamic> toJson() {
var json = super.toJson();
json['code'] = code;
json['format'] = format;
return json;
}
}
class _LocationResult extends MobileActionResult {
num latitude;
num longitude;
_LocationResult(this.latitude, this.longitude);
@override
Map<String, dynamic> toJson() {
var json = super.toJson();
json['latitude'] = latitude;
json['longitude'] = longitude;
return json;
}
}
enum WidgetMobileActionType {
takePictureFromGallery,
takePhoto,
mapDirection,
mapLocation,
scanQrCode,
makePhoneCall,
getLocation,
takeScreenshot,
unknown
}
WidgetMobileActionType widgetMobileActionTypeFromString(String value) {
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 {
var result = await _handleWidgetMobileAction(args, controller);
return result.toJson();
}
Future<WidgetMobileActionResult> _handleWidgetMobileAction(List<dynamic> args, InAppWebViewController controller) async {
if (args.isNotEmpty && args[0] is String) {
var actionType = widgetMobileActionTypeFromString(args[0]);
switch(actionType) {
case WidgetMobileActionType.takePictureFromGallery:
return await _takePicture(ImageSource.gallery);
case WidgetMobileActionType.takePhoto:
return await _takePicture(ImageSource.camera);
case WidgetMobileActionType.mapDirection:
return await _launchMap(args, true);
case WidgetMobileActionType.mapLocation:
return await _launchMap(args, false);
case WidgetMobileActionType.scanQrCode:
return await _scanQrCode();
case WidgetMobileActionType.makePhoneCall:
return await _makePhoneCall(args);
case WidgetMobileActionType.getLocation:
return await _getLocation();
case WidgetMobileActionType.takeScreenshot:
return await _takeScreenshot(controller);
case WidgetMobileActionType.unknown:
return WidgetMobileActionResult.errorResult('Unknown actionType: ${args[0]}');
}
} else {
return WidgetMobileActionResult.errorResult('actionType is not provided.');
}
}
Future<WidgetMobileActionResult> _takePicture(ImageSource source) async {
try {
final picker = ImagePicker();
final pickedFile = await picker.getImage(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();
return WidgetMobileActionResult.successResult(
MobileActionResult.image(imageUrl));
} else {
return WidgetMobileActionResult.errorResult(
'Unknown picture mime type');
}
} else {
return WidgetMobileActionResult.emptyResult();
}
} catch (e) {
return _handleError(e);
}
}
Future<WidgetMobileActionResult> _launchMap(List<dynamic> args, bool directionElseLocation) async {
try {
num? lat;
num? lon;
if (args.length > 2 && args[1] is num && args[2] is num) {
lat = args[1];
lon = args[2];
} else {
return WidgetMobileActionResult.errorResult(
'Missing target latitude or longitude arguments!');
}
var url = 'https://www.google.com/maps/';
url += directionElseLocation
? 'dir/?api=1&destination=$lat,$lon'
: 'search/?api=1&query=$lat,$lon';
return WidgetMobileActionResult.successResult(await _tryLaunch(url));
} catch (e) {
return _handleError(e);
}
}
Future<WidgetMobileActionResult> _scanQrCode() async {
try {
Barcode? barcode = await tbContext.navigateTo('/qrCodeScan', transition: TransitionType.nativeModal);
if (barcode != null) {
return WidgetMobileActionResult.successResult(MobileActionResult.qrCode(barcode.code, describeEnum(barcode.format)));
} else {
return WidgetMobileActionResult.emptyResult();
}
} catch (e) {
return _handleError(e);
}
}
Future<WidgetMobileActionResult> _makePhoneCall(List<dynamic> args) async {
try {
var phoneNumber;
if (args.length > 1 && args[1] != null) {
phoneNumber = args[1];
} else {
return WidgetMobileActionResult.errorResult(
'Missing or invalid phone number!');
}
return WidgetMobileActionResult.successResult(
await _tryLaunch('tel://$phoneNumber'));
} catch (e) {
return _handleError(e);
}
}
Future<WidgetMobileActionResult> _getLocation() async {
try {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return WidgetMobileActionResult.errorResult(
'Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return WidgetMobileActionResult.errorResult(
'Location permissions are denied.');
}
}
if (permission == LocationPermission.deniedForever) {
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));
} catch (e) {
return _handleError(e);
}
}
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));
} else {
return WidgetMobileActionResult.emptyResult();
}
} catch (e) {
return _handleError(e);
}
}
Future<MobileActionResult> _tryLaunch(String url) async {
if (await canLaunch(url)) {
await launch(url);
return MobileActionResult.launched(true);
} else {
log.error('Could not launch $url');
return MobileActionResult.launched(false);
}
}
WidgetMobileActionResult _handleError(e) {
String error;
if (e is PlatformException) {
error = e.message ?? e.code;
} else {
error = e.toString();
}
return WidgetMobileActionResult.errorResult(error);
}
}

View File

@@ -0,0 +1,136 @@
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, _QrCodeScannerPageState> {
QrCodeScannerPage(TbContext tbContext) : super(tbContext);
@override
_QrCodeScannerPageState createState() => _QrCodeScannerPageState();
}
class _QrCodeScannerPageState extends TbPageState<QrCodeScannerPage, _QrCodeScannerPageState> {
Timer? simulatedQrTimer;
QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
@override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
controller!.pauseCamera();
}
controller!.resumeCamera();
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
controller?.dispose();
if (simulatedQrTimer != null) {
simulatedQrTimer!.cancel();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
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)))
),
Positioned(
child:
AppBar(
leading: Container(),
backgroundColor: Colors.transparent,
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)
? 150.0
: 300.0;
// To ensure the Scanner view is properly sizes after rotation
// we need to listen for Flutter SizeChanged notification and update controller
return QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.red,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea),
);
}
void _onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
});
if (isPhysicalDevice) {
controller.scannedDataStream.take(1).listen((scanData) {
pop(scanData);
});
} else {
simulatedQrTimer = Timer(Duration(seconds: 3), () {
pop(Barcode('test code', BarcodeFormat.qrcode, null));
});
}
}
}

View File

@@ -0,0 +1,20 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
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) {
return QrCodeScannerPage(tbContext);
});
UiUtilsRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/qrCodeScan", handler: qrCodeScannerHandler);
}
}

View File

@@ -13,6 +13,8 @@ class TbAppBar extends TbContextWidget<TbAppBar, _TbAppBarState> implements Pref
final Widget? title;
final bool? showProfile;
final bool? showLogout;
final double? elevation;
final bool showLoadingIndicator;
final ValueNotifier<bool>? searchModeNotifier;
final String? searchHint;
final void Function(String searchText)? onSearch;
@@ -21,8 +23,9 @@ class TbAppBar extends TbContextWidget<TbAppBar, _TbAppBarState> implements Pref
@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),
TbAppBar(TbContext tbContext, {this.title, this.elevation, this.showProfile = true, this.showLogout = false,
this.showLoadingIndicator = true, this.searchModeNotifier, this.searchHint, this.onSearch, this.onSearchClosed}) :
preferredSize = Size.fromHeight(kToolbarHeight + (showLoadingIndicator ? 4 : 0)),
super(tbContext);
@override
@@ -70,18 +73,20 @@ class _TbAppBarState extends TbContextState<TbAppBar, _TbAppBarState> {
} else {
children.add(buildDefaultBar());
}
children.add(
ValueListenableBuilder(
valueListenable: loadingNotifier,
builder: (context, bool loading, child) {
if (loading) {
return LinearProgressIndicator();
} else {
return Container(height: 4);
if (widget.showLoadingIndicator) {
children.add(
ValueListenableBuilder(
valueListenable: loadingNotifier,
builder: (context, bool loading, child) {
if (loading) {
return LinearProgressIndicator();
} else {
return Container(height: 4);
}
}
}
)
);
)
);
}
return Column(
children: children,
);
@@ -135,7 +140,7 @@ class _TbAppBarState extends TbContextState<TbAppBar, _TbAppBarState> {
),
),
onPressed: () {
tbContext.tbClient.logout(
tbClient.logout(
requestConfig: RequestConfig(ignoreErrors: true));
}
)
@@ -144,6 +149,7 @@ class _TbAppBarState extends TbContextState<TbAppBar, _TbAppBarState> {
return AppBar(
title: widget.title,
actions: actions,
elevation: widget.elevation,
);
}
}

View File

@@ -70,7 +70,21 @@ packages:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.3"
device_info:
dependency: "direct main"
description:
name: device_info
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
device_info_platform_interface:
dependency: transitive
description:
name: device_info_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
dio:
dependency: transitive
description:
@@ -78,6 +92,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
fading_edge_scrollview:
dependency: "direct main"
description:
name: fading_edge_scrollview
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
fake_async:
dependency: transitive
description:
@@ -97,6 +118,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
url: "https://pub.dartlang.org"
source: hosted
version: "5.3.2"
flutter_launcher_icons:
dependency: "direct dev"
description:
@@ -104,13 +132,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
version: "4.2.0"
flutter_speed_dial:
dependency: "direct main"
description:
@@ -130,6 +165,39 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
geolocator:
dependency: "direct main"
description:
name: geolocator
url: "https://pub.dartlang.org"
source: hosted
version: "7.0.3"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.3"
http_parser:
dependency: transitive
description:
@@ -144,6 +212,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.4"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
infinite_scroll_pagination:
dependency: "direct main"
description:
@@ -151,6 +240,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
jwt_decoder:
dependency: transitive
description:
@@ -158,6 +261,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
logger:
dependency: "direct main"
description:
name: logger
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
matcher:
dependency: transitive
description:
@@ -172,6 +282,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
mime:
dependency: "direct main"
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
@@ -185,14 +302,21 @@ packages:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
version: "0.5.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
version: "0.2.1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.0"
petitparser:
dependency: transitive
description:
@@ -200,6 +324,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
qr_code_scanner:
dependency: "direct main"
description:
name: qr_code_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
sky_engine:
dependency: transitive
description: flutter
@@ -211,7 +349,7 @@ packages:
name: sliver_tools
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
version: "0.2.2"
source_span:
dependency: transitive
description:
@@ -266,7 +404,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "5850092955f10142eca48d8c8680db980f580f72"
resolved-ref: "4c2463beceb4f397c9de79ac1b0dee628aaf8add"
url: "git@github.com:thingsboard/dart_thingsboard_client.git"
source: git
version: "1.0.0"
@@ -277,6 +415,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.3"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
vector_math:
dependency: transitive
description:

View File

@@ -13,13 +13,23 @@ dependencies:
sdk: flutter
thingsboard_client:
git: git@github.com:thingsboard/dart_thingsboard_client.git
intl: ^0.17.0
flutter_secure_storage: ^4.1.0
flutter_speed_dial: ^3.0.5
cupertino_icons: ^1.0.2
fluro: ^2.0.3
flutter_svg: ^0.22.0
infinite_scroll_pagination: ^3.0.1
fading_edge_scrollview: ^2.0.0
stream_transform: ^2.0.0
flutter_inappwebview: ^5.3.2
url_launcher: ^6.0.3
image_picker: ^0.7.4
mime: ^1.0.0
logger: ^1.0.0
qr_code_scanner: ^0.4.0
device_info: ^2.0.0
geolocator: ^7.0.3
dev_dependencies:
flutter_test:
@@ -60,4 +70,5 @@ flutter:
flutter_icons:
android: "launcher_icon"
ios: true
remove_alpha_ios: true
image_path: "assets/images/thingsboard.png"