Commit d5b95558ca33ef8cfcb3c59ccb7d32e4e88a0b8e

Authored by 常守达
1 parent 08bbfa57

fix(today):数据展示兼容

... ... @@ -667,9 +667,9 @@ class TodayController extends GetMaterialController {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
var today = DateUtils.dateOnly(DateTime.now());
lastSelectableDay.value = today;
changeDate(today);
// var today = DateUtils.dateOnly(DateTime.now());
// lastSelectableDay.value = today;
// changeDate(today);
checkAddFriendVisible();
checkHrvAdBannerVisible();
checkHealthDataAuthCardVisible();
... ... @@ -682,9 +682,9 @@ class TodayController extends GetMaterialController {
_isFirstLoad = false;
return;
}
var today = DateUtils.dateOnly(DateTime.now());
lastSelectableDay.value = today;
changeDate(today);
// var today = DateUtils.dateOnly(DateTime.now());
// lastSelectableDay.value = today;
// changeDate(today);
checkAddFriendVisible();
checkHrvAdBannerVisible();
checkHealthDataAuthCardVisible();
... ...
... ... @@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
/// 传入 [score](0~100 压力分):
/// - null → 加载态:四段等宽 20px,无 indicator
/// - 0~20 → 优秀·绿段(status 3)
/// - 21~50 → 正常·蓝段(status 2)
/// - 51~80 → 注意压力·橙段(status 1)
/// - 21~60 → 正常·蓝段(status 2)
/// - 61~80 → 注意压力·橙段(status 1)
/// - 81~100 → 压力过载·红段(status 0)
///
/// indicator 在命中段内的横向位置由分数在该区间的位置决定:
... ... @@ -43,19 +43,19 @@ class StressProgressBar extends StatelessWidget {
static const _curve = Curves.easeInOut;
// ── 分数区间边界(左闭右闭)───────────────────────────
// status 3 (绿): 0~20 status 2 (蓝): 21~50
// status 1 (橙): 51~80 status 0 (红): 81~100
// status 3 (绿): 0~20 status 2 (蓝): 21~60
// status 1 (橙): 61~80 status 0 (红): 81~100
static const _ranges = [
(lo: 81, hi: 100), // status 0
(lo: 51, hi: 80), // status 1
(lo: 21, hi: 50), // status 2
(lo: 61, hi: 80), // status 1
(lo: 21, hi: 60), // status 2
(lo: 0, hi: 20), // status 3
];
/// 由压力分计算命中段(0=过载, 1=注意, 2=正常, 3=活力)
static int _statusFromScore(int s) {
if (s <= 20) return 3;
if (s <= 50) return 2;
if (s <= 60) return 2;
if (s <= 80) return 1;
return 0;
}
... ...
... ... @@ -376,10 +376,11 @@ class TodayHrvChartCard extends StatelessWidget {
return l10n.waitingForData;
}
List<TextSpan>? _getTooltip(BarChartRodData rod, int x, int? state) {
List<TextSpan>? _getTooltip(
BarChartRodData rod, int x, int? state, bool? isWorkout) {
return [
TextSpan(
text: '○ ',
text: isWorkout == true ? '◉ ' : '○ ',
style: TextStyle(
fontSize: 14,
color: _getColorByState(state),
... ... @@ -390,7 +391,9 @@ class TodayHrvChartCard extends StatelessWidget {
),
),
TextSpan(
text: '${_getStatusByState(state)} ${_formatNumber(rod.toY)}%',
text: isWorkout == true
? l10n.frequentMovement
: '${_getStatusByState(state)} ${_formatNumber(rod.toY)}%',
style: TextStyle(
fontSize: 18,
color: _getColorByState(state),
... ... @@ -531,13 +534,16 @@ class _HrvLineChartState extends State<_HrvLineChart> {
@override
Widget build(BuildContext context) {
final maxY = widget.spots.isEmpty
? 100.0
: widget.spots.map((s) => s.y).reduce((a, b) => a > b ? a : b) * 1.1;
return LineChart(
LineChartData(
clipData: const FlClipData.none(),
minX: 0,
maxX: widget.maxSeconds,
minY: 0,
maxY: 100,
maxY: maxY,
gridData: FlGridData(
show: true,
drawVerticalLine: true,
... ... @@ -626,7 +632,7 @@ class _HrvLineChartState extends State<_HrvLineChart> {
],
lineTouchData: LineTouchData(
getTouchLineStart: (barData, spotIndex) => 0,
getTouchLineEnd: (barData, spotIndex) => 100,
getTouchLineEnd: (barData, spotIndex) => maxY,
handleBuiltInTouches: false,
touchSpotThreshold: 20.0,
touchCallback:
... ... @@ -743,7 +749,7 @@ class _StressBarChart extends StatefulWidget {
final VoidCallback onTapPurchase;
final double maxSeconds;
final Color Function(int?) getColorByState;
final List<TextSpan>? Function(BarChartRodData, int, int?) getTooltip;
final List<TextSpan>? Function(BarChartRodData, int, int?, bool?) getTooltip;
final String? Function(double) timeLabel;
@override
... ... @@ -802,24 +808,26 @@ class _StressBarChartState extends State<_StressBarChart> {
..sort((a, b) => a.fromTime.compareTo(b.fromTime));
// 2. 合并相邻且间隔小于 2 小时的睡眠区间,避免因短暂醒来分段导致多个床图标或跨天首尾大面积相连
final mergedList = <V2SleepTimeRange>[];
for (final range in sortedList) {
if (mergedList.isEmpty) {
mergedList.add(range);
} else {
final lastRange = mergedList.last;
if (range.fromTime - lastRange.toTime <= 2 * 3600) {
mergedList[mergedList.length - 1] = V2SleepTimeRange(
fromTime: lastRange.fromTime,
toTime: range.toTime > lastRange.toTime
? range.toTime
: lastRange.toTime,
);
} else {
mergedList.add(range);
}
}
}
// final mergedList = <V2SleepTimeRange>[];
// for (final range in sortedList) {
// if (mergedList.isEmpty) {
// mergedList.add(range);
// } else {
// final lastRange = mergedList.last;
// if (range.fromTime - lastRange.toTime <= 2 * 3600) {
// mergedList[mergedList.length - 1] = V2SleepTimeRange(
// fromTime: lastRange.fromTime,
// toTime: range.toTime > lastRange.toTime
// ? range.toTime
// : lastRange.toTime,
// );
// } else {
// mergedList.add(range);
// }
// }
// }
// 2. 不合并睡眠段,直接使用排序后的列表
final mergedList = sortedList;
// 3. 渲染合并后的睡眠区间 Positioned widgets
for (final range in mergedList) {
... ... @@ -888,7 +896,8 @@ class _StressBarChartState extends State<_StressBarChart> {
}
// 槽时间秒 → state 映射
final slotStateMap = <int, ({int value, int? state})>{};
final slotStateMap =
<int, ({int value, int? state, bool? isWorkout})>{};
for (final p in widget.stressPoints) {
final sec =
TodayHrvChartCard._tsToSecondsFromMidnight(p.time).round();
... ... @@ -896,7 +905,14 @@ class _StressBarChartState extends State<_StressBarChart> {
final v = p.value ?? 0;
final existing = slotStateMap[slot];
if (existing == null || v > existing.value) {
slotStateMap[slot] = (value: v, state: p.state);
final isWorkout = (p.isWorkout ?? 0) == 1;
final isSuspectedActivity = (p.isSuspectedActivity ?? 0) == 1;
final isMoving = (p.isMoving ?? 0) == 1;
slotStateMap[slot] = (
value: v,
state: p.state,
isWorkout: (isWorkout || isSuspectedActivity || isMoving)
);
}
}
... ... @@ -1116,8 +1132,8 @@ class _StressBarChartState extends State<_StressBarChart> {
color: Colors.white,
fontWeight: FontWeight.bold,
),
children: widget.getTooltip(
group.barRods[0], group.x, state),
children: widget.getTooltip(group.barRods[0], group.x,
state, entry.isWorkout),
textAlign: TextAlign.start,
);
},
... ...
... ... @@ -173,7 +173,7 @@ class LoginView extends GetView<LoginController> {
),
// Terms agreement text + checkbox
Positioned(
bottom: bottom(25),
bottom: bottom(0),
left: 0,
right: 0,
child: const _AgreementText(),
... ... @@ -300,8 +300,10 @@ class _AgreementText extends GetView<LoginController> {
return GestureDetector(
onTap: controller.toggleTermsChecked,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.fromLTRB(24.0, 20, 24.0, 15),
child: Container(
height: 85,
alignment: Alignment(0.0, -0.25),
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text.rich(
TextSpan(
style: baseStyle,
... ...
... ... @@ -321,17 +321,32 @@ class V2HrvTrendData {
/// Single item in v2/realtime_stress/ list
class V2RealtimeStressItem {
const V2RealtimeStressItem({this.time, this.value, this.state});
const V2RealtimeStressItem(
{this.time,
this.value,
this.state,
this.isWorkout,
this.isSuspectedActivity,
this.isMoving,
this.isAsleep});
final int? time;
final int? value;
final int? state;
final int? isWorkout;
final int? isSuspectedActivity;
final int? isMoving;
final int? isAsleep;
factory V2RealtimeStressItem.fromJson(Map<String, dynamic> json) {
return V2RealtimeStressItem(
time: _parseInt(json['time']),
value: _parseInt(json['value']),
state: _parseInt(json['state']),
isWorkout: _parseInt(json['is_workout']),
isSuspectedActivity: _parseInt(json['is_suspected_activity']),
isMoving: _parseInt(json['is_moving']),
isAsleep: _parseInt(json['is_asleep']),
);
}
... ... @@ -340,6 +355,11 @@ class V2RealtimeStressItem {
if (time != null) val['time'] = time;
if (value != null) val['value'] = value;
if (state != null) val['state'] = state;
if (isWorkout != null) val['is_workout'] = isWorkout;
if (isSuspectedActivity != null)
val['is_suspected_activity'] = isSuspectedActivity;
if (isMoving != null) val['is_moving'] = isMoving;
if (isAsleep != null) val['is_asleep'] = isAsleep;
return val;
}
}
... ...
... ... @@ -21,7 +21,7 @@
"switchLanguage": "Switch Language",
"settings": "Settings",
"onboardingIntroTitle": "DoubleFeel is a health companion app built for Apple Watch",
"onboardingIntroBody": "We hope to help you\n<em>notice changes in your mind and body, and help the people who love you</em> see when you are <em>tired or need support</em>",
"onboardingIntroBody": "<em>Understand yourself better</em>, and let people who care about you <em>notice when you need support.</em>",
"onboardingStateQuestion": "Which of these often happens to you?",
"onboardingStateStressAnxiety": "I often feel stressed or anxious",
"onboardingStateTired": "I get tired easily",
... ... @@ -41,7 +41,7 @@
"onboardingReliefSun": "More sunlight",
"onboardingReliefWater": "Drink more water",
"onboardingReliefMeditation": "Mindfulness meditation",
"onboardingKeyDataTitle": "Did you know?",
"onboardingKeyDataTitle": "",
"onboardingKeyDataSubtitle": "Everyone has a magical and important body metric that can help us:",
"onboardingKeyDataStress": "Monitor stress",
"onboardingKeyDataFatigue": "Avoid overwork and physical fatigue",
... ... @@ -53,7 +53,7 @@
"onboardingHrvSubtitle": "It helps us measure overall stress and health",
"onboardingHrvDescription": "Heart rate variability (HRV) is the tiny variation in time between heartbeats. It reflects autonomic nervous system activity and how the body responds to stress.",
"onboardingTellMeMore": "Tell me more",
"onboardingResearchTitle": "Many studies show that HRV changes are closely related to how our body and mind feel",
"onboardingResearchTitle": "Research shows HRV is closely linked to how your body feels",
"onboardingResearchFatigue": "Physical fatigue",
"onboardingResearchEnergy": "Full of energy",
"onboardingResearchHrvDown": "HRV down",
... ... @@ -62,10 +62,10 @@
"onboardingHealthPermissionBody": "DoubleFeel needs connected wearable health data to send reminders, count stress moments, and provide suggestions.",
"onboardingHealthPermissionPrivacy": "Your health data is stored locally. We do not upload any related data.",
"onboardingNotificationTitle": "Turn on notifications",
"onboardingNotificationSubtitle": "Learn about every body change in time",
"onboardingNotificationBody": "After Apple Watch data updates, we can remind you in time and help you act to improve your stress state.",
"onboardingMemberTitle": "Get an annual membership offer",
"onboardingMemberBody": "Start your pressure alert and health companion journey, so love and care are always present.",
"onboardingNotificationSubtitle": "",
"onboardingNotificationBody": "Get notified when your body shows unusual stress or fatigue signals.",
"onboardingMemberTitle": "Get Annual Membership Offer",
"onboardingMemberBody": "Start your stress tracking and wellness journey, and never miss caring moments.",
"onboardingMemberAllOptions": "View all purchase options",
"healthCompanionIsNowAvailable": "Health Companion is now available",
"youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired": "You can now view each other's HRV, stress levels, and sleep patterns, and reach out to check in when the other person seems tired.",
... ... @@ -82,7 +82,7 @@
"onboardingResearchHealthy": "Feeling Great",
"onboardingResearchPoorSleep": "Poor Sleep",
"onboardingResearchGoodSleep": "Good Sleep",
"loginSlogan": "Start your pressure alert and health companion journey\nso love and care are always present",
"loginSlogan": "Start your journey of stress insights and caring connection.",
"loginWithPhone": "Sign in with Phone",
"loginWithApple": "Sign in with Apple",
"loginLastUsed": "Last used",
... ... @@ -696,5 +696,6 @@
"feedbackInvalidEmail": "Invalid email format, please enter again",
"feedbackSubmitSuccessTitle": "Feedback submitted successfully",
"feedbackSubmitSuccessMessage": "Thank you for your feedback. If further communication is needed, we will contact you via the email address you left as soon as possible. Please keep an eye on your inbox.",
"feedbackSubmitSuccessConfirm": "OK"
}
"feedbackSubmitSuccessConfirm": "OK",
"frequentMovement": "Frequent movement"
}
\ No newline at end of file
... ...
... ... @@ -1075,5 +1075,6 @@
"feedbackInvalidEmail": "邮箱格式错误,请重新输入",
"feedbackSubmitSuccessTitle": "反馈提交成功",
"feedbackSubmitSuccessMessage": "谢谢您的反馈。如需进一步沟通,我们会尽快通过您留下的邮箱地址与您联系,请留意查收邮件。",
"feedbackSubmitSuccessConfirm": "好的"
}
"feedbackSubmitSuccessConfirm": "好的",
"frequentMovement": "频繁移动"
}
\ No newline at end of file
... ...
... ... @@ -3998,6 +3998,12 @@ abstract class AppLocalizations {
/// In zh, this message translates to:
/// **'好的'**
String get feedbackSubmitSuccessConfirm;
/// No description provided for @frequentMovement.
///
/// In zh, this message translates to:
/// **'频繁移动'**
String get frequentMovement;
}
class _AppLocalizationsDelegate
... ...
... ... @@ -74,7 +74,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get onboardingIntroBody =>
'We hope to help you\n<em>notice changes in your mind and body, and help the people who love you</em> see when you are <em>tired or need support</em>';
'<em>Understand yourself better</em>, and let people who care about you <em>notice when you need support.</em>';
@override
String get onboardingStateQuestion => 'Which of these often happens to you?';
... ... @@ -139,7 +139,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingReliefMeditation => 'Mindfulness meditation';
@override
String get onboardingKeyDataTitle => 'Did you know?';
String get onboardingKeyDataTitle => '';
@override
String get onboardingKeyDataSubtitle =>
... ... @@ -180,7 +180,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get onboardingResearchTitle =>
'Many studies show that HRV changes are closely related to how our body and mind feel';
'Research shows HRV is closely linked to how your body feels';
@override
String get onboardingResearchFatigue => 'Physical fatigue';
... ... @@ -209,19 +209,18 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingNotificationTitle => 'Turn on notifications';
@override
String get onboardingNotificationSubtitle =>
'Learn about every body change in time';
String get onboardingNotificationSubtitle => '';
@override
String get onboardingNotificationBody =>
'After Apple Watch data updates, we can remind you in time and help you act to improve your stress state.';
'Get notified when your body shows unusual stress or fatigue signals.';
@override
String get onboardingMemberTitle => 'Get an annual membership offer';
String get onboardingMemberTitle => 'Get Annual Membership Offer';
@override
String get onboardingMemberBody =>
'Start your pressure alert and health companion journey, so love and care are always present.';
'Start your stress tracking and wellness journey, and never miss caring moments.';
@override
String get onboardingMemberAllOptions => 'View all purchase options';
... ... @@ -276,7 +275,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get loginSlogan =>
'Start your pressure alert and health companion journey\nso love and care are always present';
'Start your journey of stress insights and caring connection.';
@override
String get loginWithPhone => 'Sign in with Phone';
... ... @@ -2240,4 +2239,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get feedbackSubmitSuccessConfirm => 'OK';
@override
String get frequentMovement => 'Frequent movement';
}
... ...
... ... @@ -2130,4 +2130,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get feedbackSubmitSuccessConfirm => '好的';
@override
String get frequentMovement => '频繁移动';
}
... ...