feat: enhance error handling; implement copyable SnackBar for user messages across various screens
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-04 22:10:23 +02:00
parent 2c8d6b69ae
commit 1b1d5d006d
15 changed files with 60 additions and 31 deletions
+17 -1
View File
@@ -1,4 +1,5 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import 'api_exception.dart'; import 'api_exception.dart';
@@ -21,3 +22,18 @@ String mapErrorToUserMessage(Object error, BuildContext context) {
} }
return l10n.unexpectedError; 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)),
);
},
),
);
}
@@ -94,7 +94,7 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} finally { } finally {
if (mounted) setState(() => _isSaving = false); if (mounted) setState(() => _isSaving = false);
@@ -133,7 +133,7 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} }
} }
@@ -259,3 +259,4 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
return content; return content;
} }
} }
@@ -33,7 +33,7 @@ class _AdminDatabasePanelState extends ConsumerState<AdminDatabasePanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} finally { } finally {
if (mounted) setState(() => _isRefreshingCategories = false); if (mounted) setState(() => _isRefreshingCategories = false);
@@ -180,3 +180,4 @@ class _AdminDatabasePanelState extends ConsumerState<AdminDatabasePanel> {
); );
} }
} }
@@ -57,7 +57,7 @@ class _AdminPendingProductsPanelState
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} finally { } finally {
if (mounted) setState(() => _processingId = null); if (mounted) setState(() => _processingId = null);
@@ -152,4 +152,4 @@ class _AdminPendingProductsPanelState
], ],
); );
} }
} }
@@ -132,7 +132,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} finally { } finally {
if (mounted) setState(() => _isApplying = false); if (mounted) setState(() => _isApplying = false);
@@ -181,7 +181,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} finally { } finally {
if (mounted) setState(() => _isAiRunning = false); if (mounted) setState(() => _isAiRunning = false);
@@ -321,7 +321,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} }
} }
@@ -358,7 +358,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} }
} }
@@ -380,7 +380,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} finally { } finally {
if (mounted) setState(() => _isApplying = false); if (mounted) setState(() => _isApplying = false);
@@ -399,7 +399,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} }
} }
@@ -435,7 +435,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
setState(() => _rowCategorySaving.remove(product.id)); setState(() => _rowCategorySaving.remove(product.id));
} }
@@ -811,4 +811,4 @@ class _AiApplyDialogState extends State<_AiApplyDialog> {
], ],
); );
} }
} }
@@ -52,7 +52,7 @@ class _ConsumeInventoryScreenState
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context)))); .showSnackBar(buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))));
} }
} finally { } finally {
if (mounted) setState(() => _saving = false); if (mounted) setState(() => _saving = false);
@@ -139,3 +139,4 @@ class _ConsumeInventoryScreenState
); );
} }
} }
@@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -76,7 +76,7 @@ class _CreateInventoryScreenState
if (mounted) setState(() => _loadingProducts = false); if (mounted) setState(() => _loadingProducts = false);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} }
} }
@@ -136,7 +136,7 @@ class _CreateInventoryScreenState
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context)))); .showSnackBar(buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))));
} }
} finally { } finally {
if (mounted) setState(() => _saving = false); if (mounted) setState(() => _saving = false);
@@ -326,3 +326,4 @@ class _CreateInventoryScreenState
); );
} }
} }
@@ -122,7 +122,7 @@ class _DeleteButton extends ConsumerWidget {
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} }
} }
@@ -159,3 +159,4 @@ class _InfoRow extends StatelessWidget {
); );
} }
} }
@@ -111,7 +111,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context)))); .showSnackBar(buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))));
} }
} finally { } finally {
if (mounted) setState(() => _saving = false); if (mounted) setState(() => _saving = false);
@@ -297,3 +297,4 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
); );
} }
} }
@@ -122,7 +122,7 @@ class _SwipeableInventoryTileState
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} finally { } finally {
if (mounted) setState(() => _acting = false); if (mounted) setState(() => _acting = false);
@@ -372,7 +372,7 @@ class _DeleteButton extends ConsumerWidget {
ref.invalidate(inventoryProvider); ref.invalidate(inventoryProvider);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} }
} }
@@ -381,3 +381,4 @@ class _DeleteButton extends ConsumerWidget {
); );
} }
} }
@@ -56,7 +56,7 @@ class _MealPlanScreenState extends ConsumerState<MealPlanScreen> {
} catch (error) { } catch (error) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context))),
); );
} finally { } finally {
if (mounted) { if (mounted) {
@@ -593,4 +593,4 @@ class _EnrichedShoppingItem {
} }
String _formatQuantity(double value) => formatQuantity(value); String _formatQuantity(double value) => formatQuantity(value);
} }
@@ -166,7 +166,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
_logger.severe('Failed to add item to inventory: $error'); _logger.severe('Failed to add item to inventory: $error');
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context))),
); );
} }
} }
@@ -185,7 +185,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
_logger.severe('Failed to add pantry item: $error'); _logger.severe('Failed to add pantry item: $error');
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context))),
); );
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);
@@ -221,7 +221,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
_logger.severe('Failed to remove pantry item: $error'); _logger.severe('Failed to remove pantry item: $error');
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context))),
); );
} }
} }
@@ -419,3 +419,4 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
); );
} }
} }
@@ -85,7 +85,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} finally { } finally {
if (mounted) setState(() => _isSaving = false); if (mounted) setState(() => _isSaving = false);
@@ -288,3 +288,4 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
); );
} }
} }
@@ -193,7 +193,7 @@ class RecipeDetailScreen extends ConsumerWidget {
} on ApiException catch (e) { } on ApiException catch (e) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( 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) { } on ApiException catch (e) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( 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) { } on ApiException catch (e) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))), buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context))),
); );
} }
} }
@@ -697,3 +697,4 @@ class _IngredientPreviewRow extends StatelessWidget {
); );
} }
} }
+3
View File
@@ -17,6 +17,9 @@ class RecipeApp extends ConsumerWidget {
final router = ref.watch(appRouterProvider); final router = ref.watch(appRouterProvider);
return MaterialApp.router( return MaterialApp.router(
onGenerateTitle: (context) => context.l10n.appTitle, onGenerateTitle: (context) => context.l10n.appTitle,
builder: (context, child) {
return SelectionArea(child: child ?? const SizedBox.shrink());
},
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green), colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true, useMaterial3: true,