Base pages implementation

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

View File

@@ -0,0 +1,22 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'login/login_page.dart';
class AuthRoutes extends TbRoutes {
late var loginHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return LoginPage(tbContext);
});
AuthRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/login", handler: loginHandler);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/core/entity/entities_base.dart';
import 'package:thingsboard_app/widgets/tb_app_bar.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
abstract class EntitiesPage<T extends BaseData> extends TbContextWidget<EntitiesPage<T>, _EntitiesPageState<T>> with EntitiesBase<T> {
EntitiesPage(TbContext tbContext): super(tbContext);
String get searchHint;
String get noMoreItemsText;
@override
_EntitiesPageState createState() => _EntitiesPageState();
}
class _EntitiesPageState<T extends BaseData> extends TbContextState<EntitiesPage<T>, _EntitiesPageState<T>> {
final _searchModeNotifier = ValueNotifier<bool>(false);
final PagingController<PageLink, T> _pagingController = PagingController(firstPageKey: PageLink(10, 0, null, SortOrder('createdTime', Direction.DESC)));
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
bool _dataLoading = false;
bool _scheduleRefresh = false;
void _refresh() {
if (_dataLoading) {
_scheduleRefresh = true;
} else {
_pagingController.refresh();
}
}
Future<void> _fetchPage(PageLink pageKey) async {
if (mounted) {
_dataLoading = true;
try {
hideNotification();
final pageData = await widget.fetchEntities(pageKey);
final isLastPage = !pageData.hasNext;
if (isLastPage) {
_pagingController.appendLastPage(pageData.data);
} else {
final nextPageKey = pageKey.nextPageLink();
_pagingController.appendPage(pageData.data, nextPageKey);
}
} catch (error) {
if (mounted) {
_pagingController.error = error;
}
} finally {
_dataLoading = false;
if (_scheduleRefresh) {
_scheduleRefresh = false;
if (mounted) {
_pagingController.refresh();
}
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TbAppBar(
tbContext,
title: Text(widget.title),
searchModeNotifier: _searchModeNotifier,
searchHint: widget.searchHint,
onSearch: (String searchText) {
_pagingController.firstPageKey.textSearch = searchText;
_pagingController.firstPageKey.page = 0;
_refresh();
},
),
body: RefreshIndicator(
onRefresh: () => Future.sync(
() => _refresh(),
),
child: PagedListView(
pagingController: _pagingController,
padding: EdgeInsets.all(0),
builderDelegate: PagedChildBuilderDelegate<T>(
itemBuilder: (context, item, index) => EntityCard<T>(
item,
entityCardWidgetBuilder: widget.buildEntityCard,
onDetails: widget.onEntityDetails,
briefView: false
),
firstPageProgressIndicatorBuilder: (context) {
return Stack( children: [
Positioned(
top: 20,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [RefreshProgressIndicator()],
),
)
]);
},
newPageProgressIndicatorBuilder: (context) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
bottom: 16,
),
child: Center(child: RefreshProgressIndicator()),
);
},
noItemsFoundIndicatorBuilder: (context) => FirstPageExceptionIndicator(
title: widget.noItemsFoundText,
message: 'The list is currently empty.',
onTryAgain: () => _refresh(),
)
)
)
)
/* bottomNavigationBar: BottomAppBar(
child: Row(
children: [
IconButton(icon: Icon(Icons.refresh), onPressed: () {
_refresh();
}),
Spacer(),
IconButton(icon: Icon(Icons.search), onPressed: () {
_searchModeNotifier.value = true;
})
]
)
) */
);
}
}
class FirstPageExceptionIndicator extends StatelessWidget {
const FirstPageExceptionIndicator({
required this.title,
this.message,
this.onTryAgain,
Key? key,
}) : super(key: key);
final String title;
final String? message;
final VoidCallback? onTryAgain;
@override
Widget build(BuildContext context) {
final message = this.message;
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 16),
child: Column(
children: [
Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline6,
),
if (message != null)
const SizedBox(
height: 16,
),
if (message != null)
Text(
message,
textAlign: TextAlign.center,
),
if (onTryAgain != null)
const SizedBox(
height: 48,
),
if (onTryAgain != null)
SizedBox(
height: 50,
width: double.infinity,
child: ElevatedButton.icon(
onPressed: onTryAgain,
icon: const Icon(
Icons.refresh,
color: Colors.white,
),
label: const Text(
'Try Again',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
),
),
],
),
),
);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:thingsboard_app/config/routes/router.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'init_app.dart';
class InitRoutes extends TbRoutes {
late var initHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return ThingsboardInitApp(tbContext);
});
InitRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/", handler: initHandler);
}
}