Skip to content

Commit

Permalink
Photo gallery with tabs (openfoodfacts#5872)
Browse files Browse the repository at this point in the history
  • Loading branch information
g123k authored Nov 18, 2024
1 parent c102a0e commit 2bffec1
Show file tree
Hide file tree
Showing 17 changed files with 1,287 additions and 483 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/database/transient_file.dart';
import 'package:smooth_app/helpers/image_field_extension.dart';
import 'package:smooth_app/pages/image_crop_page.dart';
import 'package:smooth_app/pages/product/product_image_gallery_view.dart';
import 'package:smooth_app/pages/product/gallery_view/product_image_gallery_view.dart';
import 'package:smooth_app/query/product_query.dart';

/// Displays a product image in the carousel: access to gallery, or new image.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:smooth_app/cards/product_cards/smooth_product_image.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/helpers/extension_on_text_helper.dart';
import 'package:smooth_app/helpers/product_cards_helper.dart';
import 'package:smooth_app/pages/product/product_image_gallery_view.dart';
import 'package:smooth_app/pages/product/gallery_view/product_image_gallery_view.dart';

class ProductTitleCard extends StatelessWidget {
const ProductTitleCard(
Expand Down Expand Up @@ -69,7 +69,7 @@ class ProductTitleCard extends StatelessWidget {
verticalOffset: imageSize.width / 2,
preferBelow: true,
),
child: ProductPicture(
child: ProductPicture.fromProduct(
product: product,
imageField: ImageField.FRONT,
fallbackUrl: product.imageFrontUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,63 @@ import 'package:smooth_app/themes/smooth_theme_colors.dart';
import 'package:smooth_app/themes/theme_provider.dart';

class ProductPicture extends StatefulWidget {
ProductPicture({
ProductPicture.fromProduct({
required Product product,
required ImageField imageField,
required Size size,
String? fallbackUrl,
VoidCallback? onTap,
String? heroTag,
bool? showObsoleteIcon,
BorderRadius? borderRadius,
double? imageFoundBorder,
double? imageNotFoundBorder,
TextStyle? errorTextStyle,
}) : this._(
transientFile: null,
product: product,
imageField: imageField,
size: size,
fallbackUrl: fallbackUrl,
heroTag: heroTag,
onTap: onTap,
borderRadius: borderRadius,
imageFoundBorder: imageFoundBorder ?? 0.0,
imageNotFoundBorder: imageNotFoundBorder ?? 0.0,
errorTextStyle: errorTextStyle,
showObsoleteIcon: showObsoleteIcon ?? false,
);

ProductPicture.fromTransientFile({
required TransientFile transientFile,
required Size size,
String? fallbackUrl,
VoidCallback? onTap,
String? heroTag,
bool? showObsoleteIcon,
BorderRadius? borderRadius,
double? imageFoundBorder,
double? imageNotFoundBorder,
TextStyle? errorTextStyle,
}) : this._(
transientFile: transientFile,
product: null,
imageField: null,
size: size,
fallbackUrl: fallbackUrl,
heroTag: heroTag,
onTap: onTap,
borderRadius: borderRadius,
imageFoundBorder: imageFoundBorder ?? 0.0,
imageNotFoundBorder: imageNotFoundBorder ?? 0.0,
errorTextStyle: errorTextStyle,
showObsoleteIcon: showObsoleteIcon ?? false,
);

ProductPicture._({
required this.product,
required this.imageField,
required this.transientFile,
required this.size,
this.fallbackUrl,
this.heroTag,
Expand All @@ -35,8 +89,9 @@ class ProductPicture extends StatefulWidget {
assert(heroTag == null || heroTag.isNotEmpty),
assert(size.width >= 0.0 && size.height >= 0.0);

final Product product;
final ImageField imageField;
final Product? product;
final ImageField? imageField;
final TransientFile? transientFile;
final Size size;
final String? fallbackUrl;
final VoidCallback? onTap;
Expand All @@ -63,8 +118,9 @@ class _ProductPictureState extends State<ProductPicture> {

@override
Widget build(BuildContext context) {
final (ImageProvider, bool)? imageProvider = _getImageProvider(
final (ImageProvider?, bool)? imageProvider = _getImageProvider(
widget.product,
widget.transientFile,
);

final Widget? inkWell = widget.onTap != null
Expand All @@ -91,9 +147,9 @@ class _ProductPictureState extends State<ProductPicture> {
border: widget.imageNotFoundBorder,
child: inkWell,
);
} else if (imageProvider != null) {
} else if (imageProvider?.$1 != null) {
child = _ProductPictureWithImageProvider(
imageProvider: imageProvider.$1,
imageProvider: imageProvider!.$1!,
outdated: imageProvider.$2,
heroTag: widget.heroTag,
size: widget.size,
Expand Down Expand Up @@ -143,16 +199,24 @@ class _ProductPictureState extends State<ProductPicture> {
/// Returns the image provider for the product.
/// If this is a [TransientFile], the boolean indicates whether the image is
/// outdated or not.
(ImageProvider, bool)? _getImageProvider(Product product) {
final TransientFile transientFile = TransientFile.fromProduct(
product,
widget.imageField,
(ImageProvider?, bool)? _getImageProvider(
Product? product,
TransientFile? transientFile,
) {
if (transientFile != null) {
return (transientFile.getImageProvider(), transientFile.expired);
}

final TransientFile productTransientFile = TransientFile.fromProduct(
product!,
widget.imageField!,
ProductQuery.getLanguage(),
);
final ImageProvider? imageProvider = transientFile.getImageProvider();
final ImageProvider? imageProvider =
productTransientFile.getImageProvider();

if (imageProvider != null) {
return (imageProvider, transientFile.expired);
return (imageProvider, productTransientFile.expired);
} else if (widget.fallbackUrl?.isNotEmpty == true) {
return (NetworkImage(widget.fallbackUrl!), false);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class LanguageSelector extends StatelessWidget {
/// Product from which we can extract the languages that matter.
final Product? product;

static const Languages _languages = Languages();
static final Languages _languages = Languages();

@override
Widget build(BuildContext context) {
Expand All @@ -61,7 +61,7 @@ class LanguageSelector extends StatelessWidget {
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
final OpenFoodFactsLanguage? language = await _openLanguageSelector(
final OpenFoodFactsLanguage? language = await openLanguageSelector(
context,
selectedLanguages: selectedLanguages,
languagePriority: languagePriority,
Expand Down Expand Up @@ -117,7 +117,8 @@ class LanguageSelector extends StatelessWidget {
/// Returns the language selected by the user.
///
/// [selectedLanguages] have a specific "more important" display.
Future<OpenFoodFactsLanguage?> _openLanguageSelector(
// TODO(g123k): Improve the language selector to usable without the Widget
static Future<OpenFoodFactsLanguage?> openLanguageSelector(
final BuildContext context, {
final Iterable<OpenFoodFactsLanguage>? selectedLanguages,
required final LanguagePriority languagePriority,
Expand Down Expand Up @@ -212,7 +213,7 @@ class LanguageSelector extends StatelessWidget {
);
}

String _getCompleteName(
static String _getCompleteName(
final OpenFoodFactsLanguage language,
) {
final String nameInLanguage = _languages.getNameInLanguage(language);
Expand Down
32 changes: 32 additions & 0 deletions packages/smooth_app/lib/helpers/border_radius_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:flutter/widgets.dart';

class BorderRadiusHelper {
BorderRadiusHelper._();

/// [InkWell] only supports [BorderRadius].
/// This helps to create a [BorderRadius] from a [BorderRadiusDirectional].
static BorderRadius fromDirectional({
required BuildContext context,
Radius? topStart,
Radius? topEnd,
Radius? bottomStart,
Radius? bottomEnd,
}) {
final TextDirection textDirection = Directionality.of(context);

return BorderRadius.only(
topLeft: textDirection == TextDirection.ltr
? topStart ?? Radius.zero
: topEnd ?? Radius.zero,
topRight: textDirection == TextDirection.ltr
? topEnd ?? Radius.zero
: topStart ?? Radius.zero,
bottomLeft: textDirection == TextDirection.ltr
? bottomStart ?? Radius.zero
: bottomEnd ?? Radius.zero,
bottomRight: textDirection == TextDirection.ltr
? bottomEnd ?? Radius.zero
: bottomStart ?? Radius.zero,
);
}
}
43 changes: 43 additions & 0 deletions packages/smooth_app/lib/helpers/ui_helpers.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
Expand Down Expand Up @@ -45,3 +47,44 @@ Color? getTextColorFromKnowledgePanelElementEvaluation(Evaluation evaluation) {
return DARK_GREEN_COLOR;
}
}

extension BoxConstraintsExtension on BoxConstraints {
double get minSide => math.min(maxWidth, maxHeight);
}

extension StatelessWidgetExtension on StatelessWidget {
void onNextFrame(VoidCallback callback) {
WidgetsBinding.instance.addPostFrameCallback((_) {
callback();
});
}
}

extension StateExtension on State {
void onNextFrame(VoidCallback callback) {
WidgetsBinding.instance.addPostFrameCallback((_) {
callback();
});
}
}

extension ScrollMetricsExtension on ScrollMetrics {
double get page => extentBefore / extentInside;

bool get hasScrolled => extentBefore % extentInside != 0;
}

extension ScrollControllerExtension on ScrollController {
void jumpBy(double offset) => jumpTo(position.pixels + offset);

void animateBy(
double offset, {
required Duration duration,
required Curve curve,
}) =>
animateTo(
position.pixels + offset,
duration: duration,
curve: curve,
);
}
4 changes: 4 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3216,5 +3216,9 @@
"product_page_action_bar_item_disable": "Disable action",
"@product_page_action_bar_item_disable": {
"description": "Accessibility label to disable action (= make it invisible)"
},
"product_add_a_language": "Add a language",
"@product_add_a_language": {
"description": "Button to add a language (eg: for photos) to a product"
}
}
Loading

0 comments on commit 2bffec1

Please sign in to comment.