From 64d38f17dafb715499857fc4611bb3f25cdbb524 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Tue, 18 Jun 2024 13:42:39 +0200 Subject: [PATCH] feat: Improve photo gallery accessibility + internationalization (#5366) * Photo gallery: accessibility improvements + date format translatable * Fix typos * Use `DateFormat.ymd` instead * Card based layout --- packages/smooth_app/lib/l10n/app_en.arb | 18 +++ .../product_image_gallery_other_view.dart | 32 +++-- .../lib/pages/image/product_image_widget.dart | 131 +++++++++++++----- 3 files changed, 137 insertions(+), 44 deletions(-) diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 365f0483080..6cd64568fcf 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -750,6 +750,24 @@ "@product_refreshed": { "description": "Confirmation, that the product data refresh is done" }, + "product_image_accessibility_label": "Image taken on {date}", + "@product_image_accessibility_label": { + "placeholders": { + "date": { + "type": "String", + "description": "The date of picture (in localized format for YYYY-MM-DD)" + } + } + }, + "product_image_outdated_accessibility_label": "Image taken on {date}. This image may be outdated", + "@product_image_outdated_accessibility_label": { + "placeholders": { + "date": { + "type": "String", + "description": "The date of picture (in localized format for YYYY-MM-DD)" + } + } + }, "homepage_main_card_logo_description": "Welcome to Open Food Facts", "@homepage_main_card_logo_description": { "description": "Description for accessibility of the Open Food Facts logo on the homepage" diff --git a/packages/smooth_app/lib/pages/image/product_image_gallery_other_view.dart b/packages/smooth_app/lib/pages/image/product_image_gallery_other_view.dart index 677a5506017..3f40df632f7 100644 --- a/packages/smooth_app/lib/pages/image/product_image_gallery_other_view.dart +++ b/packages/smooth_app/lib/pages/image/product_image_gallery_other_view.dart @@ -4,6 +4,7 @@ import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/fetched_product.dart'; import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/pages/image/product_image_other_page.dart'; import 'package:smooth_app/pages/image/product_image_widget.dart'; @@ -116,20 +117,27 @@ class _RawGridGallery extends StatelessWidget { // order by descending ids index = rawImages.length - 1 - index; final ProductImage productImage = rawImages[index]; - return InkWell( - onTap: () async => Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => ProductImageOtherPage( - product, - int.parse(productImage.imgid!), + return Padding( + padding: EdgeInsetsDirectional.only( + start: VERY_SMALL_SPACE, + end: index % _columns == 0 ? VERY_SMALL_SPACE : 0.0, + bottom: VERY_SMALL_SPACE, + ), + child: InkWell( + onTap: () async => Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ProductImageOtherPage( + product, + int.parse(productImage.imgid!), + ), ), ), - ), - child: ProductImageWidget( - productImage: productImage, - barcode: product.barcode!, - squareSize: squareSize, + child: ProductImageWidget( + productImage: productImage, + barcode: product.barcode!, + squareSize: squareSize, + ), ), ); }, diff --git a/packages/smooth_app/lib/pages/image/product_image_widget.dart b/packages/smooth_app/lib/pages/image/product_image_widget.dart index 9774a555b7b..c1448413b52 100644 --- a/packages/smooth_app/lib/pages/image/product_image_widget.dart +++ b/packages/smooth_app/lib/pages/image/product_image_widget.dart @@ -1,13 +1,17 @@ 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_card.dart'; import 'package:smooth_app/query/product_query.dart'; +import 'package:smooth_app/resources/app_icons.dart'; +import 'package:smooth_app/themes/smooth_theme_colors.dart'; /// Displays a product image thumbnail with the upload date on top. -class ProductImageWidget extends StatelessWidget { +class ProductImageWidget extends StatefulWidget { const ProductImageWidget({ required this.productImage, required this.barcode, @@ -18,54 +22,117 @@ class ProductImageWidget extends StatelessWidget { final String barcode; final double squareSize; - static final DateFormat _dateFormat = DateFormat('yyyy-MM-dd'); + @override + State createState() => _ProductImageWidgetState(); +} + +class _ProductImageWidgetState extends State { + @override + void initState() { + super.initState(); + _loadImagePalette(); + } + + Future _loadImagePalette() async { + final ColorScheme palette = await ColorScheme.fromImageProvider( + provider: NetworkImage(widget.productImage.getUrl( + widget.barcode, + uriHelper: ProductQuery.uriProductHelper, + ))); + + setState(() { + backgroundColor = palette.primaryContainer; + darkBackground = backgroundColor!.computeLuminance() < 0.5; + }); + } + + Color? backgroundColor; + bool? darkBackground; @override Widget build(BuildContext context) { + final SmoothColorsThemeExtension colors = + Theme.of(context).extension()!; + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final DateFormat dateFormat = + DateFormat.yMd(ProductQuery.getLanguage().offTag); + + darkBackground = darkBackground ?? true; + final Widget image = SmoothImage( - width: squareSize, - height: squareSize, + width: widget.squareSize, + height: widget.squareSize, imageProvider: NetworkImage( - productImage.getUrl( - barcode, + widget.productImage.getUrl( + widget.barcode, uriHelper: ProductQuery.uriProductHelper, ), ), rounded: false, ); - final DateTime? uploaded = productImage.uploaded; + final DateTime? uploaded = widget.productImage.uploaded; if (uploaded == null) { return image; } - final DateTime now = DateTime.now(); - final String date = _dateFormat.format(uploaded); - final bool expired = now.difference(uploaded).inDays > 365; - return Stack( - children: [ - image, - 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: expired - ? Colors.red.withAlpha(128) - : Colors.white.withAlpha(128), - child: Center( - child: AutoSizeText( - date, - maxLines: 1, + final bool expired = DateTime.now().difference(uploaded).inDays > 365; + final String date = dateFormat.format(uploaded); + + return Semantics( + label: expired + ? appLocalizations.product_image_outdated_accessibility_label(date) + : appLocalizations.product_image_accessibility_label(date), + excludeSemantics: true, + button: true, + child: SmoothCard( + padding: EdgeInsets.zero, + color: backgroundColor ?? colors.primaryBlack, + borderRadius: ANGULAR_BORDER_RADIUS, + margin: EdgeInsets.zero, + child: ClipRRect( + borderRadius: ANGULAR_BORDER_RADIUS, + child: Column( + children: [ + Expanded( + child: image, + ), + SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: SMALL_SPACE, + vertical: VERY_SMALL_SPACE, + ), + child: Stack( + children: [ + Center( + child: AutoSizeText( + date, + maxLines: 1, + style: TextStyle( + color: darkBackground! + ? Colors.white + : colors.primaryDark, + ), + ), + ), + if (expired) + Positioned.directional( + end: 0.0, + height: 20.0, + textDirection: Directionality.of(context), + child: Outdated( + size: 18.0, + color: colors.red, + ), + ), + ], ), ), - ), - ), + ) + ], ), ), - ], + ), ); } }