Home and Alarms page
This commit is contained in:
@@ -1,13 +1,22 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.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_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);
|
||||
typedef EntityTapFunction<T> = Function(T entity);
|
||||
typedef EntityCardWidgetBuilder<T> = Widget Function(BuildContext context, T entity);
|
||||
|
||||
mixin EntitiesBase<T extends BaseData> on HasTbContext {
|
||||
class EntityCardSettings {
|
||||
bool dropShadow;
|
||||
EntityCardSettings({this.dropShadow = true});
|
||||
}
|
||||
|
||||
mixin EntitiesBase<T, P> on HasTbContext {
|
||||
|
||||
final entityDateFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
@@ -15,75 +24,255 @@ mixin EntitiesBase<T extends BaseData> on HasTbContext {
|
||||
|
||||
String get noItemsFoundText;
|
||||
|
||||
Future<PageData<T>> fetchEntities(PageLink pageLink);
|
||||
Future<PageData<T>> fetchEntities(P pageKey);
|
||||
|
||||
Widget buildEntityCard(BuildContext context, T entity, bool briefView);
|
||||
Future<void> onRefresh() => Future.value();
|
||||
|
||||
void onEntityDetails(T entity);
|
||||
Widget? buildHeading(BuildContext context) => null;
|
||||
|
||||
Widget buildEntityListCard(BuildContext context, T entity) {
|
||||
return Text('Not implemented!');
|
||||
}
|
||||
|
||||
Widget buildEntityListWidgetCard(BuildContext context, T entity) {
|
||||
return Text('Not implemented!');
|
||||
}
|
||||
|
||||
Widget buildEntityGridCard(BuildContext context, T entity) {
|
||||
return Text('Not implemented!');
|
||||
}
|
||||
|
||||
P createFirstKey({int pageSize = 10}) => throw UnimplementedError('Not implemented');
|
||||
|
||||
P nextPageKey(P pageKey) => throw UnimplementedError('Not implemented');
|
||||
|
||||
EntityCardSettings entityListCardSettings(T entity) => EntityCardSettings();
|
||||
|
||||
EntityCardSettings entityGridCardSettings(T entity) => EntityCardSettings();
|
||||
|
||||
void onEntityTap(T entity);
|
||||
|
||||
}
|
||||
|
||||
class EntityCard<T extends BaseData> extends StatelessWidget {
|
||||
final bool _briefView;
|
||||
final T _entity;
|
||||
final EntityDetailsFunction<T>? _onDetails;
|
||||
final EntityCardWidgetBuilder<T> _entityCardWidgetBuilder;
|
||||
mixin EntitiesBaseWithPageLink<T> on EntitiesBase<T, PageLink> {
|
||||
|
||||
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
|
||||
PageLink createFirstKey({int pageSize = 10}) => PageLink(pageSize, 0, null, SortOrder('createdTime', Direction.DESC));
|
||||
|
||||
@override
|
||||
PageLink nextPageKey(PageLink pageKey) => pageKey.nextPageLink();
|
||||
|
||||
}
|
||||
|
||||
mixin EntitiesBaseWithTimePageLink<T> on EntitiesBase<T, TimePageLink> {
|
||||
|
||||
@override
|
||||
TimePageLink createFirstKey({int pageSize = 10}) => TimePageLink(pageSize, 0, null, SortOrder('createdTime', Direction.DESC));
|
||||
|
||||
@override
|
||||
TimePageLink nextPageKey(TimePageLink pageKey) => pageKey.nextPageLink();
|
||||
|
||||
}
|
||||
|
||||
abstract class BaseEntitiesPageLinkWidget<T> extends BaseEntitiesWidget<T, PageLink> with EntitiesBaseWithPageLink<T> {
|
||||
BaseEntitiesPageLinkWidget(TbContext tbContext): super(tbContext);
|
||||
}
|
||||
|
||||
abstract class BaseEntitiesTimePageLinkWidget<T> extends BaseEntitiesWidget<T, TimePageLink> with EntitiesBaseWithTimePageLink<T> {
|
||||
BaseEntitiesTimePageLinkWidget(TbContext tbContext): super(tbContext);
|
||||
}
|
||||
|
||||
abstract class BaseEntitiesWidget<T, P> extends TbContextWidget<BaseEntitiesWidget<T, P>, BaseEntitiesState<T, P>> with EntitiesBase<T, P> {
|
||||
|
||||
BaseEntitiesWidget(TbContext tbContext): super(tbContext);
|
||||
|
||||
}
|
||||
|
||||
abstract class BaseEntitiesState<T, P> extends TbContextState<BaseEntitiesWidget<T, P>, BaseEntitiesState<T, P>> {
|
||||
|
||||
late final PagingController<P, T> pagingController;
|
||||
Completer<void>? _refreshCompleter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
pagingController = PagingController(firstPageKey: widget.createFirstKey());
|
||||
pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _dataLoading = false;
|
||||
bool _scheduleRefresh = false;
|
||||
|
||||
Future<void> _refresh() {
|
||||
if (_refreshCompleter == null) {
|
||||
_refreshCompleter = Completer();
|
||||
}
|
||||
if (_dataLoading) {
|
||||
_scheduleRefresh = true;
|
||||
} else {
|
||||
_refreshPagingController();
|
||||
}
|
||||
return _refreshCompleter!.future;
|
||||
}
|
||||
|
||||
void _refreshPagingController() {
|
||||
_fetchPage(widget.createFirstKey(), refresh: true);
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(P pageKey, {bool refresh = false}) async {
|
||||
if (mounted) {
|
||||
_dataLoading = true;
|
||||
try {
|
||||
hideNotification();
|
||||
final pageData = await widget.fetchEntities(pageKey);
|
||||
final isLastPage = !pageData.hasNext;
|
||||
if (refresh) {
|
||||
var state = pagingController.value;
|
||||
if (state.itemList != null) {
|
||||
state.itemList!.clear();
|
||||
}
|
||||
}
|
||||
if (isLastPage) {
|
||||
pagingController.appendLastPage(pageData.data);
|
||||
} else {
|
||||
final nextPageKey = widget.nextPageKey(pageKey);
|
||||
pagingController.appendPage(pageData.data, nextPageKey);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
pagingController.error = error;
|
||||
}
|
||||
} finally {
|
||||
_dataLoading = false;
|
||||
if (refresh) {
|
||||
_refreshCompleter!.complete();
|
||||
_refreshCompleter = null;
|
||||
}
|
||||
if (_scheduleRefresh) {
|
||||
_scheduleRefresh = false;
|
||||
if (mounted) {
|
||||
_refreshPagingController();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
)
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
widget.onRefresh(),
|
||||
_refresh()
|
||||
]),
|
||||
child: pagedViewBuilder(context)
|
||||
);
|
||||
}
|
||||
|
||||
Widget pagedViewBuilder(BuildContext context);
|
||||
|
||||
Widget firstPageProgressIndicatorBuilder(BuildContext context) {
|
||||
return Stack( children: [
|
||||
Positioned(
|
||||
top: 20,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [RefreshProgressIndicator()],
|
||||
),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
Widget newPageProgressIndicatorBuilder(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
),
|
||||
child: Center(child: RefreshProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget noItemsFoundIndicatorBuilder(BuildContext context) {
|
||||
return FirstPageExceptionIndicator(
|
||||
title: widget.noItemsFoundText,
|
||||
message: 'The list is currently empty.',
|
||||
onTryAgain: () => pagingController.refresh(),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
decoration: _briefView ? BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Color(0xFFDEDEDE),
|
||||
style: BorderStyle.solid,
|
||||
width: 1
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
58
lib/core/entity/entities_grid.dart
Normal file
58
lib/core/entity/entities_grid.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import 'entities_base.dart';
|
||||
import 'entity_grid_card.dart';
|
||||
|
||||
mixin EntitiesGridStateBase on StatefulWidget {
|
||||
|
||||
@override
|
||||
_EntitiesGridState createState() => _EntitiesGridState();
|
||||
|
||||
}
|
||||
|
||||
class _EntitiesGridState<T, P> extends BaseEntitiesState<T, P> {
|
||||
|
||||
@override
|
||||
Widget pagedViewBuilder(BuildContext context) {
|
||||
var heading = widget.buildHeading(context);
|
||||
List<Widget> slivers = [];
|
||||
if (heading != null) {
|
||||
slivers.add(SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: heading
|
||||
)));
|
||||
}
|
||||
slivers.add(SliverPadding(
|
||||
padding: EdgeInsets.all(16),
|
||||
sliver: PagedSliverGrid(
|
||||
showNewPageProgressIndicatorAsGridChild: false,
|
||||
showNewPageErrorIndicatorAsGridChild: false,
|
||||
showNoMoreItemsIndicatorAsGridChild: false,
|
||||
pagingController: pagingController,
|
||||
// padding: EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
childAspectRatio: 156 / 150,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
builderDelegate: PagedChildBuilderDelegate<T>(
|
||||
itemBuilder: (context, item, index) => EntityGridCard<T>(
|
||||
item,
|
||||
entityCardWidgetBuilder: widget.buildEntityGridCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
settings: widget.entityGridCardSettings(item),
|
||||
),
|
||||
firstPageProgressIndicatorBuilder: firstPageProgressIndicatorBuilder,
|
||||
newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder,
|
||||
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder
|
||||
)
|
||||
)));
|
||||
return CustomScrollView(
|
||||
slivers: slivers
|
||||
);
|
||||
}
|
||||
}
|
||||
49
lib/core/entity/entities_list.dart
Normal file
49
lib/core/entity/entities_list.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:thingsboard_app/core/entity/entities_base.dart';
|
||||
|
||||
import 'entity_list_card.dart';
|
||||
|
||||
mixin EntitiesListStateBase on StatefulWidget {
|
||||
|
||||
@override
|
||||
_EntitiesListState createState() => _EntitiesListState();
|
||||
|
||||
}
|
||||
|
||||
class _EntitiesListState<T,P> extends BaseEntitiesState<T, P> {
|
||||
|
||||
@override
|
||||
Widget pagedViewBuilder(BuildContext context) {
|
||||
var heading = widget.buildHeading(context);
|
||||
List<Widget> slivers = [];
|
||||
if (heading != null) {
|
||||
slivers.add(SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: heading
|
||||
)));
|
||||
}
|
||||
slivers.add(SliverPadding(
|
||||
padding: EdgeInsets.all(16),
|
||||
sliver: PagedSliverList.separated(
|
||||
pagingController: pagingController,
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
builderDelegate: PagedChildBuilderDelegate<T>(
|
||||
itemBuilder: (context, item, index) => EntityListCard<T>(
|
||||
item,
|
||||
entityCardWidgetBuilder: widget.buildEntityListCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
settings: widget.entityListCardSettings(item),
|
||||
),
|
||||
firstPageProgressIndicatorBuilder: firstPageProgressIndicatorBuilder,
|
||||
newPageProgressIndicatorBuilder: newPageProgressIndicatorBuilder,
|
||||
noItemsFoundIndicatorBuilder: noItemsFoundIndicatorBuilder
|
||||
)
|
||||
)));
|
||||
return CustomScrollView(
|
||||
slivers: slivers
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,23 @@ 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 {
|
||||
import 'entity_list_card.dart';
|
||||
|
||||
final List<_EntitiesWidgetState> states = [];
|
||||
class EntitiesListWidgetController {
|
||||
|
||||
void _registerEntitiesWidgetState(_EntitiesWidgetState entitiesWidgetState) {
|
||||
states.add(entitiesWidgetState);
|
||||
final List<_EntitiesListWidgetState> states = [];
|
||||
|
||||
void _registerEntitiesWidgetState(_EntitiesListWidgetState entitiesListWidgetState) {
|
||||
states.add(entitiesListWidgetState);
|
||||
}
|
||||
|
||||
void _unregisterEntitiesWidgetState(_EntitiesWidgetState entitiesWidgetState) {
|
||||
states.remove(entitiesWidgetState);
|
||||
void _unregisterEntitiesWidgetState(_EntitiesListWidgetState entitiesListWidgetState) {
|
||||
states.remove(entitiesListWidgetState);
|
||||
}
|
||||
|
||||
Future<void> refresh() {
|
||||
@@ -31,29 +32,32 @@ class EntitiesWidgetController {
|
||||
|
||||
}
|
||||
|
||||
abstract class EntitiesWidget<T extends BaseData> extends TbContextWidget<EntitiesWidget<T>, _EntitiesWidgetState<T>> with EntitiesBase<T> {
|
||||
abstract class EntitiesListPageLinkWidget<T> extends EntitiesListWidget<T, PageLink> with EntitiesBaseWithPageLink<T> {
|
||||
EntitiesListPageLinkWidget(TbContext tbContext, {EntitiesListWidgetController? controller}): super(tbContext, controller: controller);
|
||||
}
|
||||
|
||||
final entityDateFormat = DateFormat('yyyy-MM-dd');
|
||||
final EntitiesWidgetController? _controller;
|
||||
abstract class EntitiesListWidget<T, P> extends TbContextWidget<EntitiesListWidget<T,P>, _EntitiesListWidgetState<T,P>> with EntitiesBase<T,P> {
|
||||
|
||||
EntitiesWidget(TbContext tbContext, {EntitiesWidgetController? controller}):
|
||||
final EntitiesListWidgetController? _controller;
|
||||
|
||||
EntitiesListWidget(TbContext tbContext, {EntitiesListWidgetController? controller}):
|
||||
_controller = controller,
|
||||
super(tbContext);
|
||||
|
||||
@override
|
||||
_EntitiesWidgetState createState() => _EntitiesWidgetState(_controller);
|
||||
_EntitiesListWidgetState createState() => _EntitiesListWidgetState(_controller);
|
||||
|
||||
void onViewAll();
|
||||
|
||||
}
|
||||
|
||||
class _EntitiesWidgetState<T extends BaseData> extends TbContextState<EntitiesWidget<T>, _EntitiesWidgetState<T>> {
|
||||
class _EntitiesListWidgetState<T,P> extends TbContextState<EntitiesListWidget<T,P>, _EntitiesListWidgetState<T,P>> {
|
||||
|
||||
final EntitiesWidgetController? _controller;
|
||||
final EntitiesListWidgetController? _controller;
|
||||
|
||||
final StreamController<PageData<T>?> _entitiesStreamController = StreamController.broadcast();
|
||||
|
||||
_EntitiesWidgetState(EntitiesWidgetController? controller):
|
||||
_EntitiesListWidgetState(EntitiesListWidgetController? controller):
|
||||
_controller = controller;
|
||||
|
||||
@override
|
||||
@@ -76,7 +80,7 @@ class _EntitiesWidgetState<T extends BaseData> extends TbContextState<EntitiesWi
|
||||
|
||||
Future<void> _refresh() {
|
||||
_entitiesStreamController.add(null);
|
||||
var entitiesFuture = widget.fetchEntities(PageLink(5, 0, null, SortOrder('createdTime', Direction.DESC)));
|
||||
var entitiesFuture = widget.fetchEntities(widget.createFirstKey(pageSize: 5));
|
||||
entitiesFuture.then((value) => _entitiesStreamController.add(value));
|
||||
return entitiesFuture;
|
||||
}
|
||||
@@ -204,11 +208,12 @@ class _EntitiesWidgetState<T extends BaseData> extends TbContextState<EntitiesWi
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: ScrollController(),
|
||||
children: entities.map((entity) => EntityCard<T>(
|
||||
children: entities.map((entity) => EntityListCard<T>(
|
||||
entity,
|
||||
entityCardWidgetBuilder: widget.buildEntityCard,
|
||||
onDetails: widget.onEntityDetails,
|
||||
briefView: true
|
||||
entityCardWidgetBuilder: widget.buildEntityListWidgetCard,
|
||||
onEntityTap: widget.onEntityTap,
|
||||
settings: widget.entityListCardSettings(entity),
|
||||
listWidgetCard: true
|
||||
)).toList()
|
||||
));
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
63
lib/core/entity/entity_grid_card.dart
Normal file
63
lib/core/entity/entity_grid_card.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:thingsboard_client/thingsboard_client.dart';
|
||||
|
||||
import 'entities_base.dart';
|
||||
|
||||
class EntityGridCard<T> extends StatelessWidget {
|
||||
final T _entity;
|
||||
final EntityTapFunction<T>? _onEntityTap;
|
||||
final EntityCardWidgetBuilder<T> _entityCardWidgetBuilder;
|
||||
final EntityCardSettings _settings;
|
||||
|
||||
EntityGridCard(T entity, {EntityTapFunction<T>? onEntityTap,
|
||||
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
|
||||
required EntityCardSettings settings}):
|
||||
this._entity = entity,
|
||||
this._onEntityTap = onEntityTap,
|
||||
this._entityCardWidgetBuilder = entityCardWidgetBuilder,
|
||||
this._settings = settings;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child:
|
||||
Container(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: _entityCardWidgetBuilder(context, _entity)
|
||||
)
|
||||
),
|
||||
decoration: _settings.dropShadow ? 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)
|
||||
),
|
||||
],
|
||||
) : null,
|
||||
),
|
||||
onTap: () {
|
||||
if (_onEntityTap != null) {
|
||||
_onEntityTap!(_entity);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
73
lib/core/entity/entity_list_card.dart
Normal file
73
lib/core/entity/entity_list_card.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'entities_base.dart';
|
||||
|
||||
class EntityListCard<T> extends StatelessWidget {
|
||||
final bool _listWidgetCard;
|
||||
final T _entity;
|
||||
final EntityTapFunction<T>? _onEntityTap;
|
||||
final EntityCardWidgetBuilder<T> _entityCardWidgetBuilder;
|
||||
final EntityCardSettings _settings;
|
||||
|
||||
EntityListCard(T entity, {EntityTapFunction<T>? onEntityTap,
|
||||
required EntityCardWidgetBuilder<T> entityCardWidgetBuilder,
|
||||
required EntityCardSettings settings,
|
||||
bool listWidgetCard = false}):
|
||||
this._entity = entity,
|
||||
this._onEntityTap = onEntityTap,
|
||||
this._entityCardWidgetBuilder = entityCardWidgetBuilder,
|
||||
this._settings = settings,
|
||||
this._listWidgetCard = listWidgetCard;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child:
|
||||
Container(
|
||||
margin: _listWidgetCard ? EdgeInsets.only(right: 8) : EdgeInsets.zero,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(_listWidgetCard ? 4 : 6),
|
||||
),
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: _entityCardWidgetBuilder(context, _entity)
|
||||
)
|
||||
),
|
||||
decoration: _listWidgetCard ? 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 (_onEntityTap != null) {
|
||||
_onEntityTap!(_entity);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user