import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_remix/flutter_remix.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../../common/config/app_h5_urls.dart'; import '../../../common/router/app_router.dart'; import '../../../common/theme/app_text.dart'; import '../../../common/theme/spacing.dart'; import '../../../common/theme/theme_controller.dart'; import '../../../common/widgets/adaptive.dart'; import '../../../common/widgets/app_toast.dart'; import '../../../common/widgets/circle_back_button.dart'; import '../../auth/application/auth_controller.dart'; import '../../auth/data/auth_models.dart'; import '../../profile/application/profile_settings_controller.dart'; import '../../profile/data/profile_models.dart'; class SettingsPage extends HookConsumerWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final cs = ShadTheme.of(context).colorScheme; final themeMode = ref.watch(themeModeControllerProvider); final profileSettings = ref.watch(profileSettingsControllerProvider); final authAsync = ref.watch(authProvider); final loggedIn = authAsync.valueOrNull is LoggedInAuthState; final contactDebugTapCount = useState(0); final contactLastTapAt = useRef(null); return Scaffold( backgroundColor: cs.background, appBar: AppBar( backgroundColor: cs.background, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, title: Text( '设置', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: cs.foreground, ), ), leading: const CircleBackButton(), ), body: ListView( padding: Adaptive.screenPadding(context, top: 8, bottom: 32), children: [ const _Section('外观'), _ThemeSegmentCard( themeMode: themeMode, onChanged: (mode) => ref.read(themeModeControllerProvider.notifier).set(mode), ), const SizedBox(height: 22), const _Section('资产参数'), _Card( children: [ _SwitchRow( title: '隐藏金额', subtitle: '在资产页和持仓列表中隐藏金额信息', value: profileSettings.amountHidden, onChanged: (_) => ref .read(profileSettingsControllerProvider.notifier) .toggleAmountHidden(), ), Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: FlutterRemix.line_chart_line, label: '涨跌颜色', trailing: profileSettings.priceColorMode.label, onTap: () => _showPriceColorSheet(context, ref), ), Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: FlutterRemix.coins_line, label: '纯度系数', trailing: '${profileSettings.purityPresets.length} 项', onTap: () => _showPuritySheet(context, profileSettings), ), Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: FlutterRemix.exchange_box_line, label: '回收折扣', trailing: '已设置', onTap: () => _showRecycleSheet(context, profileSettings), ), ], ), const SizedBox(height: 22), const _Section('备份'), _Card( children: [ _NavRow( icon: FlutterRemix.download_2_line, label: '本地导出备份', trailing: '导出', onTap: () => _showLocalBackupSheet(context), ), Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: FlutterRemix.cloud_line, label: '云备份', trailing: '占位', onTap: () => _showCloudBackupSheet(context), ), ], ), const SizedBox(height: 22), const _Section('工具'), _Card( children: [ _NavRow( icon: FlutterRemix.layout_grid_line, label: '桌面小组件', trailing: '占位', onTap: () => _showWidgetSheet(context), ), if (kDebugMode) ...[ Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: Icons.bug_report_outlined, label: '调试工具', onTap: () => context.push(AppRoutes.debug), ), ], ], ), const SizedBox(height: 22), const _Section('法律'), _Card( children: [ _NavRow( icon: FlutterRemix.file_list_3_line, label: '用户协议', onTap: () => launchUrlString( AppH5UrlsHelper.withCacheBuster(AppH5Urls.userProtocolUrl), mode: LaunchMode.inAppBrowserView, ), ), Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: FlutterRemix.shield_check_line, label: '隐私政策', onTap: () => launchUrlString( AppH5UrlsHelper.withCacheBuster(AppH5Urls.privacyUrl), mode: LaunchMode.inAppBrowserView, ), ), Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: FlutterRemix.alert_line, label: '免责声明', onTap: () => _showDisclaimerSheet(context), ), ], ), if (loggedIn) ...[ const SizedBox(height: 22), const _Section('账户'), _Card( children: [ _NavRow( icon: FlutterRemix.logout_box_r_line, label: '退出登录', onTap: () => _confirmLogout(context, ref), ), Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: FlutterRemix.delete_bin_line, label: '注销账号', destructive: true, onTap: () => context.push(AppRoutes.deleteAccount), ), ], ), ], const SizedBox(height: 22), const _Section('关于'), _Card( children: [ const _AboutRow(), Divider(height: 1, thickness: 1, color: cs.border), _NavRow( icon: FlutterRemix.mail_line, label: '联系我们', trailing: 'business@hnsiderui.cn', onTap: () { final now = DateTime.now(); final lastTapAt = contactLastTapAt.value; if (lastTapAt == null || now.difference(lastTapAt) > const Duration(milliseconds: 500)) { contactDebugTapCount.value = 0; } contactLastTapAt.value = now; contactDebugTapCount.value += 1; if (contactDebugTapCount.value >= 10) { contactDebugTapCount.value = 0; contactLastTapAt.value = null; context.push(AppRoutes.debug); } }, ), ], ), const SizedBox(height: 14), _RiskFooter(cs: cs), ], ), ); } Future _confirmLogout(BuildContext context, WidgetRef ref) async { final cs = ShadTheme.of(context).colorScheme; final ok = await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '退出登录', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '确定要退出登录吗?', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 22), _LogoutSheetButton( label: '退出', primary: true, onTap: () => Navigator.of(ctx).pop(true), ), const SizedBox(height: 12), _LogoutSheetButton( label: '取消', primary: false, onTap: () => Navigator.of(ctx).pop(false), ), ], ), ), ), ); if (ok == true && context.mounted) { await ref.read(authProvider.notifier).logout(); } } Future _showPriceColorSheet(BuildContext context, WidgetRef ref) async { final cs = ShadTheme.of(context).colorScheme; final current = ref.read(profileSettingsControllerProvider).priceColorMode; final selected = await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '涨跌颜色', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '选择行情上涨和下跌的配色方案。', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 18), _ChoiceButton( label: PriceColorMode.redUpGreenDown.label, selected: current == PriceColorMode.redUpGreenDown, onTap: () => Navigator.of(ctx).pop(PriceColorMode.redUpGreenDown), ), const SizedBox(height: 12), _ChoiceButton( label: PriceColorMode.greenUpRedDown.label, selected: current == PriceColorMode.greenUpRedDown, onTap: () => Navigator.of(ctx).pop(PriceColorMode.greenUpRedDown), ), ], ), ), ), ); if (selected != null && context.mounted) { ref.read(profileSettingsControllerProvider.notifier).setPriceColorMode( selected, ); } } Future _showPuritySheet( BuildContext context, ProfileSettings settings, ) async { final cs = ShadTheme.of(context).colorScheme; await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '纯度系数', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '用于估值换算的本地纯度预设。', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 16), for (final entry in settings.purityPresets.entries) ...[ _ValueRow( label: entry.key, value: '${(entry.value * 100).toStringAsFixed(2)}%', ), if (entry.key != settings.purityPresets.keys.last) ...[ const SizedBox(height: 10), ], ], const SizedBox(height: 18), _SheetPrimaryButton( label: '知道了', onTap: () => Navigator.of(ctx).pop(), ), ], ), ), ), ); } Future _showRecycleSheet( BuildContext context, ProfileSettings settings, ) async { final cs = ShadTheme.of(context).colorScheme; await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '回收折扣', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '用于计算金、铂、银的回收参考值。', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 16), for (final entry in settings.recycleDiscounts.entries) ...[ _ValueRow( label: entry.key.label, value: '${(entry.value * 100).toStringAsFixed(0)}%', ), if (entry.key != settings.recycleDiscounts.keys.last) ...[ const SizedBox(height: 10), ], ], const SizedBox(height: 18), _SheetPrimaryButton( label: '知道了', onTap: () => Navigator.of(ctx).pop(), ), ], ), ), ), ); } Future _showLocalBackupSheet(BuildContext context) async { final cs = ShadTheme.of(context).colorScheme; final ok = await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '本地导出备份', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '将当前持仓、偏好设置和 mock 账号状态导出到本地文件。', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 18), _SheetPrimaryButton( label: '导出备份', onTap: () => Navigator.of(ctx).pop(true), ), const SizedBox(height: 12), _SheetSecondaryButton( label: '取消', onTap: () => Navigator.of(ctx).pop(false), ), ], ), ), ), ); if (ok == true && context.mounted) { toastInfo(msg: '已导出本地备份(mock)'); } } Future _showCloudBackupSheet(BuildContext context) async { final cs = ShadTheme.of(context).colorScheme; await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '云备份', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '后续会接入云同步与多端恢复,这里先保留入口。', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 18), _SheetPrimaryButton( label: '知道了', onTap: () => Navigator.of(ctx).pop(), ), ], ), ), ), ); } Future _showWidgetSheet(BuildContext context) async { final cs = ShadTheme.of(context).colorScheme; await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '桌面小组件', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '桌面行情小组件正在整理中,先保留入口。', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 18), _SheetPrimaryButton( label: '知道了', onTap: () => Navigator.of(ctx).pop(), ), ], ), ), ), ); } Future _showDisclaimerSheet(BuildContext context) async { final cs = ShadTheme.of(context).colorScheme; await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '免责声明', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '金值提供的价格、资讯和估值均为本地 mock 数据,仅用于产品原型验证,不构成投资建议。', style: TextStyle( fontSize: 13, height: 1.55, color: cs.mutedForeground, ), ), const SizedBox(height: 18), _SheetPrimaryButton( label: '知道了', onTap: () => Navigator.of(ctx).pop(), ), ], ), ), ), ); } } class _Section extends StatelessWidget { const _Section(this.title); final String title; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: cs.foreground, ), ), ); } } class _Card extends StatelessWidget { const _Card({required this.children}); final List children; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Container( decoration: BoxDecoration( color: cs.card, border: Border.all(color: cs.border), borderRadius: BorderRadius.circular(Spacing.radiusCard), ), clipBehavior: Clip.antiAlias, child: Column( children: [ for (var i = 0; i < children.length; i++) ...[ if (i > 0) Divider(height: 1, thickness: 1, color: cs.border), children[i], ], ], ), ); } } class _ThemeSegmentCard extends StatelessWidget { const _ThemeSegmentCard({required this.themeMode, required this.onChanged}); final ThemeMode themeMode; final ValueChanged onChanged; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final options = <(ThemeMode, String, IconData)>[ (ThemeMode.system, '跟随系统', Icons.brightness_auto_outlined), (ThemeMode.light, '浅色', Icons.light_mode_outlined), (ThemeMode.dark, '深色', Icons.dark_mode_outlined), ]; return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: cs.secondary, border: Border.all(color: cs.border), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ for (final opt in options) Expanded( child: Padding( padding: EdgeInsets.only( left: opt == options.first ? 0 : 2, right: opt == options.last ? 0 : 2, ), child: _ThemeSegmentButton( label: opt.$2, icon: opt.$3, selected: themeMode == opt.$1, onTap: () => onChanged(opt.$1), ), ), ), ], ), ); } } class _ThemeSegmentButton extends StatelessWidget { const _ThemeSegmentButton({ required this.label, required this.icon, required this.selected, required this.onTap, }); final String label; final IconData icon; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final fg = selected ? cs.foreground : cs.mutedForeground; return Material( color: selected ? cs.background : Colors.transparent, borderRadius: BorderRadius.circular(10), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Container( height: 44, alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), boxShadow: selected ? [ const BoxShadow( color: Color(0x0A000000), blurRadius: 10, offset: Offset(0, 2), ), ] : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 17, color: fg), const SizedBox(width: 8), Text( label, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: fg, ), ), ], ), ), ), ); } } class _SwitchRow extends StatelessWidget { const _SwitchRow({ required this.title, required this.subtitle, required this.value, required this.onChanged, }); final String title; final String subtitle; final bool value; final ValueChanged onChanged; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: cs.foreground, ), ), const SizedBox(height: 6), Text( subtitle, style: AppText.meta.copyWith( fontSize: 13, color: cs.mutedForeground, ), ), ], ), ), const SizedBox(width: 16), ShadSwitch( value: value, onChanged: onChanged, checkedTrackColor: cs.primary, thumbColor: cs.background, width: 44, height: 26, margin: 2, ), ], ), ); } } class _NavRow extends StatelessWidget { const _NavRow({ required this.icon, required this.label, this.trailing, this.onTap, this.destructive = false, }); final IconData icon; final String label; final String? trailing; final VoidCallback? onTap; final bool destructive; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final color = destructive ? cs.destructive : cs.foreground; return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), child: Row( children: [ SizedBox(width: 24, child: Icon(icon, size: 20, color: color)), const SizedBox(width: 13), Expanded( child: Text(label, style: TextStyle(fontSize: 15, color: color)), ), if (trailing != null) Text( trailing!, style: AppText.meta.copyWith( fontSize: 13, color: cs.mutedForeground, ), ), if (onTap != null && trailing == null) Icon( FlutterRemix.arrow_right_s_line, size: 20, color: cs.mutedForeground, ), ], ), ), ); } } class _AboutRow extends StatelessWidget { const _AboutRow(); @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snap) { final v = snap.hasData ? 'v${snap.data!.version}+${snap.data!.buildNumber}' : ''; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), child: Row( children: [ SizedBox( width: 24, child: Icon( FlutterRemix.information_line, size: 20, color: cs.foreground, ), ), const SizedBox(width: 13), Expanded( child: Text( '金值', style: TextStyle(fontSize: 15, color: cs.foreground), ), ), Text( v, style: AppText.meta.copyWith( fontSize: 13, color: cs.mutedForeground, ), ), ], ), ); }, ); } } class _RiskFooter extends StatelessWidget { const _RiskFooter({required this.cs}); final ShadColorScheme cs; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: BoxDecoration( color: cs.card, border: Border.all(color: cs.border), borderRadius: BorderRadius.circular(Spacing.radiusCard), ), child: Text( '风险提示 · 数据来源 · 免责\n价格、资讯与估值均为本地 mock 数据,仅用于产品原型验证,不构成投资建议。', style: AppText.meta.copyWith( fontSize: 12, color: cs.mutedForeground, height: 1.65, ), ), ); } } class _ChoiceButton extends StatelessWidget { const _ChoiceButton({ required this.label, required this.selected, required this.onTap, }); final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: selected ? cs.primary : cs.secondary, borderRadius: BorderRadius.circular(12), border: Border.all( color: selected ? cs.primary : cs.border, ), ), child: Text( label, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: selected ? cs.primaryForeground : cs.foreground, ), ), ), ); } } class _SheetPrimaryButton extends StatelessWidget { const _SheetPrimaryButton({required this.label, required this.onTap}); final String label; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: cs.primary, borderRadius: BorderRadius.circular(12), ), child: Text( label, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: cs.primaryForeground, ), ), ), ); } } class _SheetSecondaryButton extends StatelessWidget { const _SheetSecondaryButton({required this.label, required this.onTap}); final String label; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: cs.secondary, borderRadius: BorderRadius.circular(12), border: Border.all(color: cs.border), ), child: Text( label, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: cs.foreground, ), ), ), ); } } class _ValueRow extends StatelessWidget { const _ValueRow({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Row( children: [ Expanded( child: Text( label, style: TextStyle(fontSize: 15, color: cs.foreground), ), ), Text( value, style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), ], ); } } class _LogoutSheetButton extends StatelessWidget { const _LogoutSheetButton({ required this.label, required this.primary, required this.onTap, }); final String label; final bool primary; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: primary ? cs.primary : cs.secondary, borderRadius: BorderRadius.circular(12), border: primary ? null : Border.all(color: cs.border), ), child: Text( label, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: primary ? cs.primaryForeground : cs.foreground, ), ), ), ); } }