334 lines
12 KiB
Dart
334 lines
12 KiB
Dart
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/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,
|
|
required this.onCompleted,
|
|
});
|
|
|
|
final DivinationParams params;
|
|
final List<YaoType> yaoStates;
|
|
final DivinationRunService runService;
|
|
final Future<void> Function(DivinationResultData result) onCompleted;
|
|
|
|
@override
|
|
State<DivinationProcessingScreen> createState() =>
|
|
_DivinationProcessingScreenState();
|
|
}
|
|
|
|
class _DivinationProcessingScreenState extends State<DivinationProcessingScreen>
|
|
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<void> _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: <String, dynamic>{'error': error.toString()},
|
|
);
|
|
_logger.debug(
|
|
message: 'Post-run side effect stack trace',
|
|
extra: <String, dynamic>{'stackTrace': stackTrace.toString()},
|
|
);
|
|
}
|
|
}
|
|
} catch (error, stackTrace) {
|
|
_logger.error(
|
|
message: 'Divination processing failed while waiting result events',
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
extra: <String, dynamic>{
|
|
'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).pushReplacement(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => DivinationResultScreen(data: data),
|
|
),
|
|
);
|
|
}
|
|
|
|
@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),
|
|
];
|
|
}
|
|
}
|