diff --git a/packages/smooth_app/lib/background/background_task_details.dart b/packages/smooth_app/lib/background/background_task_details.dart index dbf49437b36..e2fa4c9f969 100644 --- a/packages/smooth_app/lib/background/background_task_details.dart +++ b/packages/smooth_app/lib/background/background_task_details.dart @@ -126,6 +126,8 @@ class BackgroundTaskDetails extends BackgroundTaskBarcode { return result; } + static const String _invalidUserError = 'invalid_user_id_and_password'; + /// Uploads the product changes. @override Future upload() async { @@ -145,7 +147,19 @@ class BackgroundTaskDetails extends BackgroundTaskBarcode { ); if (result.status != ProductResultV3.statusSuccess && result.status != ProductResultV3.statusWarning) { - throw Exception('Could not save product - ${result.errors}'); + bool isInvalidUser = false; + if (result.errors != null) { + for (final ProductResultFieldAnswer answer in result.errors!) { + if (answer.message?.id == _invalidUserError) { + isInvalidUser = true; + } + } + } + throw Exception( + 'Could not save product' + ' - ' + '${result.errors}${isInvalidUser ? _getIncompleteUserData() : ''}', + ); } return; } @@ -157,7 +171,36 @@ class BackgroundTaskDetails extends BackgroundTaskBarcode { uriHelper: uriProductHelper, ); if (status.status != 1) { - throw Exception('Could not save product - ${status.error}'); + bool isInvalidUser = false; + if (status.error != null) { + if (status.error!.contains(_invalidUserError)) { + isInvalidUser = true; + } + } + throw Exception( + 'Could not save product' + ' - ' + '${status.error}${isInvalidUser ? _getIncompleteUserData() : ''}', + ); + } + } + + String _getIncompleteUserData() { + final User user = getUser(); + final StringBuffer result = StringBuffer(); + result.write(' [user:'); + result.write(user.userId); + final int length = user.password.length; + result.write(' ('); + if (length >= 8) { + result.write(user.password.substring(0, 2)); + result.write('*' * (length - 4)); + result.write(user.password.substring(length - 2)); + } else { + result.write('passwordLength:$length'); } + result.write(')'); + result.write('] '); + return result.toString(); } } diff --git a/packages/smooth_app/lib/cards/category_cards/abstract_cache.dart b/packages/smooth_app/lib/cards/category_cards/abstract_cache.dart index 24699a6e12d..38cb4bd4692 100644 --- a/packages/smooth_app/lib/cards/category_cards/abstract_cache.dart +++ b/packages/smooth_app/lib/cards/category_cards/abstract_cache.dart @@ -11,7 +11,6 @@ abstract class AbstractCache extends StatelessWidget { this.iconUrl, { this.width, this.height, - this.displayAssetWhileWaiting = true, }); /// Returns the best cache possibility: none, svg or png/jpeg @@ -33,7 +32,6 @@ abstract class AbstractCache extends StatelessWidget { final String? iconUrl; final double? width; final double? height; - final bool displayAssetWhileWaiting; /// Returns a list of possible related cached filenames. @protected @@ -75,11 +73,4 @@ abstract class AbstractCache extends StatelessWidget { size: width ?? height, color: Colors.red, ); - - @protected - Widget getCircularProgressIndicator() => SizedBox( - width: width ?? height, - height: height ?? width, - child: const CircularProgressIndicator.adaptive(), - ); } diff --git a/packages/smooth_app/lib/cards/category_cards/raster_cache.dart b/packages/smooth_app/lib/cards/category_cards/raster_cache.dart index 8a9fe9ed612..312ca179244 100644 --- a/packages/smooth_app/lib/cards/category_cards/raster_cache.dart +++ b/packages/smooth_app/lib/cards/category_cards/raster_cache.dart @@ -9,7 +9,6 @@ class RasterCache extends AbstractCache { super.iconUrl, { super.width, super.height, - super.displayAssetWhileWaiting = true, }); @override @@ -31,16 +30,14 @@ class RasterCache extends AbstractCache { if (loadingProgress == null) { return child; } - return displayAssetWhileWaiting - ? RasterAsyncAsset( - AssetCacheHelper( - fullFilenames, - iconUrl!, - width: width, - height: height, - ), - ) - : getCircularProgressIndicator(); + return RasterAsyncAsset( + AssetCacheHelper( + fullFilenames, + iconUrl!, + width: width, + height: height, + ), + ); }, ); } diff --git a/packages/smooth_app/lib/cards/category_cards/svg_cache.dart b/packages/smooth_app/lib/cards/category_cards/svg_cache.dart index 728fd0ee128..6ccfbcde84c 100644 --- a/packages/smooth_app/lib/cards/category_cards/svg_cache.dart +++ b/packages/smooth_app/lib/cards/category_cards/svg_cache.dart @@ -1,11 +1,8 @@ -import 'dart:ui' as ui; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:smooth_app/cards/category_cards/abstract_cache.dart'; import 'package:smooth_app/cards/category_cards/asset_cache_helper.dart'; -import 'package:smooth_app/cards/category_cards/svg_async_asset.dart'; +import 'package:smooth_app/cards/category_cards/svg_safe_network.dart'; /// Widget that displays a svg from network (and cache while waiting). class SvgCache extends AbstractCache { @@ -14,7 +11,6 @@ class SvgCache extends AbstractCache { super.width, super.height, this.color, - super.displayAssetWhileWaiting = true, }); final Color? color; @@ -55,26 +51,14 @@ class SvgCache extends AbstractCache { ? Colors.white : Colors.black; } - return SvgPicture.network( - iconUrl!, - colorFilter: forcedColor == null - ? null - : ui.ColorFilter.mode(forcedColor, ui.BlendMode.srcIn), - width: width, - height: height, - fit: BoxFit.contain, - semanticsLabel: getSemanticsLabel(context, iconUrl!), - placeholderBuilder: (BuildContext context) => displayAssetWhileWaiting - ? SvgAsyncAsset( - AssetCacheHelper( - cachedFilenames, - iconUrl!, - width: width, - height: height, - color: forcedColor, - ), - ) - : getCircularProgressIndicator(), + return SvgSafeNetwork( + AssetCacheHelper( + cachedFilenames, + iconUrl!, + width: width, + height: height, + color: forcedColor, + ), ); } diff --git a/packages/smooth_app/lib/cards/category_cards/svg_safe_network.dart b/packages/smooth_app/lib/cards/category_cards/svg_safe_network.dart new file mode 100644 index 00000000000..98cf6eeabc8 --- /dev/null +++ b/packages/smooth_app/lib/cards/category_cards/svg_safe_network.dart @@ -0,0 +1,79 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:http/http.dart' as http; +import 'package:smooth_app/cards/category_cards/asset_cache_helper.dart'; +import 'package:smooth_app/cards/category_cards/svg_async_asset.dart'; +import 'package:smooth_app/cards/category_cards/svg_cache.dart'; +import 'package:smooth_app/services/smooth_services.dart'; + +/// Widget with async load of SVG network file. +/// +/// We could use SvgPicture.network, but it sends tons of errors if there in no +/// internet connection. That's why we download the data ourselves. +class SvgSafeNetwork extends StatefulWidget { + const SvgSafeNetwork(this.helper); + + final AssetCacheHelper helper; + + @override + State createState() => _SvgSafeNetworkState(); +} + +class _SvgSafeNetworkState extends State { + late final Future _loading = _load(); + + String get _url => widget.helper.url; + + Future _load() async { + String? cached = _networkCache[_url]; + if (cached != null) { + return cached; + } + final http.Response response = await http.get(Uri.parse(_url)); + _networkCache[_url] = cached = response.body; + return cached; + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: _loading, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data != null) { + return SvgPicture.string( + snapshot.data!, + width: widget.helper.width, + height: widget.helper.height, + colorFilter: widget.helper.color == null + ? null + : ui.ColorFilter.mode( + widget.helper.color!, + ui.BlendMode.srcIn, + ), + fit: BoxFit.contain, + semanticsLabel: SvgCache.getSemanticsLabel(context, _url), + placeholderBuilder: (BuildContext context) => + SvgAsyncAsset(widget.helper), + ); + } + } + if (snapshot.error != null) { + final bool serverOrConnectionIssue = snapshot.error.toString() == + "Failed host lookup: 'static.openfoodfacts.org'"; + if (!serverOrConnectionIssue) { + Logs.e( + 'Could not download "$_url"', + ex: snapshot.error, + ); + } + } + return SvgAsyncAsset(widget.helper); + }, + ); +} + +/// Network cache, with url as key and SVG data as value. +Map _networkCache = {}; diff --git a/packages/smooth_app/lib/generic_lib/buttons/smooth_large_button_with_icon.dart b/packages/smooth_app/lib/generic_lib/buttons/smooth_large_button_with_icon.dart index edda7ba2172..7cdbc741ed3 100644 --- a/packages/smooth_app/lib/generic_lib/buttons/smooth_large_button_with_icon.dart +++ b/packages/smooth_app/lib/generic_lib/buttons/smooth_large_button_with_icon.dart @@ -36,7 +36,7 @@ class SmoothLargeButtonWithIcon extends StatelessWidget { final ThemeData themeData = Theme.of(context); TextStyle style = textStyle ?? themeData.textTheme.bodyMedium!; - if (style.color == null) { + if (foregroundColor != null) { style = style.copyWith(color: _getForegroundColor(themeData)); } diff --git a/packages/smooth_app/lib/pages/product/add_new_product_helper.dart b/packages/smooth_app/lib/pages/product/add_new_product_helper.dart index a7c587491d6..a04eca761a8 100644 --- a/packages/smooth_app/lib/pages/product/add_new_product_helper.dart +++ b/packages/smooth_app/lib/pages/product/add_new_product_helper.dart @@ -72,12 +72,14 @@ class AddNewProductButton extends StatelessWidget { this.iconData, this.onPressed, { required this.done, + this.trailingIconData, }); final String label; final IconData iconData; final VoidCallback? onPressed; final bool done; + final IconData? trailingIconData; static const IconData doneIconData = Icons.check; static const IconData todoIconData = Icons.add; @@ -95,7 +97,7 @@ class AddNewProductButton extends StatelessWidget { text: label, icon: iconData, onPressed: onPressed, - trailing: Icons.edit, + trailing: trailingIconData ?? Icons.edit, backgroundColor: onPressed == null ? (dark ? darkGrey : lightGrey) : done @@ -144,6 +146,7 @@ class AddNewProductEditorButton extends StatelessWidget { isLoggedInMandatory: isLoggedInMandatory, ), done: done, + trailingIconData: done ? AddNewProductButton.doneIconData : null, ); } }