feat(ux): add distinct success/info/error top overlay message styles
This commit is contained in:
parent
fded06c85a
commit
450d0a7b34
7 changed files with 49 additions and 25 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue