import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import '../../../../core/network/api_problem.dart'; import '../../../../core/network/api_problem_mapper.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../shared/theme/design_tokens.dart'; import '../../../../shared/widgets/divination/divination_shared_widgets.dart'; import '../../../../shared/widgets/divination/divination_terms.dart'; import '../../../../shared/widgets/divination/yao_legend.dart'; import '../../../../shared/widgets/divination/yao_line_row.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../../../shared/widgets/date_time_picker/date_time_picker_bottom_sheet.dart'; import '../../data/models/divination_params.dart'; import '../../data/services/divination_run_service.dart'; import 'divination_result_screen.dart'; class ManualDivinationScreen extends StatefulWidget { const ManualDivinationScreen({ super.key, required this.params, required this.runService, }); final DivinationParams params; final DivinationRunService runService; @override State createState() => _ManualDivinationScreenState(); } class _ManualDivinationScreenState extends State with TickerProviderStateMixin { late DateTime _selectedTime; final List _selectedYaos = List.filled(6, null); late final AnimationController _blinkController; bool _submitting = false; @override void initState() { super.initState(); _selectedTime = widget.params.divinationTime; _blinkController = AnimationController( vsync: this, duration: const Duration(milliseconds: 500), )..repeat(reverse: true); } @override void dispose() { _blinkController.dispose(); super.dispose(); } bool get _allSelected => _selectedYaos.every((v) => v != null); @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; return Scaffold( backgroundColor: colors.surface, appBar: AppBar( title: Text(l10n.manualScreenTitle), centerTitle: true, backgroundColor: colors.surface, surfaceTintColor: colors.surface, ), body: SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.xl), child: Column( children: [ _buildInstruction(), const SizedBox(height: AppSpacing.lg), _TimeCard(selectedTime: _selectedTime, onPickTime: _pickTime), const SizedBox(height: AppSpacing.lg), _YaoSelectionCard( selectedYaos: _selectedYaos, blinkAnimation: _blinkController, onSelect: _onSelectYao, onNeedTip: _showOrderTip, ), const SizedBox(height: AppSpacing.xl), AnimatedBuilder( animation: _blinkController, builder: (context, _) { final base = colors.primary; return SizedBox( width: double.infinity, height: 50, child: FilledButton( onPressed: _allSelected && !_submitting ? _submitRun : null, style: FilledButton.styleFrom( backgroundColor: _allSelected ? base.withValues( alpha: 0.6 + _blinkController.value * 0.4, ) : base, ), child: _submitting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(l10n.manualStartResolve), ), ); }, ), ], ), ), ); } Widget _buildInstruction() { final l10n = AppLocalizations.of(context)!; return DivinationInstructionCard( text: l10n.manualYaoInstruction, onTap: () { showDialog( context: context, builder: (_) { return DivinationGuideDialog( title: l10n.manualSelectYaoTitle, guideImages: const [ 'assets/images/qigua/lc2.jpg', 'assets/images/qigua/lc3.jpg', 'assets/images/qigua/lc4.jpg', 'assets/images/qigua/lc5.jpg', ], instructionText: l10n.manualYaoTipContent, ); }, ); }, ); } Future _pickTime() async { final result = await showDateTimePickerBottomSheet( context: context, initialDateTime: _selectedTime, minDateTime: DateTime(2000), maxDateTime: DateTime(2100), ); if (result == null || !mounted) return; setState(() { _selectedTime = result; }); } void _onSelectYao(int index, YaoType type) { setState(() { _selectedYaos[index] = type; }); HapticFeedback.selectionClick(); } Future _showOrderTip() async { final l10n = AppLocalizations.of(context)!; await showDialog( context: context, builder: (context) { return AlertDialog( title: Text(l10n.manualYaoTipTitle), content: Text(l10n.manualYaoTipContent), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l10n.divinationIAcknowledge), ), ], ); }, ); } Future _submitRun() async { final l10n = AppLocalizations.of(context)!; setState(() { _submitting = true; }); try { final aggregate = await widget.runService.run( params: widget.params.copyWith(divinationTime: _selectedTime), yaoStates: _selectedYaos.cast(), ); if (!mounted) { return; } Navigator.of(context).push( MaterialPageRoute( builder: (_) => DivinationResultScreen( data: aggregate.toViewData( widget.params.copyWith(divinationTime: _selectedTime), ), ), ), ); } catch (error) { if (!mounted) { return; } final message = error is ApiProblem ? mapApiProblemToMessage(error, l10n) : l10n.errorRequestGeneric; Toast.show(context, message, type: ToastType.error); } finally { if (!mounted) { return; } setState(() { _submitting = false; }); } } } class _TimeCard extends StatelessWidget { const _TimeCard({required this.selectedTime, required this.onPickTime}); final DateTime selectedTime; final VoidCallback onPickTime; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; return Card( margin: EdgeInsets.zero, color: colors.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.manualSelectTime, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.md), Row( children: [ Expanded( child: Text( DateFormat.yMd( Localizations.localeOf(context).toString(), ).add_Hm().format(selectedTime), style: Theme.of(context).textTheme.titleMedium, ), ), OutlinedButton( onPressed: onPickTime, child: Text(l10n.divinationModify), ), ], ), ], ), ), ); } } class _YaoSelectionCard extends StatelessWidget { const _YaoSelectionCard({ required this.selectedYaos, required this.blinkAnimation, required this.onSelect, required this.onNeedTip, }); final List selectedYaos; final Animation blinkAnimation; final void Function(int, YaoType) onSelect; final Future Function() onNeedTip; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final rowNames = DivinationTerms.yaoNames.reversed.toList(); return Card( margin: EdgeInsets.zero, color: colors.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.manualSpecifyYaoCombo, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.md), ...List.generate(rowNames.length, (i) { final yaoIndex = 5 - i; final current = selectedYaos[yaoIndex]; final enabled = yaoIndex == 0 || selectedYaos[yaoIndex - 1] != null; final shouldBlink = enabled && current == null; return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.md), child: AnimatedBuilder( animation: blinkAnimation, builder: (context, _) { final borderColor = shouldBlink ? colors.primary.withValues( alpha: 0.3 + blinkAnimation.value * 0.7, ) : (enabled ? colors.primary : colors.outline); return OutlinedButton( style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(52), padding: const EdgeInsets.symmetric( horizontal: AppSpacing.sm, ), side: BorderSide( color: borderColor, width: shouldBlink ? 2 : 1, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.sm), ), ), onPressed: () { if (!enabled) { onNeedTip(); return; } _showOptionsDialog(context, yaoIndex, onSelect); }, child: YaoLineRow( name: rowNames[i], type: current ?? YaoType.undetermined, showChangeMark: true, enabled: enabled, ), ); }, ), ); }), const SizedBox(height: AppSpacing.xs), const Align(alignment: Alignment.centerLeft, child: YaoLegend()), ], ), ), ); } Future _showOptionsDialog( BuildContext context, int yaoIndex, void Function(int, YaoType) onSelect, ) { final l10n = AppLocalizations.of(context)!; final options = <(String, YaoType)>[ ( '${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYang]} (\u2014)\uFF1A\u82B1\u5B57\u5B57', YaoType.youngYang, ), ( '${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYin]} (--)\uFF1A\u5B57\u82B1\u82B1', YaoType.youngYin, ), ( '${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYang]} (\u2014\u25CB)\uFF1A\u82B1\u82B1\u82B1', YaoType.oldYang, ), ( '${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYin]} (--\u00D7)\uFF1A\u5B57\u5B57\u5B57', YaoType.oldYin, ), ]; return showDialog( context: context, builder: (context) { return AlertDialog( title: Text(l10n.manualSelectYaoTitle), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ for (final option in options) Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: OutlinedButton( onPressed: () { onSelect(yaoIndex, option.$2); Navigator.of(context).pop(); }, style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(44), ), child: Text(option.$1), ), ), const SizedBox(height: AppSpacing.sm), const Align( alignment: Alignment.centerLeft, child: Text('字花图片说明:'), ), const SizedBox(height: AppSpacing.sm), Image.asset( 'assets/images/qigua/zihua.jpg', width: double.infinity, height: 180, fit: BoxFit.contain, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l10n.divinationClose), ), ], ); }, ); } }