Skip to content

Commit

Permalink
fix: #2729 - product query page - simplified top messages and buttons
Browse files Browse the repository at this point in the history
Impacted files:
* `product_query_page.dart`: now displaying actions as app bar icon buttons (and large buttons for empty results); replaced the banner with a simple card; refactored
* `product_query_page_helper.dart`: minor refactoring
  • Loading branch information
monsieurtanuki committed Aug 6, 2022
1 parent 4a24e98 commit a6a6839
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 77 deletions.
213 changes: 137 additions & 76 deletions packages/smooth_app/lib/pages/product/common/product_query_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dar
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/duration_constants.dart';
import 'package:smooth_app/generic_lib/loading_dialog.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_error_card.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';
import 'package:smooth_app/pages/personalized_ranking_page.dart';
Expand All @@ -28,30 +29,21 @@ class ProductQueryPage extends StatefulWidget {
const ProductQueryPage({
required this.productListSupplier,
required this.name,
this.lastUpdate,
});

final ProductListSupplier productListSupplier;
final String name;
final int? lastUpdate;

@override
State<ProductQueryPage> createState() => _ProductQueryPageState();
}

class _ProductQueryPageState extends State<ProductQueryPage>
with TraceableClientMixin {
// we have to use GlobalKey's for SnackBar's because of nested Scaffold's:
// not the 2 Scaffold's here but one of them and the one on top (PageManager)
final GlobalKey<ScaffoldMessengerState> _scaffoldKeyEmpty =
GlobalKey<ScaffoldMessengerState>();
final GlobalKey<ScaffoldMessengerState> _scaffoldKeyNotEmpty =
GlobalKey<ScaffoldMessengerState>();
bool _showBackToTopButton = false;
late ScrollController _scrollController;

late ProductQueryModel _model;
int? _lastUpdate;

@override
String get traceTitle => 'search_page';
Expand All @@ -62,7 +54,6 @@ class _ProductQueryPageState extends State<ProductQueryPage>
@override
void initState() {
super.initState();
_lastUpdate = widget.lastUpdate;
_model = _getModel(widget.productListSupplier);
_scrollController = ScrollController()
..addListener(() {
Expand Down Expand Up @@ -100,7 +91,7 @@ class _ProductQueryPageState extends State<ProductQueryPage>
);
case LoadingStatus.LOADED:
if (_model.isNotEmpty()) {
_showRefreshSnackBar(_scaffoldKeyNotEmpty);
// TODO(monsieurtanuki): add country, language?
AnalyticsHelper.trackSearch(
search: widget.name,
searchCount: _model.displayBarcodes.length,
Expand All @@ -111,14 +102,15 @@ class _ProductQueryPageState extends State<ProductQueryPage>
appLocalizations,
);
}
_showRefreshSnackBar(_scaffoldKeyEmpty);
// TODO(monsieurtanuki): should be tracked as well, shouldn't it?
return _getEmptyScreen(
screenSize,
themeData,
_getEmptyText(
themeData,
appLocalizations.no_product_found,
),
actions: _getAppBarButtons(),
);
case LoadingStatus.ERROR:
return _getErrorWidget(
Expand All @@ -131,29 +123,32 @@ class _ProductQueryPageState extends State<ProductQueryPage>
Widget _getEmptyScreen(
final Size screenSize,
final ThemeData themeData,
final Widget emptiness,
) =>
final Widget emptiness, {
final List<Widget>? actions,
}) =>
// TODO(monsieurtanuki): remove the ScaffoldMessenger as we don't need it (but that will change the format)
ScaffoldMessenger(
key: _scaffoldKeyEmpty,
child: SmoothScaffold(
appBar: AppBar(
backgroundColor: themeData.scaffoldBackgroundColor,
leading: const _BackButton(),
title: _getAppBarTitle(),
actions: actions,
),
body: Center(child: emptiness),
),
);

Widget _getAppBarTitle() => AutoSizeText(widget.name, maxLines: 2);

// TODO(monsieurtanuki): put that in a specific Widget class
Widget _getNotEmptyScreen(
final Size screenSize,
final ThemeData themeData,
final AppLocalizations appLocalizations,
) =>
// TODO(monsieurtanuki): remove the ScaffoldMessenger as we don't need it (but that will change the format)
ScaffoldMessenger(
key: _scaffoldKeyNotEmpty,
child: SmoothScaffold(
floatingActionButton: Row(
mainAxisAlignment: _showBackToTopButton
Expand Down Expand Up @@ -206,12 +201,18 @@ class _ProductQueryPageState extends State<ProductQueryPage>
automaticallyImplyLeading: false,
leading: const _BackButton(),
title: _getAppBarTitle(),
actions: _getAppBarButtons(),
),
body: RefreshIndicator(
onRefresh: () => refreshList(),
child: ListView.builder(
controller: _scrollController,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
// on top, a message
return _getTopMessagesCard();
}
index--;
if (index >= _model.displayBarcodes.length) {
// final button
final int already = _model.displayBarcodes.length;
Expand Down Expand Up @@ -263,7 +264,8 @@ class _ProductQueryPageState extends State<ProductQueryPage>
),
);
},
itemCount: _model.displayBarcodes.length + 1,
// 2 additional widgets, on top and on bottom
itemCount: _model.displayBarcodes.length + 2,
),
),
),
Expand All @@ -290,88 +292,133 @@ class _ProductQueryPageState extends State<ProductQueryPage>
Widget _getEmptyText(
final ThemeData themeData,
final String message,
) =>
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
child: Text(
message,
textAlign: TextAlign.center,
style: themeData.textTheme.subtitle1!.copyWith(fontSize: 18.0),
),
) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final PagedProductQuery pagedProductQuery = _model.supplier.productQuery;
final PagedProductQuery? worldQuery = pagedProductQuery.getWorldQuery();
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_getTopMessagesCard(),
Padding(
padding: const EdgeInsets.all(SMALL_SPACE),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: LARGE_SPACE),
child: Text(
message,
textAlign: TextAlign.center,
style:
themeData.textTheme.subtitle1!.copyWith(fontSize: 18.0),
),
),
if (worldQuery != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE),
child: _getLargeButtonWithIcon(
_getWorldAction(appLocalizations, worldQuery),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE),
child: _getLargeButtonWithIcon(
_getRefreshAction(appLocalizations),
),
),
],
),
],
);
),
EMPTY_WIDGET,
],
);
}

void _showRefreshSnackBar(
final GlobalKey<ScaffoldMessengerState> scaffoldKey,
) {
List<Widget> _getAppBarButtons() {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final List<String> messages = <String>[];
final PagedProductQuery pagedProductQuery = _model.supplier.productQuery;
final PagedProductQuery? worldQuery = pagedProductQuery.getWorldQuery();
return <Widget>[
if (worldQuery != null)
_getIconButton(_getWorldAction(appLocalizations, worldQuery)),
_getIconButton(_getRefreshAction(appLocalizations)),
];
}

Widget _getTopMessagesCard() {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final List<String> messages = <String>[];
messages.add(
appLocalizations.user_list_length(
_model.supplier.partialProductList.totalSize,
),
);
if (_lastUpdate != null) {
final int? lastUpdate = _model.supplier.timestamp;
if (lastUpdate != null) {
final String lastTime =
ProductQueryPageHelper.getDurationStringFromTimestamp(
_lastUpdate!, context);
lastUpdate, context);
messages.add('${appLocalizations.cached_results_from} $lastTime');
}
final PagedProductQuery pagedProductQuery = _model.supplier.productQuery;
if (pagedProductQuery.hasDifferentCountryWorldData() &&
pagedProductQuery.world) {
messages.add(appLocalizations.world_results_label);
}
_lastUpdate = null;

Future<void>.delayed(
Duration.zero,
() => scaffoldKey.currentState?.showMaterialBanner(
MaterialBanner(
content: Text(messages.join('\n')),
actions: <Widget>[
TextButton(
child: Text(appLocalizations.close.toUpperCase()),
onPressed: () =>
scaffoldKey.currentState?.hideCurrentMaterialBanner(),
),
if (worldQuery != null)
TextButton(
child: Text(
appLocalizations.world_results_action.toUpperCase(),
),
onPressed: () async {
scaffoldKey.currentState?.hideCurrentMaterialBanner();
await ProductQueryPageHelper().openBestChoice(
productQuery: worldQuery,
localDatabase: context.read<LocalDatabase>(),
name: widget.name,
context: context,
);
},
),
TextButton(
child: Text(appLocalizations.label_refresh.toUpperCase()),
onPressed: () async {
scaffoldKey.currentState?.hideCurrentMaterialBanner();
final bool? success =
await _loadAndRefreshDisplay(_model.loadFromTop());
if (success == true) {
_scrollToTop();
}
},
),
],
return SizedBox(
width: double.infinity,
child: SmoothCard(
child: Padding(
padding: const EdgeInsets.all(SMALL_SPACE),
child: Text(messages.join('\n')),
),
),
);
}

Widget _getLargeButtonWithIcon(final _Action action) =>
SmoothLargeButtonWithIcon(
text: action.text,
icon: action.iconData,
onPressed: action.onPressed,
);

Widget _getIconButton(final _Action action) => IconButton(
tooltip: action.text,
icon: Icon(action.iconData),
onPressed: action.onPressed,
);

_Action _getWorldAction(
final AppLocalizations appLocalizations,
final PagedProductQuery worldQuery,
) =>
_Action(
text: appLocalizations.world_results_action,
iconData: Icons.public,
onPressed: () async => ProductQueryPageHelper().openBestChoice(
productQuery: worldQuery,
localDatabase: context.read<LocalDatabase>(),
name: widget.name,
context: context,
),
);

_Action _getRefreshAction(
final AppLocalizations appLocalizations,
) =>
_Action(
text: appLocalizations.label_refresh,
iconData: Icons.refresh,
onPressed: () async {
final bool? success =
await _loadAndRefreshDisplay(_model.loadFromTop());
if (success == true) {
_scrollToTop();
}
},
);

void retryConnection() =>
setState(() => _model = _getModel(widget.productListSupplier));

Expand Down Expand Up @@ -412,6 +459,7 @@ class _ProductQueryPageState extends State<ProductQueryPage>
}
}

// TODO(monsieurtanki): put it in a specific Widget class
class _BackButton extends StatelessWidget {
const _BackButton();

Expand All @@ -424,3 +472,16 @@ class _BackButton extends StatelessWidget {
},
);
}

// TODO(monsieurtanki): put it in a specific reusable class
class _Action {
_Action({
required this.iconData,
required this.text,
required this.onPressed,
});

final IconData iconData;
final String text;
final VoidCallback onPressed;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class ProductQueryPageHelper {
builder: (BuildContext context) => ProductQueryPage(
productListSupplier: supplier,
name: name,
lastUpdate: supplier.timestamp,
),
),
);
Expand Down

0 comments on commit a6a6839

Please sign in to comment.