Files
xianyan/lib/features/progress/presentation/progress_bubble.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

948 lines
29 KiB
Dart

// ============================================================
// 闲言APP — 进度气泡组件
// 创建时间: 2026-05-30
// 更新时间: 2026-05-30
// 作用: 进度页面的时间线气泡组件 — 消息气泡/进度条/环形/倒计时
// 上次更新: 头像显示emoji+渐变背景,倒计时实时秒级更新,进度条/环形动画增强
// ============================================================
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_radius.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_typography.dart';
import '../../../shared/widgets/containers/glass_container.dart';
import '../progress_models.dart';
import '../progress_provider.dart';
IconData progressTypeIcon(ProgressItemType type) {
return switch (type) {
ProgressItemType.timeProgress => CupertinoIcons.clock,
ProgressItemType.holidayCountdown => CupertinoIcons.calendar,
ProgressItemType.userCountdown => CupertinoIcons.timer,
ProgressItemType.userProgress => CupertinoIcons.chart_bar_fill,
};
}
Color parseProgressColor(String? hex, Color fallback) {
if (hex == null || hex.isEmpty) return fallback;
try {
final c = hex.replaceFirst('#', '');
if (c.length == 6) {
return Color(int.parse('FF$c', radix: 16));
}
if (c.length == 8) {
return Color(int.parse(c, radix: 16));
}
} catch (_) {}
return fallback;
}
class ProgressTimeline extends ConsumerWidget {
const ProgressTimeline({
super.key,
required this.scrollController,
this.onEditItem,
this.onDeleteItem,
});
final ScrollController scrollController;
final void Function(ProgressItem)? onEditItem;
final void Function(ProgressItem)? onDeleteItem;
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(progressProvider);
final ext = AppTheme.ext(context);
final timeline = state.timeline;
if (timeline.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
ext.accent.withValues(alpha: 0.15),
ext.accentLight.withValues(alpha: 0.08),
],
),
borderRadius: AppRadius.xlBorder,
),
child: Center(
child: Icon(
CupertinoIcons.chart_bar,
size: 36,
color: ext.accent.withValues(alpha: 0.6),
),
),
).animate().scale(duration: 500.ms, curve: Curves.elasticOut),
const SizedBox(height: AppSpacing.md),
Text(
'暂无进度数据',
style: AppTypography.headline.copyWith(
color: ext.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
'在下方输入框添加你的第一个进度吧',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
);
}
return ListView.builder(
controller: scrollController,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
physics: const BouncingScrollPhysics(),
itemCount: timeline.length,
itemBuilder: (context, index) {
final item = timeline[index];
if (item is ProgressTimeDivider) {
return _buildTimeDivider(item, ext);
}
if (item is ProgressItem) {
return ProgressMessageBubble(
item: item,
index: index,
onEditItem: onEditItem,
onDeleteItem: onDeleteItem,
);
}
return const SizedBox.shrink();
},
);
}
Widget _buildTimeDivider(ProgressTimeDivider divider, AppThemeExtension ext) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.bgSecondary.withValues(alpha: 0.8),
borderRadius: AppRadius.fullBorder,
border: Border.all(color: ext.overlaySubtle, width: 0.5),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (divider.emoji != null)
Padding(
padding: const EdgeInsets.only(right: 4),
child: Text(
divider.emoji!,
style: const TextStyle(fontSize: 12),
),
)
else
Icon(CupertinoIcons.time, size: 12, color: ext.textHint),
const SizedBox(width: 4),
Text(
divider.text,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
),
);
}
}
class ProgressMessageBubble extends StatelessWidget {
const ProgressMessageBubble({
super.key,
required this.item,
required this.index,
this.onEditItem,
this.onDeleteItem,
});
final ProgressItem item;
final int index;
final void Function(ProgressItem)? onEditItem;
final void Function(ProgressItem)? onDeleteItem;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final isUser = item.type.isUserCreated;
return Padding(
padding: EdgeInsets.only(
left: isUser ? 48 : 0,
right: isUser ? 0 : 48,
bottom: AppSpacing.md,
),
child: GestureDetector(
onTap: isUser ? () => onEditItem?.call(item) : null,
onLongPress: isUser ? () => onDeleteItem?.call(item) : null,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
textDirection: isUser ? TextDirection.rtl : TextDirection.ltr,
children: [
_buildAvatar(ext),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
crossAxisAlignment: isUser
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [_buildBubbleContent(ext, isUser)],
),
),
],
),
),
)
.animate()
.fadeIn(duration: 250.ms)
.slideX(
begin: isUser ? 0.08 : -0.08,
end: 0,
duration: 250.ms,
delay: Duration(milliseconds: index * 25),
curve: Curves.easeOutCubic,
);
}
Widget _buildAvatar(AppThemeExtension ext) {
final isUser = item.type.isUserCreated;
final avatarGradient = isUser
? [
ext.accent.withValues(alpha: 0.85),
ext.accentLight.withValues(alpha: 0.7),
]
: [ext.bgSecondary, ext.bgElevated];
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: avatarGradient,
),
borderRadius: AppRadius.mdBorder,
boxShadow: [
BoxShadow(
color: ext.accent.withValues(alpha: isUser ? 0.2 : 0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: isUser
? Text(item.emoji, style: const TextStyle(fontSize: 16))
: Text(item.emoji, style: const TextStyle(fontSize: 16)),
),
);
}
Widget _buildBubbleContent(AppThemeExtension ext, bool isUser) {
return GlassContainer(
depth: isUser ? GlassDepth.elevated : GlassDepth.base,
padding: const EdgeInsets.all(AppSpacing.md),
borderColor: isUser ? ext.accent.withValues(alpha: 0.2) : null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBubbleHeader(ext, isUser),
const SizedBox(height: AppSpacing.sm),
_buildBubbleBody(ext),
if (item.tagText != null) ...[
const SizedBox(height: AppSpacing.sm),
_buildTag(ext),
],
if (item.note != null && item.note!.isNotEmpty) ...[
const SizedBox(height: AppSpacing.sm),
_buildNote(ext),
],
if (item.milestones.isNotEmpty) ...[
const SizedBox(height: AppSpacing.sm),
_buildMilestones(ext),
],
const SizedBox(height: AppSpacing.xs),
_buildBubbleFooter(ext, isUser),
],
),
);
}
Widget _buildBubbleHeader(AppThemeExtension ext, bool isUser) {
return Row(
children: [
Text(item.emoji, style: const TextStyle(fontSize: 16)),
const SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
item.title,
style: AppTypography.headline.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (isUser) ...[
const SizedBox(width: AppSpacing.xs),
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 2,
),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.15),
borderRadius: AppRadius.fullBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.person_fill, size: 10, color: ext.accent),
const SizedBox(width: 3),
Text(
'自定义',
style: AppTypography.caption2.copyWith(color: ext.accent),
),
],
),
),
],
],
);
}
Widget _buildBubbleBody(AppThemeExtension ext) {
if (item.isPast && item.displayStyle == ProgressDisplayStyle.tagOnly) {
return Text(
item.subtitle,
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.subtitle,
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
const SizedBox(height: AppSpacing.sm),
_buildDisplayStyleContent(ext),
],
);
}
Widget _buildDisplayStyleContent(AppThemeExtension ext) {
switch (item.displayStyle) {
case ProgressDisplayStyle.progressBar:
return _AnimatedProgressBar(item: item);
case ProgressDisplayStyle.ringProgress:
return _AnimatedRingProgress(item: item);
case ProgressDisplayStyle.countdownGrid:
return _LiveCountdownGrid(item: item);
case ProgressDisplayStyle.tagOnly:
return const SizedBox.shrink();
}
}
Widget _buildTag(AppThemeExtension ext) {
final tagColor = parseProgressColor(item.tagColor, ext.accent);
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: tagColor.withValues(alpha: 0.1),
borderRadius: AppRadius.fullBorder,
border: Border.all(color: tagColor.withValues(alpha: 0.2), width: 0.5),
),
child: Text(
item.tagText ?? '',
style: AppTypography.caption1.copyWith(
color: tagColor,
fontWeight: FontWeight.w500,
),
),
);
}
Widget _buildNote(AppThemeExtension ext) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.bgSecondary.withValues(alpha: 0.6),
borderRadius: AppRadius.smBorder,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(CupertinoIcons.doc_text, size: 12, color: ext.textHint),
const SizedBox(width: 4),
Expanded(
child: Text(
item.note ?? '',
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
height: 1.4,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildMilestones(AppThemeExtension ext) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(CupertinoIcons.flag_fill, size: 12, color: ext.accent),
const SizedBox(width: 4),
Text(
'里程碑',
style: AppTypography.caption1.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppSpacing.xs),
...item.milestones.map((m) => _buildMilestoneRow(m, ext)),
],
);
}
Widget _buildMilestoneRow(
ProgressMilestone milestone,
AppThemeExtension ext,
) {
final now = DateTime.now();
final isReached = milestone.isReached || milestone.targetDate.isBefore(now);
final daysLeft = milestone.targetDate.difference(now).inDays;
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
Text(
isReached ? '' : milestone.emoji,
style: const TextStyle(fontSize: 12),
),
const SizedBox(width: 6),
Expanded(
child: Text(
milestone.label,
style: AppTypography.caption1.copyWith(
color: isReached ? ext.textSecondary : ext.textPrimary,
decoration: isReached ? TextDecoration.lineThrough : null,
),
),
),
Text(
isReached ? '已达成' : '${daysLeft}天后',
style: AppTypography.caption2.copyWith(
color: isReached ? ext.textHint : ext.accent,
),
),
],
),
);
}
Widget _buildBubbleFooter(AppThemeExtension ext, bool isUser) {
final time = item.createdAt ?? DateTime.now();
final timeStr =
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
final sourceStr = isUser ? '自定义' : '系统';
return Row(
children: [
Text(
'$timeStr · $sourceStr',
style: AppTypography.caption2.copyWith(color: ext.textHint),
),
const Spacer(),
if (isUser) ...[
GestureDetector(
onTap: () => onEditItem?.call(item),
child: Container(
padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.08),
borderRadius: AppRadius.smBorder,
),
child: Icon(CupertinoIcons.pencil, size: 12, color: ext.accent),
),
),
const SizedBox(width: 4),
GestureDetector(
onTap: () => onDeleteItem?.call(item),
child: Container(
padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: CupertinoColors.systemRed.withValues(alpha: 0.08),
borderRadius: AppRadius.smBorder,
),
child: const Icon(
CupertinoIcons.delete,
size: 12,
color: CupertinoColors.systemRed,
),
),
),
],
],
);
}
}
class _AnimatedProgressBar extends StatelessWidget {
const _AnimatedProgressBar({required this.item});
final ProgressItem item;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final pct = (item.progressPct ?? 0).clamp(0.0, 1.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'进度',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
const Spacer(),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: pct),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Text(
'${(value * 100).toInt()}%',
style: AppTypography.caption1.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
);
},
),
],
),
const SizedBox(height: AppSpacing.xs),
ClipRRect(
borderRadius: AppRadius.fullBorder,
child: SizedBox(
height: 10,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.fullBorder,
),
),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: pct),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return FractionallySizedBox(
widthFactor: value,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ext.accent, ext.accentLight],
),
borderRadius: AppRadius.fullBorder,
boxShadow: [
BoxShadow(
color: ext.accent.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
),
);
},
),
],
),
),
),
if (item.statItems.isNotEmpty) ...[
const SizedBox(height: AppSpacing.sm),
_buildStatPills(ext),
],
],
);
}
Widget _buildStatPills(AppThemeExtension ext) {
return Wrap(
spacing: AppSpacing.xs,
runSpacing: AppSpacing.xs,
children: item.statItems.map((s) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
s.value,
style: AppTypography.callout.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
Text(
s.label,
style: AppTypography.caption2.copyWith(color: ext.textHint),
),
],
),
);
}).toList(),
);
}
}
class _AnimatedRingProgress extends StatelessWidget {
const _AnimatedRingProgress({required this.item});
final ProgressItem item;
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final ringPct = (item.ringPct ?? 0).clamp(0.0, 1.0);
final ringLabel = item.ringLabel ?? '';
final ringColor = parseProgressColor(item.ringColor, ext.accent);
final pctStr = '${(ringPct * 100).toStringAsFixed(0)}%';
return Row(
children: [
SizedBox(
width: 72,
height: 72,
child: Stack(
alignment: Alignment.center,
children: [
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: ringPct),
duration: const Duration(milliseconds: 1000),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return SizedBox(
width: 72,
height: 72,
child: CircularProgressIndicator(
value: value,
strokeWidth: 6,
backgroundColor: ext.bgSecondary,
valueColor: AlwaysStoppedAnimation<Color>(ringColor),
strokeCap: StrokeCap.round,
),
);
},
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
ringLabel.isNotEmpty ? ringLabel : pctStr,
style: AppTypography.caption1.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
if (ringLabel.isNotEmpty)
Text(
pctStr,
style: AppTypography.caption2.copyWith(
color: ext.textHint,
),
),
],
),
],
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: item.statItems.isNotEmpty
? _buildStatPills(ext)
: item.summaryRows.isNotEmpty
? _buildSummaryRows(ext)
: const SizedBox.shrink(),
),
],
);
}
Widget _buildStatPills(AppThemeExtension ext) {
return Wrap(
spacing: AppSpacing.xs,
runSpacing: AppSpacing.xs,
children: item.statItems.map((s) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
s.value,
style: AppTypography.callout.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
Text(
s.label,
style: AppTypography.caption2.copyWith(color: ext.textHint),
),
],
),
);
}).toList(),
);
}
Widget _buildSummaryRows(AppThemeExtension ext) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: item.summaryRows.map((r) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
r.label,
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
),
Text(
r.value,
style: AppTypography.callout.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
);
}).toList(),
);
}
}
class _LiveCountdownGrid extends StatefulWidget {
const _LiveCountdownGrid({required this.item});
final ProgressItem item;
@override
State<_LiveCountdownGrid> createState() => _LiveCountdownGridState();
}
class _LiveCountdownGridState extends State<_LiveCountdownGrid> {
late final ValueNotifier<Duration> _remainingNotifier;
Timer? _timer;
@override
void initState() {
super.initState();
final rem = widget.item.remaining ?? Duration.zero;
_remainingNotifier = ValueNotifier<Duration>(rem);
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (!mounted) return;
final updated = widget.item.remaining ?? Duration.zero;
if (_remainingNotifier.value != updated) {
_remainingNotifier.value = updated;
}
});
}
@override
void didUpdateWidget(covariant _LiveCountdownGrid oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.item.targetDate != widget.item.targetDate) {
_remainingNotifier.value = widget.item.remaining ?? Duration.zero;
}
}
@override
void dispose() {
_timer?.cancel();
_remainingNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return ValueListenableBuilder<Duration>(
valueListenable: _remainingNotifier,
builder: (context, remaining, _) {
final days = remaining.inDays;
final hours = remaining.inHours % 24;
final minutes = remaining.inMinutes % 60;
final seconds = remaining.inSeconds % 60;
return Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: ext.bgSecondary.withValues(alpha: 0.5),
borderRadius: AppRadius.mdBorder,
border: Border.all(color: ext.overlaySubtle, width: 0.5),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildCountdownCell('$days', '', ext, ext.accent),
_buildCountdownSeparator(ext),
_buildCountdownCell('$hours', '', ext, ext.accentLight),
_buildCountdownSeparator(ext),
_buildCountdownCell('$minutes', '', ext, ext.accent),
_buildCountdownSeparator(ext),
_buildCountdownCell('$seconds', '', ext, ext.accentLight),
],
),
);
},
);
}
Widget _buildCountdownCell(
String value,
String unit,
AppThemeExtension ext,
Color accentColor,
) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
child: Container(
key: ValueKey('$value$unit'),
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
accentColor.withValues(alpha: 0.15),
accentColor.withValues(alpha: 0.05),
],
),
borderRadius: AppRadius.mdBorder,
border: Border.all(
color: accentColor.withValues(alpha: 0.2),
width: 0.5,
),
boxShadow: [
BoxShadow(
color: ext.overlaySubtle,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Text(
value,
style: AppTypography.headline.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
),
),
),
const SizedBox(height: 4),
Text(unit, style: AppTypography.caption2.copyWith(color: ext.textHint)),
],
);
}
Widget _buildCountdownSeparator(AppThemeExtension ext) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
':',
style: AppTypography.headline.copyWith(
color: ext.textHint,
fontWeight: FontWeight.w700,
),
),
);
}
}