feat(ui): redesign feedback dialogs with pill style for legacy mode

This commit is contained in:
2026-04-23 02:04:33 +02:00
parent f0666d9848
commit 92c922a130
3 changed files with 202 additions and 40 deletions

View File

@@ -135,7 +135,7 @@ class _RequestCancelSection extends ConsumerWidget {
await showInfoDialog( await showInfoDialog(
context, context,
I18n.accountDeletionHasDevices, I18n.accountDeletionHasDevices,
autoDismiss: null, autoDismiss: const Duration(seconds: 5),
); );
return; return;
} }

View File

@@ -20,6 +20,12 @@ class SfColors extends ThemeExtension<SfColors> {
required this.cardColorSet1, required this.cardColorSet1,
required this.cardColorSet2, required this.cardColorSet2,
required this.disabledCardColors, required this.disabledCardColors,
required this.successBackground,
required this.successCircle,
required this.successText,
required this.errorBackground,
required this.errorCircle,
required this.errorText,
}); });
/// Brand secondary color. Also mirrored in [ColorScheme.tertiary]. /// Brand secondary color. Also mirrored in [ColorScheme.tertiary].
@@ -34,18 +40,38 @@ class SfColors extends ThemeExtension<SfColors> {
/// Gradient applied to disabled cards. /// Gradient applied to disabled cards.
final List<Color> disabledCardColors; final List<Color> disabledCardColors;
/// Feedback dialog colors.
final Color successBackground;
final Color successCircle;
final Color successText;
final Color errorBackground;
final Color errorCircle;
final Color errorText;
@override @override
SfColors copyWith({ SfColors copyWith({
Color? legacyPrimary, Color? legacyPrimary,
List<Color>? cardColorSet1, List<Color>? cardColorSet1,
List<Color>? cardColorSet2, List<Color>? cardColorSet2,
List<Color>? disabledCardColors, List<Color>? disabledCardColors,
Color? successBackground,
Color? successCircle,
Color? successText,
Color? errorBackground,
Color? errorCircle,
Color? errorText,
}) { }) {
return SfColors( return SfColors(
legacyPrimary: legacyPrimary ?? this.legacyPrimary, legacyPrimary: legacyPrimary ?? this.legacyPrimary,
cardColorSet1: cardColorSet1 ?? this.cardColorSet1, cardColorSet1: cardColorSet1 ?? this.cardColorSet1,
cardColorSet2: cardColorSet2 ?? this.cardColorSet2, cardColorSet2: cardColorSet2 ?? this.cardColorSet2,
disabledCardColors: disabledCardColors ?? this.disabledCardColors, disabledCardColors: disabledCardColors ?? this.disabledCardColors,
successBackground: successBackground ?? this.successBackground,
successCircle: successCircle ?? this.successCircle,
successText: successText ?? this.successText,
errorBackground: errorBackground ?? this.errorBackground,
errorCircle: errorCircle ?? this.errorCircle,
errorText: errorText ?? this.errorText,
); );
} }
@@ -57,6 +83,12 @@ class SfColors extends ThemeExtension<SfColors> {
cardColorSet1: _lerpList(cardColorSet1, other.cardColorSet1, t), cardColorSet1: _lerpList(cardColorSet1, other.cardColorSet1, t),
cardColorSet2: _lerpList(cardColorSet2, other.cardColorSet2, t), cardColorSet2: _lerpList(cardColorSet2, other.cardColorSet2, t),
disabledCardColors: _lerpList(disabledCardColors, other.disabledCardColors, t), disabledCardColors: _lerpList(disabledCardColors, other.disabledCardColors, t),
successBackground: Color.lerp(successBackground, other.successBackground, t)!,
successCircle: Color.lerp(successCircle, other.successCircle, t)!,
successText: Color.lerp(successText, other.successText, t)!,
errorBackground: Color.lerp(errorBackground, other.errorBackground, t)!,
errorCircle: Color.lerp(errorCircle, other.errorCircle, t)!,
errorText: Color.lerp(errorText, other.errorText, t)!,
); );
} }
@@ -78,6 +110,12 @@ class SfColors extends ThemeExtension<SfColors> {
Color(0xFF797676), Color(0xFF797676),
Color(0xFF5F5A5A), Color(0xFF5F5A5A),
], ],
successBackground: Color(0xFFE8F5EC),
successCircle: Color(0xFF66D693),
successText: Color(0xFF424242),
errorBackground: Color(0xFFFDE8E8),
errorCircle: Color(0xFFE57373),
errorText: Color(0xFF424242),
); );
/// Dark variant — cards kept visually consistent; brand tone lightened. /// Dark variant — cards kept visually consistent; brand tone lightened.
@@ -90,6 +128,12 @@ class SfColors extends ThemeExtension<SfColors> {
Color(0xFF494949), Color(0xFF494949),
Color(0xFF363636), Color(0xFF363636),
], ],
successBackground: Color(0xFF1B3A2A),
successCircle: Color(0xFF66D693),
successText: Color(0xFFE0E0E0),
errorBackground: Color(0xFF3A1B1B),
errorCircle: Color(0xFFE57373),
errorText: Color(0xFFE0E0E0),
); );
} }

View File

@@ -9,13 +9,14 @@ Future<void> showErrorDialog(
BuildContext context, BuildContext context,
String messageKey, { String messageKey, {
Map<String, dynamic>? args, Map<String, dynamic>? args,
Duration autoDismiss = const Duration(seconds: 3),
}) { }) {
return _showFeedbackDialog( return _showPillFeedback(
context, context,
messageKey: messageKey, messageKey: messageKey,
args: args, args: args,
kind: _FeedbackKind.error, kind: _FeedbackKind.error,
autoDismiss: null, autoDismiss: autoDismiss,
); );
} }
@@ -23,9 +24,9 @@ Future<void> showSuccessDialog(
BuildContext context, BuildContext context,
String messageKey, { String messageKey, {
Map<String, dynamic>? args, Map<String, dynamic>? args,
Duration autoDismiss = const Duration(seconds: 2), Duration autoDismiss = const Duration(seconds: 3),
}) { }) {
return _showFeedbackDialog( return _showPillFeedback(
context, context,
messageKey: messageKey, messageKey: messageKey,
args: args, args: args,
@@ -38,9 +39,9 @@ Future<void> showInfoDialog(
BuildContext context, BuildContext context,
String messageKey, { String messageKey, {
Map<String, dynamic>? args, Map<String, dynamic>? args,
Duration? autoDismiss = const Duration(seconds: 2), Duration autoDismiss = const Duration(seconds: 3),
}) { }) {
return _showFeedbackDialog( return _showPillFeedback(
context, context,
messageKey: messageKey, messageKey: messageKey,
args: args, args: args,
@@ -49,45 +50,162 @@ Future<void> showInfoDialog(
); );
} }
Future<void> _showFeedbackDialog( Future<void> _showPillFeedback(
BuildContext context, { BuildContext context, {
required String messageKey, required String messageKey,
required Map<String, dynamic>? args, required Map<String, dynamic>? args,
required _FeedbackKind kind, required _FeedbackKind kind,
required Duration? autoDismiss, required Duration autoDismiss,
}) async { }) async {
Timer? timer; final overlay = Overlay.of(context);
await showDialog<void>( final resolved = context.translate(messageKey, args: args);
context: context,
barrierDismissible: autoDismiss != null,
builder: (ctx) {
if (autoDismiss != null) {
timer = Timer(autoDismiss, () {
if (Navigator.of(ctx).canPop()) {
Navigator.of(ctx).pop();
}
});
}
final resolved = ctx.translate(messageKey, args: args); final (backgroundColor, circleColor, iconData) = switch (kind) {
final theme = Theme.of(ctx); _FeedbackKind.success => (
final (icon, color) = switch (kind) { const Color(0xFFE8F5EC),
_FeedbackKind.error => (Icons.error_outline, theme.colorScheme.error), const Color(0xFF66D693),
_FeedbackKind.success => (Icons.check_circle_outline, Colors.green), Icons.check,
_FeedbackKind.info => (Icons.info_outline, theme.colorScheme.primary), ),
}; _FeedbackKind.error => (
const Color(0xFFFDE8E8),
const Color(0xFFE57373),
Icons.close,
),
_FeedbackKind.info => (
const Color(0xFFE3F2FD),
const Color(0xFF64B5F6),
Icons.info_outline,
),
};
return AlertDialog( late final OverlayEntry entry;
icon: Icon(icon, color: color, size: 48), late final AnimationController controller;
content: Text(resolved, textAlign: TextAlign.center),
actions: [ controller = AnimationController(
TextButton( duration: const Duration(milliseconds: 350),
onPressed: () => Navigator.of(ctx).pop(), vsync: overlay,
child: Text(ctx.translate(I18n.accept)),
),
],
);
},
); );
timer?.cancel();
final curvedAnimation = CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
void dismiss() {
controller.reverse().then((_) {
entry.remove();
controller.dispose();
});
}
entry = OverlayEntry(
builder: (_) => Positioned.fill(
child: GestureDetector(
onTap: dismiss,
behavior: HitTestBehavior.translucent,
child: Center(
child: ScaleTransition(
scale: Tween<double>(begin: 0.8, end: 1.0).animate(curvedAnimation),
child: FadeTransition(
opacity: curvedAnimation,
child: Material(
color: Colors.transparent,
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.only(
left: 58,
top: 14,
bottom: 14,
right: 16,
),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Colors.white,
blurRadius: 12.85,
spreadRadius: 8.91,
offset: Offset(0, 4.11),
),
],
),
child: Text(
resolved,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.grey.shade800,
),
),
),
Positioned(
left: 30,
top: 0,
bottom: 0,
child: Center(
child: Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: circleColor,
shape: BoxShape.circle,
),
child: Icon(
iconData,
color: Colors.white,
size: 38,
),
),
),
),
Positioned(
top: -8,
right: 18,
child: GestureDetector(
onTap: dismiss,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: Icon(
Icons.close,
size: 14,
color: Colors.grey.shade500,
),
),
),
),
],
),
),
),
),
),
),
),
);
overlay.insert(entry);
controller.forward();
await Future.delayed(autoDismiss);
if (controller.isAnimating ||
controller.status == AnimationStatus.completed) {
dismiss();
}
} }