Showing
23 changed files
with
292 additions
and
118 deletions
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
| @@ -65,7 +65,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -65,7 +65,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 65 | days: _periodReports(start, 7, data), | 65 | days: _periodReports(start, 7, data), |
| 66 | totalActiveEnergyOverride: data.totalMove?.round(), | 66 | totalActiveEnergyOverride: data.totalMove?.round(), |
| 67 | totalExerciseMinutesOverride: _durationMinutes(data.totalExercise), | 67 | totalExerciseMinutesOverride: _durationMinutes(data.totalExercise), |
| 68 | - totalStandHoursOverride: _durationMinutes(data.totalStand), | 68 | + totalStandHoursOverride: _hours(data.totalStand), |
| 69 | averageDailyActiveEnergyOverride: data.avgMove?.round(), | 69 | averageDailyActiveEnergyOverride: data.avgMove?.round(), |
| 70 | activeEnergyGoalOverride: data.activityTargetInfo?.move?.round(), | 70 | activeEnergyGoalOverride: data.activityTargetInfo?.move?.round(), |
| 71 | previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.round(), | 71 | previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.round(), |
| @@ -93,7 +93,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -93,7 +93,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 93 | days: _periodReports(start, end.day, data), | 93 | days: _periodReports(start, end.day, data), |
| 94 | totalActiveEnergyOverride: data.totalMove?.round(), | 94 | totalActiveEnergyOverride: data.totalMove?.round(), |
| 95 | totalExerciseMinutesOverride: _durationMinutes(data.totalExercise), | 95 | totalExerciseMinutesOverride: _durationMinutes(data.totalExercise), |
| 96 | - totalStandHoursOverride: _durationMinutes(data.totalStand), | 96 | + totalStandHoursOverride: _hours(data.totalStand), |
| 97 | averageDailyActiveEnergyOverride: data.avgMove?.round(), | 97 | averageDailyActiveEnergyOverride: data.avgMove?.round(), |
| 98 | activeEnergyGoalOverride: data.activityTargetInfo?.move?.round(), | 98 | activeEnergyGoalOverride: data.activityTargetInfo?.move?.round(), |
| 99 | previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.round(), | 99 | previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.round(), |
| @@ -258,17 +258,19 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -258,17 +258,19 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 258 | ); | 258 | ); |
| 259 | } | 259 | } |
| 260 | 260 | ||
| 261 | - ActivityBurnMetric? _standDurationMetric(num? seconds, num? target) { | ||
| 262 | - if (seconds == null) return null; | 261 | + ActivityBurnMetric? _standDurationMetric(num? hours, num? target) { |
| 262 | + if (hours == null) return null; | ||
| 263 | return ActivityBurnMetric( | 263 | return ActivityBurnMetric( |
| 264 | - value: _secondsToWholeMinutes(seconds), | ||
| 265 | - goal: _standTargetMinutes(target), | 264 | + value: _hours(hours) ?? 0, |
| 265 | + goal: _hours(target) ?? 0, | ||
| 266 | ); | 266 | ); |
| 267 | } | 267 | } |
| 268 | 268 | ||
| 269 | int? _durationMinutes(num? seconds) => | 269 | int? _durationMinutes(num? seconds) => |
| 270 | seconds == null ? null : _secondsToWholeMinutes(seconds); | 270 | seconds == null ? null : _secondsToWholeMinutes(seconds); |
| 271 | 271 | ||
| 272 | + int? _hours(num? hours) => hours?.round(); | ||
| 273 | + | ||
| 272 | int _secondsToWholeMinutes(num? seconds) { | 274 | int _secondsToWholeMinutes(num? seconds) { |
| 273 | if (seconds == null || seconds < 60) return 0; | 275 | if (seconds == null || seconds < 60) return 0; |
| 274 | return seconds ~/ 60; | 276 | return seconds ~/ 60; |
| @@ -279,11 +281,6 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -279,11 +281,6 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 279 | return target >= 60 ? _secondsToWholeMinutes(target) : target.round(); | 281 | return target >= 60 ? _secondsToWholeMinutes(target) : target.round(); |
| 280 | } | 282 | } |
| 281 | 283 | ||
| 282 | - int _standTargetMinutes(num? target) { | ||
| 283 | - if (target == null || target <= 0) return 0; | ||
| 284 | - return target >= 60 ? _secondsToWholeMinutes(target) : target.round() * 60; | ||
| 285 | - } | ||
| 286 | - | ||
| 287 | DateTime? _parseDateTime(Object? value, {DateTime? fallbackDate}) { | 284 | DateTime? _parseDateTime(Object? value, {DateTime? fallbackDate}) { |
| 288 | if (value is num) { | 285 | if (value is num) { |
| 289 | if (value >= 1000000000) { | 286 | if (value >= 1000000000) { |
| @@ -32,3 +32,13 @@ ActivityBurnDurationText activityBurnDurationText( | @@ -32,3 +32,13 @@ ActivityBurnDurationText activityBurnDurationText( | ||
| 32 | unit: context.l10n.reportUnitMinute, | 32 | unit: context.l10n.reportUnitMinute, |
| 33 | ); | 33 | ); |
| 34 | } | 34 | } |
| 35 | + | ||
| 36 | +ActivityBurnDurationText activityBurnHoursText( | ||
| 37 | + BuildContext context, | ||
| 38 | + int? hours, | ||
| 39 | +) { | ||
| 40 | + return ActivityBurnDurationText( | ||
| 41 | + value: hours?.toString() ?? '-', | ||
| 42 | + unit: context.l10n.reportUnitHour, | ||
| 43 | + ); | ||
| 44 | +} |
| @@ -58,7 +58,7 @@ class _MonthlySummary extends StatelessWidget { | @@ -58,7 +58,7 @@ class _MonthlySummary extends StatelessWidget { | ||
| 58 | context, | 58 | context, |
| 59 | hasData ? report.totalExerciseMinutes : null, | 59 | hasData ? report.totalExerciseMinutes : null, |
| 60 | ); | 60 | ); |
| 61 | - final stand = activityBurnDurationText( | 61 | + final stand = activityBurnHoursText( |
| 62 | context, | 62 | context, |
| 63 | hasData ? report.totalStandHours : null, | 63 | hasData ? report.totalStandHours : null, |
| 64 | ); | 64 | ); |
| @@ -525,14 +525,19 @@ class _MonthEnergyTrendCardState extends State<_MonthEnergyTrendCard> { | @@ -525,14 +525,19 @@ class _MonthEnergyTrendCardState extends State<_MonthEnergyTrendCard> { | ||
| 525 | if (_touchedOffset != null) | 525 | if (_touchedOffset != null) |
| 526 | Positioned.fill( | 526 | Positioned.fill( |
| 527 | top: _chartTopInset, | 527 | top: _chartTopInset, |
| 528 | - child: ChartSelectionLineOverlay( | ||
| 529 | - offset: _touchedOffset!, | ||
| 530 | - color: ActivityBurnMonthReportView._h3, | ||
| 531 | - bottomTitleHeight: | ||
| 532 | - ActivityBurnTrendPlotFrame.bottomTitleHeight, | ||
| 533 | - tooltipMargin: 6, | ||
| 534 | - plotLeft: ActivityBurnTrendPlotFrame.horizontalInset, | ||
| 535 | - plotRight: ActivityBurnTrendPlotFrame.rightAxisWidth, | 528 | + child: LayoutBuilder( |
| 529 | + builder: (context, constraints) { | ||
| 530 | + return ChartSelectionLineOverlay( | ||
| 531 | + offset: _touchedOffset!, | ||
| 532 | + color: ActivityBurnMonthReportView._h3, | ||
| 533 | + bottomTitleHeight: | ||
| 534 | + ActivityBurnTrendPlotFrame.bottomTitleHeight, | ||
| 535 | + tooltipMargin: 6, | ||
| 536 | + plotLeft: ActivityBurnTrendPlotFrame.horizontalInset, | ||
| 537 | + plotRight: ActivityBurnTrendPlotFrame.rightAxisWidth, | ||
| 538 | + lineX: _lineXForIndex(constraints.maxWidth, 4), | ||
| 539 | + ); | ||
| 540 | + }, | ||
| 536 | ), | 541 | ), |
| 537 | ), | 542 | ), |
| 538 | if (!hasChartReference) | 543 | if (!hasChartReference) |
| @@ -590,6 +595,21 @@ class _MonthEnergyTrendCardState extends State<_MonthEnergyTrendCard> { | @@ -590,6 +595,21 @@ class _MonthEnergyTrendCardState extends State<_MonthEnergyTrendCard> { | ||
| 590 | return (value / 5).ceil() * 5.0; | 595 | return (value / 5).ceil() * 5.0; |
| 591 | } | 596 | } |
| 592 | 597 | ||
| 598 | + double? _lineXForIndex(double width, double barWidth) { | ||
| 599 | + final index = _touchedIndex; | ||
| 600 | + final count = widget.report.days.length; | ||
| 601 | + if (index == null || index < 0 || index >= count || count == 0) { | ||
| 602 | + return null; | ||
| 603 | + } | ||
| 604 | + final plotLeft = ActivityBurnTrendPlotFrame.horizontalInset; | ||
| 605 | + final plotRight = ActivityBurnTrendPlotFrame.rightAxisWidth; | ||
| 606 | + final plotWidth = width - plotLeft - plotRight; | ||
| 607 | + if (plotWidth <= 0) return null; | ||
| 608 | + if (count == 1) return plotLeft + plotWidth / 2; | ||
| 609 | + final groupsSpace = _barGroupsSpace(plotWidth, count, barWidth); | ||
| 610 | + return plotLeft + barWidth / 2 + index * (barWidth + groupsSpace); | ||
| 611 | + } | ||
| 612 | + | ||
| 593 | BarChartData _chartData(double maxY, double plotWidth) { | 613 | BarChartData _chartData(double maxY, double plotWidth) { |
| 594 | final hasData = widget.report.hasData; | 614 | final hasData = widget.report.hasData; |
| 595 | return BarChartData( | 615 | return BarChartData( |
| @@ -22,7 +22,7 @@ class ActivityBurnSummaryCard extends StatelessWidget { | @@ -22,7 +22,7 @@ class ActivityBurnSummaryCard extends StatelessWidget { | ||
| 22 | context, | 22 | context, |
| 23 | report?.exerciseMinutes?.value, | 23 | report?.exerciseMinutes?.value, |
| 24 | ); | 24 | ); |
| 25 | - final stand = activityBurnDurationText( | 25 | + final stand = activityBurnHoursText( |
| 26 | context, | 26 | context, |
| 27 | report?.standHours?.value, | 27 | report?.standHours?.value, |
| 28 | ); | 28 | ); |
| @@ -57,7 +57,7 @@ class _WeeklySummary extends StatelessWidget { | @@ -57,7 +57,7 @@ class _WeeklySummary extends StatelessWidget { | ||
| 57 | context, | 57 | context, |
| 58 | report.hasData ? report.totalExerciseMinutes : null, | 58 | report.hasData ? report.totalExerciseMinutes : null, |
| 59 | ); | 59 | ); |
| 60 | - final stand = activityBurnDurationText( | 60 | + final stand = activityBurnHoursText( |
| 61 | context, | 61 | context, |
| 62 | report.hasData ? report.totalStandHours : null, | 62 | report.hasData ? report.totalStandHours : null, |
| 63 | ); | 63 | ); |
| @@ -503,14 +503,19 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { | @@ -503,14 +503,19 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { | ||
| 503 | if (_touchedOffset != null) | 503 | if (_touchedOffset != null) |
| 504 | Positioned.fill( | 504 | Positioned.fill( |
| 505 | top: _chartTopInset, | 505 | top: _chartTopInset, |
| 506 | - child: ChartSelectionLineOverlay( | ||
| 507 | - offset: _touchedOffset!, | ||
| 508 | - color: ActivityBurnWeekReportView._h3, | ||
| 509 | - bottomTitleHeight: | ||
| 510 | - ActivityBurnTrendPlotFrame.bottomTitleHeight, | ||
| 511 | - tooltipMargin: 6, | ||
| 512 | - plotLeft: ActivityBurnTrendPlotFrame.horizontalInset, | ||
| 513 | - plotRight: ActivityBurnTrendPlotFrame.rightAxisWidth, | 506 | + child: LayoutBuilder( |
| 507 | + builder: (context, constraints) { | ||
| 508 | + return ChartSelectionLineOverlay( | ||
| 509 | + offset: _touchedOffset!, | ||
| 510 | + color: ActivityBurnWeekReportView._h3, | ||
| 511 | + bottomTitleHeight: | ||
| 512 | + ActivityBurnTrendPlotFrame.bottomTitleHeight, | ||
| 513 | + tooltipMargin: 6, | ||
| 514 | + plotLeft: ActivityBurnTrendPlotFrame.horizontalInset, | ||
| 515 | + plotRight: ActivityBurnTrendPlotFrame.rightAxisWidth, | ||
| 516 | + lineX: _lineXForIndex(constraints.maxWidth, 16), | ||
| 517 | + ); | ||
| 518 | + }, | ||
| 514 | ), | 519 | ), |
| 515 | ), | 520 | ), |
| 516 | if (!hasChartReference) | 521 | if (!hasChartReference) |
| @@ -568,6 +573,21 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { | @@ -568,6 +573,21 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { | ||
| 568 | return (value / 5).ceil() * 5.0; | 573 | return (value / 5).ceil() * 5.0; |
| 569 | } | 574 | } |
| 570 | 575 | ||
| 576 | + double? _lineXForIndex(double width, double barWidth) { | ||
| 577 | + final index = _touchedIndex; | ||
| 578 | + final count = widget.report.days.length; | ||
| 579 | + if (index == null || index < 0 || index >= count || count == 0) { | ||
| 580 | + return null; | ||
| 581 | + } | ||
| 582 | + final plotLeft = ActivityBurnTrendPlotFrame.horizontalInset; | ||
| 583 | + final plotRight = ActivityBurnTrendPlotFrame.rightAxisWidth; | ||
| 584 | + final plotWidth = width - plotLeft - plotRight; | ||
| 585 | + if (plotWidth <= 0) return null; | ||
| 586 | + if (count == 1) return plotLeft + plotWidth / 2; | ||
| 587 | + final groupsSpace = _barGroupsSpace(plotWidth, count, barWidth); | ||
| 588 | + return plotLeft + barWidth / 2 + index * (barWidth + groupsSpace); | ||
| 589 | + } | ||
| 590 | + | ||
| 571 | BarChartData _chartData(double maxY, double plotWidth) { | 591 | BarChartData _chartData(double maxY, double plotWidth) { |
| 572 | final hasData = widget.report.hasData; | 592 | final hasData = widget.report.hasData; |
| 573 | return BarChartData( | 593 | return BarChartData( |
| @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/modules/hrv_report/models/hrv_report_mode | @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/modules/hrv_report/models/hrv_report_mode | ||
| 2 | import 'package:doublefeel_flutter/app/modules/home/widgets/df_tab_bar.dart'; | 2 | import 'package:doublefeel_flutter/app/modules/home/widgets/df_tab_bar.dart'; |
| 3 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 3 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 4 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | 4 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; |
| 5 | +import 'package:doublefeel_flutter/core/util/app_toast.dart'; | ||
| 5 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 6 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 6 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 7 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 7 | import 'package:doublefeel_flutter/r.dart'; | 8 | import 'package:doublefeel_flutter/r.dart'; |
| @@ -144,7 +145,9 @@ class FriendsTab extends GetView<FriendsController> { | @@ -144,7 +145,9 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 144 | selfHealthCard, | 145 | selfHealthCard, |
| 145 | _EmptyFriendsView( | 146 | _EmptyFriendsView( |
| 146 | enabled: !isFull, | 147 | enabled: !isFull, |
| 147 | - onTap: isFull ? null : _handleAddFriend, | 148 | + onTap: isFull |
| 149 | + ? () => _showFriendsLimitReached(context) | ||
| 150 | + : _handleAddFriend, | ||
| 148 | ), | 151 | ), |
| 149 | ], | 152 | ], |
| 150 | ), | 153 | ), |
| @@ -205,7 +208,9 @@ class FriendsTab extends GetView<FriendsController> { | @@ -205,7 +208,9 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 205 | enabled: !isFull, | 208 | enabled: !isFull, |
| 206 | friendCount: friends.length, | 209 | friendCount: friends.length, |
| 207 | maxFriends: FriendsController.maxFriends, | 210 | maxFriends: FriendsController.maxFriends, |
| 208 | - onTap: isFull ? null : _handleAddFriend, | 211 | + onTap: isFull |
| 212 | + ? () => _showFriendsLimitReached(context) | ||
| 213 | + : _handleAddFriend, | ||
| 209 | ), | 214 | ), |
| 210 | if (isFull) const _FullFriendsTip(), | 215 | if (isFull) const _FullFriendsTip(), |
| 211 | ], | 216 | ], |
| @@ -244,6 +249,10 @@ class FriendsTab extends GetView<FriendsController> { | @@ -244,6 +249,10 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 244 | await controller.loadFriends(); | 249 | await controller.loadFriends(); |
| 245 | } | 250 | } |
| 246 | 251 | ||
| 252 | + void _showFriendsLimitReached(BuildContext context) { | ||
| 253 | + AppToast.show(context.l10n.friendsLimitReached); | ||
| 254 | + } | ||
| 255 | + | ||
| 247 | void _openFriendHome(FriendHealthData friend) { | 256 | void _openFriendHome(FriendHealthData friend) { |
| 248 | Get.toNamed( | 257 | Get.toNamed( |
| 249 | Routes.FRIEND_HOME, | 258 | Routes.FRIEND_HOME, |
| @@ -201,7 +201,7 @@ class ApiHrvReportDataSource implements HrvReportDataSource { | @@ -201,7 +201,7 @@ class ApiHrvReportDataSource implements HrvReportDataSource { | ||
| 201 | Map<DateTime, HrvStressLevel> levelsByDate, | 201 | Map<DateTime, HrvStressLevel> levelsByDate, |
| 202 | ) { | 202 | ) { |
| 203 | final trend = trendsByDate[date]; | 203 | final trend = trendsByDate[date]; |
| 204 | - final level = levelsByDate[date]; | 204 | + final level = _stressLevelFromApiId(trend?.state) ?? levelsByDate[date]; |
| 205 | return HrvDayReport( | 205 | return HrvDayReport( |
| 206 | date: date, | 206 | date: date, |
| 207 | averageHrv: trend?.hrvAverage?.toDouble(), | 207 | averageHrv: trend?.hrvAverage?.toDouble(), |
| @@ -234,7 +234,7 @@ class ApiHrvReportDataSource implements HrvReportDataSource { | @@ -234,7 +234,7 @@ class ApiHrvReportDataSource implements HrvReportDataSource { | ||
| 234 | date: date, | 234 | date: date, |
| 235 | averageHrv: value.toDouble(), | 235 | averageHrv: value.toDouble(), |
| 236 | averageHeartRate: trend?.hrAverage?.round(), | 236 | averageHeartRate: trend?.hrAverage?.round(), |
| 237 | - level: levelsByDate[date], | 237 | + level: _stressLevelFromApiId(trend?.state) ?? levelsByDate[date], |
| 238 | ); | 238 | ); |
| 239 | } | 239 | } |
| 240 | 240 |
| @@ -309,6 +309,23 @@ class YearlyHrvReport implements HrvPeriodReport { | @@ -309,6 +309,23 @@ class YearlyHrvReport implements HrvPeriodReport { | ||
| 309 | List<HrvDayReport> daysForMonth(int month) => | 309 | List<HrvDayReport> daysForMonth(int month) => |
| 310 | days.where((day) => day.date.month == month).toList(); | 310 | days.where((day) => day.date.month == month).toList(); |
| 311 | 311 | ||
| 312 | + List<HrvDayReport> levelDaysForMonth(int month) => | ||
| 313 | + daysForMonth(month).where((day) => day.level != null).toList(); | ||
| 314 | + | ||
| 315 | + int stressedDaysForMonth(int month) { | ||
| 316 | + return levelDaysForMonth(month).where((day) { | ||
| 317 | + return day.level == HrvStressLevel.attention || | ||
| 318 | + day.level == HrvStressLevel.overload; | ||
| 319 | + }).length; | ||
| 320 | + } | ||
| 321 | + | ||
| 322 | + int relaxedDaysForMonth(int month) { | ||
| 323 | + return levelDaysForMonth(month).where((day) { | ||
| 324 | + return day.level == HrvStressLevel.excellent || | ||
| 325 | + day.level == HrvStressLevel.normal; | ||
| 326 | + }).length; | ||
| 327 | + } | ||
| 328 | + | ||
| 312 | double? averageForMonth(int month) { | 329 | double? averageForMonth(int month) { |
| 313 | final values = daysForMonth(month) | 330 | final values = daysForMonth(month) |
| 314 | .map((day) => day.averageHrv) | 331 | .map((day) => day.averageHrv) |
| @@ -318,11 +335,80 @@ class YearlyHrvReport implements HrvPeriodReport { | @@ -318,11 +335,80 @@ class YearlyHrvReport implements HrvPeriodReport { | ||
| 318 | return values.reduce((sum, value) => sum + value) / values.length; | 335 | return values.reduce((sum, value) => sum + value) / values.length; |
| 319 | } | 336 | } |
| 320 | 337 | ||
| 338 | + HrvStressLevel? levelForMonth(int month) { | ||
| 339 | + final levels = daysForMonth(month) | ||
| 340 | + .where((day) => day.averageHrv != null) | ||
| 341 | + .map((day) => day.level) | ||
| 342 | + .whereType<HrvStressLevel>() | ||
| 343 | + .toList(); | ||
| 344 | + if (levels.isEmpty) return null; | ||
| 345 | + return levels.first; | ||
| 346 | + } | ||
| 347 | + | ||
| 321 | List<int> get monthsWithAverage => [ | 348 | List<int> get monthsWithAverage => [ |
| 322 | for (var month = 1; month <= 12; month++) | 349 | for (var month = 1; month <= 12; month++) |
| 323 | if (averageForMonth(month) != null) month, | 350 | if (averageForMonth(month) != null) month, |
| 324 | ]; | 351 | ]; |
| 325 | 352 | ||
| 353 | + ({List<int> mostStressed, List<int> leastStressed}) | ||
| 354 | + averageStressExtremeMonths() { | ||
| 355 | + final values = [ | ||
| 356 | + for (final month in monthsWithAverage) | ||
| 357 | + if (averageForMonth(month) case final average?) | ||
| 358 | + MapEntry(month, average), | ||
| 359 | + ]; | ||
| 360 | + values.sort(_compareByStressDescending); | ||
| 361 | + | ||
| 362 | + final mostStressed = <int>[]; | ||
| 363 | + final leastStressed = <int>[]; | ||
| 364 | + if (values.isEmpty) { | ||
| 365 | + return (mostStressed: mostStressed, leastStressed: leastStressed); | ||
| 366 | + } | ||
| 367 | + | ||
| 368 | + if (values.length == 1) { | ||
| 369 | + final entry = values.single; | ||
| 370 | + if (_isStressedAverage(entry.value)) { | ||
| 371 | + mostStressed.add(entry.key); | ||
| 372 | + } else { | ||
| 373 | + leastStressed.add(entry.key); | ||
| 374 | + } | ||
| 375 | + return (mostStressed: mostStressed, leastStressed: leastStressed); | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + mostStressed.add(values.first.key); | ||
| 379 | + leastStressed.add(values.last.key); | ||
| 380 | + | ||
| 381 | + if (values.length == 3) { | ||
| 382 | + final middle = values[1]; | ||
| 383 | + if (_isStressedAverage(middle.value)) { | ||
| 384 | + mostStressed.add(middle.key); | ||
| 385 | + } else { | ||
| 386 | + leastStressed.add(middle.key); | ||
| 387 | + } | ||
| 388 | + } else if (values.length >= 4) { | ||
| 389 | + mostStressed.add(values[1].key); | ||
| 390 | + leastStressed.add(values[values.length - 2].key); | ||
| 391 | + } | ||
| 392 | + | ||
| 393 | + mostStressed.sort(); | ||
| 394 | + leastStressed.sort(); | ||
| 395 | + return (mostStressed: mostStressed, leastStressed: leastStressed); | ||
| 396 | + } | ||
| 397 | + | ||
| 398 | + int _compareByStressDescending( | ||
| 399 | + MapEntry<int, double> a, | ||
| 400 | + MapEntry<int, double> b, | ||
| 401 | + ) { | ||
| 402 | + final valueComparison = a.value.compareTo(b.value); | ||
| 403 | + return valueComparison == 0 ? a.key.compareTo(b.key) : valueComparison; | ||
| 404 | + } | ||
| 405 | + | ||
| 406 | + bool _isStressedAverage(double average) { | ||
| 407 | + final level = HrvStressLevel.fromAverageHrv(average); | ||
| 408 | + return level == HrvStressLevel.attention || | ||
| 409 | + level == HrvStressLevel.overload; | ||
| 410 | + } | ||
| 411 | + | ||
| 326 | List<int> monthsAtExtreme({required bool maximum}) { | 412 | List<int> monthsAtExtreme({required bool maximum}) { |
| 327 | final values = [ | 413 | final values = [ |
| 328 | for (var month = 1; month <= 12; month++) | 414 | for (var month = 1; month <= 12; month++) |
| @@ -167,6 +167,9 @@ class _HrvBarChartState extends State<_HrvBarChart> { | @@ -167,6 +167,9 @@ class _HrvBarChartState extends State<_HrvBarChart> { | ||
| 167 | static const _plotLeft = _axisLabelWidth; | 167 | static const _plotLeft = _axisLabelWidth; |
| 168 | static const _plotRight = 0.0; | 168 | static const _plotRight = 0.0; |
| 169 | static const _bottomTitleHeight = 31.0; | 169 | static const _bottomTitleHeight = 31.0; |
| 170 | + static const _tooltipBackground = Color(0xFFF3F3F3); | ||
| 171 | + static const _tooltipDateColor = Color(0xFF78787D); | ||
| 172 | + static const _tooltipMetaColor = Color(0xFFB0B0B6); | ||
| 170 | 173 | ||
| 171 | int? _touchedIndex; | 174 | int? _touchedIndex; |
| 172 | Offset? _touchedOffset; | 175 | Offset? _touchedOffset; |
| @@ -267,41 +270,17 @@ class _HrvBarChartState extends State<_HrvBarChart> { | @@ -267,41 +270,17 @@ class _HrvBarChartState extends State<_HrvBarChart> { | ||
| 267 | } | 270 | } |
| 268 | }, | 271 | }, |
| 269 | touchTooltipData: BarTouchTooltipData( | 272 | touchTooltipData: BarTouchTooltipData( |
| 270 | - getTooltipColor: (_) => const Color(0xFFF3F3F3), | 273 | + getTooltipColor: (_) => _tooltipBackground, |
| 271 | tooltipRoundedRadius: 8, | 274 | tooltipRoundedRadius: 8, |
| 272 | tooltipBorder: BorderSide.none, | 275 | tooltipBorder: BorderSide.none, |
| 273 | - tooltipPadding: const EdgeInsets.fromLTRB(8, 7, 8, 6), | 276 | + tooltipPadding: const EdgeInsets.fromLTRB(12, 8, 12, 4), |
| 274 | tooltipMargin: 2, | 277 | tooltipMargin: 2, |
| 275 | - maxContentWidth: 128, | 278 | + maxContentWidth: 107, |
| 276 | fitInsideHorizontally: true, | 279 | fitInsideHorizontally: true, |
| 277 | fitInsideVertically: false, | 280 | fitInsideVertically: false, |
| 278 | getTooltipItem: (group, groupIndex, rod, rodIndex) { | 281 | getTooltipItem: (group, groupIndex, rod, rodIndex) { |
| 279 | final day = widget.report.days[group.x]; | 282 | final day = widget.report.days[group.x]; |
| 280 | - if (day.averageHrv == null) return null; | ||
| 281 | - return BarTooltipItem( | ||
| 282 | - '${reportMonthDay(day.date)}\n', | ||
| 283 | - const TextStyle(color: Color(0xFF78787D), fontSize: 10), | ||
| 284 | - children: [ | ||
| 285 | - TextSpan( | ||
| 286 | - text: '${day.level?.label ?? ''}\n', | ||
| 287 | - style: TextStyle( | ||
| 288 | - color: Color( | ||
| 289 | - day.level?.colorValue ?? 0xFFB0B0B6, | ||
| 290 | - ), | ||
| 291 | - fontSize: 14, | ||
| 292 | - fontWeight: FontWeight.w500, | ||
| 293 | - ), | ||
| 294 | - ), | ||
| 295 | - TextSpan( | ||
| 296 | - text: | ||
| 297 | - '${day.averageHrv?.round() ?? '-'}ms · ${day.averageHeartRate ?? '-'}bpm', | ||
| 298 | - style: const TextStyle( | ||
| 299 | - color: Color(0xFFB0B0B6), | ||
| 300 | - fontSize: 10, | ||
| 301 | - ), | ||
| 302 | - ), | ||
| 303 | - ], | ||
| 304 | - ); | 283 | + return _tooltipItem(context, day); |
| 305 | }, | 284 | }, |
| 306 | ), | 285 | ), |
| 307 | ), | 286 | ), |
| @@ -350,6 +329,40 @@ class _HrvBarChartState extends State<_HrvBarChart> { | @@ -350,6 +329,40 @@ class _HrvBarChartState extends State<_HrvBarChart> { | ||
| 350 | ); | 329 | ); |
| 351 | } | 330 | } |
| 352 | 331 | ||
| 332 | + BarTooltipItem? _tooltipItem(BuildContext context, HrvDayReport day) { | ||
| 333 | + if (day.averageHrv == null) return null; | ||
| 334 | + return BarTooltipItem( | ||
| 335 | + '${reportMonthDayWithWeekday(day.date)}\n', | ||
| 336 | + const TextStyle( | ||
| 337 | + color: _tooltipDateColor, | ||
| 338 | + fontSize: 10, | ||
| 339 | + fontWeight: FontWeight.w400, | ||
| 340 | + height: 1.7, | ||
| 341 | + ), | ||
| 342 | + children: [ | ||
| 343 | + TextSpan( | ||
| 344 | + text: '${day.level?.label ?? ''}\n', | ||
| 345 | + style: TextStyle( | ||
| 346 | + color: Color(day.level?.colorValue ?? 0xFFB0B0B6), | ||
| 347 | + fontSize: 14, | ||
| 348 | + fontWeight: FontWeight.w600, | ||
| 349 | + height: 1.55, | ||
| 350 | + ), | ||
| 351 | + ), | ||
| 352 | + TextSpan( | ||
| 353 | + text: | ||
| 354 | + '${day.averageHrv?.round() ?? '-'}ms · ${day.averageHeartRate ?? '-'}bpm', | ||
| 355 | + style: const TextStyle( | ||
| 356 | + color: _tooltipMetaColor, | ||
| 357 | + fontSize: 10, | ||
| 358 | + fontWeight: FontWeight.w400, | ||
| 359 | + height: 1.35, | ||
| 360 | + ), | ||
| 361 | + ), | ||
| 362 | + ], | ||
| 363 | + ); | ||
| 364 | + } | ||
| 365 | + | ||
| 353 | double? _lineXForIndex(double width) { | 366 | double? _lineXForIndex(double width) { |
| 354 | final index = _touchedIndex; | 367 | final index = _touchedIndex; |
| 355 | if (index == null || widget.report.days.isEmpty) return null; | 368 | if (index == null || widget.report.days.isEmpty) return null; |
| @@ -34,19 +34,7 @@ class _YearTrendCard extends StatelessWidget { | @@ -34,19 +34,7 @@ class _YearTrendCard extends StatelessWidget { | ||
| 34 | 34 | ||
| 35 | @override | 35 | @override |
| 36 | Widget build(BuildContext context) { | 36 | Widget build(BuildContext context) { |
| 37 | - final monthsWithAverage = report.monthsWithAverage; | ||
| 38 | - var minimumMonths = report.monthsAtExtreme(maximum: false); | ||
| 39 | - var maximumMonths = report.monthsAtExtreme(maximum: true); | ||
| 40 | - if (monthsWithAverage.length == 1) { | ||
| 41 | - final month = monthsWithAverage.single; | ||
| 42 | - final level = HrvStressLevel.fromAverageHrv( | ||
| 43 | - report.averageForMonth(month)!, | ||
| 44 | - ); | ||
| 45 | - final isStressed = | ||
| 46 | - level == HrvStressLevel.attention || level == HrvStressLevel.overload; | ||
| 47 | - minimumMonths = isStressed ? [month] : const []; | ||
| 48 | - maximumMonths = isStressed ? const [] : [month]; | ||
| 49 | - } | 37 | + final extremeMonths = report.averageStressExtremeMonths(); |
| 50 | return _Card( | 38 | return _Card( |
| 51 | padding: const EdgeInsets.fromLTRB(20, 17, 20, 18), | 39 | padding: const EdgeInsets.fromLTRB(20, 17, 20, 18), |
| 52 | child: Column( | 40 | child: Column( |
| @@ -67,13 +55,13 @@ class _YearTrendCard extends StatelessWidget { | @@ -67,13 +55,13 @@ class _YearTrendCard extends StatelessWidget { | ||
| 67 | Expanded( | 55 | Expanded( |
| 68 | child: _MonthExtreme( | 56 | child: _MonthExtreme( |
| 69 | label: context.l10n.hrvMostStressed, | 57 | label: context.l10n.hrvMostStressed, |
| 70 | - months: minimumMonths, | 58 | + months: extremeMonths.mostStressed, |
| 71 | ), | 59 | ), |
| 72 | ), | 60 | ), |
| 73 | Expanded( | 61 | Expanded( |
| 74 | child: _MonthExtreme( | 62 | child: _MonthExtreme( |
| 75 | label: context.l10n.hrvLeastStressed, | 63 | label: context.l10n.hrvLeastStressed, |
| 76 | - months: maximumMonths, | 64 | + months: extremeMonths.leastStressed, |
| 77 | ), | 65 | ), |
| 78 | ), | 66 | ), |
| 79 | ], | 67 | ], |
| @@ -121,8 +109,7 @@ class _YearBarChartState extends State<_YearBarChart> { | @@ -121,8 +109,7 @@ class _YearBarChartState extends State<_YearBarChart> { | ||
| 121 | BarChartRodData( | 109 | BarChartRodData( |
| 122 | toY: widget.report.averageForMonth(month) ?? 0, | 110 | toY: widget.report.averageForMonth(month) ?? 0, |
| 123 | width: 8, | 111 | width: 8, |
| 124 | - color: _colorForValue( | ||
| 125 | - widget.report.averageForMonth(month)), | 112 | + color: _colorForMonth(month), |
| 126 | borderRadius: const BorderRadius.vertical( | 113 | borderRadius: const BorderRadius.vertical( |
| 127 | top: Radius.circular(5), | 114 | top: Radius.circular(5), |
| 128 | ), | 115 | ), |
| @@ -201,8 +188,12 @@ class _YearBarChartState extends State<_YearBarChart> { | @@ -201,8 +188,12 @@ class _YearBarChartState extends State<_YearBarChart> { | ||
| 201 | if (widget.report.averageForMonth(month) == null) { | 188 | if (widget.report.averageForMonth(month) == null) { |
| 202 | return null; | 189 | return null; |
| 203 | } | 190 | } |
| 204 | - final stressedDays = _stressedDaysForMonth(month); | ||
| 205 | - final relaxedDays = _relaxedDaysForMonth(month); | 191 | + final stressedDays = widget.report.stressedDaysForMonth( |
| 192 | + month, | ||
| 193 | + ); | ||
| 194 | + final relaxedDays = widget.report.relaxedDaysForMonth( | ||
| 195 | + month, | ||
| 196 | + ); | ||
| 206 | return BarTooltipItem( | 197 | return BarTooltipItem( |
| 207 | '${reportMonth(month)}\n', | 198 | '${reportMonth(month)}\n', |
| 208 | const TextStyle( | 199 | const TextStyle( |
| @@ -275,25 +266,17 @@ class _YearBarChartState extends State<_YearBarChart> { | @@ -275,25 +266,17 @@ class _YearBarChartState extends State<_YearBarChart> { | ||
| 275 | ); | 266 | ); |
| 276 | } | 267 | } |
| 277 | 268 | ||
| 269 | + Color _colorForMonth(int month) { | ||
| 270 | + final level = widget.report.levelForMonth(month); | ||
| 271 | + if (level != null) return Color(level.colorValue); | ||
| 272 | + return _colorForValue(widget.report.averageForMonth(month)); | ||
| 273 | + } | ||
| 274 | + | ||
| 278 | Color _colorForValue(double? value) { | 275 | Color _colorForValue(double? value) { |
| 279 | if (value == null) return Colors.transparent; | 276 | if (value == null) return Colors.transparent; |
| 280 | return Color(HrvStressLevel.fromAverageHrv(value).colorValue); | 277 | return Color(HrvStressLevel.fromAverageHrv(value).colorValue); |
| 281 | } | 278 | } |
| 282 | 279 | ||
| 283 | - int _relaxedDaysForMonth(int month) { | ||
| 284 | - return widget.report.daysForMonth(month).where((day) { | ||
| 285 | - return day.level == HrvStressLevel.excellent || | ||
| 286 | - day.level == HrvStressLevel.normal; | ||
| 287 | - }).length; | ||
| 288 | - } | ||
| 289 | - | ||
| 290 | - int _stressedDaysForMonth(int month) { | ||
| 291 | - return widget.report.daysForMonth(month).where((day) { | ||
| 292 | - return day.level == HrvStressLevel.attention || | ||
| 293 | - day.level == HrvStressLevel.overload; | ||
| 294 | - }).length; | ||
| 295 | - } | ||
| 296 | - | ||
| 297 | double? _lineXForMonth(double width) { | 280 | double? _lineXForMonth(double width) { |
| 298 | final month = _touchedMonth; | 281 | final month = _touchedMonth; |
| 299 | if (month == null) return null; | 282 | if (month == null) return null; |
| @@ -13,4 +13,9 @@ String reportWeekdayLabel(int weekday) => switch (weekday) { | @@ -13,4 +13,9 @@ String reportWeekdayLabel(int weekday) => switch (weekday) { | ||
| 13 | String reportMonthDay(DateTime date) => | 13 | String reportMonthDay(DateTime date) => |
| 14 | l10n.reportDateMonthDay(date.month, date.day); | 14 | l10n.reportDateMonthDay(date.month, date.day); |
| 15 | 15 | ||
| 16 | +String reportMonthDayWithWeekday(DateTime date) => l10n.reportDateWithWeekday( | ||
| 17 | + reportMonthDay(date), | ||
| 18 | + reportWeekdayLabel(date.weekday), | ||
| 19 | + ); | ||
| 20 | + | ||
| 16 | String reportMonth(int month) => l10n.reportDateMonth(month); | 21 | String reportMonth(int month) => l10n.reportDateMonth(month); |
| 1 | import 'package:doublefeel_flutter/r.dart'; | 1 | import 'package:doublefeel_flutter/r.dart'; |
| 2 | import 'package:fl_chart/fl_chart.dart'; | 2 | import 'package:fl_chart/fl_chart.dart'; |
| 3 | +import 'package:flutter/foundation.dart'; | ||
| 3 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 4 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 5 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 5 | 6 | ||
| @@ -26,6 +27,39 @@ class SleepWeekReportView extends StatelessWidget { | @@ -26,6 +27,39 @@ class SleepWeekReportView extends StatelessWidget { | ||
| 26 | static const h3 = Color(0xFFB0B0B6); | 27 | static const h3 = Color(0xFFB0B0B6); |
| 27 | static const grid = Color(0xFFF3F3F3); | 28 | static const grid = Color(0xFFF3F3F3); |
| 28 | 29 | ||
| 30 | + static bool get _usesAppleTextRenderer => | ||
| 31 | + defaultTargetPlatform == TargetPlatform.iOS || | ||
| 32 | + defaultTargetPlatform == TargetPlatform.macOS; | ||
| 33 | + | ||
| 34 | + static TextStyle get titleTextStyle { | ||
| 35 | + const baseStyle = TextStyle( | ||
| 36 | + color: h1, | ||
| 37 | + fontSize: 16, | ||
| 38 | + fontWeight: FontWeight.w600, | ||
| 39 | + height: 1.2, | ||
| 40 | + ); | ||
| 41 | + if (!_usesAppleTextRenderer) return baseStyle; | ||
| 42 | + return baseStyle.copyWith( | ||
| 43 | + fontFamily: 'PingFang SC', | ||
| 44 | + fontFamilyFallback: const ['Heiti SC', 'Arial Unicode MS'], | ||
| 45 | + ); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + static StrutStyle get titleStrutStyle { | ||
| 49 | + if (!_usesAppleTextRenderer) { | ||
| 50 | + return const StrutStyle( | ||
| 51 | + fontSize: 16, | ||
| 52 | + height: 1.2, | ||
| 53 | + ); | ||
| 54 | + } | ||
| 55 | + return const StrutStyle( | ||
| 56 | + fontSize: 16, | ||
| 57 | + fontFamily: 'PingFang SC', | ||
| 58 | + fontFamilyFallback: ['Heiti SC', 'Arial Unicode MS'], | ||
| 59 | + height: 1.2, | ||
| 60 | + ); | ||
| 61 | + } | ||
| 62 | + | ||
| 29 | static int minutesFromEvening(DateTime time) { | 63 | static int minutesFromEvening(DateTime time) { |
| 30 | final minutes = time.hour * 60 + time.minute; | 64 | final minutes = time.hour * 60 + time.minute; |
| 31 | return minutes < 12 * 60 ? minutes + 24 * 60 : minutes; | 65 | return minutes < 12 * 60 ? minutes + 24 * 60 : minutes; |
| @@ -411,12 +445,8 @@ class _TrendCard extends StatelessWidget { | @@ -411,12 +445,8 @@ class _TrendCard extends StatelessWidget { | ||
| 411 | children: [ | 445 | children: [ |
| 412 | Text( | 446 | Text( |
| 413 | title, | 447 | title, |
| 414 | - style: const TextStyle( | ||
| 415 | - color: SleepWeekReportView.h1, | ||
| 416 | - fontSize: 16, | ||
| 417 | - fontWeight: FontWeight.w600, | ||
| 418 | - height: 1.2, | ||
| 419 | - ), | 448 | + strutStyle: SleepWeekReportView.titleStrutStyle, |
| 449 | + style: SleepWeekReportView.titleTextStyle, | ||
| 420 | ), | 450 | ), |
| 421 | const SizedBox(height: 14), | 451 | const SizedBox(height: 14), |
| 422 | Expanded(child: chart), | 452 | Expanded(child: chart), |
| @@ -914,12 +944,7 @@ BarTouchData _barTouchData({ | @@ -914,12 +944,7 @@ BarTouchData _barTouchData({ | ||
| 914 | BarTooltipItem _barTooltipItem(_ChartSummaryData data) { | 944 | BarTooltipItem _barTooltipItem(_ChartSummaryData data) { |
| 915 | return BarTooltipItem( | 945 | return BarTooltipItem( |
| 916 | data.title, | 946 | data.title, |
| 917 | - TextStyle( | ||
| 918 | - color: data.titleColor, | ||
| 919 | - fontSize: 16, | ||
| 920 | - fontWeight: FontWeight.w600, | ||
| 921 | - height: 1.2, | ||
| 922 | - ), | 947 | + SleepWeekReportView.titleTextStyle.copyWith(color: data.titleColor), |
| 923 | textAlign: TextAlign.start, | 948 | textAlign: TextAlign.start, |
| 924 | children: [ | 949 | children: [ |
| 925 | TextSpan( | 950 | TextSpan( |
| @@ -962,12 +987,7 @@ LineTouchTooltipData _lineTooltipData({ | @@ -962,12 +987,7 @@ LineTouchTooltipData _lineTooltipData({ | ||
| 962 | LineTooltipItem _lineTooltipItem(_ChartSummaryData data) { | 987 | LineTooltipItem _lineTooltipItem(_ChartSummaryData data) { |
| 963 | return LineTooltipItem( | 988 | return LineTooltipItem( |
| 964 | data.title, | 989 | data.title, |
| 965 | - TextStyle( | ||
| 966 | - color: data.titleColor, | ||
| 967 | - fontSize: 16, | ||
| 968 | - fontWeight: FontWeight.w600, | ||
| 969 | - height: 1.2, | ||
| 970 | - ), | 990 | + SleepWeekReportView.titleTextStyle.copyWith(color: data.titleColor), |
| 971 | textAlign: TextAlign.start, | 991 | textAlign: TextAlign.start, |
| 972 | children: [ | 992 | children: [ |
| 973 | TextSpan( | 993 | TextSpan( |
| @@ -186,23 +186,27 @@ class HrvTrendList { | @@ -186,23 +186,27 @@ class HrvTrendList { | ||
| 186 | this.timeKey, | 186 | this.timeKey, |
| 187 | this.hrvAverage, | 187 | this.hrvAverage, |
| 188 | this.hrAverage, | 188 | this.hrAverage, |
| 189 | + this.state, | ||
| 189 | }); | 190 | }); |
| 190 | 191 | ||
| 191 | HrvTrendList.fromJson(dynamic json) { | 192 | HrvTrendList.fromJson(dynamic json) { |
| 192 | timeKey = json['time_key']; | 193 | timeKey = json['time_key']; |
| 193 | hrvAverage = _parseDouble(json['hrv_average']); | 194 | hrvAverage = _parseDouble(json['hrv_average']); |
| 194 | hrAverage = _parseDouble(json['hr_average']); | 195 | hrAverage = _parseDouble(json['hr_average']); |
| 196 | + state = _parseInt(json['state']); | ||
| 195 | } | 197 | } |
| 196 | 198 | ||
| 197 | Object? timeKey; | 199 | Object? timeKey; |
| 198 | double? hrvAverage; | 200 | double? hrvAverage; |
| 199 | double? hrAverage; | 201 | double? hrAverage; |
| 202 | + int? state; | ||
| 200 | 203 | ||
| 201 | Map<String, dynamic> toJson() { | 204 | Map<String, dynamic> toJson() { |
| 202 | final map = <String, dynamic>{}; | 205 | final map = <String, dynamic>{}; |
| 203 | map['time_key'] = timeKey; | 206 | map['time_key'] = timeKey; |
| 204 | map['hrv_average'] = hrvAverage; | 207 | map['hrv_average'] = hrvAverage; |
| 205 | map['hr_average'] = hrAverage; | 208 | map['hr_average'] = hrAverage; |
| 209 | + map['state'] = state; | ||
| 206 | return map; | 210 | return map; |
| 207 | } | 211 | } |
| 208 | } | 212 | } |
| @@ -213,3 +217,10 @@ double? _parseDouble(Object? value) { | @@ -213,3 +217,10 @@ double? _parseDouble(Object? value) { | ||
| 213 | if (value is String) return double.tryParse(value); | 217 | if (value is String) return double.tryParse(value); |
| 214 | return null; | 218 | return null; |
| 215 | } | 219 | } |
| 220 | + | ||
| 221 | +int? _parseInt(Object? value) { | ||
| 222 | + if (value == null) return null; | ||
| 223 | + if (value is num) return value.toInt(); | ||
| 224 | + if (value is String) return int.tryParse(value); | ||
| 225 | + return null; | ||
| 226 | +} |
| @@ -376,7 +376,7 @@ | @@ -376,7 +376,7 @@ | ||
| 376 | "sleepQualityNormalRange": "60–85 pts", | 376 | "sleepQualityNormalRange": "60–85 pts", |
| 377 | "sleepQualityExcellentRange": ">85 pts", | 377 | "sleepQualityExcellentRange": ">85 pts", |
| 378 | "friendsAddCloseContactDescription": "Add a close contact so someone else can look out for your health", | 378 | "friendsAddCloseContactDescription": "Add a close contact so someone else can look out for your health", |
| 379 | - "friendsLimitReached": "Friend limit reached", | 379 | + "friendsLimitReached": "You can add up to 10 friends", |
| 380 | "friendsAddCloseContact": "Add a close contact", | 380 | "friendsAddCloseContact": "Add a close contact", |
| 381 | "friendsAddCloseContactWithCount": "Add a close contact ({count}/{max})", | 381 | "friendsAddCloseContactWithCount": "Add a close contact ({count}/{max})", |
| 382 | "friendsMe": "Me", | 382 | "friendsMe": "Me", |
| @@ -599,7 +599,7 @@ | @@ -599,7 +599,7 @@ | ||
| 599 | "sleepQualityNormalRange": "60~85分", | 599 | "sleepQualityNormalRange": "60~85分", |
| 600 | "sleepQualityExcellentRange": ">85分", | 600 | "sleepQualityExcellentRange": ">85分", |
| 601 | "friendsAddCloseContactDescription": "添加亲密联系人,多一个人关注你的健康", | 601 | "friendsAddCloseContactDescription": "添加亲密联系人,多一个人关注你的健康", |
| 602 | - "friendsLimitReached": "好友数量已达上限", | 602 | + "friendsLimitReached": "最多只能添加10个好友哦", |
| 603 | "friendsAddCloseContact": "添加亲密联系人", | 603 | "friendsAddCloseContact": "添加亲密联系人", |
| 604 | "friendsAddCloseContactWithCount": "添加亲密联系人({count}/{max})", | 604 | "friendsAddCloseContactWithCount": "添加亲密联系人({count}/{max})", |
| 605 | "@friendsAddCloseContactWithCount": { | 605 | "@friendsAddCloseContactWithCount": { |
| @@ -2358,7 +2358,7 @@ abstract class AppLocalizations { | @@ -2358,7 +2358,7 @@ abstract class AppLocalizations { | ||
| 2358 | /// No description provided for @friendsLimitReached. | 2358 | /// No description provided for @friendsLimitReached. |
| 2359 | /// | 2359 | /// |
| 2360 | /// In zh, this message translates to: | 2360 | /// In zh, this message translates to: |
| 2361 | - /// **'好友数量已达上限'** | 2361 | + /// **'最多只能添加10个好友哦'** |
| 2362 | String get friendsLimitReached; | 2362 | String get friendsLimitReached; |
| 2363 | 2363 | ||
| 2364 | /// No description provided for @friendsAddCloseContact. | 2364 | /// No description provided for @friendsAddCloseContact. |
| @@ -1294,7 +1294,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1294,7 +1294,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1294 | 'Add a close contact so someone else can look out for your health'; | 1294 | 'Add a close contact so someone else can look out for your health'; |
| 1295 | 1295 | ||
| 1296 | @override | 1296 | @override |
| 1297 | - String get friendsLimitReached => 'Friend limit reached'; | 1297 | + String get friendsLimitReached => 'You can add up to 10 friends'; |
| 1298 | 1298 | ||
| 1299 | @override | 1299 | @override |
| 1300 | String get friendsAddCloseContact => 'Add a close contact'; | 1300 | String get friendsAddCloseContact => 'Add a close contact'; |
| @@ -1231,7 +1231,7 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1231,7 +1231,7 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1231 | String get friendsAddCloseContactDescription => '添加亲密联系人,多一个人关注你的健康'; | 1231 | String get friendsAddCloseContactDescription => '添加亲密联系人,多一个人关注你的健康'; |
| 1232 | 1232 | ||
| 1233 | @override | 1233 | @override |
| 1234 | - String get friendsLimitReached => '好友数量已达上限'; | 1234 | + String get friendsLimitReached => '最多只能添加10个好友哦'; |
| 1235 | 1235 | ||
| 1236 | @override | 1236 | @override |
| 1237 | String get friendsAddCloseContact => '添加亲密联系人'; | 1237 | String get friendsAddCloseContact => '添加亲密联系人'; |
-
Please register or login to post a comment