feat(ux): add distinct success/info/error top overlay message styles

This commit is contained in:
Lukas Müllner 2026-03-05 19:03:02 +01:00
parent fded06c85a
commit 450d0a7b34
7 changed files with 49 additions and 25 deletions

View file

@ -54,20 +54,44 @@ final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
OverlayEntry? _activeMessageOverlay; OverlayEntry? _activeMessageOverlay;
Timer? _activeMessageOverlayTimer; Timer? _activeMessageOverlayTimer;
enum GlobalMessageType { info, success, error }
/// Show a snackbar safely through the global key. /// Show a snackbar safely through the global key.
void showGlobalSnackBar(String message, {bool isError = false}) { 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; final overlay = navigatorKey.currentState?.overlay;
if (overlay == null) return; if (overlay == null) return;
_activeMessageOverlayTimer?.cancel(); _activeMessageOverlayTimer?.cancel();
_activeMessageOverlay?.remove(); _activeMessageOverlay?.remove();
final backgroundColor = isError ? Colors.red : const Color(0xFF1F2937); final backgroundColor = switch (type) {
final leadingIcon = isError ? Icons.error_outline : Icons.info_outline; 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( _activeMessageOverlay = OverlayEntry(
builder: (context) { builder: (context) {
@ -343,7 +367,7 @@ class _ResetPasswordDialogState extends State<_ResetPasswordDialog> {
); );
if (!mounted) return; if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
showGlobalSnackBar('Password updated successfully!'); showGlobalSuccess('Password updated successfully!');
} on AuthException catch (e) { } on AuthException catch (e) {
if (!mounted) return; if (!mounted) return;
setState(() => _isSaving = false); setState(() => _isSaving = false);

View file

@ -212,7 +212,7 @@ class CollectionsScreenState extends State<CollectionsScreen>
name: nameCtrl.text.trim(), name: nameCtrl.text.trim(),
description: descCtrl.text.trim(), description: descCtrl.text.trim(),
); );
showGlobalSnackBar('Collection created!'); showGlobalSuccess('Collection created!');
_load(); _load();
} catch (e) { } catch (e) {
showGlobalError( showGlobalError(
@ -258,7 +258,7 @@ class CollectionsScreenState extends State<CollectionsScreen>
if (!mounted) return; if (!mounted) return;
setState(() => _activeCollectionId = collectionId); setState(() => _activeCollectionId = collectionId);
MainCollectionSync.notifyChanged(); MainCollectionSync.notifyChanged();
showGlobalSnackBar('Main collection set for scanning.'); showGlobalInfo('Main collection set for scanning.');
} }
@override @override

View file

@ -913,7 +913,7 @@ class GarageScreenState extends State<GarageScreen> {
); );
if (xFile == null) return; if (xFile == null) return;
showGlobalSnackBar('Uploading photo…'); showGlobalInfo('Uploading photo…');
final oldPath = car['user_image_url'] as String?; final oldPath = car['user_image_url'] as String?;
String newPath; String newPath;
@ -937,7 +937,7 @@ class GarageScreenState extends State<GarageScreen> {
.update({'user_image_url': newPath}) .update({'user_image_url': newPath})
.eq('id', car['id']); .eq('id', car['id']);
showGlobalSnackBar('Photo updated!'); showGlobalSuccess('Photo updated!');
if (sheetContext.mounted) Navigator.pop(sheetContext); if (sheetContext.mounted) Navigator.pop(sheetContext);
_loadCars(reset: true); // refresh grid _loadCars(reset: true); // refresh grid
} catch (e) { } catch (e) {
@ -968,7 +968,7 @@ class GarageScreenState extends State<GarageScreen> {
.update(updated) .update(updated)
.eq('id', car['id']); .eq('id', car['id']);
showGlobalSnackBar('Car updated!'); showGlobalSuccess('Car updated!');
if (sheetContext.mounted) Navigator.pop(sheetContext); if (sheetContext.mounted) Navigator.pop(sheetContext);
_loadCars(reset: true); _loadCars(reset: true);
} catch (e) { } catch (e) {
@ -992,7 +992,7 @@ class GarageScreenState extends State<GarageScreen> {
.maybeSingle(); .maybeSingle();
if (existingVote != null) { if (existingVote != null) {
showGlobalSnackBar('You already confirmed this catalog entry.'); showGlobalInfo('You already confirmed this catalog entry.');
return; return;
} }
@ -1001,7 +1001,7 @@ class GarageScreenState extends State<GarageScreen> {
'user_id': user.id, 'user_id': user.id,
}); });
showGlobalSnackBar('Thanks! Your validation vote was recorded.'); showGlobalSuccess('Thanks! Your validation vote was recorded.');
if (sheetContext.mounted) Navigator.pop(sheetContext); if (sheetContext.mounted) Navigator.pop(sheetContext);
_loadCars(reset: true); _loadCars(reset: true);
} catch (e) { } catch (e) {
@ -1044,7 +1044,7 @@ class GarageScreenState extends State<GarageScreen> {
.maybeSingle(); .maybeSingle();
if (existingOpen != null) { if (existingOpen != null) {
showGlobalSnackBar('You already have an open report for this car.'); showGlobalInfo('You already have an open report for this car.');
return; return;
} }
@ -1056,7 +1056,7 @@ class GarageScreenState extends State<GarageScreen> {
'note': payload.note, 'note': payload.note,
}); });
showGlobalSnackBar('Thanks for reporting. We will review this entry.'); showGlobalSuccess('Thanks for reporting. We will review this entry.');
} catch (e) { } catch (e) {
showGlobalError( showGlobalError(
e, e,
@ -1185,7 +1185,7 @@ class GarageScreenState extends State<GarageScreen> {
if (!mounted) return; if (!mounted) return;
if (sheetContext.mounted) Navigator.pop(sheetContext); if (sheetContext.mounted) Navigator.pop(sheetContext);
showGlobalSnackBar(widget.isOwner showGlobalSuccess(widget.isOwner
? '${car['hw_id']} moved to another collection.' ? '${car['hw_id']} moved to another collection.'
: '${car['hw_id']} copied to another collection.'); : '${car['hw_id']} copied to another collection.');
await _loadCars(reset: true); await _loadCars(reset: true);
@ -1242,7 +1242,7 @@ class GarageScreenState extends State<GarageScreen> {
if (sheetContext.mounted) { if (sheetContext.mounted) {
Navigator.pop(sheetContext); // close bottom sheet 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); _loadCars(reset: true);
} catch (e) { } catch (e) {
showGlobalError( showGlobalError(

View file

@ -78,7 +78,7 @@ class _LoginScreenState extends State<LoginScreen>
email, email,
redirectTo: 'hwcollector://login/recovery', redirectTo: 'hwcollector://login/recovery',
); );
showGlobalSnackBar('Password reset email sent! Check your inbox.'); showGlobalSuccess('Password reset email sent! Check your inbox.');
} on AuthException catch (e) { } on AuthException catch (e) {
showGlobalSnackBar(e.message, isError: true); showGlobalSnackBar(e.message, isError: true);
} }

View file

@ -125,7 +125,7 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
memberCount: _collection.memberCount, memberCount: _collection.memberCount,
); );
}); });
showGlobalSnackBar('Collection renamed!'); showGlobalSuccess('Collection renamed!');
} catch (e) { } catch (e) {
showGlobalError( showGlobalError(
e, e,
@ -244,7 +244,7 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
email: email, email: email,
role: inviteRole, role: inviteRole,
); );
showGlobalSnackBar( showGlobalSuccess(
inviteRole == 'viewer' ? 'Viewer invited!' : 'Member invited!', inviteRole == 'viewer' ? 'Viewer invited!' : 'Member invited!',
); );
await _loadMembers(); await _loadMembers();
@ -286,7 +286,7 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
collectionId: _collection.id, collectionId: _collection.id,
memberUserId: member.userId, memberUserId: member.userId,
); );
showGlobalSnackBar('Member removed.'); showGlobalSuccess('Member removed.');
await _loadMembers(); await _loadMembers();
} catch (e) { } catch (e) {
showGlobalError( showGlobalError(
@ -322,7 +322,7 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
try { try {
await CollectionService.leave(_collection.id); await CollectionService.leave(_collection.id);
showGlobalSnackBar('Left "${_collection.name}".'); showGlobalSuccess('Left "${_collection.name}".');
if (mounted) Navigator.pop(context); if (mounted) Navigator.pop(context);
} catch (e) { } catch (e) {
showGlobalError( showGlobalError(
@ -358,7 +358,7 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
try { try {
await CollectionService.delete(_collection.id); await CollectionService.delete(_collection.id);
showGlobalSnackBar('Collection deleted.'); showGlobalSuccess('Collection deleted.');
if (mounted) Navigator.pop(context); if (mounted) Navigator.pop(context);
} catch (e) { } catch (e) {
showGlobalError( showGlobalError(

View file

@ -208,7 +208,7 @@ class ProfileScreen extends StatelessWidget {
UserAttributes(password: pw), UserAttributes(password: pw),
); );
if (context.mounted) Navigator.pop(context); if (context.mounted) Navigator.pop(context);
showGlobalSnackBar('Password updated!'); showGlobalSuccess('Password updated!');
} on AuthException catch (e) { } on AuthException catch (e) {
showGlobalSnackBar(e.message, isError: true); showGlobalSnackBar(e.message, isError: true);
} }

View file

@ -391,7 +391,7 @@ class ScanTabState extends State<ScanTab> {
await _addToCollection(collection.id, hwId); await _addToCollection(collection.id, hwId);
await _ensureValidationVote(hwId); await _ensureValidationVote(hwId);
if (!mounted) return false; if (!mounted) return false;
showGlobalSnackBar('$hwId added to "${collection.name}"! 🎉'); showGlobalSuccess('$hwId added to "${collection.name}"! 🎉');
} }
} else { } else {
final discovery = await showModalBottomSheet<_NewDiscoveryData>( final discovery = await showModalBottomSheet<_NewDiscoveryData>(
@ -415,7 +415,7 @@ class ScanTabState extends State<ScanTab> {
); );
await _addToCollection(collection.id, hwId, notes: discovery.notes); await _addToCollection(collection.id, hwId, notes: discovery.notes);
if (!mounted) return false; if (!mounted) return false;
showGlobalSnackBar('$hwId added to "${collection.name}"! 🎉'); showGlobalSuccess('$hwId added to "${collection.name}"! 🎉');
} }
} }
return true; return true;