import 'package:flutter/material.dart'; import '../../../../core/logging/logger.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/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../data/apis/divination_api.dart'; import '../../data/models/divination_params.dart'; import '../../data/models/divination_result.dart'; import '../../data/services/divination_run_service.dart'; import 'divination_result_screen.dart'; enum _ProcessingStep { preparing, deriving, done } class DivinationProcessingScreen extends StatefulWidget { const DivinationProcessingScreen({ super.key, required this.params, required this.yaoStates, required this.runService, this.divinationApi, required this.onCompleted, }); final DivinationParams params; final List yaoStates; final DivinationRunService runService; final DivinationApi? divinationApi; final Future Function(DivinationResultData result) onCompleted; @override State createState() => _DivinationProcessingScreenState(); } class _DivinationProcessingScreenState extends State with TickerProviderStateMixin { static final Logger _logger = getLogger( 'features.divination.processing_screen', ); static const int _iChingCardCount = 8; _ProcessingStep _step = _ProcessingStep.preparing; DivinationResultData? _resultData; String? _errorMessage; late final AnimationController _cardRotationController; int _currentCardIndex = 0; @override void initState() { super.initState(); _cardRotationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), )..addStatusListener((status) { if (status != AnimationStatus.completed || !mounted) { return; } setState(() { _currentCardIndex = (_currentCardIndex + 1) % _iChingCardCount; }); _cardRotationController.forward(from: 0); }); _cardRotationController.forward(); _startRun(); } @override void dispose() { _cardRotationController.dispose(); super.dispose(); } Future _startRun() async { try { final aggregate = await widget.runService.run( params: widget.params, yaoStates: widget.yaoStates, onDerived: () { if (!mounted) { return; } setState(() { _step = _ProcessingStep.deriving; }); }, onTextMessageEnd: () { if (!mounted) { return; } setState(() { _step = _ProcessingStep.done; }); }, ); if (!mounted) { return; } setState(() { _resultData = aggregate.toViewData(widget.params); _step = _ProcessingStep.done; }); _cardRotationController.stop(); final data = _resultData; if (data != null) { try { await widget.onCompleted(data); } catch (error, stackTrace) { _logger.warning( message: 'Failed to persist post-run side effects', extra: {'error': error.toString()}, ); _logger.debug( message: 'Post-run side effect stack trace', extra: {'stackTrace': stackTrace.toString()}, ); } } } catch (error, stackTrace) { _logger.error( message: 'Divination processing failed while waiting result events', error: error, stackTrace: stackTrace, extra: { 'step': _step.name, 'method': widget.params.method.name, 'questionType': widget.params.questionType.name, }, ); if (!mounted) { return; } final l10n = AppLocalizations.of(context)!; final message = error is ApiProblem ? mapApiProblemToMessage(error, l10n) : l10n.errorRequestGeneric; setState(() { _errorMessage = message; }); Toast.show(context, message, type: ToastType.error); } } void _openResult() { final data = _resultData; if (data == null) { return; } Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (_) => DivinationResultScreen( data: data, divinationApi: data.threadId == null ? null : widget.divinationApi, enableIntroTransition: true, ), ), (route) => route.isFirst, ); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; final statusText = switch (_step) { _ProcessingStep.preparing => l10n.transitionPreparing, _ProcessingStep.deriving => l10n.transitionDeriving, _ProcessingStep.done => l10n.transitionDone, }; final cardDataList = _iChingCardData(l10n); final canContinue = _step == _ProcessingStep.done && _resultData != null; return Scaffold( backgroundColor: colors.surface, body: SafeArea( child: Center( child: Padding( padding: const EdgeInsets.all(AppSpacing.xl), child: _errorMessage == null ? GestureDetector( onTap: canContinue ? _openResult : null, child: AnimatedBuilder( animation: _cardRotationController, builder: (context, _) { final angle = canContinue ? 0.0 : _rotationForProgress( _cardRotationController.value, ); final card = cardDataList[_currentCardIndex]; return Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.0011) ..rotateY(angle), child: Container( width: 220, height: 320, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ colors.primaryContainer.withValues( alpha: 0.55, ), colors.secondaryContainer.withValues( alpha: 0.38, ), colors.surface, ], ), borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: colors.primary.withValues(alpha: 0.3), ), boxShadow: [ BoxShadow( color: colors.shadow.withValues(alpha: 0.18), blurRadius: 26, offset: const Offset(0, 14), ), ], ), padding: const EdgeInsets.all(AppSpacing.lg), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (canContinue) Icon( Icons.visibility, color: colors.primary, size: 34, ) else ...[ Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.sm, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: colors.surface.withValues( alpha: 0.75, ), borderRadius: BorderRadius.circular( AppRadius.full, ), ), child: Text( l10n.iChingTitle, style: Theme.of(context) .textTheme .labelSmall ?.copyWith( color: colors.primary, letterSpacing: 0.3, fontWeight: FontWeight.w700, ), ), ), const SizedBox(height: AppSpacing.md), Text( card.$1, textAlign: TextAlign.center, style: Theme.of(context) .textTheme .titleSmall ?.copyWith( color: colors.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: AppSpacing.md), Text( card.$2, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall ?.copyWith( height: 1.5, color: colors.onSurface.withValues( alpha: 0.86, ), ), ), ], const SizedBox(height: AppSpacing.lg), Text( statusText, textAlign: TextAlign.center, style: Theme.of( context, ).textTheme.titleMedium, ), ], ), ), ); }, ), ) : Text( _errorMessage!, textAlign: TextAlign.center, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(color: colors.error), ), ), ), ), ); } double _rotationForProgress(double progress) { if (progress < 0.25) { return (1 - progress / 0.25) * (3.1415926 / 2); } if (progress < 0.75) { return 0; } return ((progress - 0.75) / 0.25) * (3.1415926 / 2); } List<(String, String)> _iChingCardData(AppLocalizations l10n) { return <(String, String)>[ (l10n.processingCardQianTitle, l10n.processingCardQianQuote), (l10n.processingCardDuiTitle, l10n.processingCardDuiQuote), (l10n.processingCardLiTitle, l10n.processingCardLiQuote), (l10n.processingCardZhenTitle, l10n.processingCardZhenQuote), (l10n.processingCardXunTitle, l10n.processingCardXunQuote), (l10n.processingCardKanTitle, l10n.processingCardKanQuote), (l10n.processingCardGenTitle, l10n.processingCardGenQuote), (l10n.processingCardKunTitle, l10n.processingCardKunQuote), ]; } }