diff --git a/apps/assets/images/qigua/yangmian.jpg b/apps/assets/images/qigua/hua.jpg similarity index 100% rename from apps/assets/images/qigua/yangmian.jpg rename to apps/assets/images/qigua/hua.jpg diff --git a/apps/assets/images/qigua/lc1.jpg b/apps/assets/images/qigua/lc1.jpg deleted file mode 100644 index 1ec291d..0000000 Binary files a/apps/assets/images/qigua/lc1.jpg and /dev/null differ diff --git a/apps/assets/images/qigua/lc2.jpg b/apps/assets/images/qigua/lc2.jpg deleted file mode 100644 index d2ce765..0000000 Binary files a/apps/assets/images/qigua/lc2.jpg and /dev/null differ diff --git a/apps/assets/images/qigua/lc3.jpg b/apps/assets/images/qigua/lc3.jpg deleted file mode 100644 index 7f4fa29..0000000 Binary files a/apps/assets/images/qigua/lc3.jpg and /dev/null differ diff --git a/apps/assets/images/qigua/lc4.jpg b/apps/assets/images/qigua/lc4.jpg deleted file mode 100644 index 4abd1dd..0000000 Binary files a/apps/assets/images/qigua/lc4.jpg and /dev/null differ diff --git a/apps/assets/images/qigua/lc5.jpg b/apps/assets/images/qigua/lc5.jpg deleted file mode 100644 index df93315..0000000 Binary files a/apps/assets/images/qigua/lc5.jpg and /dev/null differ diff --git a/apps/assets/images/qigua/yinmian.jpg b/apps/assets/images/qigua/zi.jpg similarity index 100% rename from apps/assets/images/qigua/yinmian.jpg rename to apps/assets/images/qigua/zi.jpg diff --git a/apps/assets/images/qigua/zihua.jpg b/apps/assets/images/qigua/zihua.jpg deleted file mode 100644 index 3b16bf1..0000000 Binary files a/apps/assets/images/qigua/zihua.jpg and /dev/null differ diff --git a/apps/assets/images/tutorial/tutorial_1.png b/apps/assets/images/tutorial/tutorial_1.png new file mode 100644 index 0000000..094d8ba Binary files /dev/null and b/apps/assets/images/tutorial/tutorial_1.png differ diff --git a/apps/assets/images/tutorial/tutorial_2.png b/apps/assets/images/tutorial/tutorial_2.png new file mode 100644 index 0000000..cedb964 Binary files /dev/null and b/apps/assets/images/tutorial/tutorial_2.png differ diff --git a/apps/assets/images/tutorial/tutorial_3.png b/apps/assets/images/tutorial/tutorial_3.png new file mode 100644 index 0000000..1c0ed9e Binary files /dev/null and b/apps/assets/images/tutorial/tutorial_3.png differ diff --git a/apps/lib/features/divination/data/apis/divination_api.dart b/apps/lib/features/divination/data/apis/divination_api.dart index 4fdc47e..00ac152 100644 --- a/apps/lib/features/divination/data/apis/divination_api.dart +++ b/apps/lib/features/divination/data/apis/divination_api.dart @@ -77,7 +77,6 @@ class DivinationApi { final aggregate = DivinationRunAggregate( derived: derived, signLevel: _asString(agentOutputRaw['sign_level']), - summary: _asString(agentOutputRaw['summary']), conclusion: _asStringList(agentOutputRaw['conclusion']), focusPoints: _asStringList(agentOutputRaw['focus_points']), advice: _asStringList(agentOutputRaw['advice']), diff --git a/apps/lib/features/divination/data/models/divination_backend_models.dart b/apps/lib/features/divination/data/models/divination_backend_models.dart index 2290035..eeaa29a 100644 --- a/apps/lib/features/divination/data/models/divination_backend_models.dart +++ b/apps/lib/features/divination/data/models/divination_backend_models.dart @@ -54,7 +54,6 @@ class DivinationRunAggregate { const DivinationRunAggregate({ required this.derived, required this.signLevel, - required this.summary, required this.conclusion, required this.focusPoints, required this.advice, @@ -64,7 +63,6 @@ class DivinationRunAggregate { final DerivedDivinationData derived; final String signLevel; - final String summary; final List conclusion; final List focusPoints; final List advice; @@ -82,8 +80,9 @@ class DivinationRunAggregate { lowerName: derived.lowerName, signType: signLevel, keywords: keywords.join('、'), + focusPoints: focusPoints, conclusion: _asBullet(conclusion), - analysis: summary.isEmpty ? answer : '$summary\n\n$answer', + analysis: answer, suggestion: _asBullet(advice), ganzhi: GanzhiData( yearGanZhi: derived.ganzhi.yearGanZhi, diff --git a/apps/lib/features/divination/data/models/divination_params.dart b/apps/lib/features/divination/data/models/divination_params.dart index 81e726c..9db2d57 100644 --- a/apps/lib/features/divination/data/models/divination_params.dart +++ b/apps/lib/features/divination/data/models/divination_params.dart @@ -22,6 +22,7 @@ class DivinationParams { required this.divinationTime, required this.coinBalance, required this.userId, + this.allowVibration = true, }); final DivinationMethod method; @@ -30,6 +31,7 @@ class DivinationParams { final DateTime divinationTime; final int coinBalance; final String userId; + final bool allowVibration; DivinationParams copyWith({ DivinationMethod? method, @@ -38,6 +40,7 @@ class DivinationParams { DateTime? divinationTime, int? coinBalance, String? userId, + bool? allowVibration, }) { return DivinationParams( method: method ?? this.method, @@ -46,6 +49,7 @@ class DivinationParams { divinationTime: divinationTime ?? this.divinationTime, coinBalance: coinBalance ?? this.coinBalance, userId: userId ?? this.userId, + allowVibration: allowVibration ?? this.allowVibration, ); } diff --git a/apps/lib/features/divination/data/models/divination_result.dart b/apps/lib/features/divination/data/models/divination_result.dart index ec24209..3fdacbe 100644 --- a/apps/lib/features/divination/data/models/divination_result.dart +++ b/apps/lib/features/divination/data/models/divination_result.dart @@ -11,6 +11,7 @@ class DivinationResultData { required this.lowerName, required this.signType, required this.keywords, + required this.focusPoints, required this.conclusion, required this.analysis, required this.suggestion, @@ -29,6 +30,7 @@ class DivinationResultData { final String lowerName; final String signType; final String keywords; + final List focusPoints; final String conclusion; final String analysis; final String suggestion; @@ -50,6 +52,7 @@ class DivinationResultData { 'lowerName': lowerName, 'signType': signType, 'keywords': keywords, + 'focusPoints': focusPoints, 'conclusion': conclusion, 'analysis': analysis, 'suggestion': suggestion, @@ -86,6 +89,7 @@ class DivinationResultData { lowerName: _requiredString(json, 'lowerName'), signType: _requiredString(json, 'signType'), keywords: _requiredString(json, 'keywords'), + focusPoints: _requiredStringList(json, 'focusPoints'), conclusion: _requiredString(json, 'conclusion'), analysis: _requiredString(json, 'analysis'), suggestion: _requiredString(json, 'suggestion'), @@ -113,6 +117,21 @@ class DivinationResultData { } } +List _requiredStringList(Map json, String key) { + final raw = json[key]; + if (raw is! List) { + throw FormatException('Invalid $key payload'); + } + return raw + .map((item) { + if (item is! String) { + throw FormatException('Invalid $key item payload'); + } + return item; + }) + .toList(growable: false); +} + class GanzhiData { const GanzhiData({ required this.yearGanZhi, diff --git a/apps/lib/features/divination/data/services/divination_run_service.dart b/apps/lib/features/divination/data/services/divination_run_service.dart index 6b71365..10ce4ec 100644 --- a/apps/lib/features/divination/data/services/divination_run_service.dart +++ b/apps/lib/features/divination/data/services/divination_run_service.dart @@ -33,7 +33,6 @@ class DivinationRunService { DerivedDivinationData? derived; String signLevel = ''; - String summary = ''; List conclusion = const []; List focusPoints = const []; List advice = const []; @@ -64,7 +63,6 @@ class DivinationRunService { } if (type == 'TEXT_MESSAGE_END') { signLevel = _requiredString(event, 'sign_level'); - summary = _requiredString(event, 'summary'); conclusion = _requiredStringList(event, 'conclusion'); focusPoints = _requiredStringList(event, 'focus_points'); advice = _requiredStringList(event, 'advice'); @@ -107,7 +105,6 @@ class DivinationRunService { return DivinationRunAggregate( derived: derived, signLevel: signLevel, - summary: summary, conclusion: conclusion, focusPoints: focusPoints, advice: advice, diff --git a/apps/lib/features/divination/presentation/screens/auto_divination_screen.dart b/apps/lib/features/divination/presentation/screens/auto_divination_screen.dart index 86df843..725b075 100644 --- a/apps/lib/features/divination/presentation/screens/auto_divination_screen.dart +++ b/apps/lib/features/divination/presentation/screens/auto_divination_screen.dart @@ -300,6 +300,9 @@ class _AutoDivinationScreenState extends State } Future _vibrateStrong() async { + if (!widget.params.allowVibration) { + return; + } final hasVibrator = await Vibration.hasVibrator(); if (hasVibrator == true) { await Vibration.vibrate(duration: 280, amplitude: 255); @@ -313,15 +316,17 @@ class _AutoDivinationScreenState extends State context: context, builder: (context) { return DivinationGuideDialog( - title: l10n.autoGuideTitle, + title: l10n.divinationManualGuideTitle, guideImages: const [ - 'assets/images/qigua/lc1.jpg', - 'assets/images/qigua/lc2.jpg', - 'assets/images/qigua/lc3.jpg', - 'assets/images/qigua/lc4.jpg', - 'assets/images/qigua/lc5.jpg', + ['assets/images/tutorial/tutorial_1.png'], + ['assets/images/tutorial/tutorial_2.png'], + ['assets/images/tutorial/tutorial_3.png'], + ], + instructions: [ + l10n.divinationManualGuideStep1, + l10n.divinationManualGuideStep2, + l10n.divinationManualGuideStep3, ], - instructionText: l10n.autoGuideInstruction, ); }, ); @@ -527,7 +532,7 @@ class _CoinColumn extends StatelessWidget { ), const SizedBox(height: AppSpacing.sm), Text( - DivinationTerms.yinYang[isYang] ?? '', + DivinationTerms.ziHua[isYang] ?? '', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colors.onSurface, fontWeight: FontWeight.w700, @@ -562,8 +567,8 @@ class _CoinFace extends StatelessWidget { : (isYang ? 0 : 180); final showingYang = isSpinning ? rotationY < 90 : isYang; final image = showingYang - ? 'assets/images/qigua/yangmian.jpg' - : 'assets/images/qigua/yinmian.jpg'; + ? 'assets/images/qigua/hua.jpg' + : 'assets/images/qigua/zi.jpg'; return Transform( alignment: Alignment.center, transform: Matrix4.identity() diff --git a/apps/lib/features/divination/presentation/screens/divination_processing_screen.dart b/apps/lib/features/divination/presentation/screens/divination_processing_screen.dart index 17c2577..c3e5a64 100644 --- a/apps/lib/features/divination/presentation/screens/divination_processing_screen.dart +++ b/apps/lib/features/divination/presentation/screens/divination_processing_screen.dart @@ -244,7 +244,7 @@ class _DivinationProcessingScreenState extends State ), ), child: Text( - 'I Ching', + l10n.iChingTitle, style: Theme.of(context) .textTheme .labelSmall diff --git a/apps/lib/features/divination/presentation/screens/divination_result_screen.dart b/apps/lib/features/divination/presentation/screens/divination_result_screen.dart index 3da6d78..a49d60c 100644 --- a/apps/lib/features/divination/presentation/screens/divination_result_screen.dart +++ b/apps/lib/features/divination/presentation/screens/divination_result_screen.dart @@ -175,6 +175,8 @@ class _DivinationResultScreenState extends State { content: widget.data.conclusion, ), const SizedBox(height: AppSpacing.md), + _FocusPointsCard(points: widget.data.focusPoints), + const SizedBox(height: AppSpacing.md), _AnalysisCard( title: l10n.resultAnalysis, content: widget.data.analysis, @@ -426,6 +428,71 @@ class _KeywordCard extends StatelessWidget { } } +class _FocusPointsCard extends StatelessWidget { + const _FocusPointsCard({required this.points}); + + final List 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.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), + ), + ), + ], + ), + ); + }), + ], + ), + ), + ); + } +} + class _AnalysisCard extends StatelessWidget { const _AnalysisCard({required this.title, required this.content}); @@ -715,8 +782,11 @@ class _HexagramDetailCard extends StatelessWidget { for (int idx = 5; idx >= 0; idx--) _YaoDetailRow( line: data.yaoLines[idx], - target: data.targetYaoLines[idx], - showTarget: data.hasChangingYao, + target: idx < data.targetYaoLines.length + ? data.targetYaoLines[idx] + : data.yaoLines[idx], + showTarget: + data.hasChangingYao && idx < data.targetYaoLines.length, ), const SizedBox(height: AppSpacing.sm), const Align( diff --git a/apps/lib/features/divination/presentation/screens/divination_screen.dart b/apps/lib/features/divination/presentation/screens/divination_screen.dart index 7c8aa3e..0f18494 100644 --- a/apps/lib/features/divination/presentation/screens/divination_screen.dart +++ b/apps/lib/features/divination/presentation/screens/divination_screen.dart @@ -24,12 +24,14 @@ class DivinationScreen extends StatefulWidget { required this.userId, required this.onCompleted, this.runServiceOverride, + this.allowVibration = true, }); final SessionStore sessionStore; final String userId; final Future Function(DivinationResultData result) onCompleted; final DivinationRunService? runServiceOverride; + final bool allowVibration; @override State createState() => _DivinationScreenState(); @@ -51,7 +53,7 @@ class _DivinationScreenState extends State { widget.runServiceOverride ?? DivinationRunService(api: DivinationApi(apiClient: apiClient)); _params = DivinationParams( - method: DivinationMethod.manual, + method: DivinationMethod.auto, questionType: QuestionType.career, question: '', divinationTime: DateTime.now(), @@ -169,7 +171,10 @@ class _DivinationScreenState extends State { return; } - final nextParams = _params.copyWith(divinationTime: DateTime.now()); + final nextParams = _params.copyWith( + divinationTime: DateTime.now(), + allowVibration: widget.allowVibration, + ); Navigator.of(context).push( MaterialPageRoute( builder: (_) => AutoDivinationScreen( @@ -406,13 +411,15 @@ Future _showGuide(BuildContext context, AppLocalizations l10n) { return DivinationGuideDialog( title: l10n.divinationManualGuideTitle, guideImages: const [ - 'assets/images/qigua/lc1.jpg', - 'assets/images/qigua/lc2.jpg', - 'assets/images/qigua/lc3.jpg', - 'assets/images/qigua/lc4.jpg', - 'assets/images/qigua/lc5.jpg', + ['assets/images/tutorial/tutorial_1.png'], + ['assets/images/tutorial/tutorial_2.png'], + ['assets/images/tutorial/tutorial_3.png'], + ], + instructions: [ + l10n.divinationManualGuideStep1, + l10n.divinationManualGuideStep2, + l10n.divinationManualGuideStep3, ], - instructionText: l10n.divinationManualGuideInstruction, ); }, ); diff --git a/apps/lib/features/divination/presentation/screens/manual_divination_screen.dart b/apps/lib/features/divination/presentation/screens/manual_divination_screen.dart index f14bbe0..d8d7cb5 100644 --- a/apps/lib/features/divination/presentation/screens/manual_divination_screen.dart +++ b/apps/lib/features/divination/presentation/screens/manual_divination_screen.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; @@ -130,8 +132,16 @@ class _ManualDivinationScreenState extends State builder: (_) { return DivinationGuideDialog( title: l10n.manualSelectYaoTitle, - guideImages: const ['lc2.jpg', 'lc3.jpg', 'lc4.jpg', 'lc5.jpg'], - instructionText: l10n.manualYaoTipContent, + guideImages: const [ + ['assets/images/tutorial/tutorial_1.png'], + ['assets/images/tutorial/tutorial_2.png'], + ['assets/images/tutorial/tutorial_3.png'], + ], + instructions: [ + l10n.divinationManualGuideStep1, + l10n.divinationManualGuideStep2, + l10n.divinationManualGuideStep3, + ], ); }, ); @@ -409,208 +419,199 @@ class _YaoSelectionCard extends StatelessWidget { int yaoIndex, void Function(int, YaoType) onSelect, ) { - final l10n = AppLocalizations.of(context)!; - final colors = Theme.of(context).colorScheme; - final options = <(String, YaoType, String)>[ - ( - '${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYang]}(${DivinationTerms.youngYangSymbol})', - YaoType.youngYang, - '字字字', - ), - ( - '${DivinationTerms.yaoTypeLabels[YaoTypeLabel.youngYin]}(${DivinationTerms.youngYinSymbol})', - YaoType.youngYin, - '花花花', - ), - ( - '${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYang]}(${DivinationTerms.oldYangSymbol})', - YaoType.oldYang, - '字字字', - ), - ( - '${DivinationTerms.yaoTypeLabels[YaoTypeLabel.oldYin]}(${DivinationTerms.oldYinSymbol})', - YaoType.oldYin, - '花花花', - ), - ]; return showDialog( context: context, builder: (context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.lg), - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - l10n.manualSelectYaoTitle, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: colors.primary, - fontWeight: FontWeight.w700, - ), - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - const SizedBox(height: AppSpacing.md), - ...options.map((option) { - return Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.sm), - child: _YaoOptionCard( - label: option.$1, - pattern: option.$3, - isSelected: false, - onTap: () { - onSelect(yaoIndex, option.$2); - Navigator.of(context).pop(); - }, - ), - ); - }), - const SizedBox(height: AppSpacing.md), - Container( - padding: const EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(AppRadius.md), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.coinFaceGuideTitle, - style: Theme.of(context).textTheme.titleSmall - ?.copyWith( - color: colors.onSurface, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: AppSpacing.sm), - ClipRRect( - borderRadius: BorderRadius.circular(AppRadius.sm), - child: Image.asset( - 'assets/images/qigua/zihua.jpg', - width: double.infinity, - height: 120, - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) => - Container( - height: 120, - color: colors.errorContainer, - child: Center( - child: Text( - l10n.divinationClose, - style: TextStyle( - color: colors.onErrorContainer, - ), - ), - ), - ), - ), - ), - const SizedBox(height: AppSpacing.xs), - Text( - l10n.coinFaceGuideDescription, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(color: colors.onSurfaceVariant), - ), - ], - ), - ), - ], - ), - ), - ), + return _ThreeCoinSelectorDialog( + onConfirm: (yaoType) { + onSelect(yaoIndex, yaoType); + Navigator.of(context).pop(); + }, ); }, ); } } -class _YaoOptionCard extends StatelessWidget { - const _YaoOptionCard({ - required this.label, - required this.pattern, - required this.isSelected, - required this.onTap, - }); +class _ThreeCoinSelectorDialog extends StatefulWidget { + const _ThreeCoinSelectorDialog({required this.onConfirm}); - final String label; - final String pattern; - final bool isSelected; - final VoidCallback onTap; + final void Function(YaoType) onConfirm; + + @override + State<_ThreeCoinSelectorDialog> createState() => + _ThreeCoinSelectorDialogState(); +} + +class _ThreeCoinSelectorDialogState extends State<_ThreeCoinSelectorDialog> { + final List _coinStates = [false, false, false]; + + YaoType get _currentYaoType { + final huaCount = _coinStates.where((isHua) => isHua).length; + return switch (huaCount) { + 0 => YaoType.oldYin, + 1 => YaoType.youngYang, + 2 => YaoType.youngYin, + 3 => YaoType.oldYang, + _ => YaoType.undetermined, + }; + } + + void _toggleCoin(int index) { + setState(() { + _coinStates[index] = !_coinStates[index]; + }); + } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; - return Material( - color: isSelected ? colors.primaryContainer : colors.surface, - borderRadius: BorderRadius.circular(AppRadius.md), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(AppRadius.md), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm + 4, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all( - color: isSelected ? colors.primary : colors.outlineVariant, - width: isSelected ? 2 : 1, - ), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: isSelected - ? colors.onPrimaryContainer - : colors.onSurface, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 2), - Text( - pattern, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: isSelected - ? colors.onPrimaryContainer.withValues(alpha: 0.7) - : colors.onSurfaceVariant, - ), - ), - ], + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + l10n.manualSelectYaoTitle, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: colors.primary, + fontWeight: FontWeight.w700, + ), ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const SizedBox(height: AppSpacing.lg), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(3, (index) { + return _FlippingCoin( + isHua: _coinStates[index], + onTap: () => _toggleCoin(index), + ); + }), + ), + const SizedBox(height: AppSpacing.md), + Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + borderRadius: BorderRadius.circular(AppRadius.md), ), - Icon( - Icons.chevron_right, - color: isSelected - ? colors.onPrimaryContainer - : colors.onSurfaceVariant, + child: Text( + l10n.manualCoinSelectHint, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant), + textAlign: TextAlign.center, ), - ], - ), + ), + const SizedBox(height: AppSpacing.lg), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => widget.onConfirm(_currentYaoType), + child: Text(l10n.confirm), + ), + ), + ], ), ), ); } } + +class _FlippingCoin extends StatefulWidget { + const _FlippingCoin({required this.isHua, required this.onTap}); + + final bool isHua; + final VoidCallback onTap; + + @override + State<_FlippingCoin> createState() => _FlippingCoinState(); +} + +class _FlippingCoinState extends State<_FlippingCoin> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + bool _showHua = false; + + @override + void initState() { + super.initState(); + _showHua = widget.isHua; + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _animation = Tween( + begin: 0, + end: 1, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void didUpdateWidget(_FlippingCoin oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isHua != widget.isHua) { + if (widget.isHua != _showHua) { + _controller.forward().then((_) { + setState(() { + _showHua = widget.isHua; + }); + _controller.reverse(); + }); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onTap, + child: AnimatedBuilder( + animation: _animation, + builder: (context, child) { + final angle = _animation.value * pi; + return Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..setEntry(3, 2, 0.001) + ..rotateY(angle), + child: ClipOval( + child: Image.asset( + _showHua + ? 'assets/images/qigua/hua.jpg' + : 'assets/images/qigua/zi.jpg', + width: 80, + height: 80, + fit: BoxFit.cover, + ), + ), + ); + }, + ), + ); + } +} diff --git a/apps/lib/l10n/app_en.arb b/apps/lib/l10n/app_en.arb index ba93a75..6506e75 100644 --- a/apps/lib/l10n/app_en.arb +++ b/apps/lib/l10n/app_en.arb @@ -80,11 +80,15 @@ "signBad": "Inauspicious", "language": "Language", "settingsTitle": "Settings", - "settingsSectionGeneral": "General", + "settingsSectionGeneral": "Preferences", "settingsSectionQuickAccess": "Primary Menu", "settingsSectionAccount": "Account", "settingsSectionPrivacy": "Privacy", "settingsSectionNotification": "Notifications", + "settingsInterfaceLanguage": "Interface Language", + "settingsAiLanguage": "AI Response Language", + "settingsNotificationAllow": "Allow Notifications", + "settingsNotificationVibration": "Allow Vibration", "settingsSectionAbout": "About", "settingsGeneralTitle": "General Settings", "settingsGeneralSubtitle": "Language: {currentLanguage}. Other fields are reserved to match profiles.settings.", @@ -166,14 +170,14 @@ } } }, - "settingsCoinCenterDescription": "Payment is not connected yet. The UI now shows packages and the recharge entry.", + "settingsCoinCenterDescription": "", "settingsCoinRechargeSection": "Recharge Packages", "settingsCoinPackBasic": "Starter Pack", "settingsCoinPackPopular": "Popular Pack", "settingsCoinPackPremium": "Premium Pack", "settingsCoinPackPopularBadge": "Popular", "settingsPurchaseButton": "Pay Now", - "settingsPurchasePending": "Payment is not connected yet", + "settingsPurchasePending": "", "settingsCoinAmount": "{amount} credits", "@settingsCoinAmount": { "placeholders": { @@ -226,7 +230,9 @@ "divinationMethodTipManual": "Manual: Prepare three identical coins.", "divinationMethodTipRecommend": "Manual casting provides higher accuracy.", "divinationManualGuideTitle": "Manual Casting Tutorial", - "divinationManualGuideInstruction": "Prepare three identical coins and cast six times following the guide.", + "divinationManualGuideStep1": "Left: Pattern side. Right: Inscription side. Prepare three identical coins or similar tokens.", + "divinationManualGuideStep2": "Hold the coins in both hands, think about your question, then toss them onto a table. Record how many inscription sides and pattern sides appear.", + "divinationManualGuideStep3": "Record each result by whether the inscription side or pattern side faces up. Repeat 6 times, recording from bottom to top.", "divinationIAcknowledge": "I Understand", "divinationClose": "Close", "divinationModify": "Modify", @@ -287,6 +293,7 @@ "transitionPreparing": "Deriving...", "transitionDeriving": "Analyzing...", "transitionDone": "Complete\nTap to view", + "iChingTitle": "I Ching", "processingCardQianTitle": "Qian • The Creative", "processingCardQianQuote": "The movement of Heaven is full of power; thus the noble one makes himself strong and tireless.", "processingCardDuiTitle": "Dui • The Joyous", @@ -321,6 +328,7 @@ "manualYaoInstruction": "Tap to view casting method and coin combination guide", "manualYaoTipTitle": "Tip", "manualYaoTipContent": "Select from bottom to top, not top to bottom.\n\nCast three coins together, select once each time, six times total.", + "manualCoinSelectHint": "Tap coins to flip between inscription and pattern. Record your casting result.", "autoScreenTitle": "Auto Casting", "autoSelectTime": "Select time", "autoCoinDivination": "Coin Casting", @@ -365,5 +373,33 @@ "cancel": "Cancel", "autoSelectTime": "Select Time", "coinFaceGuideTitle": "Coin Face Guide", - "coinFaceGuideDescription": "字 = side with inscription\n花 = side with pattern" + "coinFaceGuideDescription": "Left: Pattern side. Right: Inscription side.", + "settingsInviteTitle": "My Invitation", + "settingsInviteSubtitle": "Invite friends, earn rewards together", + "settingsInviteMyCode": "My Invite Code", + "settingsInviteCopySuccess": "Invite code copied", + "settingsInviteCopied": "Copied", + "settingsInviteCopy": "Copy", + "settingsInviteStats": "Invited: {count} friend(s)", + "@settingsInviteStats": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "settingsInviteBindCode": "Bind Invite Code", + "settingsInviteBindHint": "Enter your friend's invite code", + "settingsInviteBindPlaceholder": "6-digit code", + "settingsInviteBindButton": "Bind", + "settingsInviteBindSuccess": "Invite code bound successfully", + "settingsInviteBindFailed": "Failed to bind invite code", + "settingsInviteGenerateTitle": "Generate My Invite Code", + "settingsInviteGenerateButton": "Generate My Invite Code", + "settingsInviteGenerateSuccess": "Invite code generated", + "settingsInviteEmptyTitle": "Invite Friends, Earn Rewards", + "settingsInviteEmptyDescription": "Each friend who registers using your invite code gives you bonus credits", + "settingsInviteInputLabel": "Enter invite code (optional)", + "settingsInviteInputHint": "Enter code to bind your inviter", + "settingsInviteInvalidCode": "Please enter a valid 6-character invite code" } diff --git a/apps/lib/l10n/app_localizations.dart b/apps/lib/l10n/app_localizations.dart index 7260e52..56b8758 100644 --- a/apps/lib/l10n/app_localizations.dart +++ b/apps/lib/l10n/app_localizations.dart @@ -497,7 +497,7 @@ abstract class AppLocalizations { /// No description provided for @settingsSectionGeneral. /// /// In zh, this message translates to: - /// **'通用设置'** + /// **'偏好设置'** String get settingsSectionGeneral; /// No description provided for @settingsSectionQuickAccess. @@ -524,6 +524,30 @@ abstract class AppLocalizations { /// **'通知设置'** String get settingsSectionNotification; + /// No description provided for @settingsInterfaceLanguage. + /// + /// In zh, this message translates to: + /// **'界面语言'** + String get settingsInterfaceLanguage; + + /// No description provided for @settingsAiLanguage. + /// + /// In zh, this message translates to: + /// **'AI 回复语言'** + String get settingsAiLanguage; + + /// No description provided for @settingsNotificationAllow. + /// + /// In zh, this message translates to: + /// **'允许通知'** + String get settingsNotificationAllow; + + /// No description provided for @settingsNotificationVibration. + /// + /// In zh, this message translates to: + /// **'允许振动'** + String get settingsNotificationVibration; + /// No description provided for @settingsSectionAbout. /// /// In zh, this message translates to: @@ -584,12 +608,6 @@ abstract class AppLocalizations { /// **'点数可用于后续起卦与相关服务消费'** String get settingsCoinHeroSubtitle; - /// No description provided for @settingsAiLanguage. - /// - /// In zh, this message translates to: - /// **'AI 回复语言'** - String get settingsAiLanguage; - /// No description provided for @settingsAiLanguageHint. /// /// In zh, this message translates to: @@ -845,7 +863,7 @@ abstract class AppLocalizations { /// No description provided for @settingsCoinCenterDescription. /// /// In zh, this message translates to: - /// **'充值入口暂未接入支付逻辑,先展示套餐与购买流程入口。'** + /// **''** String get settingsCoinCenterDescription; /// No description provided for @settingsCoinRechargeSection. @@ -887,7 +905,7 @@ abstract class AppLocalizations { /// No description provided for @settingsPurchasePending. /// /// In zh, this message translates to: - /// **'支付能力暂未接入'** + /// **''** String get settingsPurchasePending; /// No description provided for @settingsCoinAmount. @@ -1118,11 +1136,23 @@ abstract class AppLocalizations { /// **'手动起卦教程'** String get divinationManualGuideTitle; - /// No description provided for @divinationManualGuideInstruction. + /// No description provided for @divinationManualGuideStep1. /// /// In zh, this message translates to: - /// **'准备三枚同样铜钱,按页面引导连续完成六次摇卦。'** - String get divinationManualGuideInstruction; + /// **'左侧为花面,右侧为字面。准备三枚相同的钱币,任何类似款式均可。'** + String get divinationManualGuideStep1; + + /// No description provided for @divinationManualGuideStep2. + /// + /// In zh, this message translates to: + /// **'双手捧起钱币,闭目心中默念所问之事,然后抛掷钱币于桌面,记录字面和花面出现的次数。'** + String get divinationManualGuideStep2; + + /// No description provided for @divinationManualGuideStep3. + /// + /// In zh, this message translates to: + /// **'记录每次结果,按照「字面在上还是花面在上」记录。重复六次,从下往上记录。'** + String get divinationManualGuideStep3; /// No description provided for @divinationIAcknowledge. /// @@ -1382,100 +1412,106 @@ abstract class AppLocalizations { /// **'解卦完成\n点击查看'** String get transitionDone; + /// No description provided for @iChingTitle. + /// + /// In zh, this message translates to: + /// **'周易'** + String get iChingTitle; + /// No description provided for @processingCardQianTitle. /// /// In zh, this message translates to: - /// **'Qian • The Creative'** + /// **'乾 • 元亨利贞'** String get processingCardQianTitle; /// No description provided for @processingCardQianQuote. /// /// In zh, this message translates to: - /// **'The movement of Heaven is full of power; thus the noble one makes himself strong and tireless.'** + /// **'天行健,君子以自强不息。'** String get processingCardQianQuote; /// No description provided for @processingCardDuiTitle. /// /// In zh, this message translates to: - /// **'Dui • The Joyous'** + /// **'兑 • 亨利贞'** String get processingCardDuiTitle; /// No description provided for @processingCardDuiQuote. /// /// In zh, this message translates to: - /// **'Joy grounded in integrity brings openness, harmony, and right expression.'** + /// **'丽泽兑,君子以朋友讲习。'** String get processingCardDuiQuote; /// No description provided for @processingCardLiTitle. /// /// In zh, this message translates to: - /// **'Li • The Clinging Fire'** + /// **'离 • 明两作亨利贞'** String get processingCardLiTitle; /// No description provided for @processingCardLiQuote. /// /// In zh, this message translates to: - /// **'With clear brilliance, the great one illumines all directions.'** + /// **'大人以继明照于四方。'** String get processingCardLiQuote; /// No description provided for @processingCardZhenTitle. /// /// In zh, this message translates to: - /// **'Zhen • The Arousing Thunder'** + /// **'震 • 亨震来虩虩,笑言哑哑'** String get processingCardZhenTitle; /// No description provided for @processingCardZhenQuote. /// /// In zh, this message translates to: - /// **'Shock awakens the heart; composure turns fear into growth.'** + /// **'震惊百里,惊远而惧迩也。'** String get processingCardZhenQuote; /// No description provided for @processingCardXunTitle. /// /// In zh, this message translates to: - /// **'Xun • The Gentle Wind'** + /// **'巽 • 小亨利贞'** String get processingCardXunTitle; /// No description provided for @processingCardXunQuote. /// /// In zh, this message translates to: - /// **'Gentle penetration furthers progress and helps one meet the right people.'** + /// **'随风,君子以申命行事。'** String get processingCardXunQuote; /// No description provided for @processingCardKanTitle. /// /// In zh, this message translates to: - /// **'Kan • The Abysmal Water'** + /// **'坎 • 习坎有孚维心亨'** String get processingCardKanTitle; /// No description provided for @processingCardKanQuote. /// /// In zh, this message translates to: - /// **'In danger, sincerity and disciplined action carry one through.'** + /// **'水流而不盈,行险而不失其信。'** String get processingCardKanQuote; /// No description provided for @processingCardGenTitle. /// /// In zh, this message translates to: - /// **'Gen • Keeping Still Mountain'** + /// **'艮 • 艮其背不获其身'** String get processingCardGenTitle; /// No description provided for @processingCardGenQuote. /// /// In zh, this message translates to: - /// **'Stillness at the proper time keeps one centered and steady in place.'** + /// **'时止则止,时行则行,动静不失其时。'** String get processingCardGenQuote; /// No description provided for @processingCardKunTitle. /// /// In zh, this message translates to: - /// **'Kun • The Receptive Earth'** + /// **'坤 • 元亨利牝马之贞'** String get processingCardKunTitle; /// No description provided for @processingCardKunQuote. /// /// In zh, this message translates to: - /// **'The Earth\'s condition is devoted receptivity; the noble one carries all with broad virtue.'** + /// **'地势坤,君子以厚德载物。'** String get processingCardKunQuote; /// No description provided for @ganZhiInfo. @@ -1586,6 +1622,12 @@ abstract class AppLocalizations { /// **'请从下往上选,不是从上往下选。\n\n三枚铜钱一起摇,摇完一次选一次,一共摇六次。'** String get manualYaoTipContent; + /// No description provided for @manualCoinSelectHint. + /// + /// In zh, this message translates to: + /// **'点击硬币可翻转,调整字面和花面。记录摇卦结果。'** + String get manualCoinSelectHint; + /// No description provided for @autoScreenTitle. /// /// In zh, this message translates to: @@ -1715,14 +1757,140 @@ abstract class AppLocalizations { /// No description provided for @coinFaceGuideTitle. /// /// In zh, this message translates to: - /// **'字花图片说明'** + /// **'字花对照说明'** String get coinFaceGuideTitle; /// No description provided for @coinFaceGuideDescription. /// /// In zh, this message translates to: - /// **'字=铜钱有字的一面\n花=铜钱有花纹的一面'** + /// **'左侧为花面,右侧为字面。'** String get coinFaceGuideDescription; + + /// No description provided for @settingsInviteTitle. + /// + /// In zh, this message translates to: + /// **'我的邀请'** + String get settingsInviteTitle; + + /// No description provided for @settingsInviteSubtitle. + /// + /// In zh, this message translates to: + /// **'邀请好友,共同获得奖励'** + String get settingsInviteSubtitle; + + /// No description provided for @settingsInviteMyCode. + /// + /// In zh, this message translates to: + /// **'我的邀请码'** + String get settingsInviteMyCode; + + /// No description provided for @settingsInviteCopySuccess. + /// + /// In zh, this message translates to: + /// **'邀请码已复制'** + String get settingsInviteCopySuccess; + + /// No description provided for @settingsInviteCopied. + /// + /// In zh, this message translates to: + /// **'已复制'** + String get settingsInviteCopied; + + /// No description provided for @settingsInviteCopy. + /// + /// In zh, this message translates to: + /// **'复制'** + String get settingsInviteCopy; + + /// No description provided for @settingsInviteStats. + /// + /// In zh, this message translates to: + /// **'已邀请:{count} 位好友'** + String settingsInviteStats(int count); + + /// No description provided for @settingsInviteBindCode. + /// + /// In zh, this message translates to: + /// **'绑定邀请码'** + String get settingsInviteBindCode; + + /// No description provided for @settingsInviteBindHint. + /// + /// In zh, this message translates to: + /// **'输入好友的邀请码'** + String get settingsInviteBindHint; + + /// No description provided for @settingsInviteBindPlaceholder. + /// + /// In zh, this message translates to: + /// **'6位邀请码'** + String get settingsInviteBindPlaceholder; + + /// No description provided for @settingsInviteBindButton. + /// + /// In zh, this message translates to: + /// **'绑定'** + String get settingsInviteBindButton; + + /// No description provided for @settingsInviteBindSuccess. + /// + /// In zh, this message translates to: + /// **'邀请码绑定成功'** + String get settingsInviteBindSuccess; + + /// No description provided for @settingsInviteBindFailed. + /// + /// In zh, this message translates to: + /// **'邀请码绑定失败'** + String get settingsInviteBindFailed; + + /// No description provided for @settingsInviteGenerateTitle. + /// + /// In zh, this message translates to: + /// **'生成我的邀请码'** + String get settingsInviteGenerateTitle; + + /// No description provided for @settingsInviteGenerateButton. + /// + /// In zh, this message translates to: + /// **'生成我的邀请码'** + String get settingsInviteGenerateButton; + + /// No description provided for @settingsInviteGenerateSuccess. + /// + /// In zh, this message translates to: + /// **'邀请码生成成功'** + String get settingsInviteGenerateSuccess; + + /// No description provided for @settingsInviteEmptyTitle. + /// + /// In zh, this message translates to: + /// **'邀请好友,获得奖励'** + String get settingsInviteEmptyTitle; + + /// No description provided for @settingsInviteEmptyDescription. + /// + /// In zh, this message translates to: + /// **'每成功邀请一位好友注册,您将获得积分奖励'** + String get settingsInviteEmptyDescription; + + /// No description provided for @settingsInviteInputLabel. + /// + /// In zh, this message translates to: + /// **'输入邀请码(选填)'** + String get settingsInviteInputLabel; + + /// No description provided for @settingsInviteInputHint. + /// + /// In zh, this message translates to: + /// **'输入邀请码绑定您的邀请人'** + String get settingsInviteInputHint; + + /// No description provided for @settingsInviteInvalidCode. + /// + /// In zh, this message translates to: + /// **'请输入有效的6位邀请码'** + String get settingsInviteInvalidCode; } class _AppLocalizationsDelegate diff --git a/apps/lib/l10n/app_localizations_en.dart b/apps/lib/l10n/app_localizations_en.dart index 47b9204..dca457e 100644 --- a/apps/lib/l10n/app_localizations_en.dart +++ b/apps/lib/l10n/app_localizations_en.dart @@ -217,7 +217,7 @@ class AppLocalizationsEn extends AppLocalizations { String get settingsTitle => 'Settings'; @override - String get settingsSectionGeneral => 'General'; + String get settingsSectionGeneral => 'Preferences'; @override String get settingsSectionQuickAccess => 'Primary Menu'; @@ -231,6 +231,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsSectionNotification => 'Notifications'; + @override + String get settingsInterfaceLanguage => 'Interface Language'; + + @override + String get settingsAiLanguage => 'AI Response Language'; + + @override + String get settingsNotificationAllow => 'Allow Notifications'; + + @override + String get settingsNotificationVibration => 'Allow Vibration'; + @override String get settingsSectionAbout => 'About'; @@ -268,9 +280,6 @@ class AppLocalizationsEn extends AppLocalizations { String get settingsCoinHeroSubtitle => 'Credits will be used for casting and related services later.'; - @override - String get settingsAiLanguage => 'AI Response Language'; - @override String get settingsAiLanguageHint => 'This field will align with profiles.settings.preferences.ai_language once the real preference flow is connected.'; @@ -411,8 +420,7 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get settingsCoinCenterDescription => - 'Payment is not connected yet. The UI now shows packages and the recharge entry.'; + String get settingsCoinCenterDescription => ''; @override String get settingsCoinRechargeSection => 'Recharge Packages'; @@ -433,7 +441,7 @@ class AppLocalizationsEn extends AppLocalizations { String get settingsPurchaseButton => 'Pay Now'; @override - String get settingsPurchasePending => 'Payment is not connected yet'; + String get settingsPurchasePending => ''; @override String settingsCoinAmount(int amount) { @@ -566,8 +574,16 @@ class AppLocalizationsEn extends AppLocalizations { String get divinationManualGuideTitle => 'Manual Casting Tutorial'; @override - String get divinationManualGuideInstruction => - 'Prepare three identical coins and cast six times following the guide.'; + String get divinationManualGuideStep1 => + 'Left: Pattern side. Right: Inscription side. Prepare three identical coins or similar tokens.'; + + @override + String get divinationManualGuideStep2 => + 'Hold the coins in both hands, think about your question, then toss them onto a table. Record how many inscription sides and pattern sides appear.'; + + @override + String get divinationManualGuideStep3 => + 'Record each result by whether the inscription side or pattern side faces up. Repeat 6 times, recording from bottom to top.'; @override String get divinationIAcknowledge => 'I Understand'; @@ -703,6 +719,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get transitionDone => 'Complete\nTap to view'; + @override + String get iChingTitle => 'I Ching'; + @override String get processingCardQianTitle => 'Qian • The Creative'; @@ -815,6 +834,10 @@ class AppLocalizationsEn extends AppLocalizations { String get manualYaoTipContent => 'Select from bottom to top, not top to bottom.\n\nCast three coins together, select once each time, six times total.'; + @override + String get manualCoinSelectHint => + 'Tap coins to flip between inscription and pattern. Record your casting result.'; + @override String get autoScreenTitle => 'Auto Casting'; @@ -890,5 +913,72 @@ class AppLocalizationsEn extends AppLocalizations { @override String get coinFaceGuideDescription => - '字 = side with inscription\n花 = side with pattern'; + 'Left: Pattern side. Right: Inscription side.'; + + @override + String get settingsInviteTitle => 'My Invitation'; + + @override + String get settingsInviteSubtitle => 'Invite friends, earn rewards together'; + + @override + String get settingsInviteMyCode => 'My Invite Code'; + + @override + String get settingsInviteCopySuccess => 'Invite code copied'; + + @override + String get settingsInviteCopied => 'Copied'; + + @override + String get settingsInviteCopy => 'Copy'; + + @override + String settingsInviteStats(int count) { + return 'Invited: $count friend(s)'; + } + + @override + String get settingsInviteBindCode => 'Bind Invite Code'; + + @override + String get settingsInviteBindHint => 'Enter your friend\'s invite code'; + + @override + String get settingsInviteBindPlaceholder => '6-digit code'; + + @override + String get settingsInviteBindButton => 'Bind'; + + @override + String get settingsInviteBindSuccess => 'Invite code bound successfully'; + + @override + String get settingsInviteBindFailed => 'Failed to bind invite code'; + + @override + String get settingsInviteGenerateTitle => 'Generate My Invite Code'; + + @override + String get settingsInviteGenerateButton => 'Generate My Invite Code'; + + @override + String get settingsInviteGenerateSuccess => 'Invite code generated'; + + @override + String get settingsInviteEmptyTitle => 'Invite Friends, Earn Rewards'; + + @override + String get settingsInviteEmptyDescription => + 'Each friend who registers using your invite code gives you bonus credits'; + + @override + String get settingsInviteInputLabel => 'Enter invite code (optional)'; + + @override + String get settingsInviteInputHint => 'Enter code to bind your inviter'; + + @override + String get settingsInviteInvalidCode => + 'Please enter a valid 6-character invite code'; } diff --git a/apps/lib/l10n/app_localizations_zh.dart b/apps/lib/l10n/app_localizations_zh.dart index 424c7d4..16c9b97 100644 --- a/apps/lib/l10n/app_localizations_zh.dart +++ b/apps/lib/l10n/app_localizations_zh.dart @@ -215,7 +215,7 @@ class AppLocalizationsZh extends AppLocalizations { String get settingsTitle => '设置'; @override - String get settingsSectionGeneral => '通用设置'; + String get settingsSectionGeneral => '偏好设置'; @override String get settingsSectionQuickAccess => '一级菜单'; @@ -229,6 +229,18 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settingsSectionNotification => '通知设置'; + @override + String get settingsInterfaceLanguage => '界面语言'; + + @override + String get settingsAiLanguage => 'AI 回复语言'; + + @override + String get settingsNotificationAllow => '允许通知'; + + @override + String get settingsNotificationVibration => '允许振动'; + @override String get settingsSectionAbout => '关于'; @@ -264,9 +276,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settingsCoinHeroSubtitle => '点数可用于后续起卦与相关服务消费'; - @override - String get settingsAiLanguage => 'AI 回复语言'; - @override String get settingsAiLanguageHint => '该字段将对齐 profiles.settings.preferences.ai_language,后续接入真实偏好设置。'; @@ -403,7 +412,7 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get settingsCoinCenterDescription => '充值入口暂未接入支付逻辑,先展示套餐与购买流程入口。'; + String get settingsCoinCenterDescription => ''; @override String get settingsCoinRechargeSection => '充值套餐'; @@ -424,7 +433,7 @@ class AppLocalizationsZh extends AppLocalizations { String get settingsPurchaseButton => '立即支付'; @override - String get settingsPurchasePending => '支付能力暂未接入'; + String get settingsPurchasePending => ''; @override String settingsCoinAmount(int amount) { @@ -549,7 +558,15 @@ class AppLocalizationsZh extends AppLocalizations { String get divinationManualGuideTitle => '手动起卦教程'; @override - String get divinationManualGuideInstruction => '准备三枚同样铜钱,按页面引导连续完成六次摇卦。'; + String get divinationManualGuideStep1 => '左侧为花面,右侧为字面。准备三枚相同的钱币,任何类似款式均可。'; + + @override + String get divinationManualGuideStep2 => + '双手捧起钱币,闭目心中默念所问之事,然后抛掷钱币于桌面,记录字面和花面出现的次数。'; + + @override + String get divinationManualGuideStep3 => + '记录每次结果,按照「字面在上还是花面在上」记录。重复六次,从下往上记录。'; @override String get divinationIAcknowledge => '我知道了'; @@ -686,60 +703,55 @@ class AppLocalizationsZh extends AppLocalizations { String get transitionDone => '解卦完成\n点击查看'; @override - String get processingCardQianTitle => 'Qian • The Creative'; + String get iChingTitle => '周易'; @override - String get processingCardQianQuote => - 'The movement of Heaven is full of power; thus the noble one makes himself strong and tireless.'; + String get processingCardQianTitle => '乾 • 元亨利贞'; @override - String get processingCardDuiTitle => 'Dui • The Joyous'; + String get processingCardQianQuote => '天行健,君子以自强不息。'; @override - String get processingCardDuiQuote => - 'Joy grounded in integrity brings openness, harmony, and right expression.'; + String get processingCardDuiTitle => '兑 • 亨利贞'; @override - String get processingCardLiTitle => 'Li • The Clinging Fire'; + String get processingCardDuiQuote => '丽泽兑,君子以朋友讲习。'; @override - String get processingCardLiQuote => - 'With clear brilliance, the great one illumines all directions.'; + String get processingCardLiTitle => '离 • 明两作亨利贞'; @override - String get processingCardZhenTitle => 'Zhen • The Arousing Thunder'; + String get processingCardLiQuote => '大人以继明照于四方。'; @override - String get processingCardZhenQuote => - 'Shock awakens the heart; composure turns fear into growth.'; + String get processingCardZhenTitle => '震 • 亨震来虩虩,笑言哑哑'; @override - String get processingCardXunTitle => 'Xun • The Gentle Wind'; + String get processingCardZhenQuote => '震惊百里,惊远而惧迩也。'; @override - String get processingCardXunQuote => - 'Gentle penetration furthers progress and helps one meet the right people.'; + String get processingCardXunTitle => '巽 • 小亨利贞'; @override - String get processingCardKanTitle => 'Kan • The Abysmal Water'; + String get processingCardXunQuote => '随风,君子以申命行事。'; @override - String get processingCardKanQuote => - 'In danger, sincerity and disciplined action carry one through.'; + String get processingCardKanTitle => '坎 • 习坎有孚维心亨'; @override - String get processingCardGenTitle => 'Gen • Keeping Still Mountain'; + String get processingCardKanQuote => '水流而不盈,行险而不失其信。'; @override - String get processingCardGenQuote => - 'Stillness at the proper time keeps one centered and steady in place.'; + String get processingCardGenTitle => '艮 • 艮其背不获其身'; @override - String get processingCardKunTitle => 'Kun • The Receptive Earth'; + String get processingCardGenQuote => '时止则止,时行则行,动静不失其时。'; @override - String get processingCardKunQuote => - 'The Earth\'s condition is devoted receptivity; the noble one carries all with broad virtue.'; + String get processingCardKunTitle => '坤 • 元亨利牝马之贞'; + + @override + String get processingCardKunQuote => '地势坤,君子以厚德载物。'; @override String get ganZhiInfo => '干支信息'; @@ -795,6 +807,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get manualYaoTipContent => '请从下往上选,不是从上往下选。\n\n三枚铜钱一起摇,摇完一次选一次,一共摇六次。'; + @override + String get manualCoinSelectHint => '点击硬币可翻转,调整字面和花面。记录摇卦结果。'; + @override String get autoScreenTitle => '自动起卦'; @@ -865,8 +880,73 @@ class AppLocalizationsZh extends AppLocalizations { String get cancel => '取消'; @override - String get coinFaceGuideTitle => '字花图片说明'; + String get coinFaceGuideTitle => '字花对照说明'; @override - String get coinFaceGuideDescription => '字=铜钱有字的一面\n花=铜钱有花纹的一面'; + String get coinFaceGuideDescription => '左侧为花面,右侧为字面。'; + + @override + String get settingsInviteTitle => '我的邀请'; + + @override + String get settingsInviteSubtitle => '邀请好友,共同获得奖励'; + + @override + String get settingsInviteMyCode => '我的邀请码'; + + @override + String get settingsInviteCopySuccess => '邀请码已复制'; + + @override + String get settingsInviteCopied => '已复制'; + + @override + String get settingsInviteCopy => '复制'; + + @override + String settingsInviteStats(int count) { + return '已邀请:$count 位好友'; + } + + @override + String get settingsInviteBindCode => '绑定邀请码'; + + @override + String get settingsInviteBindHint => '输入好友的邀请码'; + + @override + String get settingsInviteBindPlaceholder => '6位邀请码'; + + @override + String get settingsInviteBindButton => '绑定'; + + @override + String get settingsInviteBindSuccess => '邀请码绑定成功'; + + @override + String get settingsInviteBindFailed => '邀请码绑定失败'; + + @override + String get settingsInviteGenerateTitle => '生成我的邀请码'; + + @override + String get settingsInviteGenerateButton => '生成我的邀请码'; + + @override + String get settingsInviteGenerateSuccess => '邀请码生成成功'; + + @override + String get settingsInviteEmptyTitle => '邀请好友,获得奖励'; + + @override + String get settingsInviteEmptyDescription => '每成功邀请一位好友注册,您将获得积分奖励'; + + @override + String get settingsInviteInputLabel => '输入邀请码(选填)'; + + @override + String get settingsInviteInputHint => '输入邀请码绑定您的邀请人'; + + @override + String get settingsInviteInvalidCode => '请输入有效的6位邀请码'; } diff --git a/apps/lib/l10n/app_zh.arb b/apps/lib/l10n/app_zh.arb index 8d514a5..231fd78 100644 --- a/apps/lib/l10n/app_zh.arb +++ b/apps/lib/l10n/app_zh.arb @@ -80,11 +80,15 @@ "signBad": "下下签", "language": "语言", "settingsTitle": "设置", - "settingsSectionGeneral": "通用设置", + "settingsSectionGeneral": "偏好设置", "settingsSectionQuickAccess": "一级菜单", "settingsSectionAccount": "账户操作", "settingsSectionPrivacy": "隐私设置", "settingsSectionNotification": "通知设置", + "settingsInterfaceLanguage": "界面语言", + "settingsAiLanguage": "AI回复语言", + "settingsNotificationAllow": "允许通知", + "settingsNotificationVibration": "允许振动", "settingsSectionAbout": "关于", "settingsGeneralTitle": "通用设置", "settingsGeneralSubtitle": "语言:{currentLanguage},其余字段按 profiles.settings 结构预留", @@ -166,14 +170,14 @@ } } }, - "settingsCoinCenterDescription": "充值入口暂未接入支付逻辑,先展示套餐与购买流程入口。", + "settingsCoinCenterDescription": "", "settingsCoinRechargeSection": "充值套餐", "settingsCoinPackBasic": "入门补充包", "settingsCoinPackPopular": "常用加量包", "settingsCoinPackPremium": "高频进阶包", "settingsCoinPackPopularBadge": "推荐", "settingsPurchaseButton": "立即支付", - "settingsPurchasePending": "支付能力暂未接入", + "settingsPurchasePending": "", "settingsCoinAmount": "{amount} 点数", "@settingsCoinAmount": { "placeholders": { @@ -226,7 +230,9 @@ "divinationMethodTipManual": "手动起卦:需要准备三枚同样的铜钱或硬币。", "divinationMethodTipRecommend": "推荐使用手动起卦,卦象解读准确概率更高。", "divinationManualGuideTitle": "手动起卦教程", - "divinationManualGuideInstruction": "准备三枚同样铜钱,按页面引导连续完成六次摇卦。", + "divinationManualGuideStep1": "左侧为花面,右侧为字面。准备三枚相同的钱币,任何类似款式均可。", + "divinationManualGuideStep2": "双手捧起钱币,闭目心中默念所问之事,然后抛掷钱币于桌面,记录字面和花面出现的次数。", + "divinationManualGuideStep3": "记录每次结果,按照「字面在上还是花面在上」记录。重复六次,从下往上记录。", "divinationIAcknowledge": "我知道了", "divinationClose": "关闭", "divinationModify": "修改", @@ -287,22 +293,23 @@ "transitionPreparing": "天机推演中", "transitionDeriving": "正在解卦", "transitionDone": "解卦完成\n点击查看", - "processingCardQianTitle": "Qian • The Creative", - "processingCardQianQuote": "The movement of Heaven is full of power; thus the noble one makes himself strong and tireless.", - "processingCardDuiTitle": "Dui • The Joyous", - "processingCardDuiQuote": "Joy grounded in integrity brings openness, harmony, and right expression.", - "processingCardLiTitle": "Li • The Clinging Fire", - "processingCardLiQuote": "With clear brilliance, the great one illumines all directions.", - "processingCardZhenTitle": "Zhen • The Arousing Thunder", - "processingCardZhenQuote": "Shock awakens the heart; composure turns fear into growth.", - "processingCardXunTitle": "Xun • The Gentle Wind", - "processingCardXunQuote": "Gentle penetration furthers progress and helps one meet the right people.", - "processingCardKanTitle": "Kan • The Abysmal Water", - "processingCardKanQuote": "In danger, sincerity and disciplined action carry one through.", - "processingCardGenTitle": "Gen • Keeping Still Mountain", - "processingCardGenQuote": "Stillness at the proper time keeps one centered and steady in place.", - "processingCardKunTitle": "Kun • The Receptive Earth", - "processingCardKunQuote": "The Earth's condition is devoted receptivity; the noble one carries all with broad virtue.", + "iChingTitle": "周易", + "processingCardQianTitle": "乾 • 元亨利贞", + "processingCardQianQuote": "天行健,君子以自强不息。", + "processingCardDuiTitle": "兑 • 亨利贞", + "processingCardDuiQuote": "丽泽兑,君子以朋友讲习。", + "processingCardLiTitle": "离 • 明两作亨利贞", + "processingCardLiQuote": "大人以继明照于四方。", + "processingCardZhenTitle": "震 • 亨震来虩虩,笑言哑哑", + "processingCardZhenQuote": "震惊百里,惊远而惧迩也。", + "processingCardXunTitle": "巽 • 小亨利贞", + "processingCardXunQuote": "随风,君子以申命行事。", + "processingCardKanTitle": "坎 • 习坎有孚维心亨", + "processingCardKanQuote": "水流而不盈,行险而不失其信。", + "processingCardGenTitle": "艮 • 艮其背不获其身", + "processingCardGenQuote": "时止则止,时行则行,动静不失其时。", + "processingCardKunTitle": "坤 • 元亨利牝马之贞", + "processingCardKunQuote": "地势坤,君子以厚德载物。", "ganZhiInfo": "干支信息", "wuXingWangShuai": "五行旺衰", "ganZhiKongWang": "空亡信息", @@ -321,6 +328,7 @@ "manualYaoInstruction": "点击查看起卦方法与铜钱字花组合说明", "manualYaoTipTitle": "提示", "manualYaoTipContent": "请从下往上选,不是从上往下选。\n\n三枚铜钱一起摇,摇完一次选一次,一共摇六次。", + "manualCoinSelectHint": "点击硬币可翻转,调整字面和花面。记录摇卦结果。", "autoScreenTitle": "自动起卦", "autoSelectTime": "选择起卦时间", "autoCoinDivination": "铜钱摇卦", @@ -364,6 +372,34 @@ "confirm": "确认", "cancel": "取消", "autoSelectTime": "选择时间", - "coinFaceGuideTitle": "字花图片说明", - "coinFaceGuideDescription": "字=铜钱有字的一面\n花=铜钱有花纹的一面" + "coinFaceGuideTitle": "字花对照说明", + "coinFaceGuideDescription": "左侧为花面,右侧为字面。", + "settingsInviteTitle": "我的邀请", + "settingsInviteSubtitle": "邀请好友,共同获得奖励", + "settingsInviteMyCode": "我的邀请码", + "settingsInviteCopySuccess": "邀请码已复制", + "settingsInviteCopied": "已复制", + "settingsInviteCopy": "复制", + "settingsInviteStats": "已邀请:{count} 位好友", + "@settingsInviteStats": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "settingsInviteBindCode": "绑定邀请码", + "settingsInviteBindHint": "输入好友的邀请码", + "settingsInviteBindPlaceholder": "6位邀请码", + "settingsInviteBindButton": "绑定", + "settingsInviteBindSuccess": "邀请码绑定成功", + "settingsInviteBindFailed": "邀请码绑定失败", + "settingsInviteGenerateTitle": "生成我的邀请码", + "settingsInviteGenerateButton": "生成我的邀请码", + "settingsInviteGenerateSuccess": "邀请码生成成功", + "settingsInviteEmptyTitle": "邀请好友,获得奖励", + "settingsInviteEmptyDescription": "每成功邀请一位好友注册,您将获得积分奖励", + "settingsInviteInputLabel": "输入邀请码(选填)", + "settingsInviteInputHint": "输入邀请码绑定您的邀请人", + "settingsInviteInvalidCode": "请输入有效的6位邀请码" } diff --git a/apps/lib/shared/widgets/divination/divination_shared_widgets.dart b/apps/lib/shared/widgets/divination/divination_shared_widgets.dart index de7cc1c..85fbe5e 100644 --- a/apps/lib/shared/widgets/divination/divination_shared_widgets.dart +++ b/apps/lib/shared/widgets/divination/divination_shared_widgets.dart @@ -5,15 +5,24 @@ import '../../theme/app_color_palette.dart'; import '../../theme/design_tokens.dart'; class DivinationGuideImage extends StatelessWidget { - const DivinationGuideImage({super.key, required this.path}); + const DivinationGuideImage({super.key, required this.paths}); - final String path; + final List paths; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), - child: Image.asset(path, fit: BoxFit.contain), + child: paths.length == 1 + ? Image.asset(paths.first, fit: BoxFit.contain) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded(child: Image.asset(paths[0], fit: BoxFit.contain)), + const SizedBox(width: AppSpacing.md), + Expanded(child: Image.asset(paths[1], fit: BoxFit.contain)), + ], + ), ); } } @@ -67,17 +76,31 @@ class DivinationInstructionCard extends StatelessWidget { } } -class DivinationGuideDialog extends StatelessWidget { +class DivinationGuideDialog extends StatefulWidget { const DivinationGuideDialog({ super.key, required this.title, required this.guideImages, - required this.instructionText, + required this.instructions, }); final String title; - final List guideImages; - final String instructionText; + final List> guideImages; + final List instructions; + + @override + State createState() => _DivinationGuideDialogState(); +} + +class _DivinationGuideDialogState extends State { + final PageController _pageController = PageController(); + int _currentPage = 0; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -90,7 +113,7 @@ class DivinationGuideDialog extends StatelessWidget { children: [ const SizedBox(height: AppSpacing.lg), Text( - title, + widget.title, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w700, @@ -98,15 +121,25 @@ class DivinationGuideDialog extends StatelessWidget { ), const SizedBox(height: AppSpacing.md), Expanded( - child: PageView( - children: guideImages - .map((path) => DivinationGuideImage(path: path)) - .toList(), + child: PageView.builder( + controller: _pageController, + onPageChanged: (index) { + setState(() { + _currentPage = index; + }); + }, + itemCount: widget.guideImages.length, + itemBuilder: (context, index) { + return DivinationGuideImage(paths: widget.guideImages[index]); + }, ), ), Padding( padding: const EdgeInsets.all(AppSpacing.lg), - child: Text(instructionText), + child: Text( + widget.instructions[_currentPage], + textAlign: TextAlign.center, + ), ), Padding( padding: const EdgeInsets.fromLTRB( diff --git a/apps/lib/shared/widgets/divination/divination_terms.dart b/apps/lib/shared/widgets/divination/divination_terms.dart index 243727c..b27184b 100644 --- a/apps/lib/shared/widgets/divination/divination_terms.dart +++ b/apps/lib/shared/widgets/divination/divination_terms.dart @@ -12,6 +12,8 @@ abstract final class DivinationTerms { static const yinYang = {true: '阳', false: '阴'}; + static const ziHua = {true: '字', false: '花'}; + static const wuXing = ['木', '火', '土', '金', '水']; static const yuanZhi = '元'; diff --git a/apps/pubspec.yaml b/apps/pubspec.yaml index 7b71eda..789b8db 100644 --- a/apps/pubspec.yaml +++ b/apps/pubspec.yaml @@ -80,6 +80,7 @@ flutter: assets: - assets/images/logo.png - assets/images/qigua/ + - assets/images/tutorial/ - assets/legal/en/ - assets/legal/zh/