Base pages implementation
This commit is contained in:
22
lib/core/auth/auth_routes.dart
Normal file
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
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
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
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
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
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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user