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;
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);

View file

@ -212,7 +212,7 @@ class CollectionsScreenState extends State<CollectionsScreen>
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<CollectionsScreen>
if (!mounted) return;
setState(() => _activeCollectionId = collectionId);
MainCollectionSync.notifyChanged();
showGlobalSnackBar('Main collection set for scanning.');
showGlobalInfo('Main collection set for scanning.');
}
@override

View file

@ -913,7 +913,7 @@ class GarageScreenState extends State<GarageScreen> {
);
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<GarageScreen> {
.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<GarageScreen> {
.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<GarageScreen> {
.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<GarageScreen> {
'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<GarageScreen> {
.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<GarageScreen> {
'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<GarageScreen> {
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<GarageScreen> {
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(

View file

@ -78,7 +78,7 @@ class _LoginScreenState extends State<LoginScreen>
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);
}

View file

@ -125,7 +125,7 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
memberCount: _collection.memberCount,
);
});
showGlobalSnackBar('Collection renamed!');
showGlobalSuccess('Collection renamed!');
} catch (e) {
showGlobalError(
e,
@ -244,7 +244,7 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
email: email,
role: inviteRole,
);
showGlobalSnackBar(
showGlobalSuccess(
inviteRole == 'viewer' ? 'Viewer invited!' : 'Member invited!',
);
await _loadMembers();
@ -286,7 +286,7 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
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<ManageCollectionScreen> {
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<ManageCollectionScreen> {
try {
await CollectionService.delete(_collection.id);
showGlobalSnackBar('Collection deleted.');
showGlobalSuccess('Collection deleted.');
if (mounted) Navigator.pop(context);
} catch (e) {
showGlobalError(

View file

@ -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);
}

View file

@ -391,7 +391,7 @@ class ScanTabState extends State<ScanTab> {
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<ScanTab> {
);
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;