import 'package:flutter/material.dart'; import '../../core/app_state.dart'; import '../../core/app_theme.dart'; import '../../widgets/app_widgets.dart'; /// Opens a "我的" sub page. Pushed routes sit above the shell, so each page /// listens to [state] itself to stay current. Future openProfilePage( BuildContext context, { required AppState state, required String title, required WidgetBuilder builder, }) => Navigator.of(context).push( MaterialPageRoute( builder: (context) => AppPage( appBar: AppBar(title: Text(title)), child: ListenableBuilder( listenable: state, builder: (context, _) => builder(context), ), ), ), ); /// A titled card whose rows are separated by dividers. class MenuGroup extends StatelessWidget { const MenuGroup({super.key, this.title, this.note, required this.children}); final String? title; final String? note; final List children; @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title != null || note != null) Padding( padding: const EdgeInsets.only(left: 4, bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title != null) Text( title!, style: TextStyle(fontSize: 13, color: AppColors.muted), ), if (note != null) Text( note!, style: TextStyle(fontSize: 12, color: AppColors.muted), ), ], ), ), SectionCard( padding: EdgeInsets.zero, child: Column( children: [ for (var i = 0; i < children.length; i++) ...[ if (i > 0) Divider(height: 1, color: AppColors.line), children[i], ], ], ), ), ], ); } /// One row of a [MenuGroup]: icon, title, optional current value and chevron. class MenuTile extends StatelessWidget { const MenuTile({ super.key, required this.icon, required this.title, this.subtitle, this.value, this.iconColor, this.trailing, this.onTap, }); final IconData icon; final String title; final String? subtitle; final String? value; final Color? iconColor; final Widget? trailing; final VoidCallback? onTap; @override Widget build(BuildContext context) => ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 14), leading: Icon(icon, color: iconColor ?? AppColors.muted), title: Text(title), subtitle: subtitle == null ? null : Text(subtitle!), trailing: trailing ?? Row( mainAxisSize: MainAxisSize.min, children: [ if (value != null) Text( value!, style: TextStyle(fontSize: 13, color: AppColors.muted), ), if (onTap != null) Icon(Icons.chevron_right, color: AppColors.muted), ], ), onTap: onTap, ); } /// A [MenuTile]-aligned switch row. class MenuSwitch extends StatelessWidget { const MenuSwitch({ super.key, required this.icon, required this.title, this.subtitle, required this.value, required this.onChanged, }); final IconData icon; final String title; final String? subtitle; final bool value; final ValueChanged onChanged; @override Widget build(BuildContext context) => SwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 14), secondary: Icon(icon, color: AppColors.muted), title: Text(title), subtitle: subtitle == null ? null : Text(subtitle!), value: value, activeThumbColor: AppColors.green, onChanged: onChanged, ); } /// A labelled progress bar, e.g. "可使用 31 / 48". class GoalProgressRow extends StatelessWidget { const GoalProgressRow({ super.key, required this.icon, required this.title, required this.note, required this.value, required this.goal, }); final IconData icon; final String title; final String note; final int value; final int goal; @override Widget build(BuildContext context) { final completed = value >= goal; return Padding( padding: const EdgeInsets.all(14), child: Row( children: [ Icon(icon, color: completed ? AppColors.green : AppColors.muted), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( title, style: const TextStyle(fontWeight: FontWeight.w600), ), ), Text( '$value / $goal', style: TextStyle(color: AppColors.muted), ), ], ), const SizedBox(height: 6), ProgressBar(value: goal == 0 ? 0 : value / goal), const SizedBox(height: 6), Text(note, style: Theme.of(context).textTheme.bodyMedium), ], ), ), ], ), ); } } class ProgressBar extends StatelessWidget { const ProgressBar({super.key, required this.value, this.background}); final double value; final Color? background; @override Widget build(BuildContext context) => ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: value.clamp(0, 1).toDouble(), minHeight: 6, color: AppColors.green, backgroundColor: background ?? AppColors.surfaceMuted, ), ); } /// A red row that asks for explicit acknowledgement before [onConfirm]. class DangerAction extends StatelessWidget { const DangerAction({ super.key, required this.label, required this.message, this.details, this.acknowledgement, required this.onConfirm, }); final String label; final String message; final String? details; /// When set, the confirm button stays disabled until this box is ticked. final String? acknowledgement; final VoidCallback onConfirm; @override Widget build(BuildContext context) => ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 14), leading: const Icon(Icons.delete_forever_outlined, color: Colors.redAccent), title: Text(label, style: const TextStyle(color: Colors.redAccent)), onTap: () => showDialog( context: context, builder: (context) { var acknowledged = acknowledgement == null; return StatefulBuilder( builder: (context, setDialogState) => AlertDialog( title: Text(label), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(message), if (details != null) ...[ const SizedBox(height: 12), Text(details!), ], if (acknowledgement != null) CheckboxListTile( contentPadding: EdgeInsets.zero, value: acknowledged, onChanged: (value) => setDialogState(() => acknowledged = value ?? false), title: Text(acknowledgement!), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: Colors.redAccent, ), onPressed: acknowledged ? () { Navigator.pop(context); onConfirm(); } : null, child: const Text('确认清除'), ), ], ), ); }, ), ); } String relativeTime(DateTime time) { final difference = DateTime.now().difference(time); if (difference.inMinutes < 1) return '刚刚'; if (difference.inHours < 1) return '${difference.inMinutes} 分钟前'; if (difference.inDays < 1) return '${difference.inHours} 小时前'; return '${difference.inDays} 天前'; }