From 1b1d5d006d5387be7d1a1f253a53ad67744545b6 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Mon, 4 May 2026 22:10:23 +0200 Subject: [PATCH] feat: enhance error handling; implement copyable SnackBar for user messages across various screens --- flutter/lib/core/api/api_error_mapper.dart | 18 +++++++++++++++++- .../presentation/admin_aliases_panel.dart | 5 +++-- .../presentation/admin_database_panel.dart | 3 ++- .../admin_pending_products_panel.dart | 4 ++-- .../presentation/admin_products_panel.dart | 16 ++++++++-------- .../presentation/consume_inventory_screen.dart | 3 ++- .../presentation/create_inventory_screen.dart | 7 ++++--- .../presentation/inventory_detail_screen.dart | 3 ++- .../presentation/inventory_edit_screen.dart | 3 ++- .../presentation/swipeable_inventory_tile.dart | 5 +++-- .../presentation/meal_plan_screen.dart | 4 ++-- .../pantry/presentation/pantry_screen.dart | 7 ++++--- .../profile/presentation/profile_screen.dart | 3 ++- .../presentation/recipe_detail_screen.dart | 7 ++++--- flutter/lib/main.dart | 3 +++ 15 files changed, 60 insertions(+), 31 deletions(-) diff --git a/flutter/lib/core/api/api_error_mapper.dart b/flutter/lib/core/api/api_error_mapper.dart index 019df5f7..3e949a19 100644 --- a/flutter/lib/core/api/api_error_mapper.dart +++ b/flutter/lib/core/api/api_error_mapper.dart @@ -1,4 +1,5 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../l10n/l10n.dart'; import 'api_exception.dart'; @@ -21,3 +22,18 @@ String mapErrorToUserMessage(Object error, BuildContext context) { } return l10n.unexpectedError; } + +SnackBar buildCopyableErrorSnackBar(BuildContext context, String message) { + return SnackBar( + content: Text(message), + action: SnackBarAction( + label: context.l10n.errorDialogCopy, + onPressed: () { + Clipboard.setData(ClipboardData(text: message)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.errorDialogCopied)), + ); + }, + ), + ); +} diff --git a/flutter/lib/features/admin/presentation/admin_aliases_panel.dart b/flutter/lib/features/admin/presentation/admin_aliases_panel.dart index 310fd9f5..264579c8 100644 --- a/flutter/lib/features/admin/presentation/admin_aliases_panel.dart +++ b/flutter/lib/features/admin/presentation/admin_aliases_panel.dart @@ -94,7 +94,7 @@ class _AdminAliasesPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } finally { if (mounted) setState(() => _isSaving = false); @@ -133,7 +133,7 @@ class _AdminAliasesPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -259,3 +259,4 @@ class _AdminAliasesPanelState extends ConsumerState { return content; } } + diff --git a/flutter/lib/features/admin/presentation/admin_database_panel.dart b/flutter/lib/features/admin/presentation/admin_database_panel.dart index 4185f252..c43664a1 100644 --- a/flutter/lib/features/admin/presentation/admin_database_panel.dart +++ b/flutter/lib/features/admin/presentation/admin_database_panel.dart @@ -33,7 +33,7 @@ class _AdminDatabasePanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } finally { if (mounted) setState(() => _isRefreshingCategories = false); @@ -180,3 +180,4 @@ class _AdminDatabasePanelState extends ConsumerState { ); } } + diff --git a/flutter/lib/features/admin/presentation/admin_pending_products_panel.dart b/flutter/lib/features/admin/presentation/admin_pending_products_panel.dart index f26f425f..24f6c704 100644 --- a/flutter/lib/features/admin/presentation/admin_pending_products_panel.dart +++ b/flutter/lib/features/admin/presentation/admin_pending_products_panel.dart @@ -57,7 +57,7 @@ class _AdminPendingProductsPanelState } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } finally { if (mounted) setState(() => _processingId = null); @@ -152,4 +152,4 @@ class _AdminPendingProductsPanelState ], ); } -} \ No newline at end of file +} diff --git a/flutter/lib/features/admin/presentation/admin_products_panel.dart b/flutter/lib/features/admin/presentation/admin_products_panel.dart index 4ef20025..4bbf7a61 100644 --- a/flutter/lib/features/admin/presentation/admin_products_panel.dart +++ b/flutter/lib/features/admin/presentation/admin_products_panel.dart @@ -132,7 +132,7 @@ class _AdminProductsPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } finally { if (mounted) setState(() => _isApplying = false); @@ -181,7 +181,7 @@ class _AdminProductsPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } finally { if (mounted) setState(() => _isAiRunning = false); @@ -321,7 +321,7 @@ class _AdminProductsPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -358,7 +358,7 @@ class _AdminProductsPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -380,7 +380,7 @@ class _AdminProductsPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } finally { if (mounted) setState(() => _isApplying = false); @@ -399,7 +399,7 @@ class _AdminProductsPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -435,7 +435,7 @@ class _AdminProductsPanelState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); setState(() => _rowCategorySaving.remove(product.id)); } @@ -811,4 +811,4 @@ class _AiApplyDialogState extends State<_AiApplyDialog> { ], ); } -} \ No newline at end of file +} diff --git a/flutter/lib/features/inventory/presentation/consume_inventory_screen.dart b/flutter/lib/features/inventory/presentation/consume_inventory_screen.dart index 119ba48b..b3a84cea 100644 --- a/flutter/lib/features/inventory/presentation/consume_inventory_screen.dart +++ b/flutter/lib/features/inventory/presentation/consume_inventory_screen.dart @@ -52,7 +52,7 @@ class _ConsumeInventoryScreenState } catch (e) { if (mounted) { ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context)))); + .showSnackBar(buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)))); } } finally { if (mounted) setState(() => _saving = false); @@ -139,3 +139,4 @@ class _ConsumeInventoryScreenState ); } } + diff --git a/flutter/lib/features/inventory/presentation/create_inventory_screen.dart b/flutter/lib/features/inventory/presentation/create_inventory_screen.dart index 8d38bf48..d0498d98 100644 --- a/flutter/lib/features/inventory/presentation/create_inventory_screen.dart +++ b/flutter/lib/features/inventory/presentation/create_inventory_screen.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -76,7 +76,7 @@ class _CreateInventoryScreenState if (mounted) setState(() => _loadingProducts = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -136,7 +136,7 @@ class _CreateInventoryScreenState } catch (e) { if (mounted) { ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context)))); + .showSnackBar(buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)))); } } finally { if (mounted) setState(() => _saving = false); @@ -326,3 +326,4 @@ class _CreateInventoryScreenState ); } } + diff --git a/flutter/lib/features/inventory/presentation/inventory_detail_screen.dart b/flutter/lib/features/inventory/presentation/inventory_detail_screen.dart index f761dd6e..a05277d7 100644 --- a/flutter/lib/features/inventory/presentation/inventory_detail_screen.dart +++ b/flutter/lib/features/inventory/presentation/inventory_detail_screen.dart @@ -122,7 +122,7 @@ class _DeleteButton extends ConsumerWidget { } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -159,3 +159,4 @@ class _InfoRow extends StatelessWidget { ); } } + diff --git a/flutter/lib/features/inventory/presentation/inventory_edit_screen.dart b/flutter/lib/features/inventory/presentation/inventory_edit_screen.dart index fa64f565..3bf26a6b 100644 --- a/flutter/lib/features/inventory/presentation/inventory_edit_screen.dart +++ b/flutter/lib/features/inventory/presentation/inventory_edit_screen.dart @@ -111,7 +111,7 @@ class _InventoryEditScreenState extends ConsumerState { } catch (e) { if (mounted) { ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context)))); + .showSnackBar(buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)))); } } finally { if (mounted) setState(() => _saving = false); @@ -297,3 +297,4 @@ class _InventoryEditScreenState extends ConsumerState { ); } } + diff --git a/flutter/lib/features/inventory/presentation/swipeable_inventory_tile.dart b/flutter/lib/features/inventory/presentation/swipeable_inventory_tile.dart index c8bae50f..7123da55 100644 --- a/flutter/lib/features/inventory/presentation/swipeable_inventory_tile.dart +++ b/flutter/lib/features/inventory/presentation/swipeable_inventory_tile.dart @@ -122,7 +122,7 @@ class _SwipeableInventoryTileState } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } finally { if (mounted) setState(() => _acting = false); @@ -372,7 +372,7 @@ class _DeleteButton extends ConsumerWidget { ref.invalidate(inventoryProvider); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -381,3 +381,4 @@ class _DeleteButton extends ConsumerWidget { ); } } + diff --git a/flutter/lib/features/meal_plan/presentation/meal_plan_screen.dart b/flutter/lib/features/meal_plan/presentation/meal_plan_screen.dart index c4af3bcb..7f760cec 100644 --- a/flutter/lib/features/meal_plan/presentation/meal_plan_screen.dart +++ b/flutter/lib/features/meal_plan/presentation/meal_plan_screen.dart @@ -56,7 +56,7 @@ class _MealPlanScreenState extends ConsumerState { } catch (error) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(error, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context))), ); } finally { if (mounted) { @@ -593,4 +593,4 @@ class _EnrichedShoppingItem { } String _formatQuantity(double value) => formatQuantity(value); -} \ No newline at end of file +} diff --git a/flutter/lib/features/pantry/presentation/pantry_screen.dart b/flutter/lib/features/pantry/presentation/pantry_screen.dart index 2e8e4091..a2c1f991 100644 --- a/flutter/lib/features/pantry/presentation/pantry_screen.dart +++ b/flutter/lib/features/pantry/presentation/pantry_screen.dart @@ -166,7 +166,7 @@ class _PantryScreenState extends ConsumerState { _logger.severe('Failed to add item to inventory: $error'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(error, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context))), ); } } @@ -185,7 +185,7 @@ class _PantryScreenState extends ConsumerState { _logger.severe('Failed to add pantry item: $error'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(error, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context))), ); } finally { if (mounted) setState(() => _isSubmitting = false); @@ -221,7 +221,7 @@ class _PantryScreenState extends ConsumerState { _logger.severe('Failed to remove pantry item: $error'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(error, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context))), ); } } @@ -419,3 +419,4 @@ class _PantryScreenState extends ConsumerState { ); } } + diff --git a/flutter/lib/features/profile/presentation/profile_screen.dart b/flutter/lib/features/profile/presentation/profile_screen.dart index ebd277c9..2145ac6f 100644 --- a/flutter/lib/features/profile/presentation/profile_screen.dart +++ b/flutter/lib/features/profile/presentation/profile_screen.dart @@ -85,7 +85,7 @@ class _ProfileScreenState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } finally { if (mounted) setState(() => _isSaving = false); @@ -288,3 +288,4 @@ class _ProfileScreenState extends ConsumerState { ); } } + diff --git a/flutter/lib/features/recipes/presentation/recipe_detail_screen.dart b/flutter/lib/features/recipes/presentation/recipe_detail_screen.dart index 6ff69888..00a33eb6 100644 --- a/flutter/lib/features/recipes/presentation/recipe_detail_screen.dart +++ b/flutter/lib/features/recipes/presentation/recipe_detail_screen.dart @@ -193,7 +193,7 @@ class RecipeDetailScreen extends ConsumerWidget { } on ApiException catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -281,7 +281,7 @@ class RecipeDetailScreen extends ConsumerWidget { } on ApiException catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -353,7 +353,7 @@ class _DeleteButton extends ConsumerWidget { } on ApiException catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(mapErrorToUserMessage(e, context))), + buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))), ); } } @@ -697,3 +697,4 @@ class _IngredientPreviewRow extends StatelessWidget { ); } } + diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 964e678b..36cfcf1d 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -17,6 +17,9 @@ class RecipeApp extends ConsumerWidget { final router = ref.watch(appRouterProvider); return MaterialApp.router( onGenerateTitle: (context) => context.l10n.appTitle, + builder: (context, child) { + return SelectionArea(child: child ?? const SizedBox.shrink()); + }, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.green), useMaterial3: true,