diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 50390b78bd9..4bf432693ef 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1709,6 +1709,27 @@ } } }, + "prices_proofs_list_length_one_page": "{count,plural, =0{No proof yet} =1{Only one proof} other{All {count} proofs}}", + "@prices_proofs_list_length_one_page": { + "description": "Number of proofs for one-page result", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "prices_proofs_list_length_many_pages": "Latest {pageSize} proofs (total: {total})", + "@prices_proofs_list_length_many_pages": { + "description": "Number of proofs for one-page result", + "placeholders": { + "pageSize": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, "prices_users_list_length_many_pages": "Top {pageSize} contributors (total: {total})", "@prices_users_list_length_many_pages": { "description": "Number of users for one-page result", @@ -1813,6 +1834,14 @@ "@user_search_prices_title": { "description": "User prices: list tile title" }, + "user_search_proofs_title": "My proofs", + "@user_search_proofs_title": { + "description": "User proofs: list tile title" + }, + "user_search_proof_title": "My proof", + "@user_search_proof_title": { + "description": "User proof: page title" + }, "user_any_search_prices_title": "Contributor prices", "@user_any_search_prices_title": { "description": "User prices (everybody except me): list tile title" diff --git a/packages/smooth_app/lib/l10n/app_fr.arb b/packages/smooth_app/lib/l10n/app_fr.arb index e6a2fbb625d..81c01ace21f 100644 --- a/packages/smooth_app/lib/l10n/app_fr.arb +++ b/packages/smooth_app/lib/l10n/app_fr.arb @@ -1711,6 +1711,27 @@ } } }, + "prices_proofs_list_length_one_page": "{count,plural, =0{Aucune preuve} =1{Une seule preuve} other{Toutes les {count} preuves}}", + "@prices_proofs_list_length_one_page": { + "description": "Number of proofs for one-page result", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "prices_proofs_list_length_many_pages": "{pageSize} preuves les plus récentes (total : {total})", + "@prices_proofs_list_length_many_pages": { + "description": "Number of proofs for one-page result", + "placeholders": { + "pageSize": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, "prices_users_list_length_many_pages": "Top {pageSize} contributeurs (total : {total})", "@prices_users_list_length_many_pages": { "description": "Number of users for one-page result", @@ -1810,6 +1831,14 @@ "@user_search_prices_title": { "description": "User prices: list tile title" }, + "user_search_proofs_title": "Mes preuves", + "@user_search_proofs_title": { + "description": "User proofs: list tile title" + }, + "user_search_proof_title": "Ma preuve", + "@user_search_proof_title": { + "description": "User proof: page title" + }, "user_any_search_prices_title": "Prix d'un contributeur", "@user_any_search_prices_title": { "description": "User prices (everybody except me): list tile title" diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart index 63d179ddd82..8104857b724 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -20,6 +20,7 @@ import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; import 'package:smooth_app/pages/prices/get_prices_model.dart'; import 'package:smooth_app/pages/prices/price_user_button.dart'; import 'package:smooth_app/pages/prices/prices_page.dart'; +import 'package:smooth_app/pages/prices/prices_proofs_page.dart'; import 'package:smooth_app/pages/prices/prices_users_page.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/user_management/login_page.dart'; @@ -227,6 +228,15 @@ class UserPreferencesAccount extends AbstractUserPreferences { CupertinoIcons.money_dollar_circle, myCount: _getPricesCount(owner: ProductQuery.getWriteUser().userId), ), + _getListTile( + appLocalizations.user_search_proofs_title, + () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => const PricesProofsPage(), + ), + ), + Icons.receipt, + ), _getListTile( appLocalizations.all_search_prices_latest_title, () async => Navigator.of(context).push( diff --git a/packages/smooth_app/lib/pages/prices/price_data_widget.dart b/packages/smooth_app/lib/pages/prices/price_data_widget.dart index c16daeece66..142f6d7c1e3 100644 --- a/packages/smooth_app/lib/pages/prices/price_data_widget.dart +++ b/packages/smooth_app/lib/pages/prices/price_data_widget.dart @@ -3,10 +3,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; -import 'package:smooth_app/helpers/launch_url_helper.dart'; import 'package:smooth_app/pages/prices/emoji_helper.dart'; import 'package:smooth_app/pages/prices/get_prices_model.dart'; import 'package:smooth_app/pages/prices/price_button.dart'; +import 'package:smooth_app/pages/prices/price_proof_page.dart'; import 'package:smooth_app/pages/prices/price_user_button.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/query/product_query.dart'; @@ -99,13 +99,14 @@ class PriceDataWidget extends StatelessWidget { if (price.proof?.filePath != null) PriceButton( iconData: Icons.image, - onPressed: () async => LaunchUrlHelper.launchURL( - price.proof! - .getFileUrl( - uriProductHelper: ProductQuery.uriProductHelper, - ) - .toString(), - ), + onPressed: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => PriceProofPage( + price.proof!, + ), + ), + ), // PriceProofPage ), ], ); diff --git a/packages/smooth_app/lib/pages/prices/price_proof_page.dart b/packages/smooth_app/lib/pages/prices/price_proof_page.dart new file mode 100644 index 00000000000..d0ddf0eabdf --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/price_proof_page.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/helpers/launch_url_helper.dart'; +import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/widgets/smooth_app_bar.dart'; +import 'package:smooth_app/widgets/smooth_scaffold.dart'; + +/// Full page display of a proof. +class PriceProofPage extends StatelessWidget { + const PriceProofPage( + this.proof, + ); + + final Proof proof; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final DateFormat dateFormat = + DateFormat.yMd(ProductQuery.getLocaleString()).add_Hms(); + return SmoothScaffold( + appBar: SmoothAppBar( + title: Text(appLocalizations.user_search_proof_title), + subTitle: Text(dateFormat.format(proof.created)), + actions: [ + IconButton( + tooltip: appLocalizations.prices_app_button, + icon: const Icon(Icons.open_in_new), + onPressed: () async => LaunchUrlHelper.launchURL(_getUrl()), + ), + ], + ), + body: Image( + image: NetworkImage(_getUrl()), + fit: BoxFit.cover, + ), + ); + } + + String _getUrl() => proof + .getFileUrl(uriProductHelper: ProductQuery.uriProductHelper) + .toString(); +} diff --git a/packages/smooth_app/lib/pages/prices/prices_proofs_page.dart b/packages/smooth_app/lib/pages/prices/prices_proofs_page.dart new file mode 100644 index 00000000000..fe5f202231c --- /dev/null +++ b/packages/smooth_app/lib/pages/prices/prices_proofs_page.dart @@ -0,0 +1,233 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/widgets/images/smooth_image.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; +import 'package:smooth_app/helpers/launch_url_helper.dart'; +import 'package:smooth_app/pages/prices/price_proof_page.dart'; +import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/widgets/smooth_app_bar.dart'; +import 'package:smooth_app/widgets/smooth_scaffold.dart'; + +/// Page that displays the latest proofs of the current user. +class PricesProofsPage extends StatefulWidget { + const PricesProofsPage(); + + @override + State createState() => _PricesProofsPageState(); +} + +class _PricesProofsPageState extends State { + late final Future> _results = _download(); + + static const int _columns = 3; + static const int _rows = 5; + static const int _pageSize = _columns * _rows; + + @override + Widget build(BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + return SmoothScaffold( + appBar: SmoothAppBar( + centerTitle: false, + leading: const SmoothBackButton(), + title: Text( + appLocalizations.user_search_proofs_title, + ), + actions: [ + IconButton( + tooltip: appLocalizations.prices_app_button, + icon: const Icon(Icons.open_in_new), + onPressed: () async => LaunchUrlHelper.launchURL( + OpenPricesAPIClient.getUri( + path: 'app/dashboard/proofs', + uriHelper: ProductQuery.uriProductHelper, + ).toString(), + ), + ), + ], + ), + body: FutureBuilder>( + future: _results, + builder: ( + final BuildContext context, + final AsyncSnapshot> snapshot, + ) { + if (snapshot.connectionState != ConnectionState.done) { + return const CircularProgressIndicator(); + } + if (snapshot.hasError) { + return Text(snapshot.error!.toString()); + } + // highly improbable + if (!snapshot.hasData) { + return const Text('no data'); + } + if (snapshot.data!.isError) { + return Text(snapshot.data!.error!); + } + final GetProofsResult result = snapshot.data!.value; + // highly improbable + if (result.items == null) { + return const Text('empty list'); + } + final double squareSize = MediaQuery.sizeOf(context).width / _columns; + + final AppLocalizations appLocalizations = + AppLocalizations.of(context); + final String title = result.numberOfPages == 1 + ? appLocalizations.prices_proofs_list_length_one_page( + result.items!.length, + ) + : appLocalizations.prices_proofs_list_length_many_pages( + _pageSize, + result.total!, + ); + return Column( + children: [ + SmoothCard( + child: ListTile( + title: Text(title), + ), + ), + if (result.items!.isNotEmpty) + Expanded( + child: CustomScrollView( + slivers: [ + SliverGrid( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: _columns, + ), + delegate: SliverChildBuilderDelegate( + ( + final BuildContext context, + final int index, + ) { + final Proof proof = result.items![index]; + if (proof.filePath == null) { + // highly improbable + return SizedBox( + width: squareSize, + height: squareSize, + ); + } + return InkWell( + onTap: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + PriceProofPage( + proof, + ), + ), + ), // PriceProofPage + child: _PriceProofImage(proof, + squareSize: squareSize), + ); + }, + addAutomaticKeepAlives: false, + childCount: result.items!.length, + ), + ), + ], + ), + ), + ], + ); + }, + ), + ); + } + + static Future> _download() async { + final User user = ProductQuery.getWriteUser(); + final MaybeError token = + await OpenPricesAPIClient.getAuthenticationToken( + username: user.userId, + password: user.password, + uriHelper: ProductQuery.uriProductHelper, + ); + final String bearerToken = token.value; + + final MaybeError result = + await OpenPricesAPIClient.getProofs( + GetProofsParameters() + ..orderBy = >[ + const OrderBy( + field: GetProofsOrderField.created, + ascending: false, + ), + ] + ..pageSize = _pageSize + ..pageNumber = 1, + uriHelper: ProductQuery.uriProductHelper, + bearerToken: bearerToken, + ); + + await OpenPricesAPIClient.deleteUserSession( + uriHelper: ProductQuery.uriProductHelper, + bearerToken: bearerToken, + ); + + return result; + } +} + +// TODO(monsieurtanuki): reuse whatever will be coded in https://github.com/openfoodfacts/smooth-app/pull/5366 +class _PriceProofImage extends StatelessWidget { + const _PriceProofImage( + this.proof, { + required this.squareSize, + }); + + final Proof proof; + final double squareSize; + + @override + Widget build(BuildContext context) { + final DateFormat dateFormat = + DateFormat.yMd(ProductQuery.getLocaleString()); + final String date = dateFormat.format(proof.created); + return Stack( + children: [ + SmoothImage( + width: squareSize, + height: squareSize, + imageProvider: NetworkImage( + proof + .getFileUrl( + uriProductHelper: ProductQuery.uriProductHelper, + ) + .toString(), + ), + rounded: false, + ), + SizedBox( + width: squareSize, + height: squareSize, + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.all(SMALL_SPACE), + child: Container( + height: VERY_LARGE_SPACE, + color: Colors.white.withAlpha(128), + child: Center( + child: AutoSizeText( + date, + maxLines: 1, + ), + ), + ), + ), + ), + ), + ], + ); + } +}