Base pages implementation
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 6.2 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/thingsboard.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/thingsboard.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 8.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/thingsboard.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/thingsboard.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 19 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/thingsboard.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -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>
|
||||
|
||||
1
android/settings_aar.gradle
Normal file
@@ -0,0 +1 @@
|
||||
include ':app'
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 841 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 48 KiB |
56
ios/Runner/Info-Debug.plist
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
const thingsBoardApiEndpoint = 'https://demo.thingsboard.io';
|
||||
const username = 'ikulikov82@gmail.com';
|
||||
const password = 'qwerty';
|
||||
const thingsBoardApiEndpoint = 'http://localhost:8080';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
abstract class ThingsboardImage {
|
||||
static final thingsBoardLogoBlue = 'assets/images/thingsboard_logo_blue.svg';
|
||||
static final thingsboard = 'assets/images/thingsboard.png';
|
||||
}
|
||||
|
||||
22
lib/core/auth/auth_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
89
lib/core/entity/entities_base.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
220
lib/core/entity/entities_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
215
lib/core/entity/entities_widget.dart
Normal 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()
|
||||
));
|
||||
}
|
||||
}
|
||||
97
lib/core/entity/entity_details_page.dart
Normal 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());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
22
lib/core/init/init_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
27
lib/modules/asset/asset_details_page.dart
Normal 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}'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
27
lib/modules/asset/asset_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
89
lib/modules/asset/assets_base.dart
Normal 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}');
|
||||
}
|
||||
|
||||
}
|
||||
16
lib/modules/asset/assets_page.dart
Normal 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';
|
||||
|
||||
}
|
||||
16
lib/modules/asset/assets_widget.dart
Normal 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');
|
||||
}
|
||||
|
||||
}
|
||||
250
lib/modules/dashboard/dashboard.dart
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
62
lib/modules/dashboard/dashboard_page.dart
Normal 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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
31
lib/modules/dashboard/dashboard_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
104
lib/modules/dashboard/dashboards_base.dart
Normal 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}');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
lib/modules/dashboard/dashboards_page.dart
Normal 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';
|
||||
|
||||
}
|
||||
15
lib/modules/dashboard/dashboards_widget.dart
Normal 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');
|
||||
}
|
||||
|
||||
}
|
||||
27
lib/modules/device/device_details_page.dart
Normal 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}'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
28
lib/modules/device/device_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
98
lib/modules/device/devices_base.dart
Normal 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())
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
],
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
15
lib/modules/device/devices_widget.dart
Normal 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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
20
lib/modules/home/home_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
176
lib/modules/main/main_page.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
21
lib/modules/profile/profile_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
305
lib/utils/services/widget_action_handler.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
136
lib/utils/ui/qr_code_scanner.dart
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
20
lib/utils/ui_utils_routes.dart
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
192
pubspec.lock
@@ -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:
|
||||
|
||||
11
pubspec.yaml
@@ -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"
|
||||
|
||||