feat(divination): add DateTimePickerBottomSheet with iOS wheel style

This commit is contained in:
qzl
2026-04-03 18:16:18 +08:00
parent 39575ddf37
commit a034962d6b
6 changed files with 256 additions and 45 deletions
+17 -9
View File
@@ -95,8 +95,8 @@
"settingsPrivacyAndNotificationSubtitle": "Manage placeholders for privacy and notification groups",
"settingsLegalCenterTitle": "About & Agreements",
"settingsLegalCenterSubtitle": "Read About Us, Privacy Policy, and Terms of Service",
"settingsCoinCenterTitle": "Coin Center",
"settingsCoinCenterSubtitle": "Balance: {balance} coins. View packages and recharge entry.",
"settingsCoinCenterTitle": "Credits Center",
"settingsCoinCenterSubtitle": "Balance: {balance} credits. View packages and recharge entry.",
"@settingsCoinCenterSubtitle": {
"placeholders": {
"balance": {
@@ -104,7 +104,7 @@
}
}
},
"settingsCoinHeroSubtitle": "Coins will be used for casting and related services later.",
"settingsCoinHeroSubtitle": "Credits will be used for casting and related services later.",
"settingsAiLanguage": "AI Response Language",
"settingsAiLanguageHint": "This field will align with profiles.settings.preferences.ai_language once the real preference flow is connected.",
"settingsTimezone": "Time Zone",
@@ -140,8 +140,8 @@
"settingsLogoutConfirmHint": "Tap again to confirm logout",
"settingsLogoutConfirmAction": "Tap again to logout",
"settingsLanguageSection": "Interface Language",
"settingsCoinBalanceLabel": "Current Coins",
"settingsCoinBalanceValue": "{balance} coins",
"settingsCoinBalanceLabel": "Current Credits",
"settingsCoinBalanceValue": "{balance} credits",
"@settingsCoinBalanceValue": {
"placeholders": {
"balance": {
@@ -157,7 +157,7 @@
"settingsCoinPackPopularBadge": "Popular",
"settingsPurchaseButton": "Pay Now",
"settingsPurchasePending": "Payment is not connected yet",
"settingsCoinAmount": "{amount} coins",
"settingsCoinAmount": "{amount} credits",
"@settingsCoinAmount": {
"placeholders": {
"amount": {
@@ -184,6 +184,8 @@
"errorServiceUnavailable": "Service unavailable, please try again later",
"errorServerGeneric": "Server error, please try again later",
"errorRequestGeneric": "Request failed, please try again",
"errorRunLimitExceeded": "Run limit reached in this session. Please start a new divination.",
"errorDivinationPayloadRequired": "Missing divination payload. Please cast again.",
"divinationScreenTitle": "Cast Hexagram",
"divinationSelectMethod": "Select divination method",
"divinationManualMethod": "Manual",
@@ -192,7 +194,7 @@
"divinationQuestionInputPrompt": "Enter your question",
"divinationQuestionInputHint": "Describe your question in detail for more accurate reading",
"divinationStartButton": "Start Casting",
"divinationCoinBalance": "Simulated coin balance: {balance}",
"divinationCoinBalance": "Available coins: {balance}",
"@divinationCoinBalance": {
"placeholders": {
"balance": {
@@ -200,6 +202,7 @@
}
}
},
"divinationRefreshBalance": "Refresh balance",
"divinationRecommendManual": "Manual casting is recommended for more accurate readings! Prepare three identical coins and click here for the tutorial.",
"divinationMethodTipTitle": "Divination Method",
"divinationMethodTipAuto": "Auto: No coins needed, just follow the instructions.",
@@ -289,7 +292,7 @@
},
"autoShakeComplete": "Tap the button below to analyze",
"autoTryShakePhone": "You can also shake your phone",
"autoSimBalance": "Balance: {balance}",
"autoSimBalance": "Available coins: {balance}",
"@autoSimBalance": {
"placeholders": {
"balance": {
@@ -298,5 +301,10 @@
}
},
"autoGuideTitle": "Auto Casting Tutorial",
"autoGuideInstruction": "Shake your phone or tap the button, cast 6 times to form a complete hexagram."
"autoGuideInstruction": "Shake your phone or tap the button, cast 6 times to form a complete hexagram.",
"dateTab": "Date",
"timeTab": "Time",
"confirm": "Confirm",
"cancel": "Cancel",
"autoSelectTime": "Select Time"
}
+51 -9
View File
@@ -545,19 +545,19 @@ abstract class AppLocalizations {
/// No description provided for @settingsCoinCenterTitle.
///
/// In zh, this message translates to:
/// **'铜币中心'**
/// **'点数中心'**
String get settingsCoinCenterTitle;
/// No description provided for @settingsCoinCenterSubtitle.
///
/// In zh, this message translates to:
/// **'当前余额 {balance} 枚铜币,查看套餐与充值入口'**
/// **'当前余额 {balance} 点数,查看套餐与充值入口'**
String settingsCoinCenterSubtitle(int balance);
/// No description provided for @settingsCoinHeroSubtitle.
///
/// In zh, this message translates to:
/// **'铜币可用于后续起卦与相关服务消费'**
/// **'点数可用于后续起卦与相关服务消费'**
String get settingsCoinHeroSubtitle;
/// No description provided for @settingsAiLanguage.
@@ -731,13 +731,13 @@ abstract class AppLocalizations {
/// No description provided for @settingsCoinBalanceLabel.
///
/// In zh, this message translates to:
/// **'当前铜币'**
/// **'当前点数'**
String get settingsCoinBalanceLabel;
/// No description provided for @settingsCoinBalanceValue.
///
/// In zh, this message translates to:
/// **'{balance} 枚铜币'**
/// **'{balance} 点数'**
String settingsCoinBalanceValue(int balance);
/// No description provided for @settingsCoinCenterDescription.
@@ -791,7 +791,7 @@ abstract class AppLocalizations {
/// No description provided for @settingsCoinAmount.
///
/// In zh, this message translates to:
/// **'{amount} 枚铜币'**
/// **'{amount} 点数'**
String settingsCoinAmount(int amount);
/// No description provided for @english.
@@ -908,6 +908,18 @@ abstract class AppLocalizations {
/// **'请求失败,请稍后重试'**
String get errorRequestGeneric;
/// No description provided for @errorRunLimitExceeded.
///
/// In zh, this message translates to:
/// **'本次会话追问次数已达上限,请新起一卦'**
String get errorRunLimitExceeded;
/// No description provided for @errorDivinationPayloadRequired.
///
/// In zh, this message translates to:
/// **'缺少六爻输入数据,请重新起卦'**
String get errorDivinationPayloadRequired;
/// No description provided for @divinationScreenTitle.
///
/// In zh, this message translates to:
@@ -959,9 +971,15 @@ abstract class AppLocalizations {
/// No description provided for @divinationCoinBalance.
///
/// In zh, this message translates to:
/// **'模拟铜钱余额{balance} 枚'**
/// **'当前可用铜币{balance} 枚'**
String divinationCoinBalance(int balance);
/// No description provided for @divinationRefreshBalance.
///
/// In zh, this message translates to:
/// **'刷新余额'**
String get divinationRefreshBalance;
/// No description provided for @divinationRecommendManual.
///
/// In zh, this message translates to:
@@ -1295,7 +1313,7 @@ abstract class AppLocalizations {
/// No description provided for @autoSelectTime.
///
/// In zh, this message translates to:
/// **'选择起卦时间'**
/// **'选择时间'**
String get autoSelectTime;
/// No description provided for @autoCoinDivination.
@@ -1373,7 +1391,7 @@ abstract class AppLocalizations {
/// No description provided for @autoSimBalance.
///
/// In zh, this message translates to:
/// **'模拟余额{balance} 枚'**
/// **'当前可用铜币{balance} 枚'**
String autoSimBalance(int balance);
/// No description provided for @autoGuideTitle.
@@ -1387,6 +1405,30 @@ abstract class AppLocalizations {
/// In zh, this message translates to:
/// **'摇晃手机或点击按钮,连续摇 6 次即可形成完整卦象。'**
String get autoGuideInstruction;
/// No description provided for @dateTab.
///
/// In zh, this message translates to:
/// **'日期'**
String get dateTab;
/// No description provided for @timeTab.
///
/// In zh, this message translates to:
/// **'时间'**
String get timeTab;
/// No description provided for @confirm.
///
/// In zh, this message translates to:
/// **'确认'**
String get confirm;
/// No description provided for @cancel.
///
/// In zh, this message translates to:
/// **'取消'**
String get cancel;
}
class _AppLocalizationsDelegate
+32 -9
View File
@@ -245,16 +245,16 @@ class AppLocalizationsEn extends AppLocalizations {
'Read About Us, Privacy Policy, and Terms of Service';
@override
String get settingsCoinCenterTitle => 'Coin Center';
String get settingsCoinCenterTitle => 'Credits Center';
@override
String settingsCoinCenterSubtitle(int balance) {
return 'Balance: $balance coins. View packages and recharge entry.';
return 'Balance: $balance credits. View packages and recharge entry.';
}
@override
String get settingsCoinHeroSubtitle =>
'Coins will be used for casting and related services later.';
'Credits will be used for casting and related services later.';
@override
String get settingsAiLanguage => 'AI Response Language';
@@ -350,11 +350,11 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsLanguageSection => 'Interface Language';
@override
String get settingsCoinBalanceLabel => 'Current Coins';
String get settingsCoinBalanceLabel => 'Current Credits';
@override
String settingsCoinBalanceValue(int balance) {
return '$balance coins';
return '$balance credits';
}
@override
@@ -384,7 +384,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String settingsCoinAmount(int amount) {
return '$amount coins';
return '$amount credits';
}
@override
@@ -449,6 +449,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get errorRequestGeneric => 'Request failed, please try again';
@override
String get errorRunLimitExceeded =>
'Run limit reached in this session. Please start a new divination.';
@override
String get errorDivinationPayloadRequired =>
'Missing divination payload. Please cast again.';
@override
String get divinationScreenTitle => 'Cast Hexagram';
@@ -476,9 +484,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String divinationCoinBalance(int balance) {
return 'Simulated coin balance: $balance';
return 'Available coins: $balance';
}
@override
String get divinationRefreshBalance => 'Refresh balance';
@override
String get divinationRecommendManual =>
'Manual casting is recommended for more accurate readings! Prepare three identical coins and click here for the tutorial.';
@@ -655,7 +666,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get autoScreenTitle => 'Auto Casting';
@override
String get autoSelectTime => 'Select time';
String get autoSelectTime => 'Select Time';
@override
String get autoCoinDivination => 'Coin Casting';
@@ -699,7 +710,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String autoSimBalance(int balance) {
return 'Balance: $balance';
return 'Available coins: $balance';
}
@override
@@ -708,4 +719,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get autoGuideInstruction =>
'Shake your phone or tap the button, cast 6 times to form a complete hexagram.';
@override
String get dateTab => 'Date';
@override
String get timeTab => 'Time';
@override
String get confirm => 'Confirm';
@override
String get cancel => 'Cancel';
}
+30 -9
View File
@@ -242,15 +242,15 @@ class AppLocalizationsZh extends AppLocalizations {
String get settingsLegalCenterSubtitle => '查看关于我们、隐私政策与服务条款';
@override
String get settingsCoinCenterTitle => '铜币中心';
String get settingsCoinCenterTitle => '点数中心';
@override
String settingsCoinCenterSubtitle(int balance) {
return '当前余额 $balance 枚铜币,查看套餐与充值入口';
return '当前余额 $balance 点数,查看套餐与充值入口';
}
@override
String get settingsCoinHeroSubtitle => '铜币可用于后续起卦与相关服务消费';
String get settingsCoinHeroSubtitle => '点数可用于后续起卦与相关服务消费';
@override
String get settingsAiLanguage => 'AI 回复语言';
@@ -344,11 +344,11 @@ class AppLocalizationsZh extends AppLocalizations {
String get settingsLanguageSection => '界面语言';
@override
String get settingsCoinBalanceLabel => '当前铜币';
String get settingsCoinBalanceLabel => '当前点数';
@override
String settingsCoinBalanceValue(int balance) {
return '$balance 枚铜币';
return '$balance 点数';
}
@override
@@ -377,7 +377,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String settingsCoinAmount(int amount) {
return '$amount 枚铜币';
return '$amount 点数';
}
@override
@@ -440,6 +440,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get errorRequestGeneric => '请求失败,请稍后重试';
@override
String get errorRunLimitExceeded => '本次会话追问次数已达上限,请新起一卦';
@override
String get errorDivinationPayloadRequired => '缺少六爻输入数据,请重新起卦';
@override
String get divinationScreenTitle => '起卦';
@@ -466,9 +472,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String divinationCoinBalance(int balance) {
return '模拟铜钱余额$balance';
return '当前可用铜币$balance';
}
@override
String get divinationRefreshBalance => '刷新余额';
@override
String get divinationRecommendManual =>
'推荐使用手动起卦,解卦更准确!准备三枚一样的铜钱或硬币,点击这里查看手动起卦教程。';
@@ -639,7 +648,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get autoScreenTitle => '自动起卦';
@override
String get autoSelectTime => '选择起卦时间';
String get autoSelectTime => '选择时间';
@override
String get autoCoinDivination => '铜钱摇卦';
@@ -683,7 +692,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String autoSimBalance(int balance) {
return '模拟余额$balance';
return '当前可用铜币$balance';
}
@override
@@ -691,4 +700,16 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get autoGuideInstruction => '摇晃手机或点击按钮,连续摇 6 次即可形成完整卦象。';
@override
String get dateTab => '日期';
@override
String get timeTab => '时间';
@override
String get confirm => '确认';
@override
String get cancel => '取消';
}
+17 -9
View File
@@ -95,8 +95,8 @@
"settingsPrivacyAndNotificationSubtitle": "分组管理 privacy 与 notification 占位设置",
"settingsLegalCenterTitle": "关于与协议",
"settingsLegalCenterSubtitle": "查看关于我们、隐私政策与服务条款",
"settingsCoinCenterTitle": "铜币中心",
"settingsCoinCenterSubtitle": "当前余额 {balance} 枚铜币,查看套餐与充值入口",
"settingsCoinCenterTitle": "点数中心",
"settingsCoinCenterSubtitle": "当前余额 {balance} 点数,查看套餐与充值入口",
"@settingsCoinCenterSubtitle": {
"placeholders": {
"balance": {
@@ -104,7 +104,7 @@
}
}
},
"settingsCoinHeroSubtitle": "铜币可用于后续起卦与相关服务消费",
"settingsCoinHeroSubtitle": "点数可用于后续起卦与相关服务消费",
"settingsAiLanguage": "AI 回复语言",
"settingsAiLanguageHint": "该字段将对齐 profiles.settings.preferences.ai_language,后续接入真实偏好设置。",
"settingsTimezone": "时区",
@@ -140,8 +140,8 @@
"settingsLogoutConfirmHint": "再次点击确认退出登录",
"settingsLogoutConfirmAction": "再次点击确认退出",
"settingsLanguageSection": "界面语言",
"settingsCoinBalanceLabel": "当前铜币",
"settingsCoinBalanceValue": "{balance} 枚铜币",
"settingsCoinBalanceLabel": "当前点数",
"settingsCoinBalanceValue": "{balance} 点数",
"@settingsCoinBalanceValue": {
"placeholders": {
"balance": {
@@ -157,7 +157,7 @@
"settingsCoinPackPopularBadge": "推荐",
"settingsPurchaseButton": "立即支付",
"settingsPurchasePending": "支付能力暂未接入",
"settingsCoinAmount": "{amount} 枚铜币",
"settingsCoinAmount": "{amount} 点数",
"@settingsCoinAmount": {
"placeholders": {
"amount": {
@@ -184,6 +184,8 @@
"errorServiceUnavailable": "服务暂时不可用,请稍后重试",
"errorServerGeneric": "服务异常,请稍后重试",
"errorRequestGeneric": "请求失败,请稍后重试",
"errorRunLimitExceeded": "本次会话追问次数已达上限,请新起一卦",
"errorDivinationPayloadRequired": "缺少六爻输入数据,请重新起卦",
"divinationScreenTitle": "起卦",
"divinationSelectMethod": "选择起卦方式",
"divinationManualMethod": "手动起卦",
@@ -192,7 +194,7 @@
"divinationQuestionInputPrompt": "请输入您想占卜的问题",
"divinationQuestionInputHint": "请描述您的问题,描述越详细解卦越准确",
"divinationStartButton": "开始起卦",
"divinationCoinBalance": "模拟铜钱余额{balance} 枚",
"divinationCoinBalance": "当前可用铜币{balance} 枚",
"@divinationCoinBalance": {
"placeholders": {
"balance": {
@@ -200,6 +202,7 @@
}
}
},
"divinationRefreshBalance": "刷新余额",
"divinationRecommendManual": "推荐使用手动起卦,解卦更准确!准备三枚一样的铜钱或硬币,点击这里查看手动起卦教程。",
"divinationMethodTipTitle": "起卦方式说明",
"divinationMethodTipAuto": "自动起卦:不需要铜钱或硬币,按照引导完成摇卦。",
@@ -289,7 +292,7 @@
},
"autoShakeComplete": "点击页面底部开始解卦",
"autoTryShakePhone": "您也可以试试摇晃手机来起卦",
"autoSimBalance": "模拟余额{balance} 枚",
"autoSimBalance": "当前可用铜币{balance} 枚",
"@autoSimBalance": {
"placeholders": {
"balance": {
@@ -298,5 +301,10 @@
}
},
"autoGuideTitle": "自动起卦教程",
"autoGuideInstruction": "摇晃手机或点击按钮,连续摇 6 次即可形成完整卦象。"
"autoGuideInstruction": "摇晃手机或点击按钮,连续摇 6 次即可形成完整卦象。",
"dateTab": "日期",
"timeTab": "时间",
"confirm": "确认",
"cancel": "取消",
"autoSelectTime": "选择时间"
}
@@ -0,0 +1,109 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
class DateTimePickerBottomSheet extends StatefulWidget {
const DateTimePickerBottomSheet({
super.key,
required this.initialDateTime,
this.minDateTime,
this.maxDateTime,
});
final DateTime initialDateTime;
final DateTime? minDateTime;
final DateTime? maxDateTime;
@override
State<DateTimePickerBottomSheet> createState() =>
_DateTimePickerBottomSheetState();
}
class _DateTimePickerBottomSheetState extends State<DateTimePickerBottomSheet> {
late DateTime _selectedDateTime;
int _selectedTab = 0;
@override
void initState() {
super.initState();
_selectedDateTime = widget.initialDateTime;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Container(
height: 400,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.cancel),
),
Text(
l10n.autoSelectTime,
style: Theme.of(context).textTheme.titleMedium,
),
TextButton(
onPressed: () => Navigator.pop(context, _selectedDateTime),
child: Text(l10n.confirm),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CupertinoSlidingSegmentedControl<int>(
groupValue: _selectedTab,
children: {0: Text(l10n.dateTab), 1: Text(l10n.timeTab)},
onValueChanged: (value) =>
setState(() => _selectedTab = value ?? 0),
),
),
const SizedBox(height: 16),
Expanded(
child: CupertinoDatePicker(
mode: _selectedTab == 0
? CupertinoDatePickerMode.date
: CupertinoDatePickerMode.time,
initialDateTime: _selectedDateTime,
minimumDate: widget.minDateTime,
maximumDate: widget.maxDateTime,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => _selectedDateTime = newDateTime);
},
),
),
],
),
);
}
}
Future<DateTime?> showDateTimePickerBottomSheet({
required BuildContext context,
required DateTime initialDateTime,
DateTime? minDateTime,
DateTime? maxDateTime,
}) {
return showModalBottomSheet<DateTime>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => DateTimePickerBottomSheet(
initialDateTime: initialDateTime,
minDateTime: minDateTime,
maxDateTime: maxDateTime,
),
);
}