2026-04-03 16:56:47 +08:00
|
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
|
|
|
2026-04-08 17:23:02 +08:00
|
|
|
|
import '../../../../core/logging/logger.dart';
|
2026-04-03 16:56:47 +08:00
|
|
|
|
import '../../../../l10n/app_localizations.dart';
|
|
|
|
|
|
import '../../../../shared/theme/app_color_palette.dart';
|
|
|
|
|
|
import '../../../../shared/theme/design_tokens.dart';
|
|
|
|
|
|
import '../../../../shared/widgets/divination/divination_terms.dart';
|
|
|
|
|
|
import '../../../../shared/widgets/divination/yao_glyph.dart';
|
|
|
|
|
|
import '../../../../shared/widgets/divination/yao_legend.dart';
|
|
|
|
|
|
import '../../../../shared/widgets/toast/toast.dart';
|
|
|
|
|
|
import '../../../../shared/widgets/toast/toast_type.dart';
|
2026-04-08 17:23:02 +08:00
|
|
|
|
import '../../data/apis/divination_api.dart';
|
2026-04-03 16:56:47 +08:00
|
|
|
|
import '../../data/models/divination_params.dart';
|
|
|
|
|
|
import '../../data/models/divination_result.dart';
|
2026-04-08 17:23:02 +08:00
|
|
|
|
import 'follow_up_chat_screen.dart';
|
2026-04-03 16:56:47 +08:00
|
|
|
|
|
|
|
|
|
|
class DivinationResultScreen extends StatefulWidget {
|
2026-04-08 17:23:02 +08:00
|
|
|
|
const DivinationResultScreen({
|
|
|
|
|
|
super.key,
|
|
|
|
|
|
required this.data,
|
|
|
|
|
|
this.divinationApi,
|
|
|
|
|
|
this.enableIntroTransition = false,
|
|
|
|
|
|
});
|
2026-04-03 16:56:47 +08:00
|
|
|
|
|
|
|
|
|
|
final DivinationResultData data;
|
2026-04-08 17:23:02 +08:00
|
|
|
|
final DivinationApi? divinationApi;
|
|
|
|
|
|
final bool enableIntroTransition;
|
2026-04-03 16:56:47 +08:00
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<DivinationResultScreen> createState() => _DivinationResultScreenState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _DivinationResultScreenState extends State<DivinationResultScreen> {
|
2026-04-08 17:23:02 +08:00
|
|
|
|
static final Logger _logger = getLogger('features.divination.result_screen');
|
|
|
|
|
|
|
|
|
|
|
|
bool _showIntro = false;
|
2026-04-03 19:04:46 +08:00
|
|
|
|
bool _introCollapsed = false;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
Rect? _introTargetRect;
|
|
|
|
|
|
final GlobalKey _stackKey = GlobalKey();
|
|
|
|
|
|
final GlobalKey _finalSignCardKey = GlobalKey();
|
2026-04-08 17:23:02 +08:00
|
|
|
|
bool _followUpEligibilityLoading = false;
|
|
|
|
|
|
bool _canSendFollowUp = true;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
void _backToHome() {
|
|
|
|
|
|
final navigator = Navigator.of(context);
|
|
|
|
|
|
navigator.popUntil((route) => route.isFirst);
|
|
|
|
|
|
}
|
2026-04-03 16:56:47 +08:00
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
2026-04-08 17:23:02 +08:00
|
|
|
|
if (widget.enableIntroTransition) {
|
|
|
|
|
|
_showIntro = true;
|
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
|
_prepareIntro();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
_loadFollowUpEligibility();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _loadFollowUpEligibility() async {
|
|
|
|
|
|
if (widget.divinationApi == null || widget.data.threadId == null) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_followUpEligibilityLoading = true;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
});
|
2026-04-08 17:23:02 +08:00
|
|
|
|
try {
|
|
|
|
|
|
final messages = await widget.divinationApi!.getSessionMessages(
|
|
|
|
|
|
threadId: widget.data.threadId!,
|
|
|
|
|
|
);
|
|
|
|
|
|
final userCount = messages.where((msg) => msg.role == 'user').length;
|
|
|
|
|
|
if (!mounted) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_canSendFollowUp = userCount < 2;
|
|
|
|
|
|
_followUpEligibilityLoading = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
|
|
_logger.error(
|
|
|
|
|
|
message: 'Failed to load follow-up eligibility',
|
|
|
|
|
|
error: error,
|
|
|
|
|
|
stackTrace: stackTrace,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!mounted) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_canSendFollowUp = false;
|
|
|
|
|
|
_followUpEligibilityLoading = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-06 01:28:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _prepareIntro() async {
|
|
|
|
|
|
for (int i = 0; i < 12; i++) {
|
|
|
|
|
|
if (!mounted) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_measureIntroTargetRect()) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
await Future<void>.delayed(const Duration(milliseconds: 16));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!mounted) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-03 19:04:46 +08:00
|
|
|
|
_playIntro();
|
2026-04-03 16:56:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 01:28:10 +08:00
|
|
|
|
bool _measureIntroTargetRect() {
|
|
|
|
|
|
final stackContext = _stackKey.currentContext;
|
|
|
|
|
|
final targetContext = _finalSignCardKey.currentContext;
|
|
|
|
|
|
if (stackContext == null || targetContext == null) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
final stackRender = stackContext.findRenderObject();
|
|
|
|
|
|
final targetRender = targetContext.findRenderObject();
|
|
|
|
|
|
if (stackRender is! RenderBox || targetRender is! RenderBox) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
final offset = targetRender.localToGlobal(
|
|
|
|
|
|
Offset.zero,
|
|
|
|
|
|
ancestor: stackRender,
|
|
|
|
|
|
);
|
|
|
|
|
|
final targetRect = offset & targetRender.size;
|
|
|
|
|
|
if (_introTargetRect == targetRect) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_introTargetRect = targetRect;
|
|
|
|
|
|
});
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 19:04:46 +08:00
|
|
|
|
Future<void> _playIntro() async {
|
2026-04-06 01:28:10 +08:00
|
|
|
|
await Future<void>.delayed(const Duration(milliseconds: 180));
|
2026-04-03 16:56:47 +08:00
|
|
|
|
if (!mounted) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setState(() {
|
2026-04-03 19:04:46 +08:00
|
|
|
|
_introCollapsed = true;
|
2026-04-03 16:56:47 +08:00
|
|
|
|
});
|
2026-04-06 01:28:10 +08:00
|
|
|
|
await Future<void>.delayed(const Duration(milliseconds: 1450));
|
2026-04-03 16:56:47 +08:00
|
|
|
|
if (!mounted) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setState(() {
|
2026-04-03 19:04:46 +08:00
|
|
|
|
_showIntro = false;
|
2026-04-03 16:56:47 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 01:28:10 +08:00
|
|
|
|
Rect _introStartRect(Size size) {
|
|
|
|
|
|
const startWidth = 332.0;
|
|
|
|
|
|
const startHeight = 234.0;
|
|
|
|
|
|
return Rect.fromLTWH(
|
|
|
|
|
|
(size.width - startWidth) / 2,
|
|
|
|
|
|
(size.height - startHeight) / 2,
|
|
|
|
|
|
startWidth,
|
|
|
|
|
|
startHeight,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:56:47 +08:00
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
|
|
|
|
|
final palette = Theme.of(context).extension<AppColorPalette>()!;
|
|
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
return PopScope<void>(
|
|
|
|
|
|
canPop: false,
|
|
|
|
|
|
onPopInvokedWithResult: (didPop, result) {
|
|
|
|
|
|
if (didPop) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
_backToHome();
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Scaffold(
|
2026-04-03 16:56:47 +08:00
|
|
|
|
backgroundColor: colors.surface,
|
2026-04-06 01:28:10 +08:00
|
|
|
|
appBar: AppBar(
|
|
|
|
|
|
leading: IconButton(
|
|
|
|
|
|
onPressed: _backToHome,
|
|
|
|
|
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
|
|
|
|
|
),
|
|
|
|
|
|
backgroundColor: colors.surface,
|
|
|
|
|
|
surfaceTintColor: colors.surface,
|
|
|
|
|
|
title: Text(l10n.resultScreenTitle),
|
|
|
|
|
|
centerTitle: true,
|
|
|
|
|
|
),
|
2026-04-08 17:23:02 +08:00
|
|
|
|
bottomNavigationBar: _buildFollowUpBar(context),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
body: LayoutBuilder(
|
|
|
|
|
|
builder: (context, constraints) {
|
|
|
|
|
|
final stackSize = Size(constraints.maxWidth, constraints.maxHeight);
|
|
|
|
|
|
final startRect = _introStartRect(stackSize);
|
|
|
|
|
|
final targetRect = _introTargetRect ?? startRect;
|
|
|
|
|
|
final currentRect = _introCollapsed ? targetRect : startRect;
|
|
|
|
|
|
|
|
|
|
|
|
return Stack(
|
|
|
|
|
|
key: _stackKey,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
AnimatedOpacity(
|
|
|
|
|
|
opacity: _showIntro ? 0 : 1,
|
|
|
|
|
|
duration: const Duration(milliseconds: 260),
|
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(
|
|
|
|
|
|
AppSpacing.xl,
|
|
|
|
|
|
AppSpacing.lg,
|
|
|
|
|
|
AppSpacing.xl,
|
|
|
|
|
|
AppSpacing.xl,
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
child: Column(
|
2026-04-03 19:04:46 +08:00
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
2026-04-06 01:28:10 +08:00
|
|
|
|
_ResultHeader(data: widget.data),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
_SignCard(
|
|
|
|
|
|
key: _finalSignCardKey,
|
|
|
|
|
|
signType: widget.data.signType,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
_KeywordCard(keywords: widget.data.keywords),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
_AnalysisCard(
|
|
|
|
|
|
title: l10n.resultConclusion,
|
|
|
|
|
|
content: widget.data.conclusion,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
2026-04-07 18:41:08 +08:00
|
|
|
|
_FocusPointsCard(points: widget.data.focusPoints),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
_AnalysisCard(
|
|
|
|
|
|
title: l10n.resultAnalysis,
|
|
|
|
|
|
content: widget.data.analysis,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
_AnalysisCard(
|
|
|
|
|
|
title: l10n.resultSuggestion,
|
|
|
|
|
|
content: widget.data.suggestion,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
Container(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: palette.warningContainer,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Icon(
|
|
|
|
|
|
Icons.warning,
|
|
|
|
|
|
color: palette.warning,
|
|
|
|
|
|
size: 20,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: AppSpacing.sm),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
l10n.resultWarning,
|
|
|
|
|
|
style: Theme.of(context).textTheme.bodyMedium
|
|
|
|
|
|
?.copyWith(
|
|
|
|
|
|
color: palette.warning,
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
|
height: 1.35,
|
|
|
|
|
|
),
|
2026-04-03 19:04:46 +08:00
|
|
|
|
),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-04-03 19:04:46 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
const SizedBox(height: AppSpacing.xl),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
l10n.resultBasicInfo,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
_InfoCard(data: widget.data),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.xl),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
l10n.resultHexagramDetail,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
_HexagramDetailCard(data: widget.data),
|
2026-04-03 19:04:46 +08:00
|
|
|
|
],
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
2026-04-03 19:04:46 +08:00
|
|
|
|
),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
),
|
|
|
|
|
|
if (_showIntro)
|
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
|
child: IgnorePointer(
|
|
|
|
|
|
child: ColoredBox(color: colors.surface),
|
|
|
|
|
|
),
|
2026-04-03 19:04:46 +08:00
|
|
|
|
),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
if (_showIntro)
|
|
|
|
|
|
AnimatedPositioned(
|
|
|
|
|
|
duration: const Duration(milliseconds: 1450),
|
|
|
|
|
|
curve: Curves.easeInOutCubicEmphasized,
|
|
|
|
|
|
left: currentRect.left,
|
|
|
|
|
|
top: currentRect.top,
|
|
|
|
|
|
width: currentRect.width,
|
|
|
|
|
|
height: currentRect.height,
|
|
|
|
|
|
child: IgnorePointer(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
|
|
|
|
image: DecorationImage(
|
|
|
|
|
|
image: AssetImage(
|
|
|
|
|
|
_signImageAssetForType(
|
|
|
|
|
|
context,
|
|
|
|
|
|
widget.data.signType,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
),
|
|
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: colors.shadow.withValues(alpha: 0.24),
|
|
|
|
|
|
blurRadius: 24,
|
|
|
|
|
|
offset: const Offset(0, 10),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
2026-04-03 19:04:46 +08:00
|
|
|
|
),
|
2026-04-08 17:23:02 +08:00
|
|
|
|
Positioned(
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
bottom: 0,
|
|
|
|
|
|
height: 80,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
begin: Alignment.topCenter,
|
|
|
|
|
|
end: Alignment.bottomCenter,
|
|
|
|
|
|
colors: [
|
|
|
|
|
|
colors.surface.withValues(alpha: 0),
|
|
|
|
|
|
colors.surface,
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-08 17:23:02 +08:00
|
|
|
|
|
|
|
|
|
|
Widget? _buildFollowUpBar(BuildContext context) {
|
|
|
|
|
|
if (widget.divinationApi == null || widget.data.threadId == null) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
|
|
|
|
|
return SafeArea(
|
|
|
|
|
|
top: false,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(
|
|
|
|
|
|
AppSpacing.md,
|
|
|
|
|
|
AppSpacing.sm,
|
|
|
|
|
|
AppSpacing.md,
|
|
|
|
|
|
AppSpacing.md,
|
|
|
|
|
|
),
|
|
|
|
|
|
decoration: BoxDecoration(color: colors.surface),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
_canSendFollowUp
|
|
|
|
|
|
? l10n.followUpEntryHint
|
|
|
|
|
|
: l10n.followUpQuotaUsed,
|
|
|
|
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: AppSpacing.sm),
|
|
|
|
|
|
FilledButton(
|
|
|
|
|
|
onPressed: _followUpEligibilityLoading
|
|
|
|
|
|
? null
|
|
|
|
|
|
: () async {
|
|
|
|
|
|
await Navigator.of(context).push(
|
|
|
|
|
|
MaterialPageRoute<void>(
|
|
|
|
|
|
builder: (_) => FollowUpChatScreen(
|
|
|
|
|
|
result: widget.data,
|
|
|
|
|
|
api: widget.divinationApi!,
|
|
|
|
|
|
threadId: widget.data.threadId!,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!mounted) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
await _loadFollowUpEligibility();
|
|
|
|
|
|
},
|
|
|
|
|
|
child: _followUpEligibilityLoading
|
|
|
|
|
|
? SizedBox(
|
|
|
|
|
|
width: 18,
|
|
|
|
|
|
height: 18,
|
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
|
color: colors.onPrimary,
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
: Text(
|
|
|
|
|
|
_canSendFollowUp
|
|
|
|
|
|
? l10n.followUpEntryAction
|
|
|
|
|
|
: l10n.followUpViewHistory,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-03 16:56:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _ResultHeader extends StatelessWidget {
|
|
|
|
|
|
const _ResultHeader({required this.data});
|
|
|
|
|
|
|
|
|
|
|
|
final DivinationResultData data;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
|
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
|
return Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
l10n.resultAIAnalysis,
|
|
|
|
|
|
style: Theme.of(
|
|
|
|
|
|
context,
|
|
|
|
|
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
|
|
|
|
|
),
|
|
|
|
|
|
const Spacer(),
|
|
|
|
|
|
TextButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
final payload = <String, dynamic>{
|
|
|
|
|
|
'signType': data.signType,
|
|
|
|
|
|
'question': data.params.question,
|
|
|
|
|
|
'keywords': data.keywords,
|
|
|
|
|
|
'conclusion': data.conclusion,
|
|
|
|
|
|
};
|
|
|
|
|
|
Clipboard.setData(
|
|
|
|
|
|
ClipboardData(
|
|
|
|
|
|
text: const JsonEncoder.withIndent(' ').convert(payload),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
Toast.show(
|
|
|
|
|
|
context,
|
|
|
|
|
|
l10n.toastContentCopied,
|
|
|
|
|
|
type: ToastType.success,
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
style: TextButton.styleFrom(foregroundColor: colors.primary),
|
|
|
|
|
|
child: Text(l10n.resultShare),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _SignCard extends StatelessWidget {
|
2026-04-06 01:28:10 +08:00
|
|
|
|
const _SignCard({super.key, required this.signType});
|
2026-04-03 16:56:47 +08:00
|
|
|
|
|
|
|
|
|
|
final String signType;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
|
final image = _signImageAssetForType(context, signType);
|
|
|
|
|
|
final localizedSignType = _localizedSignTypeLabel(l10n, signType);
|
2026-04-03 16:56:47 +08:00
|
|
|
|
return Card(
|
|
|
|
|
|
margin: EdgeInsets.zero,
|
|
|
|
|
|
color: colors.surface,
|
|
|
|
|
|
elevation: 2,
|
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: AppSpacing.lg),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Image.asset(
|
|
|
|
|
|
image,
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: 220,
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.sm),
|
|
|
|
|
|
Text(
|
2026-04-06 01:28:10 +08:00
|
|
|
|
localizedSignType,
|
2026-04-03 16:56:47 +08:00
|
|
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
|
|
|
|
color: colors.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 01:28:10 +08:00
|
|
|
|
String _localizedSignTypeLabel(AppLocalizations l10n, String signType) {
|
|
|
|
|
|
final normalized = signType.trim();
|
|
|
|
|
|
if (normalized.contains('上上')) {
|
|
|
|
|
|
return l10n.signTypeShangShang;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.contains('中上')) {
|
|
|
|
|
|
return l10n.signTypeZhongShang;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.contains('下下')) {
|
|
|
|
|
|
return l10n.signTypeXiaXia;
|
|
|
|
|
|
}
|
|
|
|
|
|
return l10n.signTypeZhongXia;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String _signImageAssetForType(BuildContext context, String signType) {
|
|
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
|
final normalized = _localizedSignTypeLabel(l10n, signType);
|
|
|
|
|
|
if (normalized == l10n.signTypeShangShang) {
|
|
|
|
|
|
return 'assets/images/qigua/shangshang.jpg';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized == l10n.signTypeZhongShang) {
|
|
|
|
|
|
return 'assets/images/qigua/zhongshang.jpg';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized == l10n.signTypeXiaXia) {
|
|
|
|
|
|
return 'assets/images/qigua/xiaxia.jpg';
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'assets/images/qigua/zhongxia.jpg';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:56:47 +08:00
|
|
|
|
class _KeywordCard extends StatelessWidget {
|
|
|
|
|
|
const _KeywordCard({required this.keywords});
|
|
|
|
|
|
|
|
|
|
|
|
final String keywords;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final palette = Theme.of(context).extension<AppColorPalette>()!;
|
|
|
|
|
|
return Card(
|
|
|
|
|
|
margin: EdgeInsets.zero,
|
|
|
|
|
|
color: palette.warningContainer,
|
|
|
|
|
|
elevation: 0,
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
|
|
|
|
|
child: Center(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
keywords,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
color: Theme.of(context).colorScheme.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 18:41:08 +08:00
|
|
|
|
class _FocusPointsCard extends StatelessWidget {
|
|
|
|
|
|
const _FocusPointsCard({required this.points});
|
|
|
|
|
|
|
|
|
|
|
|
final List<String> points;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
|
|
|
|
|
final languageCode = Localizations.localeOf(context).languageCode;
|
|
|
|
|
|
final title = languageCode == 'en' ? 'Focus Points' : '断卦要点';
|
|
|
|
|
|
if (points.isEmpty) {
|
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
|
}
|
|
|
|
|
|
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(
|
|
|
|
|
|
title,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
color: colors.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.sm),
|
|
|
|
|
|
...List<Widget>.generate(points.length, (index) {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'${index + 1}. ',
|
|
|
|
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
|
|
|
|
color: colors.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
points[index],
|
|
|
|
|
|
style: Theme.of(
|
|
|
|
|
|
context,
|
|
|
|
|
|
).textTheme.bodyMedium?.copyWith(height: 1.55),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:56:47 +08:00
|
|
|
|
class _AnalysisCard extends StatelessWidget {
|
|
|
|
|
|
const _AnalysisCard({required this.title, required this.content});
|
|
|
|
|
|
|
|
|
|
|
|
final String title;
|
|
|
|
|
|
final String content;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
2026-04-03 16:56:47 +08:00
|
|
|
|
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(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
title,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
color: colors.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const Spacer(),
|
|
|
|
|
|
TextButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
Clipboard.setData(ClipboardData(text: content));
|
2026-04-06 01:28:10 +08:00
|
|
|
|
Toast.show(
|
|
|
|
|
|
context,
|
|
|
|
|
|
l10n.toastContentCopiedWithTitle(title),
|
|
|
|
|
|
type: ToastType.success,
|
|
|
|
|
|
);
|
2026-04-03 16:56:47 +08:00
|
|
|
|
},
|
2026-04-06 01:28:10 +08:00
|
|
|
|
child: Text(l10n.resultCopy),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.sm),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
content,
|
|
|
|
|
|
style: Theme.of(
|
|
|
|
|
|
context,
|
|
|
|
|
|
).textTheme.bodyMedium?.copyWith(height: 1.65),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _InfoCard extends StatelessWidget {
|
|
|
|
|
|
const _InfoCard({required this.data});
|
|
|
|
|
|
|
|
|
|
|
|
final DivinationResultData data;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
2026-04-03 16:56:47 +08:00
|
|
|
|
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),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
child: SizedBox(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
l10n.resultDivinationInfo,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
color: colors.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
_kv(
|
|
|
|
|
|
context,
|
|
|
|
|
|
l10n.resultDivinationTime,
|
|
|
|
|
|
DateFormat.yMd(
|
|
|
|
|
|
Localizations.localeOf(context).toString(),
|
|
|
|
|
|
).add_Hm().format(data.params.divinationTime),
|
|
|
|
|
|
),
|
|
|
|
|
|
_kv(
|
|
|
|
|
|
context,
|
|
|
|
|
|
l10n.resultDivinationMethod,
|
|
|
|
|
|
data.params.method == DivinationMethod.auto
|
|
|
|
|
|
? l10n.resultAutoMethod
|
|
|
|
|
|
: l10n.resultManualMethod,
|
|
|
|
|
|
),
|
|
|
|
|
|
_kv(
|
|
|
|
|
|
context,
|
|
|
|
|
|
l10n.resultQuestionType,
|
|
|
|
|
|
_typeLabel(context, data.params.questionType),
|
|
|
|
|
|
),
|
|
|
|
|
|
_kv(context, l10n.resultQuestion, data.params.question),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _kv(BuildContext context, String k, String v) {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
|
|
|
|
|
|
child: RichText(
|
|
|
|
|
|
text: TextSpan(
|
|
|
|
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
TextSpan(
|
|
|
|
|
|
text: '$k:',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: Theme.of(
|
|
|
|
|
|
context,
|
|
|
|
|
|
).colorScheme.onSurface.withValues(alpha: 0.75),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
TextSpan(
|
|
|
|
|
|
text: v,
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 01:28:10 +08:00
|
|
|
|
String _typeLabel(BuildContext context, QuestionType type) {
|
|
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
2026-04-03 16:56:47 +08:00
|
|
|
|
return switch (type) {
|
2026-04-06 01:28:10 +08:00
|
|
|
|
QuestionType.career => l10n.questionTypeCareer,
|
|
|
|
|
|
QuestionType.love => l10n.questionTypeLove,
|
|
|
|
|
|
QuestionType.wealth => l10n.questionTypeWealth,
|
|
|
|
|
|
QuestionType.fortune => l10n.questionTypeFortune,
|
|
|
|
|
|
QuestionType.dream => l10n.questionTypeDream,
|
|
|
|
|
|
QuestionType.health => l10n.questionTypeHealth,
|
|
|
|
|
|
QuestionType.study => l10n.questionTypeStudy,
|
|
|
|
|
|
QuestionType.search => l10n.questionTypeSearch,
|
|
|
|
|
|
QuestionType.other => l10n.questionTypeOther,
|
2026-04-03 16:56:47 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _HexagramDetailCard extends StatelessWidget {
|
|
|
|
|
|
const _HexagramDetailCard({required this.data});
|
|
|
|
|
|
|
|
|
|
|
|
final DivinationResultData data;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
2026-04-03 16:56:47 +08:00
|
|
|
|
return Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
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(
|
2026-04-06 01:28:10 +08:00
|
|
|
|
l10n.ganZhiInfo,
|
2026-04-03 16:56:47 +08:00
|
|
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
|
|
|
|
color: colors.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
2026-04-06 01:28:10 +08:00
|
|
|
|
child: _miniKV(
|
|
|
|
|
|
context,
|
|
|
|
|
|
DivinationTerms.yueJian,
|
|
|
|
|
|
data.ganzhi.yueJian,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: _miniKV(
|
|
|
|
|
|
context,
|
|
|
|
|
|
DivinationTerms.riChen,
|
|
|
|
|
|
data.ganzhi.riChen,
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.sm),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
2026-04-06 01:28:10 +08:00
|
|
|
|
child: _miniKV(
|
|
|
|
|
|
context,
|
|
|
|
|
|
DivinationTerms.yuePo,
|
|
|
|
|
|
data.ganzhi.yuePo,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: _miniKV(
|
|
|
|
|
|
context,
|
|
|
|
|
|
DivinationTerms.riChong,
|
|
|
|
|
|
data.ganzhi.riChong,
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
Text(
|
|
|
|
|
|
l10n.wuXingWangShuai,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
|
|
|
|
color: colors.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
const SizedBox(height: AppSpacing.sm),
|
|
|
|
|
|
_WuXingTable(data: data),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
2026-04-06 01:28:10 +08:00
|
|
|
|
Text(
|
|
|
|
|
|
l10n.ganZhiKongWang,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
|
|
|
|
color: colors.primary,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
const SizedBox(height: AppSpacing.sm),
|
|
|
|
|
|
_KongWangTable(data: data),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
Card(
|
|
|
|
|
|
margin: EdgeInsets.zero,
|
|
|
|
|
|
color: colors.surfaceContainerLow,
|
|
|
|
|
|
elevation: 2,
|
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
data.guaName,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium
|
|
|
|
|
|
?.copyWith(fontWeight: FontWeight.w700),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (data.hasChangingYao)
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
data.targetGuaName,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
style: Theme.of(context).textTheme.titleMedium
|
|
|
|
|
|
?.copyWith(fontWeight: FontWeight.w700),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
for (int idx = 5; idx >= 0; idx--)
|
|
|
|
|
|
_YaoDetailRow(
|
|
|
|
|
|
line: data.yaoLines[idx],
|
2026-04-07 18:41:08 +08:00
|
|
|
|
target: idx < data.targetYaoLines.length
|
|
|
|
|
|
? data.targetYaoLines[idx]
|
|
|
|
|
|
: data.yaoLines[idx],
|
|
|
|
|
|
showTarget:
|
|
|
|
|
|
data.hasChangingYao && idx < data.targetYaoLines.length,
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.sm),
|
|
|
|
|
|
const Align(
|
|
|
|
|
|
alignment: Alignment.centerLeft,
|
|
|
|
|
|
child: YaoLegend(),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _miniKV(BuildContext context, String key, String value) {
|
|
|
|
|
|
return Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text('$key:'),
|
|
|
|
|
|
Text(value, style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _WuXingTable extends StatelessWidget {
|
|
|
|
|
|
const _WuXingTable({required this.data});
|
|
|
|
|
|
|
|
|
|
|
|
final DivinationResultData data;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
border: Border.all(color: colors.outline),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: DivinationTerms.wuXing
|
|
|
|
|
|
.map(
|
|
|
|
|
|
(k) => Expanded(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
|
vertical: AppSpacing.sm,
|
|
|
|
|
|
),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: colors.surfaceContainerHigh,
|
|
|
|
|
|
border: Border(
|
|
|
|
|
|
right: BorderSide(
|
|
|
|
|
|
color: k == DivinationTerms.wuXing.last
|
|
|
|
|
|
? colors.surfaceContainerHigh
|
|
|
|
|
|
: colors.outline,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Text(k, textAlign: TextAlign.center),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
.toList(),
|
|
|
|
|
|
),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: DivinationTerms.wuXing
|
|
|
|
|
|
.map(
|
|
|
|
|
|
(k) => Expanded(
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
|
vertical: AppSpacing.sm,
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
data.wuXingStatus[k] ?? '',
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
.toList(),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _KongWangTable extends StatelessWidget {
|
|
|
|
|
|
const _KongWangTable({required this.data});
|
|
|
|
|
|
|
|
|
|
|
|
final DivinationResultData data;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final colors = Theme.of(context).colorScheme;
|
2026-04-06 01:28:10 +08:00
|
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
|
final header = <String>[
|
|
|
|
|
|
l10n.resultPillarColumn,
|
|
|
|
|
|
l10n.resultYearPillar,
|
|
|
|
|
|
l10n.resultMonthPillar,
|
|
|
|
|
|
l10n.resultDayPillar,
|
|
|
|
|
|
l10n.resultTimePillar,
|
|
|
|
|
|
];
|
|
|
|
|
|
final rows = <List<String>>[
|
|
|
|
|
|
<String>[
|
|
|
|
|
|
l10n.resultGanZhiLabel,
|
|
|
|
|
|
data.ganzhi.yearGanZhi,
|
|
|
|
|
|
data.ganzhi.monthGanZhi,
|
|
|
|
|
|
data.ganzhi.dayGanZhi,
|
|
|
|
|
|
data.ganzhi.timeGanZhi,
|
|
|
|
|
|
],
|
|
|
|
|
|
<String>[
|
|
|
|
|
|
l10n.resultKongWangLabel,
|
|
|
|
|
|
data.ganzhi.yearKongWang,
|
|
|
|
|
|
data.ganzhi.monthKongWang,
|
|
|
|
|
|
data.ganzhi.dayKongWang,
|
|
|
|
|
|
data.ganzhi.timeKongWang,
|
|
|
|
|
|
],
|
2026-04-03 16:56:47 +08:00
|
|
|
|
];
|
2026-04-06 01:28:10 +08:00
|
|
|
|
|
|
|
|
|
|
Widget buildCell(
|
|
|
|
|
|
String text, {
|
|
|
|
|
|
bool isHeader = false,
|
|
|
|
|
|
bool isLast = false,
|
|
|
|
|
|
bool isFirst = false,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return Expanded(
|
|
|
|
|
|
flex: isFirst ? 2 : 3,
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
|
horizontal: AppSpacing.xs,
|
|
|
|
|
|
vertical: AppSpacing.sm,
|
|
|
|
|
|
),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: isHeader ? colors.surfaceContainerHigh : colors.surface,
|
|
|
|
|
|
border: Border(
|
|
|
|
|
|
right: isLast
|
|
|
|
|
|
? BorderSide.none
|
|
|
|
|
|
: BorderSide(color: colors.outline),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
text,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
|
|
|
|
fontWeight: isHeader || isFirst
|
|
|
|
|
|
? FontWeight.w700
|
|
|
|
|
|
: FontWeight.w500,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:56:47 +08:00
|
|
|
|
return Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
border: Border.all(color: colors.outline),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
2026-04-06 01:28:10 +08:00
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
for (int i = 0; i < header.length; i++)
|
|
|
|
|
|
buildCell(
|
|
|
|
|
|
header[i],
|
|
|
|
|
|
isHeader: true,
|
|
|
|
|
|
isFirst: i == 0,
|
|
|
|
|
|
isLast: i == header.length - 1,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
for (int r = 0; r < rows.length; r++)
|
|
|
|
|
|
Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
border: Border(top: BorderSide(color: colors.outline)),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
2026-04-06 01:28:10 +08:00
|
|
|
|
for (int c = 0; c < rows[r].length; c++)
|
|
|
|
|
|
buildCell(
|
|
|
|
|
|
rows[r][c],
|
|
|
|
|
|
isFirst: c == 0,
|
|
|
|
|
|
isLast: c == rows[r].length - 1,
|
|
|
|
|
|
),
|
2026-04-03 16:56:47 +08:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _YaoDetailRow extends StatelessWidget {
|
|
|
|
|
|
const _YaoDetailRow({
|
|
|
|
|
|
required this.line,
|
|
|
|
|
|
required this.target,
|
|
|
|
|
|
required this.showTarget,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
final YaoLineData line;
|
|
|
|
|
|
final YaoLineData target;
|
|
|
|
|
|
final bool showTarget;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(child: _lineCell(context, line, showMark: true)),
|
|
|
|
|
|
if (showTarget)
|
|
|
|
|
|
Expanded(child: _lineCell(context, target, showMark: false)),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _lineCell(
|
|
|
|
|
|
BuildContext context,
|
|
|
|
|
|
YaoLineData data, {
|
|
|
|
|
|
required bool showMark,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 20,
|
|
|
|
|
|
child: Text(data.spirit, textAlign: TextAlign.center),
|
|
|
|
|
|
),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 28,
|
|
|
|
|
|
child: Text(data.relation, textAlign: TextAlign.center),
|
|
|
|
|
|
),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 18,
|
|
|
|
|
|
child: Text(data.branch, textAlign: TextAlign.center),
|
|
|
|
|
|
),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 18,
|
|
|
|
|
|
child: Text(data.element, textAlign: TextAlign.center),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: AppSpacing.xs),
|
|
|
|
|
|
Expanded(child: YaoGlyph(type: data.type, height: 6)),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 18,
|
|
|
|
|
|
child: Text(_changeMark(data.type), textAlign: TextAlign.center),
|
|
|
|
|
|
),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 18,
|
|
|
|
|
|
child: Text(showMark ? data.mark : '', textAlign: TextAlign.center),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String _changeMark(YaoType type) {
|
|
|
|
|
|
return type.changeMark;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|