From 450d0a7b3427956bb1530edb365830dcc3501429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCllner?= Date: Thu, 5 Mar 2026 19:03:02 +0100 Subject: [PATCH] feat(ux): add distinct success/info/error top overlay message styles --- lib/main.dart | 34 +++++++++++++++++++---- lib/screens/collections_screen.dart | 4 +-- lib/screens/garage_screen.dart | 18 ++++++------ lib/screens/login_screen.dart | 2 +- lib/screens/manage_collection_screen.dart | 10 +++---- lib/screens/profile_screen.dart | 2 +- lib/screens/scan_tab.dart | 4 +-- 7 files changed, 49 insertions(+), 25 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 33e28ef..56db27a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -54,20 +54,44 @@ final scaffoldMessengerKey = GlobalKey(); OverlayEntry? _activeMessageOverlay; Timer? _activeMessageOverlayTimer; +enum GlobalMessageType { info, success, error } + /// Show a snackbar safely through the global key. void showGlobalSnackBar(String message, {bool isError = false}) { - _showGlobalMessageOverlay(message, isError: isError); + _showGlobalMessageOverlay( + message, + type: isError ? GlobalMessageType.error : GlobalMessageType.info, + ); } -void _showGlobalMessageOverlay(String message, {required bool isError}) { +void showGlobalSuccess(String message) { + _showGlobalMessageOverlay(message, type: GlobalMessageType.success); +} + +void showGlobalInfo(String message) { + _showGlobalMessageOverlay(message, type: GlobalMessageType.info); +} + +void _showGlobalMessageOverlay( + String message, { + required GlobalMessageType type, +}) { final overlay = navigatorKey.currentState?.overlay; if (overlay == null) return; _activeMessageOverlayTimer?.cancel(); _activeMessageOverlay?.remove(); - final backgroundColor = isError ? Colors.red : const Color(0xFF1F2937); - final leadingIcon = isError ? Icons.error_outline : Icons.info_outline; + final backgroundColor = switch (type) { + GlobalMessageType.error => Colors.red, + GlobalMessageType.success => Colors.green, + GlobalMessageType.info => const Color(0xFF1F2937), + }; + final leadingIcon = switch (type) { + GlobalMessageType.error => Icons.error_outline, + GlobalMessageType.success => Icons.check_circle_outline, + GlobalMessageType.info => Icons.info_outline, + }; _activeMessageOverlay = OverlayEntry( builder: (context) { @@ -343,7 +367,7 @@ class _ResetPasswordDialogState extends State<_ResetPasswordDialog> { ); if (!mounted) return; Navigator.of(context).pop(); - showGlobalSnackBar('Password updated successfully!'); + showGlobalSuccess('Password updated successfully!'); } on AuthException catch (e) { if (!mounted) return; setState(() => _isSaving = false); diff --git a/lib/screens/collections_screen.dart b/lib/screens/collections_screen.dart index e1a88a4..cd0fc97 100644 --- a/lib/screens/collections_screen.dart +++ b/lib/screens/collections_screen.dart @@ -212,7 +212,7 @@ class CollectionsScreenState extends State name: nameCtrl.text.trim(), description: descCtrl.text.trim(), ); - showGlobalSnackBar('Collection created!'); + showGlobalSuccess('Collection created!'); _load(); } catch (e) { showGlobalError( @@ -258,7 +258,7 @@ class CollectionsScreenState extends State if (!mounted) return; setState(() => _activeCollectionId = collectionId); MainCollectionSync.notifyChanged(); - showGlobalSnackBar('Main collection set for scanning.'); + showGlobalInfo('Main collection set for scanning.'); } @override diff --git a/lib/screens/garage_screen.dart b/lib/screens/garage_screen.dart index fb29b0f..0924bd2 100644 --- a/lib/screens/garage_screen.dart +++ b/lib/screens/garage_screen.dart @@ -913,7 +913,7 @@ class GarageScreenState extends State { ); if (xFile == null) return; - showGlobalSnackBar('Uploading photo…'); + showGlobalInfo('Uploading photo…'); final oldPath = car['user_image_url'] as String?; String newPath; @@ -937,7 +937,7 @@ class GarageScreenState extends State { .update({'user_image_url': newPath}) .eq('id', car['id']); - showGlobalSnackBar('Photo updated!'); + showGlobalSuccess('Photo updated!'); if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); // refresh grid } catch (e) { @@ -968,7 +968,7 @@ class GarageScreenState extends State { .update(updated) .eq('id', car['id']); - showGlobalSnackBar('Car updated!'); + showGlobalSuccess('Car updated!'); if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); } catch (e) { @@ -992,7 +992,7 @@ class GarageScreenState extends State { .maybeSingle(); if (existingVote != null) { - showGlobalSnackBar('You already confirmed this catalog entry.'); + showGlobalInfo('You already confirmed this catalog entry.'); return; } @@ -1001,7 +1001,7 @@ class GarageScreenState extends State { 'user_id': user.id, }); - showGlobalSnackBar('Thanks! Your validation vote was recorded.'); + showGlobalSuccess('Thanks! Your validation vote was recorded.'); if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); } catch (e) { @@ -1044,7 +1044,7 @@ class GarageScreenState extends State { .maybeSingle(); if (existingOpen != null) { - showGlobalSnackBar('You already have an open report for this car.'); + showGlobalInfo('You already have an open report for this car.'); return; } @@ -1056,7 +1056,7 @@ class GarageScreenState extends State { 'note': payload.note, }); - showGlobalSnackBar('Thanks for reporting. We will review this entry.'); + showGlobalSuccess('Thanks for reporting. We will review this entry.'); } catch (e) { showGlobalError( e, @@ -1185,7 +1185,7 @@ class GarageScreenState extends State { if (!mounted) return; if (sheetContext.mounted) Navigator.pop(sheetContext); - showGlobalSnackBar(widget.isOwner + showGlobalSuccess(widget.isOwner ? '${car['hw_id']} moved to another collection.' : '${car['hw_id']} copied to another collection.'); await _loadCars(reset: true); @@ -1242,7 +1242,7 @@ class GarageScreenState extends State { if (sheetContext.mounted) { Navigator.pop(sheetContext); // close bottom sheet } - showGlobalSnackBar('${car['hw_id']} removed from your garage.'); + showGlobalSuccess('${car['hw_id']} removed from your garage.'); _loadCars(reset: true); } catch (e) { showGlobalError( diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index bf4d457..b9164fd 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -78,7 +78,7 @@ class _LoginScreenState extends State email, redirectTo: 'hwcollector://login/recovery', ); - showGlobalSnackBar('Password reset email sent! Check your inbox.'); + showGlobalSuccess('Password reset email sent! Check your inbox.'); } on AuthException catch (e) { showGlobalSnackBar(e.message, isError: true); } diff --git a/lib/screens/manage_collection_screen.dart b/lib/screens/manage_collection_screen.dart index 2e519a7..ef0ce9c 100644 --- a/lib/screens/manage_collection_screen.dart +++ b/lib/screens/manage_collection_screen.dart @@ -125,7 +125,7 @@ class _ManageCollectionScreenState extends State { memberCount: _collection.memberCount, ); }); - showGlobalSnackBar('Collection renamed!'); + showGlobalSuccess('Collection renamed!'); } catch (e) { showGlobalError( e, @@ -244,7 +244,7 @@ class _ManageCollectionScreenState extends State { email: email, role: inviteRole, ); - showGlobalSnackBar( + showGlobalSuccess( inviteRole == 'viewer' ? 'Viewer invited!' : 'Member invited!', ); await _loadMembers(); @@ -286,7 +286,7 @@ class _ManageCollectionScreenState extends State { collectionId: _collection.id, memberUserId: member.userId, ); - showGlobalSnackBar('Member removed.'); + showGlobalSuccess('Member removed.'); await _loadMembers(); } catch (e) { showGlobalError( @@ -322,7 +322,7 @@ class _ManageCollectionScreenState extends State { try { await CollectionService.leave(_collection.id); - showGlobalSnackBar('Left "${_collection.name}".'); + showGlobalSuccess('Left "${_collection.name}".'); if (mounted) Navigator.pop(context); } catch (e) { showGlobalError( @@ -358,7 +358,7 @@ class _ManageCollectionScreenState extends State { try { await CollectionService.delete(_collection.id); - showGlobalSnackBar('Collection deleted.'); + showGlobalSuccess('Collection deleted.'); if (mounted) Navigator.pop(context); } catch (e) { showGlobalError( diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 2d297dd..fbcfae6 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -208,7 +208,7 @@ class ProfileScreen extends StatelessWidget { UserAttributes(password: pw), ); if (context.mounted) Navigator.pop(context); - showGlobalSnackBar('Password updated!'); + showGlobalSuccess('Password updated!'); } on AuthException catch (e) { showGlobalSnackBar(e.message, isError: true); } diff --git a/lib/screens/scan_tab.dart b/lib/screens/scan_tab.dart index 8a6d06c..416d6be 100644 --- a/lib/screens/scan_tab.dart +++ b/lib/screens/scan_tab.dart @@ -391,7 +391,7 @@ class ScanTabState extends State { await _addToCollection(collection.id, hwId); await _ensureValidationVote(hwId); if (!mounted) return false; - showGlobalSnackBar('$hwId added to "${collection.name}"! 🎉'); + showGlobalSuccess('$hwId added to "${collection.name}"! 🎉'); } } else { final discovery = await showModalBottomSheet<_NewDiscoveryData>( @@ -415,7 +415,7 @@ class ScanTabState extends State { ); await _addToCollection(collection.id, hwId, notes: discovery.notes); if (!mounted) return false; - showGlobalSnackBar('$hwId added to "${collection.name}"! 🎉'); + showGlobalSuccess('$hwId added to "${collection.name}"! 🎉'); } } return true;