Showing
22 changed files
with
278 additions
and
251 deletions
| @@ -63,12 +63,12 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -63,12 +63,12 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 63 | weekStart: start, | 63 | weekStart: start, |
| 64 | weekEnd: start.add(const Duration(days: 6)), | 64 | weekEnd: start.add(const Duration(days: 6)), |
| 65 | days: _periodReports(start, 7, data), | 65 | days: _periodReports(start, 7, data), |
| 66 | - totalActiveEnergyOverride: data.totalMove?.round(), | 66 | + totalActiveEnergyOverride: data.totalMove?.toInt(), |
| 67 | totalExerciseMinutesOverride: _durationMinutes(data.totalExercise), | 67 | totalExerciseMinutesOverride: _durationMinutes(data.totalExercise), |
| 68 | totalStandHoursOverride: _hours(data.totalStand), | 68 | totalStandHoursOverride: _hours(data.totalStand), |
| 69 | - averageDailyActiveEnergyOverride: data.avgMove?.round(), | ||
| 70 | - activeEnergyGoalOverride: data.activityTargetInfo?.move?.round(), | ||
| 71 | - previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.round(), | 69 | + averageDailyActiveEnergyOverride: data.avgMove?.toInt(), |
| 70 | + activeEnergyGoalOverride: data.activityTargetInfo?.move?.toInt(), | ||
| 71 | + previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.toInt(), | ||
| 72 | ), | 72 | ), |
| 73 | AppFailure() => WeeklyActivityBurnReport.empty(start), | 73 | AppFailure() => WeeklyActivityBurnReport.empty(start), |
| 74 | }; | 74 | }; |
| @@ -91,12 +91,12 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -91,12 +91,12 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 91 | monthStart: start, | 91 | monthStart: start, |
| 92 | monthEnd: end, | 92 | monthEnd: end, |
| 93 | days: _periodReports(start, end.day, data), | 93 | days: _periodReports(start, end.day, data), |
| 94 | - totalActiveEnergyOverride: data.totalMove?.round(), | 94 | + totalActiveEnergyOverride: data.totalMove?.toInt(), |
| 95 | totalExerciseMinutesOverride: _durationMinutes(data.totalExercise), | 95 | totalExerciseMinutesOverride: _durationMinutes(data.totalExercise), |
| 96 | totalStandHoursOverride: _hours(data.totalStand), | 96 | totalStandHoursOverride: _hours(data.totalStand), |
| 97 | - averageDailyActiveEnergyOverride: data.avgMove?.round(), | ||
| 98 | - activeEnergyGoalOverride: data.activityTargetInfo?.move?.round(), | ||
| 99 | - previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.round(), | 97 | + averageDailyActiveEnergyOverride: data.avgMove?.toInt(), |
| 98 | + activeEnergyGoalOverride: data.activityTargetInfo?.move?.toInt(), | ||
| 99 | + previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.toInt(), | ||
| 100 | ), | 100 | ), |
| 101 | AppFailure() => MonthlyActivityBurnReport.empty(start), | 101 | AppFailure() => MonthlyActivityBurnReport.empty(start), |
| 102 | }; | 102 | }; |
| @@ -247,7 +247,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -247,7 +247,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 247 | 247 | ||
| 248 | ActivityBurnMetric? _metric(num? value, num? goal) { | 248 | ActivityBurnMetric? _metric(num? value, num? goal) { |
| 249 | if (value == null) return null; | 249 | if (value == null) return null; |
| 250 | - return ActivityBurnMetric(value: value.round(), goal: goal?.round() ?? 0); | 250 | + return ActivityBurnMetric(value: value.toInt(), goal: goal?.toInt() ?? 0); |
| 251 | } | 251 | } |
| 252 | 252 | ||
| 253 | ActivityBurnMetric? _exerciseDurationMetric(num? seconds, num? target) { | 253 | ActivityBurnMetric? _exerciseDurationMetric(num? seconds, num? target) { |
| @@ -269,7 +269,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -269,7 +269,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 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(); | 272 | + int? _hours(num? hours) => hours?.toInt(); |
| 273 | 273 | ||
| 274 | int _secondsToWholeMinutes(num? seconds) { | 274 | int _secondsToWholeMinutes(num? seconds) { |
| 275 | if (seconds == null || seconds < 60) return 0; | 275 | if (seconds == null || seconds < 60) return 0; |
| @@ -278,7 +278,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -278,7 +278,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 278 | 278 | ||
| 279 | int _exerciseTargetMinutes(num? target) { | 279 | int _exerciseTargetMinutes(num? target) { |
| 280 | if (target == null || target <= 0) return 0; | 280 | if (target == null || target <= 0) return 0; |
| 281 | - return target >= 60 ? _secondsToWholeMinutes(target) : target.round(); | 281 | + return target >= 60 ? _secondsToWholeMinutes(target) : target.toInt(); |
| 282 | } | 282 | } |
| 283 | 283 | ||
| 284 | DateTime? _parseDateTime(Object? value, {DateTime? fallbackDate}) { | 284 | DateTime? _parseDateTime(Object? value, {DateTime? fallbackDate}) { |
| @@ -289,7 +289,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -289,7 +289,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 289 | return DateTime.fromMillisecondsSinceEpoch(milliseconds); | 289 | return DateTime.fromMillisecondsSinceEpoch(milliseconds); |
| 290 | } | 290 | } |
| 291 | if (fallbackDate != null && value >= 0 && value < 86400) { | 291 | if (fallbackDate != null && value >= 0 && value < 86400) { |
| 292 | - return fallbackDate.add(Duration(seconds: value.round())); | 292 | + return fallbackDate.add(Duration(seconds: value.toInt())); |
| 293 | } | 293 | } |
| 294 | } | 294 | } |
| 295 | final raw = value?.toString(); | 295 | final raw = value?.toString(); |
| @@ -126,7 +126,7 @@ class WeeklyActivityBurnReport { | @@ -126,7 +126,7 @@ class WeeklyActivityBurnReport { | ||
| 126 | int get averageDailyActiveEnergy { | 126 | int get averageDailyActiveEnergy { |
| 127 | if (averageDailyActiveEnergyOverride case final value?) return value; | 127 | if (averageDailyActiveEnergyOverride case final value?) return value; |
| 128 | if (daysWithData.isEmpty) return 0; | 128 | if (daysWithData.isEmpty) return 0; |
| 129 | - return (totalActiveEnergy / daysWithData.length).round(); | 129 | + return (totalActiveEnergy / daysWithData.length).toInt(); |
| 130 | } | 130 | } |
| 131 | 131 | ||
| 132 | int get activeEnergyGoal => | 132 | int get activeEnergyGoal => |
| @@ -141,7 +141,7 @@ class WeeklyActivityBurnReport { | @@ -141,7 +141,7 @@ class WeeklyActivityBurnReport { | ||
| 141 | int? get activeEnergyComparisonPercent { | 141 | int? get activeEnergyComparisonPercent { |
| 142 | final previous = previousAverageDailyActiveEnergy; | 142 | final previous = previousAverageDailyActiveEnergy; |
| 143 | if (previous == null || previous <= 0) return null; | 143 | if (previous == null || previous <= 0) return null; |
| 144 | - return (((averageDailyActiveEnergy - previous) / previous) * 100).round(); | 144 | + return (((averageDailyActiveEnergy - previous) / previous) * 100).toInt(); |
| 145 | } | 145 | } |
| 146 | 146 | ||
| 147 | int get perfectRingDays => daysWithData | 147 | int get perfectRingDays => daysWithData |
| @@ -232,7 +232,7 @@ class MonthlyActivityBurnReport { | @@ -232,7 +232,7 @@ class MonthlyActivityBurnReport { | ||
| 232 | int get averageDailyActiveEnergy { | 232 | int get averageDailyActiveEnergy { |
| 233 | if (averageDailyActiveEnergyOverride case final value?) return value; | 233 | if (averageDailyActiveEnergyOverride case final value?) return value; |
| 234 | if (daysWithData.isEmpty) return 0; | 234 | if (daysWithData.isEmpty) return 0; |
| 235 | - return (totalActiveEnergy / daysWithData.length).round(); | 235 | + return (totalActiveEnergy / daysWithData.length).toInt(); |
| 236 | } | 236 | } |
| 237 | 237 | ||
| 238 | int get activeEnergyGoal => | 238 | int get activeEnergyGoal => |
| @@ -247,7 +247,7 @@ class MonthlyActivityBurnReport { | @@ -247,7 +247,7 @@ class MonthlyActivityBurnReport { | ||
| 247 | int? get activeEnergyComparisonPercent { | 247 | int? get activeEnergyComparisonPercent { |
| 248 | final previous = previousAverageDailyActiveEnergy; | 248 | final previous = previousAverageDailyActiveEnergy; |
| 249 | if (previous == null || previous <= 0) return null; | 249 | if (previous == null || previous <= 0) return null; |
| 250 | - return (((averageDailyActiveEnergy - previous) / previous) * 100).round(); | 250 | + return (((averageDailyActiveEnergy - previous) / previous) * 100).toInt(); |
| 251 | } | 251 | } |
| 252 | 252 | ||
| 253 | int get perfectRingDays => daysWithData | 253 | int get perfectRingDays => daysWithData |
| @@ -400,13 +400,13 @@ class ActivityBurnHeartRateZoneCard extends StatelessWidget { | @@ -400,13 +400,13 @@ class ActivityBurnHeartRateZoneCard extends StatelessWidget { | ||
| 400 | } | 400 | } |
| 401 | 401 | ||
| 402 | String? _bottomLabel(double value, DateTime start, double maxX) { | 402 | String? _bottomLabel(double value, DateTime start, double maxX) { |
| 403 | - final rounded = value.round(); | ||
| 404 | - if (rounded % 360 != 0 || rounded < 0 || rounded > maxX.round()) { | 403 | + final minutes = value.toInt(); |
| 404 | + if (minutes % 360 != 0 || minutes < 0 || minutes > maxX.toInt()) { | ||
| 405 | return null; | 405 | return null; |
| 406 | } | 406 | } |
| 407 | - if (rounded == 1440) return '24:00'; | 407 | + if (minutes == 1440) return '24:00'; |
| 408 | 408 | ||
| 409 | - final time = start.add(Duration(minutes: rounded)); | 409 | + final time = start.add(Duration(minutes: minutes)); |
| 410 | return DateFormat('HH:mm').format(time); | 410 | return DateFormat('HH:mm').format(time); |
| 411 | } | 411 | } |
| 412 | } | 412 | } |
| @@ -484,7 +484,7 @@ class _HeartRateAxis { | @@ -484,7 +484,7 @@ class _HeartRateAxis { | ||
| 484 | } | 484 | } |
| 485 | 485 | ||
| 486 | String formatYLabel(double value) { | 486 | String formatYLabel(double value) { |
| 487 | - if (value == value.roundToDouble()) return value.round().toString(); | 487 | + if (value == value.truncateToDouble()) return value.toInt().toString(); |
| 488 | return value.toStringAsFixed(1); | 488 | return value.toStringAsFixed(1); |
| 489 | } | 489 | } |
| 490 | 490 |
| @@ -683,7 +683,7 @@ class _MonthEnergyTrendCardState extends State<_MonthEnergyTrendCard> { | @@ -683,7 +683,7 @@ class _MonthEnergyTrendCardState extends State<_MonthEnergyTrendCard> { | ||
| 683 | reservedSize: 36, | 683 | reservedSize: 36, |
| 684 | interval: 100, | 684 | interval: 100, |
| 685 | getTitlesWidget: (value, meta) { | 685 | getTitlesWidget: (value, meta) { |
| 686 | - final isHundredTick = value.round() % 100 == 0; | 686 | + final isHundredTick = value.toInt() % 100 == 0; |
| 687 | final isMax = (value - maxY).abs() < 0.001; | 687 | final isMax = (value - maxY).abs() < 0.001; |
| 688 | if (!isHundredTick && !isMax) return const SizedBox.shrink(); | 688 | if (!isHundredTick && !isMax) return const SizedBox.shrink(); |
| 689 | return Transform.translate( | 689 | return Transform.translate( |
| @@ -139,10 +139,12 @@ class _ActivityBurnRingPainter extends CustomPainter { | @@ -139,10 +139,12 @@ class _ActivityBurnRingPainter extends CustomPainter { | ||
| 139 | static const _track = Color(0xFF35101D); | 139 | static const _track = Color(0xFF35101D); |
| 140 | static const _innerTrack = Color(0xFF13251F); | 140 | static const _innerTrack = Color(0xFF13251F); |
| 141 | static const _center = Color(0xFF0F0F11); | 141 | static const _center = Color(0xFF0F0F11); |
| 142 | - static const _outerRadius = 88.0; | ||
| 143 | - static const _middleRadius = 60.0; | ||
| 144 | - static const _innerRadius = 32.0; | ||
| 145 | - static const _ringWidth = 24.0; | 142 | + static const _ringWidth = 20.0; |
| 143 | + static const _ringGap = 4.0; | ||
| 144 | + static const _outerEdgeRadius = 100.0; | ||
| 145 | + static const _outerRadius = _outerEdgeRadius - _ringWidth / 2; | ||
| 146 | + static const _middleRadius = _outerRadius - _ringWidth - _ringGap; | ||
| 147 | + static const _innerRadius = _middleRadius - _ringWidth - _ringGap; | ||
| 146 | 148 | ||
| 147 | @override | 149 | @override |
| 148 | void paint(Canvas canvas, Size size) { | 150 | void paint(Canvas canvas, Size size) { |
| @@ -661,7 +661,7 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { | @@ -661,7 +661,7 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { | ||
| 661 | reservedSize: 36, | 661 | reservedSize: 36, |
| 662 | interval: 100, | 662 | interval: 100, |
| 663 | getTitlesWidget: (value, meta) { | 663 | getTitlesWidget: (value, meta) { |
| 664 | - final isHundredTick = value.round() % 100 == 0; | 664 | + final isHundredTick = value.toInt() % 100 == 0; |
| 665 | final isMax = (value - maxY).abs() < 0.001; | 665 | final isMax = (value - maxY).abs() < 0.001; |
| 666 | if (!isHundredTick && !isMax) return const SizedBox.shrink(); | 666 | if (!isHundredTick && !isMax) return const SizedBox.shrink(); |
| 667 | return Transform.translate( | 667 | return Transform.translate( |
| 1 | -import 'package:doublefeel_flutter/app/modules/hrv_report/models/hrv_report_models.dart'; | ||
| 2 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 1 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 3 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 2 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 4 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart' | 3 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart' |
| @@ -7,6 +6,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | @@ -7,6 +6,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 7 | import 'package:intl/intl.dart'; | 6 | import 'package:intl/intl.dart'; |
| 8 | 7 | ||
| 9 | import '../models/friend_health_data.dart'; | 8 | import '../models/friend_health_data.dart'; |
| 9 | +import '../models/friend_stress_state.dart'; | ||
| 10 | 10 | ||
| 11 | abstract class FriendsRepository { | 11 | abstract class FriendsRepository { |
| 12 | Future<List<FriendHealthData>> getFriends({bool withHealthData = true}); | 12 | Future<List<FriendHealthData>> getFriends({bool withHealthData = true}); |
| @@ -64,7 +64,6 @@ class FriendsRepositoryImpl implements FriendsRepository { | @@ -64,7 +64,6 @@ class FriendsRepositoryImpl implements FriendsRepository { | ||
| 64 | 64 | ||
| 65 | FriendHealthData _mapFriend(api_models.FriendItem friend) { | 65 | FriendHealthData _mapFriend(api_models.FriendItem friend) { |
| 66 | final healthData = friend.healthData; | 66 | final healthData = friend.healthData; |
| 67 | - final stressValue = healthData?.latestHrv; | ||
| 68 | final nickname = friend.friendNickname?.trim(); | 67 | final nickname = friend.friendNickname?.trim(); |
| 69 | final remark = friend.remarkName?.trim(); | 68 | final remark = friend.remarkName?.trim(); |
| 70 | 69 | ||
| @@ -81,10 +80,7 @@ class FriendsRepositoryImpl implements FriendsRepository { | @@ -81,10 +80,7 @@ class FriendsRepositoryImpl implements FriendsRepository { | ||
| 81 | steps: healthData?.totalSteps == null | 80 | steps: healthData?.totalSteps == null |
| 82 | ? null | 81 | ? null |
| 83 | : l10n.friendsStepCount(healthData!.totalSteps!), | 82 | : l10n.friendsStepCount(healthData!.totalSteps!), |
| 84 | - statusText: stressValue == null | ||
| 85 | - ? l10n.friendsWaitingForData | ||
| 86 | - : HrvStressLevel.fromRealtimeStress(stressValue).label, | ||
| 87 | - stressValue: stressValue, | 83 | + stressState: FriendStressState.fromValue(healthData?.hrvState), |
| 88 | isOnWatchFace: friend.isShowInDial, | 84 | isOnWatchFace: friend.isShowInDial, |
| 89 | ); | 85 | ); |
| 90 | } | 86 | } |
| @@ -2,6 +2,8 @@ import 'package:doublefeel_flutter/data/models/friend/friend_models.dart' | @@ -2,6 +2,8 @@ import 'package:doublefeel_flutter/data/models/friend/friend_models.dart' | ||
| 2 | as api_models; | 2 | as api_models; |
| 3 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 3 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 4 | 4 | ||
| 5 | +import 'friend_stress_state.dart'; | ||
| 6 | + | ||
| 5 | class FriendHealthData { | 7 | class FriendHealthData { |
| 6 | const FriendHealthData({ | 8 | const FriendHealthData({ |
| 7 | required this.friendItem, | 9 | required this.friendItem, |
| @@ -12,8 +14,7 @@ class FriendHealthData { | @@ -12,8 +14,7 @@ class FriendHealthData { | ||
| 12 | required this.updatedAt, | 14 | required this.updatedAt, |
| 13 | required this.sleepQualityScore, | 15 | required this.sleepQualityScore, |
| 14 | required this.steps, | 16 | required this.steps, |
| 15 | - required this.statusText, | ||
| 16 | - required this.stressValue, | 17 | + required this.stressState, |
| 17 | this.isOnWatchFace = false, | 18 | this.isOnWatchFace = false, |
| 18 | }); | 19 | }); |
| 19 | 20 | ||
| @@ -25,8 +26,7 @@ class FriendHealthData { | @@ -25,8 +26,7 @@ class FriendHealthData { | ||
| 25 | final String updatedAt; | 26 | final String updatedAt; |
| 26 | final int? sleepQualityScore; | 27 | final int? sleepQualityScore; |
| 27 | final String? steps; | 28 | final String? steps; |
| 28 | - final String statusText; | ||
| 29 | - final double? stressValue; | 29 | + final FriendStressState stressState; |
| 30 | final bool isOnWatchFace; | 30 | final bool isOnWatchFace; |
| 31 | 31 | ||
| 32 | String get displayName { | 32 | String get displayName { |
| @@ -44,8 +44,7 @@ class FriendHealthData { | @@ -44,8 +44,7 @@ class FriendHealthData { | ||
| 44 | String? updatedAt, | 44 | String? updatedAt, |
| 45 | int? sleepQualityScore, | 45 | int? sleepQualityScore, |
| 46 | String? steps, | 46 | String? steps, |
| 47 | - String? statusText, | ||
| 48 | - double? stressValue, | 47 | + FriendStressState? stressState, |
| 49 | bool? isOnWatchFace, | 48 | bool? isOnWatchFace, |
| 50 | }) { | 49 | }) { |
| 51 | return FriendHealthData( | 50 | return FriendHealthData( |
| @@ -57,8 +56,7 @@ class FriendHealthData { | @@ -57,8 +56,7 @@ class FriendHealthData { | ||
| 57 | updatedAt: updatedAt ?? this.updatedAt, | 56 | updatedAt: updatedAt ?? this.updatedAt, |
| 58 | sleepQualityScore: sleepQualityScore ?? this.sleepQualityScore, | 57 | sleepQualityScore: sleepQualityScore ?? this.sleepQualityScore, |
| 59 | steps: steps ?? this.steps, | 58 | steps: steps ?? this.steps, |
| 60 | - statusText: statusText ?? this.statusText, | ||
| 61 | - stressValue: stressValue ?? this.stressValue, | 59 | + stressState: stressState ?? this.stressState, |
| 62 | isOnWatchFace: isOnWatchFace ?? this.isOnWatchFace, | 60 | isOnWatchFace: isOnWatchFace ?? this.isOnWatchFace, |
| 63 | ); | 61 | ); |
| 64 | } | 62 | } |
| 1 | +import 'package:doublefeel_flutter/r.dart'; | ||
| 2 | +import 'package:flutter/material.dart'; | ||
| 3 | + | ||
| 4 | +enum FriendStressState { | ||
| 5 | + wait(0, '等待数据'), | ||
| 6 | + stressful(1, '压力过载'), | ||
| 7 | + slightStress(2, '注意压力'), | ||
| 8 | + normal(3, '状态正常'), | ||
| 9 | + energetic(4, '状态优秀'); | ||
| 10 | + | ||
| 11 | + const FriendStressState(this.value, this.label); | ||
| 12 | + | ||
| 13 | + final int value; | ||
| 14 | + final String label; | ||
| 15 | + | ||
| 16 | + static FriendStressState fromValue(int? value) { | ||
| 17 | + return switch (value) { | ||
| 18 | + 1 => FriendStressState.stressful, | ||
| 19 | + 2 => FriendStressState.slightStress, | ||
| 20 | + 3 => FriendStressState.normal, | ||
| 21 | + 4 => FriendStressState.energetic, | ||
| 22 | + _ => FriendStressState.wait, | ||
| 23 | + }; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + bool get hasData => this != FriendStressState.wait; | ||
| 27 | + | ||
| 28 | + int? get segmentIndex { | ||
| 29 | + return switch (this) { | ||
| 30 | + FriendStressState.stressful => 0, | ||
| 31 | + FriendStressState.slightStress => 1, | ||
| 32 | + FriendStressState.normal => 2, | ||
| 33 | + FriendStressState.energetic => 3, | ||
| 34 | + FriendStressState.wait => null, | ||
| 35 | + }; | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + Color get color { | ||
| 39 | + return switch (this) { | ||
| 40 | + FriendStressState.stressful => const Color(0xFFFF5279), | ||
| 41 | + FriendStressState.slightStress => const Color(0xFFFF9A6E), | ||
| 42 | + FriendStressState.normal => const Color(0xFF7B9BFB), | ||
| 43 | + FriendStressState.energetic => const Color(0xFF3BD49D), | ||
| 44 | + FriendStressState.wait => const Color(0xFFF3F3F3), | ||
| 45 | + }; | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + String get iconPath { | ||
| 49 | + return switch (this) { | ||
| 50 | + FriendStressState.stressful => R.assetsImagesRealtimeStressOverloadIcon, | ||
| 51 | + FriendStressState.slightStress => | ||
| 52 | + R.assetsImagesRealtimeStressAttentionIcon, | ||
| 53 | + FriendStressState.normal => R.assetsImagesRealtimeStressNormalIcon, | ||
| 54 | + FriendStressState.energetic => R.assetsImagesRealtimeStressExcellentIcon, | ||
| 55 | + FriendStressState.wait => R.assetsImagesRealtimeStressNoDataIcon, | ||
| 56 | + }; | ||
| 57 | + } | ||
| 58 | +} |
| 1 | import 'package:doublefeel_flutter/app/modules/home/widgets/df_tab_bar.dart'; | 1 | import 'package:doublefeel_flutter/app/modules/home/widgets/df_tab_bar.dart'; |
| 2 | -import 'package:doublefeel_flutter/app/modules/hrv_report/models/hrv_report_models.dart'; | ||
| 3 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 2 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 4 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | 3 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; |
| 5 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 4 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| @@ -12,12 +11,13 @@ import 'package:intl/intl.dart'; | @@ -12,12 +11,13 @@ import 'package:intl/intl.dart'; | ||
| 12 | 11 | ||
| 13 | import '../controllers/friends_controller.dart'; | 12 | import '../controllers/friends_controller.dart'; |
| 14 | import '../models/friend_health_data.dart'; | 13 | import '../models/friend_health_data.dart'; |
| 14 | +import '../models/friend_stress_state.dart'; | ||
| 15 | import '../widgets/friend_health_card.dart'; | 15 | import '../widgets/friend_health_card.dart'; |
| 16 | 16 | ||
| 17 | class FriendsTab extends GetView<FriendsController> { | 17 | class FriendsTab extends GetView<FriendsController> { |
| 18 | const FriendsTab({super.key}); | 18 | const FriendsTab({super.key}); |
| 19 | 19 | ||
| 20 | - static const double _floatingAddButtonGapAboveTabBar = 23; | 20 | + static const double _floatingAddButtonBottomGap = 24; |
| 21 | 21 | ||
| 22 | @override | 22 | @override |
| 23 | Widget build(BuildContext context) { | 23 | Widget build(BuildContext context) { |
| @@ -49,9 +49,9 @@ class FriendsTab extends GetView<FriendsController> { | @@ -49,9 +49,9 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 49 | stressScore == null; | 49 | stressScore == null; |
| 50 | final isFriendsInitialLoading = | 50 | final isFriendsInitialLoading = |
| 51 | controller.isLoading.value && !hasFriends; | 51 | controller.isLoading.value && !hasFriends; |
| 52 | - final stressValue = stressScore?.state == 0 | ||
| 53 | - ? null | ||
| 54 | - : stressScore?.comprehensiveScore?.toDouble(); | 52 | + final selfStressState = FriendStressState.fromValue( |
| 53 | + stressScore?.comprehensiveScore, | ||
| 54 | + ); | ||
| 55 | final selfHealthCard = _SelfHealthCard( | 55 | final selfHealthCard = _SelfHealthCard( |
| 56 | name: currentUser?.nickname?.trim().isNotEmpty == true | 56 | name: currentUser?.nickname?.trim().isNotEmpty == true |
| 57 | ? currentUser!.nickname!.trim() | 57 | ? currentUser!.nickname!.trim() |
| @@ -69,12 +69,7 @@ class FriendsTab extends GetView<FriendsController> { | @@ -69,12 +69,7 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 69 | : context.l10n.friendsStepCount( | 69 | : context.l10n.friendsStepCount( |
| 70 | healthData!.steps!, | 70 | healthData!.steps!, |
| 71 | ), | 71 | ), |
| 72 | - statusText: stressValue == null | ||
| 73 | - ? context.l10n.friendsWaitingForData | ||
| 74 | - : HrvStressLevel.fromRealtimeStress( | ||
| 75 | - stressValue, | ||
| 76 | - ).label, | ||
| 77 | - stressValue: stressValue, | 72 | + stressState: selfStressState, |
| 78 | isLoading: isSelfInitialLoading, | 73 | isLoading: isSelfInitialLoading, |
| 79 | ); | 74 | ); |
| 80 | 75 | ||
| @@ -185,8 +180,7 @@ class FriendsTab extends GetView<FriendsController> { | @@ -185,8 +180,7 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 185 | updatedAt: friend.updatedAt, | 180 | updatedAt: friend.updatedAt, |
| 186 | sleepQualityScore: friend.sleepQualityScore, | 181 | sleepQualityScore: friend.sleepQualityScore, |
| 187 | steps: friend.steps, | 182 | steps: friend.steps, |
| 188 | - statusText: friend.statusText, | ||
| 189 | - stressValue: friend.stressValue, | 183 | + stressState: friend.stressState, |
| 190 | isOnWatchFace: friend.isOnWatchFace, | 184 | isOnWatchFace: friend.isOnWatchFace, |
| 191 | onTap: () => _openFriendHome(friend), | 185 | onTap: () => _openFriendHome(friend), |
| 192 | onMoreSelected: (action) { | 186 | onMoreSelected: (action) { |
| @@ -225,7 +219,7 @@ class FriendsTab extends GetView<FriendsController> { | @@ -225,7 +219,7 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 225 | } | 219 | } |
| 226 | 220 | ||
| 227 | double _floatingAddButtonBottom(BuildContext context) { | 221 | double _floatingAddButtonBottom(BuildContext context) { |
| 228 | - return _tabBarAvoidanceBottom(context) + _floatingAddButtonGapAboveTabBar; | 222 | + return MediaQuery.paddingOf(context).bottom + _floatingAddButtonBottomGap; |
| 229 | } | 223 | } |
| 230 | 224 | ||
| 231 | double _tabBarAvoidanceBottom(BuildContext context) { | 225 | double _tabBarAvoidanceBottom(BuildContext context) { |
| @@ -286,8 +280,7 @@ class _SelfHealthCard extends StatelessWidget { | @@ -286,8 +280,7 @@ class _SelfHealthCard extends StatelessWidget { | ||
| 286 | required this.updatedAt, | 280 | required this.updatedAt, |
| 287 | required this.sleepQualityScore, | 281 | required this.sleepQualityScore, |
| 288 | required this.steps, | 282 | required this.steps, |
| 289 | - required this.statusText, | ||
| 290 | - required this.stressValue, | 283 | + required this.stressState, |
| 291 | required this.isLoading, | 284 | required this.isLoading, |
| 292 | }); | 285 | }); |
| 293 | 286 | ||
| @@ -296,8 +289,7 @@ class _SelfHealthCard extends StatelessWidget { | @@ -296,8 +289,7 @@ class _SelfHealthCard extends StatelessWidget { | ||
| 296 | final String updatedAt; | 289 | final String updatedAt; |
| 297 | final int? sleepQualityScore; | 290 | final int? sleepQualityScore; |
| 298 | final String? steps; | 291 | final String? steps; |
| 299 | - final String statusText; | ||
| 300 | - final double? stressValue; | 292 | + final FriendStressState stressState; |
| 301 | final bool isLoading; | 293 | final bool isLoading; |
| 302 | 294 | ||
| 303 | @override | 295 | @override |
| @@ -311,8 +303,7 @@ class _SelfHealthCard extends StatelessWidget { | @@ -311,8 +303,7 @@ class _SelfHealthCard extends StatelessWidget { | ||
| 311 | updatedAt: updatedAt, | 303 | updatedAt: updatedAt, |
| 312 | sleepQualityScore: sleepQualityScore, | 304 | sleepQualityScore: sleepQualityScore, |
| 313 | steps: steps, | 305 | steps: steps, |
| 314 | - statusText: statusText, | ||
| 315 | - stressValue: stressValue, | 306 | + stressState: stressState, |
| 316 | isSelf: true, | 307 | isSelf: true, |
| 317 | borderRadius: borderRadius, | 308 | borderRadius: borderRadius, |
| 318 | contentPadding: const EdgeInsets.fromLTRB(36, 20, 35, 20), | 309 | contentPadding: const EdgeInsets.fromLTRB(36, 20, 35, 20), |
| @@ -67,8 +67,7 @@ class _SelectFriendViewState extends State<SelectFriendView> { | @@ -67,8 +67,7 @@ class _SelectFriendViewState extends State<SelectFriendView> { | ||
| 67 | updatedAt: friend.updatedAt, | 67 | updatedAt: friend.updatedAt, |
| 68 | sleepQualityScore: friend.sleepQualityScore, | 68 | sleepQualityScore: friend.sleepQualityScore, |
| 69 | steps: friend.steps, | 69 | steps: friend.steps, |
| 70 | - statusText: friend.statusText, | ||
| 71 | - stressValue: friend.stressValue, | 70 | + stressState: friend.stressState, |
| 72 | isOnWatchFace: friend.isOnWatchFace, | 71 | isOnWatchFace: friend.isOnWatchFace, |
| 73 | isSelected: isSelected, | 72 | isSelected: isSelected, |
| 74 | showCheckbox: true, | 73 | showCheckbox: true, |
| 1 | import 'dart:math' as math; | 1 | import 'dart:math' as math; |
| 2 | 2 | ||
| 3 | -import 'package:doublefeel_flutter/app/modules/hrv_report/models/hrv_report_models.dart'; | 3 | +import 'package:doublefeel_flutter/app/modules/friends/models/friend_stress_state.dart'; |
| 4 | import 'package:doublefeel_flutter/app/modules/sleep_report/models/sleep_report_models.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/sleep_report/models/sleep_report_models.dart'; |
| 5 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 5 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 6 | import 'package:doublefeel_flutter/r.dart'; | 6 | import 'package:doublefeel_flutter/r.dart'; |
| @@ -21,8 +21,7 @@ class FriendHealthCard extends StatelessWidget { | @@ -21,8 +21,7 @@ class FriendHealthCard extends StatelessWidget { | ||
| 21 | required this.updatedAt, | 21 | required this.updatedAt, |
| 22 | required this.sleepQualityScore, | 22 | required this.sleepQualityScore, |
| 23 | required this.steps, | 23 | required this.steps, |
| 24 | - required this.statusText, | ||
| 25 | - required this.stressValue, | 24 | + required this.stressState, |
| 26 | this.isSelf = false, | 25 | this.isSelf = false, |
| 27 | this.isOnWatchFace = false, | 26 | this.isOnWatchFace = false, |
| 28 | this.isSelected = false, | 27 | this.isSelected = false, |
| @@ -39,8 +38,7 @@ class FriendHealthCard extends StatelessWidget { | @@ -39,8 +38,7 @@ class FriendHealthCard extends StatelessWidget { | ||
| 39 | final String updatedAt; | 38 | final String updatedAt; |
| 40 | final int? sleepQualityScore; | 39 | final int? sleepQualityScore; |
| 41 | final String? steps; | 40 | final String? steps; |
| 42 | - final String statusText; | ||
| 43 | - final double? stressValue; | 41 | + final FriendStressState stressState; |
| 44 | final bool isSelf; | 42 | final bool isSelf; |
| 45 | final bool isOnWatchFace; | 43 | final bool isOnWatchFace; |
| 46 | final bool isSelected; | 44 | final bool isSelected; |
| @@ -92,9 +90,7 @@ class FriendHealthCard extends StatelessWidget { | @@ -92,9 +90,7 @@ class FriendHealthCard extends StatelessWidget { | ||
| 92 | children: [ | 90 | children: [ |
| 93 | Expanded( | 91 | Expanded( |
| 94 | child: _StatusFigure( | 92 | child: _StatusFigure( |
| 95 | - statusText: statusText, | ||
| 96 | - stressValue: stressValue, | ||
| 97 | - waiting: stressValue == null, | 93 | + stressState: stressState, |
| 98 | ), | 94 | ), |
| 99 | ), | 95 | ), |
| 100 | const SizedBox(width: 18), | 96 | const SizedBox(width: 18), |
| @@ -138,7 +134,8 @@ class FriendHealthCard extends StatelessWidget { | @@ -138,7 +134,8 @@ class FriendHealthCard extends StatelessWidget { | ||
| 138 | child: AnimatedContainer( | 134 | child: AnimatedContainer( |
| 139 | duration: const Duration(milliseconds: 160), | 135 | duration: const Duration(milliseconds: 160), |
| 140 | curve: Curves.easeOut, | 136 | curve: Curves.easeOut, |
| 141 | - transform: Matrix4.identity()..scale(isSelected ? 1.0 : 0.92), | 137 | + transform: Matrix4.identity() |
| 138 | + ..scaleByDouble(isSelected ? 1.0 : 0.92, 1, 1, 1), | ||
| 142 | child: _FriendSelectMark(isSelected: isSelected), | 139 | child: _FriendSelectMark(isSelected: isSelected), |
| 143 | ), | 140 | ), |
| 144 | ), | 141 | ), |
| @@ -333,21 +330,13 @@ class _AvatarFallback extends StatelessWidget { | @@ -333,21 +330,13 @@ class _AvatarFallback extends StatelessWidget { | ||
| 333 | 330 | ||
| 334 | class _StatusFigure extends StatelessWidget { | 331 | class _StatusFigure extends StatelessWidget { |
| 335 | const _StatusFigure({ | 332 | const _StatusFigure({ |
| 336 | - required this.statusText, | ||
| 337 | - required this.stressValue, | ||
| 338 | - required this.waiting, | 333 | + required this.stressState, |
| 339 | }); | 334 | }); |
| 340 | 335 | ||
| 341 | - final String statusText; | ||
| 342 | - final double? stressValue; | ||
| 343 | - final bool waiting; | 336 | + final FriendStressState stressState; |
| 344 | 337 | ||
| 345 | @override | 338 | @override |
| 346 | Widget build(BuildContext context) { | 339 | Widget build(BuildContext context) { |
| 347 | - final stressLevel = stressValue == null | ||
| 348 | - ? null | ||
| 349 | - : HrvStressLevel.fromRealtimeStress(stressValue!); | ||
| 350 | - | ||
| 351 | return SizedBox( | 340 | return SizedBox( |
| 352 | height: 144, | 341 | height: 144, |
| 353 | child: Column( | 342 | child: Column( |
| @@ -363,16 +352,14 @@ class _StatusFigure extends StatelessWidget { | @@ -363,16 +352,14 @@ class _StatusFigure extends StatelessWidget { | ||
| 363 | CustomPaint( | 352 | CustomPaint( |
| 364 | size: const Size(124, 121), | 353 | size: const Size(124, 121), |
| 365 | painter: _StressStatusRingPainter( | 354 | painter: _StressStatusRingPainter( |
| 366 | - stressValue: stressValue, | ||
| 367 | - muted: waiting, | 355 | + stressState: stressState, |
| 368 | ), | 356 | ), |
| 369 | ), | 357 | ), |
| 370 | Positioned( | 358 | Positioned( |
| 371 | left: 22, | 359 | left: 22, |
| 372 | top: 24, | 360 | top: 24, |
| 373 | child: Image.asset( | 361 | child: Image.asset( |
| 374 | - stressLevel?.realtimeStressIconPath ?? | ||
| 375 | - R.assetsImagesRealtimeStressNoDataIcon, | 362 | + stressState.iconPath, |
| 376 | width: 72, | 363 | width: 72, |
| 377 | height: 72, | 364 | height: 72, |
| 378 | fit: BoxFit.contain, | 365 | fit: BoxFit.contain, |
| @@ -383,7 +370,7 @@ class _StatusFigure extends StatelessWidget { | @@ -383,7 +370,7 @@ class _StatusFigure extends StatelessWidget { | ||
| 383 | ), | 370 | ), |
| 384 | const SizedBox(height: 3), | 371 | const SizedBox(height: 3), |
| 385 | Text( | 372 | Text( |
| 386 | - statusText, | 373 | + stressState.label, |
| 387 | style: const TextStyle( | 374 | style: const TextStyle( |
| 388 | color: Color(0xFF0F0F11), | 375 | color: Color(0xFF0F0F11), |
| 389 | fontSize: 14, | 376 | fontSize: 14, |
| @@ -399,18 +386,16 @@ class _StatusFigure extends StatelessWidget { | @@ -399,18 +386,16 @@ class _StatusFigure extends StatelessWidget { | ||
| 399 | 386 | ||
| 400 | class _StressStatusRingPainter extends CustomPainter { | 387 | class _StressStatusRingPainter extends CustomPainter { |
| 401 | const _StressStatusRingPainter({ | 388 | const _StressStatusRingPainter({ |
| 402 | - required this.stressValue, | ||
| 403 | - required this.muted, | 389 | + required this.stressState, |
| 404 | }); | 390 | }); |
| 405 | 391 | ||
| 406 | - final double? stressValue; | ||
| 407 | - final bool muted; | 392 | + final FriendStressState stressState; |
| 408 | 393 | ||
| 409 | - static const _segments = [ | ||
| 410 | - _StressStatusSegment(80, 100, Color(0xFFFF5279)), | ||
| 411 | - _StressStatusSegment(50, 80, Color(0xFFFF9A6E)), | ||
| 412 | - _StressStatusSegment(20, 50, Color(0xFF7B9BFB)), | ||
| 413 | - _StressStatusSegment(0, 20, Color(0xFF3BD49D)), | 394 | + static const _segmentColors = [ |
| 395 | + Color(0xFFFF5279), | ||
| 396 | + Color(0xFFFF9A6E), | ||
| 397 | + Color(0xFF7B9BFB), | ||
| 398 | + Color(0xFF3BD49D), | ||
| 414 | ]; | 399 | ]; |
| 415 | 400 | ||
| 416 | static const _arcSpecs = [ | 401 | static const _arcSpecs = [ |
| @@ -419,6 +404,7 @@ class _StressStatusRingPainter extends CustomPainter { | @@ -419,6 +404,7 @@ class _StressStatusRingPainter extends CustomPainter { | ||
| 419 | _ArcSpec(270, 80), | 404 | _ArcSpec(270, 80), |
| 420 | _ArcSpec(-10, 55), | 405 | _ArcSpec(-10, 55), |
| 421 | ]; | 406 | ]; |
| 407 | + static const _arcGapDegrees = 5.0; | ||
| 422 | 408 | ||
| 423 | @override | 409 | @override |
| 424 | void paint(Canvas canvas, Size size) { | 410 | void paint(Canvas canvas, Size size) { |
| @@ -436,7 +422,7 @@ class _StressStatusRingPainter extends CustomPainter { | @@ -436,7 +422,7 @@ class _StressStatusRingPainter extends CustomPainter { | ||
| 436 | ..strokeWidth = 12 | 422 | ..strokeWidth = 12 |
| 437 | ..strokeCap = StrokeCap.round; | 423 | ..strokeCap = StrokeCap.round; |
| 438 | 424 | ||
| 439 | - if (muted) { | 425 | + if (!stressState.hasData) { |
| 440 | const mutedSpec = _ArcSpec(135, 270); | 426 | const mutedSpec = _ArcSpec(135, 270); |
| 441 | final startAngle = _degreesToRadians(mutedSpec.startDegrees); | 427 | final startAngle = _degreesToRadians(mutedSpec.startDegrees); |
| 442 | final sweepAngle = _degreesToRadians(mutedSpec.sweepDegrees); | 428 | final sweepAngle = _degreesToRadians(mutedSpec.sweepDegrees); |
| @@ -453,8 +439,9 @@ class _StressStatusRingPainter extends CustomPainter { | @@ -453,8 +439,9 @@ class _StressStatusRingPainter extends CustomPainter { | ||
| 453 | 439 | ||
| 454 | const paintOrder = [3, 2, 1, 0]; | 440 | const paintOrder = [3, 2, 1, 0]; |
| 455 | for (final i in paintOrder) { | 441 | for (final i in paintOrder) { |
| 456 | - final startAngle = _degreesToRadians(_arcSpecs[i].startDegrees); | ||
| 457 | - final sweepAngle = _degreesToRadians(_arcSpecs[i].sweepDegrees); | 442 | + final visualSpec = _arcSpecs[i].withGap(_arcGapDegrees); |
| 443 | + final startAngle = _degreesToRadians(visualSpec.startDegrees); | ||
| 444 | + final sweepAngle = _degreesToRadians(visualSpec.sweepDegrees); | ||
| 458 | canvas.drawArc( | 445 | canvas.drawArc( |
| 459 | rect, | 446 | rect, |
| 460 | startAngle, | 447 | startAngle, |
| @@ -463,7 +450,7 @@ class _StressStatusRingPainter extends CustomPainter { | @@ -463,7 +450,7 @@ class _StressStatusRingPainter extends CustomPainter { | ||
| 463 | borderPaint, | 450 | borderPaint, |
| 464 | ); | 451 | ); |
| 465 | 452 | ||
| 466 | - segmentPaint.color = _segments[i].color.withValues(alpha: 0.42); | 453 | + segmentPaint.color = _segmentColors[i].withValues(alpha: 0.42); |
| 467 | canvas.drawArc( | 454 | canvas.drawArc( |
| 468 | rect, | 455 | rect, |
| 469 | startAngle, | 456 | startAngle, |
| @@ -473,21 +460,15 @@ class _StressStatusRingPainter extends CustomPainter { | @@ -473,21 +460,15 @@ class _StressStatusRingPainter extends CustomPainter { | ||
| 473 | ); | 460 | ); |
| 474 | } | 461 | } |
| 475 | 462 | ||
| 476 | - final value = stressValue; | ||
| 477 | - if (value == null) return; | ||
| 478 | - | ||
| 479 | - final segmentIndex = | ||
| 480 | - _segments.indexWhere((segment) => segment.contains(value)); | ||
| 481 | - final safeSegmentIndex = segmentIndex < 0 ? 2 : segmentIndex; | ||
| 482 | - final segment = _segments[safeSegmentIndex]; | ||
| 483 | - final spec = _arcSpecs[safeSegmentIndex]; | ||
| 484 | - final progress = segment.progressOf(value); | 463 | + final segmentIndex = stressState.segmentIndex; |
| 464 | + if (segmentIndex == null) return; | ||
| 465 | + final spec = _arcSpecs[segmentIndex].withGap(_arcGapDegrees); | ||
| 485 | final indicatorAngle = | 466 | final indicatorAngle = |
| 486 | - _degreesToRadians(spec.startDegrees + spec.sweepDegrees * progress); | 467 | + _degreesToRadians(spec.startDegrees + spec.sweepDegrees / 2); |
| 487 | final indicatorPoint = _pointOnOval(rect, indicatorAngle); | 468 | final indicatorPoint = _pointOnOval(rect, indicatorAngle); |
| 488 | 469 | ||
| 489 | final indicatorPaint = Paint() | 470 | final indicatorPaint = Paint() |
| 490 | - ..color = segment.color | 471 | + ..color = _segmentColors[segmentIndex] |
| 491 | ..style = PaintingStyle.fill; | 472 | ..style = PaintingStyle.fill; |
| 492 | canvas.drawCircle(indicatorPoint, 10, indicatorPaint); | 473 | canvas.drawCircle(indicatorPoint, 10, indicatorPaint); |
| 493 | 474 | ||
| @@ -508,24 +489,7 @@ class _StressStatusRingPainter extends CustomPainter { | @@ -508,24 +489,7 @@ class _StressStatusRingPainter extends CustomPainter { | ||
| 508 | 489 | ||
| 509 | @override | 490 | @override |
| 510 | bool shouldRepaint(covariant _StressStatusRingPainter oldDelegate) { | 491 | bool shouldRepaint(covariant _StressStatusRingPainter oldDelegate) { |
| 511 | - return oldDelegate.stressValue != stressValue || oldDelegate.muted != muted; | ||
| 512 | - } | ||
| 513 | -} | ||
| 514 | - | ||
| 515 | -class _StressStatusSegment { | ||
| 516 | - const _StressStatusSegment(this.min, this.max, this.color); | ||
| 517 | - | ||
| 518 | - final double min; | ||
| 519 | - final double max; | ||
| 520 | - final Color color; | ||
| 521 | - | ||
| 522 | - bool contains(double value) { | ||
| 523 | - final lowerMatched = min == 0 ? value >= min : value > min; | ||
| 524 | - return lowerMatched && value <= max; | ||
| 525 | - } | ||
| 526 | - | ||
| 527 | - double progressOf(double value) { | ||
| 528 | - return ((value.clamp(min, max) - min) / (max - min)).toDouble(); | 492 | + return oldDelegate.stressState != stressState; |
| 529 | } | 493 | } |
| 530 | } | 494 | } |
| 531 | 495 | ||
| @@ -534,6 +498,14 @@ class _ArcSpec { | @@ -534,6 +498,14 @@ class _ArcSpec { | ||
| 534 | 498 | ||
| 535 | final double startDegrees; | 499 | final double startDegrees; |
| 536 | final double sweepDegrees; | 500 | final double sweepDegrees; |
| 501 | + | ||
| 502 | + _ArcSpec withGap(double gapDegrees) { | ||
| 503 | + final clampedGap = gapDegrees.clamp(0, sweepDegrees / 2).toDouble(); | ||
| 504 | + return _ArcSpec( | ||
| 505 | + startDegrees + clampedGap, | ||
| 506 | + sweepDegrees - clampedGap * 2, | ||
| 507 | + ); | ||
| 508 | + } | ||
| 537 | } | 509 | } |
| 538 | 510 | ||
| 539 | class _MetricTile extends StatelessWidget { | 511 | class _MetricTile extends StatelessWidget { |
| @@ -6,28 +6,6 @@ enum HrvStressLevel { | @@ -6,28 +6,6 @@ enum HrvStressLevel { | ||
| 6 | normal, | 6 | normal, |
| 7 | attention, | 7 | attention, |
| 8 | overload; | 8 | overload; |
| 9 | - | ||
| 10 | - static HrvStressLevel fromHrvState(int hrvState) { | ||
| 11 | - if (hrvState == 1) return HrvStressLevel.overload; | ||
| 12 | - if (hrvState == 2) return HrvStressLevel.attention; | ||
| 13 | - if (hrvState == 3) return HrvStressLevel.normal; | ||
| 14 | - return HrvStressLevel.excellent; | ||
| 15 | - } | ||
| 16 | - | ||
| 17 | - static HrvStressLevel fromRealtimeStress(double value) { | ||
| 18 | - final normalizedValue = value.clamp(0, 100); | ||
| 19 | - if (normalizedValue > 80) return HrvStressLevel.overload; | ||
| 20 | - if (normalizedValue > 50) return HrvStressLevel.attention; | ||
| 21 | - if (normalizedValue > 20) return HrvStressLevel.normal; | ||
| 22 | - return HrvStressLevel.excellent; | ||
| 23 | - } | ||
| 24 | - | ||
| 25 | - static HrvStressLevel fromAverageHrv(double value) { | ||
| 26 | - if (value >= 75) return HrvStressLevel.excellent; | ||
| 27 | - if (value >= 55) return HrvStressLevel.normal; | ||
| 28 | - if (value >= 40) return HrvStressLevel.attention; | ||
| 29 | - return HrvStressLevel.overload; | ||
| 30 | - } | ||
| 31 | } | 9 | } |
| 32 | 10 | ||
| 33 | extension HrvStressLevelPresentation on HrvStressLevel { | 11 | extension HrvStressLevelPresentation on HrvStressLevel { |
| @@ -345,17 +323,15 @@ class YearlyHrvReport implements HrvPeriodReport { | @@ -345,17 +323,15 @@ class YearlyHrvReport implements HrvPeriodReport { | ||
| 345 | return levels.first; | 323 | return levels.first; |
| 346 | } | 324 | } |
| 347 | 325 | ||
| 348 | - List<int> get monthsWithAverage => [ | 326 | + List<int> get monthsWithLevel => [ |
| 349 | for (var month = 1; month <= 12; month++) | 327 | for (var month = 1; month <= 12; month++) |
| 350 | - if (averageForMonth(month) != null) month, | 328 | + if (levelForMonth(month) != null) month, |
| 351 | ]; | 329 | ]; |
| 352 | 330 | ||
| 353 | - ({List<int> mostStressed, List<int> leastStressed}) | ||
| 354 | - averageStressExtremeMonths() { | 331 | + ({List<int> mostStressed, List<int> leastStressed}) stressExtremeMonths() { |
| 355 | final values = [ | 332 | final values = [ |
| 356 | - for (final month in monthsWithAverage) | ||
| 357 | - if (averageForMonth(month) case final average?) | ||
| 358 | - MapEntry(month, average), | 333 | + for (final month in monthsWithLevel) |
| 334 | + if (levelForMonth(month) case final level?) MapEntry(month, level), | ||
| 359 | ]; | 335 | ]; |
| 360 | values.sort(_compareByStressDescending); | 336 | values.sort(_compareByStressDescending); |
| 361 | 337 | ||
| @@ -367,7 +343,7 @@ class YearlyHrvReport implements HrvPeriodReport { | @@ -367,7 +343,7 @@ class YearlyHrvReport implements HrvPeriodReport { | ||
| 367 | 343 | ||
| 368 | if (values.length == 1) { | 344 | if (values.length == 1) { |
| 369 | final entry = values.single; | 345 | final entry = values.single; |
| 370 | - if (_isStressedAverage(entry.value)) { | 346 | + if (_isStressedLevel(entry.value)) { |
| 371 | mostStressed.add(entry.key); | 347 | mostStressed.add(entry.key); |
| 372 | } else { | 348 | } else { |
| 373 | leastStressed.add(entry.key); | 349 | leastStressed.add(entry.key); |
| @@ -380,7 +356,7 @@ class YearlyHrvReport implements HrvPeriodReport { | @@ -380,7 +356,7 @@ class YearlyHrvReport implements HrvPeriodReport { | ||
| 380 | 356 | ||
| 381 | if (values.length == 3) { | 357 | if (values.length == 3) { |
| 382 | final middle = values[1]; | 358 | final middle = values[1]; |
| 383 | - if (_isStressedAverage(middle.value)) { | 359 | + if (_isStressedLevel(middle.value)) { |
| 384 | mostStressed.add(middle.key); | 360 | mostStressed.add(middle.key); |
| 385 | } else { | 361 | } else { |
| 386 | leastStressed.add(middle.key); | 362 | leastStressed.add(middle.key); |
| @@ -396,19 +372,29 @@ class YearlyHrvReport implements HrvPeriodReport { | @@ -396,19 +372,29 @@ class YearlyHrvReport implements HrvPeriodReport { | ||
| 396 | } | 372 | } |
| 397 | 373 | ||
| 398 | int _compareByStressDescending( | 374 | int _compareByStressDescending( |
| 399 | - MapEntry<int, double> a, | ||
| 400 | - MapEntry<int, double> b, | 375 | + MapEntry<int, HrvStressLevel> a, |
| 376 | + MapEntry<int, HrvStressLevel> b, | ||
| 401 | ) { | 377 | ) { |
| 402 | - final valueComparison = a.value.compareTo(b.value); | 378 | + final valueComparison = _stressRank(b.value).compareTo( |
| 379 | + _stressRank(a.value), | ||
| 380 | + ); | ||
| 403 | return valueComparison == 0 ? a.key.compareTo(b.key) : valueComparison; | 381 | return valueComparison == 0 ? a.key.compareTo(b.key) : valueComparison; |
| 404 | } | 382 | } |
| 405 | 383 | ||
| 406 | - bool _isStressedAverage(double average) { | ||
| 407 | - final level = HrvStressLevel.fromAverageHrv(average); | 384 | + bool _isStressedLevel(HrvStressLevel level) { |
| 408 | return level == HrvStressLevel.attention || | 385 | return level == HrvStressLevel.attention || |
| 409 | level == HrvStressLevel.overload; | 386 | level == HrvStressLevel.overload; |
| 410 | } | 387 | } |
| 411 | 388 | ||
| 389 | + int _stressRank(HrvStressLevel level) { | ||
| 390 | + return switch (level) { | ||
| 391 | + HrvStressLevel.overload => 3, | ||
| 392 | + HrvStressLevel.attention => 2, | ||
| 393 | + HrvStressLevel.normal => 1, | ||
| 394 | + HrvStressLevel.excellent => 0, | ||
| 395 | + }; | ||
| 396 | + } | ||
| 397 | + | ||
| 412 | List<int> monthsAtExtreme({required bool maximum}) { | 398 | List<int> monthsAtExtreme({required bool maximum}) { |
| 413 | final values = [ | 399 | final values = [ |
| 414 | for (var month = 1; month <= 12; month++) | 400 | for (var month = 1; month <= 12; month++) |
| @@ -34,7 +34,7 @@ class _YearTrendCard extends StatelessWidget { | @@ -34,7 +34,7 @@ class _YearTrendCard extends StatelessWidget { | ||
| 34 | 34 | ||
| 35 | @override | 35 | @override |
| 36 | Widget build(BuildContext context) { | 36 | Widget build(BuildContext context) { |
| 37 | - final extremeMonths = report.averageStressExtremeMonths(); | 37 | + final extremeMonths = report.stressExtremeMonths(); |
| 38 | return _Card( | 38 | return _Card( |
| 39 | padding: const EdgeInsets.fromLTRB(20, 17, 20, 18), | 39 | padding: const EdgeInsets.fromLTRB(20, 17, 20, 18), |
| 40 | child: Column( | 40 | child: Column( |
| @@ -269,12 +269,7 @@ class _YearBarChartState extends State<_YearBarChart> { | @@ -269,12 +269,7 @@ class _YearBarChartState extends State<_YearBarChart> { | ||
| 269 | Color _colorForMonth(int month) { | 269 | Color _colorForMonth(int month) { |
| 270 | final level = widget.report.levelForMonth(month); | 270 | final level = widget.report.levelForMonth(month); |
| 271 | if (level != null) return Color(level.colorValue); | 271 | if (level != null) return Color(level.colorValue); |
| 272 | - return _colorForValue(widget.report.averageForMonth(month)); | ||
| 273 | - } | ||
| 274 | - | ||
| 275 | - Color _colorForValue(double? value) { | ||
| 276 | - if (value == null) return Colors.transparent; | ||
| 277 | - return Color(HrvStressLevel.fromAverageHrv(value).colorValue); | 272 | + return Colors.transparent; |
| 278 | } | 273 | } |
| 279 | 274 | ||
| 280 | double? _lineXForMonth(double width) { | 275 | double? _lineXForMonth(double width) { |
| 1 | /// Global selectable range for report dates. | 1 | /// Global selectable range for report dates. |
| 2 | abstract final class ReportDateRangeConfig { | 2 | abstract final class ReportDateRangeConfig { |
| 3 | static const int yearsBeforeCurrent = 2; | 3 | static const int yearsBeforeCurrent = 2; |
| 4 | - static const int yearsAfterCurrent = 1; | ||
| 5 | 4 | ||
| 6 | static int firstYear([DateTime? now]) => | 5 | static int firstYear([DateTime? now]) => |
| 7 | (now ?? DateTime.now()).year - yearsBeforeCurrent; | 6 | (now ?? DateTime.now()).year - yearsBeforeCurrent; |
| 8 | 7 | ||
| 9 | - static int lastYear([DateTime? now]) => | ||
| 10 | - (now ?? DateTime.now()).year + yearsAfterCurrent; | 8 | + static int lastYear([DateTime? now]) => (now ?? DateTime.now()).year; |
| 11 | 9 | ||
| 12 | static DateTime firstDate([DateTime? now]) => DateTime(firstYear(now)); | 10 | static DateTime firstDate([DateTime? now]) => DateTime(firstYear(now)); |
| 13 | 11 | ||
| 14 | - static DateTime lastDate([DateTime? now]) => | ||
| 15 | - DateTime(lastYear(now), DateTime.december, 31); | 12 | + static DateTime lastDate([DateTime? now]) { |
| 13 | + final date = now ?? DateTime.now(); | ||
| 14 | + return DateTime(date.year, date.month, date.day); | ||
| 15 | + } | ||
| 16 | 16 | ||
| 17 | static DateTime firstMonth([DateTime? now]) => DateTime(firstYear(now)); | 17 | static DateTime firstMonth([DateTime? now]) => DateTime(firstYear(now)); |
| 18 | 18 | ||
| 19 | - static DateTime lastMonth([DateTime? now]) => | ||
| 20 | - DateTime(lastYear(now), DateTime.december); | 19 | + static DateTime lastMonth([DateTime? now]) { |
| 20 | + final date = now ?? DateTime.now(); | ||
| 21 | + return DateTime(date.year, date.month); | ||
| 22 | + } | ||
| 21 | 23 | ||
| 22 | static DateTime firstWeekStart([DateTime? now]) { | 24 | static DateTime firstWeekStart([DateTime? now]) { |
| 23 | final first = firstDate(now); | 25 | final first = firstDate(now); |
| @@ -181,8 +181,8 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -181,8 +181,8 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 181 | date: date, | 181 | date: date, |
| 182 | duration: durationSeconds == null | 182 | duration: durationSeconds == null |
| 183 | ? report.duration | 183 | ? report.duration |
| 184 | - : SleepDuration(minutes: (durationSeconds / 60).round()), | ||
| 185 | - qualityScore: score?.round() ?? report.qualityScore, | 184 | + : SleepDuration(minutes: durationSeconds ~/ 60), |
| 185 | + qualityScore: score?.toInt() ?? report.qualityScore, | ||
| 186 | heartRate: report.heartRate, | 186 | heartRate: report.heartRate, |
| 187 | ); | 187 | ); |
| 188 | } | 188 | } |
| @@ -276,9 +276,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -276,9 +276,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 276 | dailyTrend, | 276 | dailyTrend, |
| 277 | asleepTime, | 277 | asleepTime, |
| 278 | points, | 278 | points, |
| 279 | - averageBpm: data.avgHr?.round(), | ||
| 280 | - maxBpm: data.maxHr?.round(), | ||
| 281 | - minBpm: data.minHr?.round(), | 279 | + averageBpm: data.avgHr?.toInt(), |
| 280 | + maxBpm: data.maxHr?.toInt(), | ||
| 281 | + minBpm: data.minHr?.toInt(), | ||
| 282 | ); | 282 | ); |
| 283 | } | 283 | } |
| 284 | 284 | ||
| @@ -317,9 +317,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -317,9 +317,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 317 | trends[date], | 317 | trends[date], |
| 318 | asleepTimes[date], | 318 | asleepTimes[date], |
| 319 | _sleepPoints(date, asleepTimes[date], pointsByDate), | 319 | _sleepPoints(date, asleepTimes[date], pointsByDate), |
| 320 | - averageBpm: isSingleDayResponse ? data.avgHr?.round() : null, | ||
| 321 | - maxBpm: isSingleDayResponse ? data.maxHr?.round() : null, | ||
| 322 | - minBpm: isSingleDayResponse ? data.minHr?.round() : null, | 320 | + averageBpm: isSingleDayResponse ? data.avgHr?.toInt() : null, |
| 321 | + maxBpm: isSingleDayResponse ? data.maxHr?.toInt() : null, | ||
| 322 | + minBpm: isSingleDayResponse ? data.minHr?.toInt() : null, | ||
| 323 | ), | 323 | ), |
| 324 | }; | 324 | }; |
| 325 | } | 325 | } |
| @@ -351,13 +351,13 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -351,13 +351,13 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 351 | final durationSeconds = trend?.totalTime; | 351 | final durationSeconds = trend?.totalTime; |
| 352 | final sleepEnd = asleepTime == null || durationSeconds == null | 352 | final sleepEnd = asleepTime == null || durationSeconds == null |
| 353 | ? null | 353 | ? null |
| 354 | - : asleepTime.add(Duration(seconds: durationSeconds.round())); | 354 | + : asleepTime.add(Duration(seconds: durationSeconds.toInt())); |
| 355 | return SleepReport( | 355 | return SleepReport( |
| 356 | date: date, | 356 | date: date, |
| 357 | duration: durationSeconds == null | 357 | duration: durationSeconds == null |
| 358 | ? null | 358 | ? null |
| 359 | - : SleepDuration(minutes: (durationSeconds / 60).round()), | ||
| 360 | - qualityScore: trend?.score?.round(), | 359 | + : SleepDuration(minutes: durationSeconds ~/ 60), |
| 360 | + qualityScore: trend?.score?.toInt(), | ||
| 361 | heartRate: _heartRateSummary( | 361 | heartRate: _heartRateSummary( |
| 362 | points, | 362 | points, |
| 363 | asleepTime, | 363 | asleepTime, |
| @@ -395,10 +395,10 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -395,10 +395,10 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 395 | if (point.bpm < minPoint.bpm) minPoint = point; | 395 | if (point.bpm < minPoint.bpm) minPoint = point; |
| 396 | } | 396 | } |
| 397 | return SleepHeartRateSummary( | 397 | return SleepHeartRateSummary( |
| 398 | - averageBpm: averageBpm ?? (total / points.length).round(), | ||
| 399 | - maxBpm: maxBpm ?? maxPoint.bpm.round(), | 398 | + averageBpm: averageBpm ?? (total / points.length).toInt(), |
| 399 | + maxBpm: maxBpm ?? maxPoint.bpm.toInt(), | ||
| 400 | maxAt: maxPoint.time, | 400 | maxAt: maxPoint.time, |
| 401 | - minBpm: minBpm ?? minPoint.bpm.round(), | 401 | + minBpm: minBpm ?? minPoint.bpm.toInt(), |
| 402 | minAt: minPoint.time, | 402 | minAt: minPoint.time, |
| 403 | sleepStart: sleepStart ?? points.first.time, | 403 | sleepStart: sleepStart ?? points.first.time, |
| 404 | sleepEnd: sleepEnd ?? points.last.time, | 404 | sleepEnd: sleepEnd ?? points.last.time, |
| @@ -419,7 +419,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -419,7 +419,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 419 | return DateTime.fromMillisecondsSinceEpoch(milliseconds); | 419 | return DateTime.fromMillisecondsSinceEpoch(milliseconds); |
| 420 | } | 420 | } |
| 421 | if (fallbackDate != null && value >= 0 && value < 86400) { | 421 | if (fallbackDate != null && value >= 0 && value < 86400) { |
| 422 | - return fallbackDate.add(Duration(seconds: value.round())); | 422 | + return fallbackDate.add(Duration(seconds: value.toInt())); |
| 423 | } | 423 | } |
| 424 | } | 424 | } |
| 425 | final raw = value?.toString(); | 425 | final raw = value?.toString(); |
| @@ -658,10 +658,10 @@ SleepHeartRateSummary _buildHeartRateSummary({ | @@ -658,10 +658,10 @@ SleepHeartRateSummary _buildHeartRateSummary({ | ||
| 658 | } | 658 | } |
| 659 | 659 | ||
| 660 | return SleepHeartRateSummary( | 660 | return SleepHeartRateSummary( |
| 661 | - averageBpm: average.round(), | ||
| 662 | - maxBpm: maxPoint.bpm.round(), | 661 | + averageBpm: average.toInt(), |
| 662 | + maxBpm: maxPoint.bpm.toInt(), | ||
| 663 | maxAt: maxPoint.time, | 663 | maxAt: maxPoint.time, |
| 664 | - minBpm: minPoint.bpm.round(), | 664 | + minBpm: minPoint.bpm.toInt(), |
| 665 | minAt: minPoint.time, | 665 | minAt: minPoint.time, |
| 666 | sleepStart: sleepStart, | 666 | sleepStart: sleepStart, |
| 667 | sleepEnd: sleepEnd, | 667 | sleepEnd: sleepEnd, |
| @@ -166,11 +166,17 @@ class SleepReport { | @@ -166,11 +166,17 @@ class SleepReport { | ||
| 166 | this.heartRate = const SleepHeartRateSummary(), | 166 | this.heartRate = const SleepHeartRateSummary(), |
| 167 | }); | 167 | }); |
| 168 | 168 | ||
| 169 | - bool get hasData => | ||
| 170 | - duration != null || qualityScore != null || heartRate.hasData; | 169 | + bool get hasData => hasSleepData; |
| 170 | + | ||
| 171 | + bool get hasSleepData => (duration?.minutes ?? 0) > 0; | ||
| 172 | + | ||
| 173 | + int? get validQualityScore => hasSleepData ? qualityScore : null; | ||
| 174 | + | ||
| 175 | + SleepHeartRateSummary get validHeartRate => | ||
| 176 | + hasSleepData ? heartRate : const SleepHeartRateSummary(); | ||
| 171 | 177 | ||
| 172 | SleepQualityLevel get qualityLevel => | 178 | SleepQualityLevel get qualityLevel => |
| 173 | - sleepQualityLevelFromScore(qualityScore); | 179 | + sleepQualityLevelFromScore(validQualityScore); |
| 174 | 180 | ||
| 175 | factory SleepReport.empty(DateTime date) { | 181 | factory SleepReport.empty(DateTime date) { |
| 176 | return SleepReport(date: date); | 182 | return SleepReport(date: date); |
| @@ -232,14 +238,12 @@ class WeeklySleepReport { | @@ -232,14 +238,12 @@ class WeeklySleepReport { | ||
| 232 | days.where((report) => report.hasData).toList(); | 238 | days.where((report) => report.hasData).toList(); |
| 233 | 239 | ||
| 234 | bool get hasData => | 240 | bool get hasData => |
| 235 | - daysWithData.isNotEmpty || | ||
| 236 | - averageDurationSeconds != null || | ||
| 237 | - averageScore != null; | 241 | + daysWithData.isNotEmpty || _hasPositiveSeconds(averageDurationSeconds); |
| 238 | 242 | ||
| 239 | int? get averageDurationMinutes { | 243 | int? get averageDurationMinutes { |
| 240 | final averageSeconds = _computedAverageDurationSeconds; | 244 | final averageSeconds = _computedAverageDurationSeconds; |
| 241 | if (averageSeconds == null) return null; | 245 | if (averageSeconds == null) return null; |
| 242 | - return (averageSeconds / 60).round(); | 246 | + return averageSeconds ~/ 60; |
| 243 | } | 247 | } |
| 244 | 248 | ||
| 245 | num? get _computedAverageDurationSeconds { | 249 | num? get _computedAverageDurationSeconds { |
| @@ -249,22 +253,26 @@ class WeeklySleepReport { | @@ -249,22 +253,26 @@ class WeeklySleepReport { | ||
| 249 | .where((minutes) => minutes > 0) | 253 | .where((minutes) => minutes > 0) |
| 250 | .toList(); | 254 | .toList(); |
| 251 | if (values.isEmpty) { | 255 | if (values.isEmpty) { |
| 252 | - return averageDurationSeconds; | 256 | + return _hasPositiveSeconds(averageDurationSeconds) |
| 257 | + ? averageDurationSeconds | ||
| 258 | + : null; | ||
| 253 | } | 259 | } |
| 254 | return values.reduce((sum, minutes) => sum + minutes) * 60 / values.length; | 260 | return values.reduce((sum, minutes) => sum + minutes) * 60 / values.length; |
| 255 | } | 261 | } |
| 256 | 262 | ||
| 257 | int? get averageQualityScore { | 263 | int? get averageQualityScore { |
| 258 | final average = _computedAverageScore; | 264 | final average = _computedAverageScore; |
| 259 | - return average?.round(); | 265 | + return average?.toInt(); |
| 260 | } | 266 | } |
| 261 | 267 | ||
| 262 | num? get _computedAverageScore { | 268 | num? get _computedAverageScore { |
| 263 | final values = daysWithData | 269 | final values = daysWithData |
| 264 | - .map((report) => report.qualityScore) | 270 | + .map((report) => report.validQualityScore) |
| 265 | .whereType<int>() | 271 | .whereType<int>() |
| 266 | .toList(); | 272 | .toList(); |
| 267 | - if (values.isEmpty) return averageScore; | 273 | + if (values.isEmpty) { |
| 274 | + return _hasPositiveSeconds(averageDurationSeconds) ? averageScore : null; | ||
| 275 | + } | ||
| 268 | return values.reduce((sum, score) => sum + score) / values.length; | 276 | return values.reduce((sum, score) => sum + score) / values.length; |
| 269 | } | 277 | } |
| 270 | 278 | ||
| @@ -281,7 +289,7 @@ class WeeklySleepReport { | @@ -281,7 +289,7 @@ class WeeklySleepReport { | ||
| 281 | 289 | ||
| 282 | SleepHeartRateSummary get heartRate { | 290 | SleepHeartRateSummary get heartRate { |
| 283 | final dailySummaries = daysWithData | 291 | final dailySummaries = daysWithData |
| 284 | - .map((report) => report.heartRate) | 292 | + .map((report) => report.validHeartRate) |
| 285 | .where((summary) => summary.hasData) | 293 | .where((summary) => summary.hasData) |
| 286 | .toList(); | 294 | .toList(); |
| 287 | if (dailySummaries.isEmpty) return const SleepHeartRateSummary(); | 295 | if (dailySummaries.isEmpty) return const SleepHeartRateSummary(); |
| @@ -294,7 +302,7 @@ class WeeklySleepReport { | @@ -294,7 +302,7 @@ class WeeklySleepReport { | ||
| 294 | final average = averageValues.isEmpty | 302 | final average = averageValues.isEmpty |
| 295 | ? null | 303 | ? null |
| 296 | : (averageValues.reduce((sum, bpm) => sum + bpm) / averageValues.length) | 304 | : (averageValues.reduce((sum, bpm) => sum + bpm) / averageValues.length) |
| 297 | - .round(); | 305 | + .toInt(); |
| 298 | 306 | ||
| 299 | SleepHeartRatePoint? maxPoint; | 307 | SleepHeartRatePoint? maxPoint; |
| 300 | SleepHeartRatePoint? minPoint; | 308 | SleepHeartRatePoint? minPoint; |
| @@ -305,9 +313,9 @@ class WeeklySleepReport { | @@ -305,9 +313,9 @@ class WeeklySleepReport { | ||
| 305 | 313 | ||
| 306 | return SleepHeartRateSummary( | 314 | return SleepHeartRateSummary( |
| 307 | averageBpm: average, | 315 | averageBpm: average, |
| 308 | - maxBpm: maxPoint?.bpm.round(), | 316 | + maxBpm: maxPoint?.bpm.toInt(), |
| 309 | maxAt: maxPoint?.time, | 317 | maxAt: maxPoint?.time, |
| 310 | - minBpm: minPoint?.bpm.round(), | 318 | + minBpm: minPoint?.bpm.toInt(), |
| 311 | minAt: minPoint?.time, | 319 | minAt: minPoint?.time, |
| 312 | sleepStart: points.isEmpty ? null : points.first.time, | 320 | sleepStart: points.isEmpty ? null : points.first.time, |
| 313 | sleepEnd: points.isEmpty ? null : points.last.time, | 321 | sleepEnd: points.isEmpty ? null : points.last.time, |
| @@ -360,14 +368,12 @@ class MonthlySleepReport { | @@ -360,14 +368,12 @@ class MonthlySleepReport { | ||
| 360 | days.where((report) => report.hasData).toList(); | 368 | days.where((report) => report.hasData).toList(); |
| 361 | 369 | ||
| 362 | bool get hasData => | 370 | bool get hasData => |
| 363 | - daysWithData.isNotEmpty || | ||
| 364 | - averageDurationSeconds != null || | ||
| 365 | - averageScore != null; | 371 | + daysWithData.isNotEmpty || _hasPositiveSeconds(averageDurationSeconds); |
| 366 | 372 | ||
| 367 | int? get averageDurationMinutes { | 373 | int? get averageDurationMinutes { |
| 368 | final averageSeconds = _computedAverageDurationSeconds; | 374 | final averageSeconds = _computedAverageDurationSeconds; |
| 369 | if (averageSeconds == null) return null; | 375 | if (averageSeconds == null) return null; |
| 370 | - return (averageSeconds / 60).round(); | 376 | + return averageSeconds ~/ 60; |
| 371 | } | 377 | } |
| 372 | 378 | ||
| 373 | num? get _computedAverageDurationSeconds { | 379 | num? get _computedAverageDurationSeconds { |
| @@ -377,22 +383,26 @@ class MonthlySleepReport { | @@ -377,22 +383,26 @@ class MonthlySleepReport { | ||
| 377 | .where((minutes) => minutes > 0) | 383 | .where((minutes) => minutes > 0) |
| 378 | .toList(); | 384 | .toList(); |
| 379 | if (values.isEmpty) { | 385 | if (values.isEmpty) { |
| 380 | - return averageDurationSeconds; | 386 | + return _hasPositiveSeconds(averageDurationSeconds) |
| 387 | + ? averageDurationSeconds | ||
| 388 | + : null; | ||
| 381 | } | 389 | } |
| 382 | return values.reduce((sum, minutes) => sum + minutes) * 60 / values.length; | 390 | return values.reduce((sum, minutes) => sum + minutes) * 60 / values.length; |
| 383 | } | 391 | } |
| 384 | 392 | ||
| 385 | int? get averageQualityScore { | 393 | int? get averageQualityScore { |
| 386 | final average = _computedAverageScore; | 394 | final average = _computedAverageScore; |
| 387 | - return average?.round(); | 395 | + return average?.toInt(); |
| 388 | } | 396 | } |
| 389 | 397 | ||
| 390 | num? get _computedAverageScore { | 398 | num? get _computedAverageScore { |
| 391 | final values = daysWithData | 399 | final values = daysWithData |
| 392 | - .map((report) => report.qualityScore) | 400 | + .map((report) => report.validQualityScore) |
| 393 | .whereType<int>() | 401 | .whereType<int>() |
| 394 | .toList(); | 402 | .toList(); |
| 395 | - if (values.isEmpty) return averageScore; | 403 | + if (values.isEmpty) { |
| 404 | + return _hasPositiveSeconds(averageDurationSeconds) ? averageScore : null; | ||
| 405 | + } | ||
| 396 | return values.reduce((sum, score) => sum + score) / values.length; | 406 | return values.reduce((sum, score) => sum + score) / values.length; |
| 397 | } | 407 | } |
| 398 | 408 | ||
| @@ -425,3 +435,5 @@ double? _comparisonPercent(num? current, num? previous) { | @@ -425,3 +435,5 @@ double? _comparisonPercent(num? current, num? previous) { | ||
| 425 | if (current == null || previous == null || previous == 0) return null; | 435 | if (current == null || previous == null || previous == 0) return null; |
| 426 | return (current - previous) / previous * 100; | 436 | return (current - previous) / previous * 100; |
| 427 | } | 437 | } |
| 438 | + | ||
| 439 | +bool _hasPositiveSeconds(num? seconds) => seconds != null && seconds > 0; |
| @@ -134,6 +134,7 @@ class _DailyReportContent extends StatelessWidget { | @@ -134,6 +134,7 @@ class _DailyReportContent extends StatelessWidget { | ||
| 134 | SleepHeartRateCard( | 134 | SleepHeartRateCard( |
| 135 | date: report?.date, | 135 | date: report?.date, |
| 136 | summary: report?.heartRate ?? const SleepHeartRateSummary(), | 136 | summary: report?.heartRate ?? const SleepHeartRateSummary(), |
| 137 | + hasSleepData: (report?.duration?.minutes ?? 0) > 0, | ||
| 137 | ), | 138 | ), |
| 138 | ], | 139 | ], |
| 139 | ); | 140 | ); |
| @@ -11,10 +11,12 @@ class SleepHeartRateCard extends StatelessWidget { | @@ -11,10 +11,12 @@ class SleepHeartRateCard extends StatelessWidget { | ||
| 11 | super.key, | 11 | super.key, |
| 12 | this.date, | 12 | this.date, |
| 13 | required this.summary, | 13 | required this.summary, |
| 14 | + this.hasSleepData = true, | ||
| 14 | }); | 15 | }); |
| 15 | 16 | ||
| 16 | final DateTime? date; | 17 | final DateTime? date; |
| 17 | final SleepHeartRateSummary summary; | 18 | final SleepHeartRateSummary summary; |
| 19 | + final bool hasSleepData; | ||
| 18 | 20 | ||
| 19 | static const _h1 = Color(0xFF0F0F11); | 21 | static const _h1 = Color(0xFF0F0F11); |
| 20 | static const _h3 = Color(0xFFB0B0B6); | 22 | static const _h3 = Color(0xFFB0B0B6); |
| @@ -46,14 +48,16 @@ class SleepHeartRateCard extends StatelessWidget { | @@ -46,14 +48,16 @@ class SleepHeartRateCard extends StatelessWidget { | ||
| 46 | ), | 48 | ), |
| 47 | ), | 49 | ), |
| 48 | const SizedBox(height: 24), | 50 | const SizedBox(height: 24), |
| 49 | - _MetricRow(summary: summary), | 51 | + _MetricRow( |
| 52 | + summary: hasSleepData ? summary : const SleepHeartRateSummary(), | ||
| 53 | + ), | ||
| 50 | const SizedBox(height: 18), | 54 | const SizedBox(height: 18), |
| 51 | Expanded( | 55 | Expanded( |
| 52 | child: Builder( | 56 | child: Builder( |
| 53 | builder: (context) { | 57 | builder: (context) { |
| 54 | final xAxis = _heartRateXAxis; | 58 | final xAxis = _heartRateXAxis; |
| 55 | final chartPoints = _chartPoints(xAxis); | 59 | final chartPoints = _chartPoints(xAxis); |
| 56 | - final hasChartData = chartPoints.isNotEmpty; | 60 | + final hasChartData = hasSleepData && chartPoints.isNotEmpty; |
| 57 | 61 | ||
| 58 | return Stack( | 62 | return Stack( |
| 59 | alignment: Alignment.center, | 63 | alignment: Alignment.center, |
| @@ -22,7 +22,7 @@ class _SleepQualityRingState extends State<SleepQualityRing> | @@ -22,7 +22,7 @@ class _SleepQualityRingState extends State<SleepQualityRing> | ||
| 22 | late Animation<double> _progress; | 22 | late Animation<double> _progress; |
| 23 | 23 | ||
| 24 | double get _targetProgress => | 24 | double get _targetProgress => |
| 25 | - ((widget.report?.qualityScore ?? 0).clamp(0, 100) / 100).toDouble(); | 25 | + ((widget.report?.validQualityScore ?? 0).clamp(0, 100) / 100).toDouble(); |
| 26 | 26 | ||
| 27 | @override | 27 | @override |
| 28 | void initState() { | 28 | void initState() { |
| @@ -38,7 +38,8 @@ class _SleepQualityRingState extends State<SleepQualityRing> | @@ -38,7 +38,8 @@ class _SleepQualityRingState extends State<SleepQualityRing> | ||
| 38 | void didUpdateWidget(covariant SleepQualityRing oldWidget) { | 38 | void didUpdateWidget(covariant SleepQualityRing oldWidget) { |
| 39 | super.didUpdateWidget(oldWidget); | 39 | super.didUpdateWidget(oldWidget); |
| 40 | final oldProgress = | 40 | final oldProgress = |
| 41 | - ((oldWidget.report?.qualityScore ?? 0).clamp(0, 100) / 100).toDouble(); | 41 | + ((oldWidget.report?.validQualityScore ?? 0).clamp(0, 100) / 100) |
| 42 | + .toDouble(); | ||
| 42 | if (oldProgress != _targetProgress) { | 43 | if (oldProgress != _targetProgress) { |
| 43 | _animateTo(_targetProgress, from: _progress.value); | 44 | _animateTo(_targetProgress, from: _progress.value); |
| 44 | } | 45 | } |
| @@ -67,7 +68,7 @@ class _SleepQualityRingState extends State<SleepQualityRing> | @@ -67,7 +68,7 @@ class _SleepQualityRingState extends State<SleepQualityRing> | ||
| 67 | builder: (context, child) => CustomPaint( | 68 | builder: (context, child) => CustomPaint( |
| 68 | painter: _SleepQualityRingPainter( | 69 | painter: _SleepQualityRingPainter( |
| 69 | progress: _progress.value, | 70 | progress: _progress.value, |
| 70 | - hasData: widget.report?.qualityScore != null, | 71 | + hasData: widget.report?.validQualityScore != null, |
| 71 | ), | 72 | ), |
| 72 | ), | 73 | ), |
| 73 | ), | 74 | ), |
| @@ -20,8 +20,8 @@ class SleepSummaryCard extends StatelessWidget { | @@ -20,8 +20,8 @@ class SleepSummaryCard extends StatelessWidget { | ||
| 20 | 20 | ||
| 21 | @override | 21 | @override |
| 22 | Widget build(BuildContext context) { | 22 | Widget build(BuildContext context) { |
| 23 | - final duration = report?.duration; | ||
| 24 | - final score = report?.qualityScore; | 23 | + final duration = report?.hasSleepData == true ? report?.duration : null; |
| 24 | + final score = report?.validQualityScore; | ||
| 25 | final quality = report?.qualityLevel ?? SleepQualityLevel.unknown; | 25 | final quality = report?.qualityLevel ?? SleepQualityLevel.unknown; |
| 26 | 26 | ||
| 27 | return Container( | 27 | return Container( |
| @@ -147,7 +147,7 @@ class _DurationValue extends StatelessWidget { | @@ -147,7 +147,7 @@ class _DurationValue extends StatelessWidget { | ||
| 147 | 147 | ||
| 148 | @override | 148 | @override |
| 149 | Widget build(BuildContext context) { | 149 | Widget build(BuildContext context) { |
| 150 | - final hasData = duration != null; | 150 | + final hasData = (duration?.minutes ?? 0) > 0; |
| 151 | 151 | ||
| 152 | return Row( | 152 | return Row( |
| 153 | crossAxisAlignment: CrossAxisAlignment.end, | 153 | crossAxisAlignment: CrossAxisAlignment.end, |
| @@ -29,7 +29,8 @@ class SleepWeekReportView extends StatelessWidget { | @@ -29,7 +29,8 @@ class SleepWeekReportView extends StatelessWidget { | ||
| 29 | 29 | ||
| 30 | static TextStyle get titleTextStyle => SleepReportTextStyles.titleTextStyle; | 30 | static TextStyle get titleTextStyle => SleepReportTextStyles.titleTextStyle; |
| 31 | 31 | ||
| 32 | - static StrutStyle get titleStrutStyle => SleepReportTextStyles.titleStrutStyle; | 32 | + static StrutStyle get titleStrutStyle => |
| 33 | + SleepReportTextStyles.titleStrutStyle; | ||
| 33 | 34 | ||
| 34 | static int minutesFromEvening(DateTime time) { | 35 | static int minutesFromEvening(DateTime time) { |
| 35 | final minutes = time.hour * 60 + time.minute; | 36 | final minutes = time.hour * 60 + time.minute; |
| @@ -202,7 +203,7 @@ class _SleepPeriodTrendContent extends StatelessWidget { | @@ -202,7 +203,7 @@ class _SleepPeriodTrendContent extends StatelessWidget { | ||
| 202 | List<SleepReport> days, { | 203 | List<SleepReport> days, { |
| 203 | required bool longest, | 204 | required bool longest, |
| 204 | }) { | 205 | }) { |
| 205 | - final values = days.where((day) => day.duration != null); | 206 | + final values = days.where((day) => day.hasSleepData); |
| 206 | if (values.isEmpty) return null; | 207 | if (values.isEmpty) return null; |
| 207 | return values.reduce((selected, day) { | 208 | return values.reduce((selected, day) { |
| 208 | final selectedValue = selected.duration!.minutes; | 209 | final selectedValue = selected.duration!.minutes; |
| @@ -216,11 +217,11 @@ class _SleepPeriodTrendContent extends StatelessWidget { | @@ -216,11 +217,11 @@ class _SleepPeriodTrendContent extends StatelessWidget { | ||
| 216 | List<SleepReport> days, { | 217 | List<SleepReport> days, { |
| 217 | required bool best, | 218 | required bool best, |
| 218 | }) { | 219 | }) { |
| 219 | - final values = days.where((day) => day.qualityScore != null); | 220 | + final values = days.where((day) => day.validQualityScore != null); |
| 220 | if (values.isEmpty) return null; | 221 | if (values.isEmpty) return null; |
| 221 | return values.reduce((selected, day) { | 222 | return values.reduce((selected, day) { |
| 222 | - final selectedValue = selected.qualityScore!; | ||
| 223 | - final dayValue = day.qualityScore!; | 223 | + final selectedValue = selected.validQualityScore!; |
| 224 | + final dayValue = day.validQualityScore!; | ||
| 224 | if (best) return dayValue > selectedValue ? day : selected; | 225 | if (best) return dayValue > selectedValue ? day : selected; |
| 225 | return dayValue < selectedValue ? day : selected; | 226 | return dayValue < selectedValue ? day : selected; |
| 226 | }); | 227 | }); |
| @@ -230,7 +231,9 @@ class _SleepPeriodTrendContent extends StatelessWidget { | @@ -230,7 +231,9 @@ class _SleepPeriodTrendContent extends StatelessWidget { | ||
| 230 | List<SleepReport> days, { | 231 | List<SleepReport> days, { |
| 231 | required bool earliest, | 232 | required bool earliest, |
| 232 | }) { | 233 | }) { |
| 233 | - final values = days.where((day) => day.heartRate.sleepStart != null); | 234 | + final values = days.where( |
| 235 | + (day) => day.hasSleepData && day.heartRate.sleepStart != null, | ||
| 236 | + ); | ||
| 234 | if (values.isEmpty) return null; | 237 | if (values.isEmpty) return null; |
| 235 | return values.reduce((selected, day) { | 238 | return values.reduce((selected, day) { |
| 236 | final selectedValue = SleepWeekReportView.minutesFromEvening( | 239 | final selectedValue = SleepWeekReportView.minutesFromEvening( |
| @@ -325,7 +328,7 @@ class _TopSummary extends StatelessWidget { | @@ -325,7 +328,7 @@ class _TopSummary extends StatelessWidget { | ||
| 325 | 328 | ||
| 326 | String _formatComparison(double? value) { | 329 | String _formatComparison(double? value) { |
| 327 | if (!hasData || value == null) return '-'; | 330 | if (!hasData || value == null) return '-'; |
| 328 | - return value.abs().round().toString(); | 331 | + return value.abs().toInt().toString(); |
| 329 | } | 332 | } |
| 330 | } | 333 | } |
| 331 | 334 | ||
| @@ -466,7 +469,7 @@ class _DurationChartState extends State<_DurationChart> { | @@ -466,7 +469,7 @@ class _DurationChartState extends State<_DurationChart> { | ||
| 466 | final values = <double>[ | 469 | final values = <double>[ |
| 467 | 8, | 470 | 8, |
| 468 | for (final day in widget.days) | 471 | for (final day in widget.days) |
| 469 | - if (day.duration != null) day.duration!.minutes / 60, | 472 | + if (day.hasSleepData) day.duration!.minutes / 60, |
| 470 | if (widget.averageDurationMinutes != null) | 473 | if (widget.averageDurationMinutes != null) |
| 471 | widget.averageDurationMinutes! / 60, | 474 | widget.averageDurationMinutes! / 60, |
| 472 | if (_targetDurationHours case final target?) target, | 475 | if (_targetDurationHours case final target?) target, |
| @@ -477,7 +480,7 @@ class _DurationChartState extends State<_DurationChart> { | @@ -477,7 +480,7 @@ class _DurationChartState extends State<_DurationChart> { | ||
| 477 | 480 | ||
| 478 | @override | 481 | @override |
| 479 | Widget build(BuildContext context) { | 482 | Widget build(BuildContext context) { |
| 480 | - final hasData = widget.days.any((day) => day.duration != null); | 483 | + final hasData = widget.days.any((day) => day.hasSleepData); |
| 481 | return Column( | 484 | return Column( |
| 482 | children: [ | 485 | children: [ |
| 483 | _ChartLegend( | 486 | _ChartLegend( |
| @@ -540,7 +543,7 @@ class _DurationChartState extends State<_DurationChart> { | @@ -540,7 +543,7 @@ class _DurationChartState extends State<_DurationChart> { | ||
| 540 | barTouchData: _barTouchData( | 543 | barTouchData: _barTouchData( |
| 541 | days: widget.days, | 544 | days: widget.days, |
| 542 | dataBuilder: _ChartSummaryData.duration, | 545 | dataBuilder: _ChartSummaryData.duration, |
| 543 | - hasData: (report) => report.duration != null, | 546 | + hasData: (report) => report.hasSleepData, |
| 544 | onSelected: (_, offset) => setState(() => _selectedOffset = offset), | 547 | onSelected: (_, offset) => setState(() => _selectedOffset = offset), |
| 545 | ), | 548 | ), |
| 546 | gridData: const FlGridData(show: false), | 549 | gridData: const FlGridData(show: false), |
| @@ -590,7 +593,7 @@ class _QualityChartState extends State<_QualityChart> { | @@ -590,7 +593,7 @@ class _QualityChartState extends State<_QualityChart> { | ||
| 590 | 593 | ||
| 591 | @override | 594 | @override |
| 592 | Widget build(BuildContext context) { | 595 | Widget build(BuildContext context) { |
| 593 | - final hasData = widget.days.any((day) => day.qualityScore != null); | 596 | + final hasData = widget.days.any((day) => day.validQualityScore != null); |
| 594 | return Column( | 597 | return Column( |
| 595 | children: [ | 598 | children: [ |
| 596 | const _ChartLegend( | 599 | const _ChartLegend( |
| @@ -646,7 +649,7 @@ class _QualityChartState extends State<_QualityChart> { | @@ -646,7 +649,7 @@ class _QualityChartState extends State<_QualityChart> { | ||
| 646 | barTouchData: _barTouchData( | 649 | barTouchData: _barTouchData( |
| 647 | days: widget.days, | 650 | days: widget.days, |
| 648 | dataBuilder: _ChartSummaryData.quality, | 651 | dataBuilder: _ChartSummaryData.quality, |
| 649 | - hasData: (report) => report.qualityScore != null, | 652 | + hasData: (report) => report.validQualityScore != null, |
| 650 | onSelected: (_, offset) => setState(() => _selectedOffset = offset), | 653 | onSelected: (_, offset) => setState(() => _selectedOffset = offset), |
| 651 | ), | 654 | ), |
| 652 | gridData: const FlGridData(show: false), | 655 | gridData: const FlGridData(show: false), |
| @@ -661,7 +664,7 @@ class _QualityChartState extends State<_QualityChart> { | @@ -661,7 +664,7 @@ class _QualityChartState extends State<_QualityChart> { | ||
| 661 | x: i, | 664 | x: i, |
| 662 | barRods: [ | 665 | barRods: [ |
| 663 | BarChartRodData( | 666 | BarChartRodData( |
| 664 | - toY: (widget.days[i].qualityScore ?? 0).toDouble(), | 667 | + toY: (widget.days[i].validQualityScore ?? 0).toDouble(), |
| 665 | width: _barWidth(widget.compact), | 668 | width: _barWidth(widget.compact), |
| 666 | color: SleepWeekReportView.blue, | 669 | color: SleepWeekReportView.blue, |
| 667 | borderRadius: const BorderRadius.vertical( | 670 | borderRadius: const BorderRadius.vertical( |
| @@ -686,7 +689,9 @@ class _BedtimeChart extends StatelessWidget { | @@ -686,7 +689,9 @@ class _BedtimeChart extends StatelessWidget { | ||
| 686 | 689 | ||
| 687 | @override | 690 | @override |
| 688 | Widget build(BuildContext context) { | 691 | Widget build(BuildContext context) { |
| 689 | - final hasData = days.any((day) => day.heartRate.sleepStart != null); | 692 | + final hasData = days.any( |
| 693 | + (day) => day.hasSleepData && day.heartRate.sleepStart != null, | ||
| 694 | + ); | ||
| 690 | final axis = _BedtimeAxis.fromReports(days); | 695 | final axis = _BedtimeAxis.fromReports(days); |
| 691 | return Stack( | 696 | return Stack( |
| 692 | alignment: Alignment.center, | 697 | alignment: Alignment.center, |
| @@ -706,7 +711,7 @@ class _BedtimeChart extends StatelessWidget { | @@ -706,7 +711,7 @@ class _BedtimeChart extends StatelessWidget { | ||
| 706 | LineChartData _chartData(_BedtimeAxis axis) { | 711 | LineChartData _chartData(_BedtimeAxis axis) { |
| 707 | final points = <FlSpot>[ | 712 | final points = <FlSpot>[ |
| 708 | for (var i = 0; i < days.length; i++) | 713 | for (var i = 0; i < days.length; i++) |
| 709 | - if (days[i].heartRate.sleepStart != null) | 714 | + if (days[i].hasSleepData && days[i].heartRate.sleepStart != null) |
| 710 | FlSpot( | 715 | FlSpot( |
| 711 | i.toDouble(), | 716 | i.toDouble(), |
| 712 | _bedtimeY(days[i].heartRate.sleepStart!), | 717 | _bedtimeY(days[i].heartRate.sleepStart!), |
| @@ -758,7 +763,7 @@ class _BedtimeChart extends StatelessWidget { | @@ -758,7 +763,7 @@ class _BedtimeChart extends StatelessWidget { | ||
| 758 | } | 763 | } |
| 759 | 764 | ||
| 760 | String _timeAxisLabel(double value) { | 765 | String _timeAxisLabel(double value) { |
| 761 | - final hour = value.round() % 24; | 766 | + final hour = value.toInt() % 24; |
| 762 | return '${hour.toString().padLeft(2, '0')}:00'; | 767 | return '${hour.toString().padLeft(2, '0')}:00'; |
| 763 | } | 768 | } |
| 764 | } | 769 | } |
| @@ -783,6 +788,7 @@ class _BedtimeAxis { | @@ -783,6 +788,7 @@ class _BedtimeAxis { | ||
| 783 | 788 | ||
| 784 | factory _BedtimeAxis.fromReports(List<SleepReport> days) { | 789 | factory _BedtimeAxis.fromReports(List<SleepReport> days) { |
| 785 | final values = days | 790 | final values = days |
| 791 | + .where((day) => day.hasSleepData) | ||
| 786 | .map((day) => day.heartRate.sleepStart) | 792 | .map((day) => day.heartRate.sleepStart) |
| 787 | .whereType<DateTime>() | 793 | .whereType<DateTime>() |
| 788 | .map(SleepWeekReportView.minutesFromEvening) | 794 | .map(SleepWeekReportView.minutesFromEvening) |
| @@ -945,10 +951,12 @@ LineTouchTooltipData _lineTooltipData({ | @@ -945,10 +951,12 @@ LineTouchTooltipData _lineTooltipData({ | ||
| 945 | fitInsideVertically: false, | 951 | fitInsideVertically: false, |
| 946 | getTooltipItems: (spots) { | 952 | getTooltipItems: (spots) { |
| 947 | return spots.map((spot) { | 953 | return spots.map((spot) { |
| 948 | - final index = spot.x.round(); | 954 | + final index = spot.x.toInt(); |
| 949 | if (index < 0 || index >= days.length) return null; | 955 | if (index < 0 || index >= days.length) return null; |
| 950 | final report = days[index]; | 956 | final report = days[index]; |
| 951 | - if (report.heartRate.sleepStart == null) return null; | 957 | + if (!report.hasSleepData || report.heartRate.sleepStart == null) { |
| 958 | + return null; | ||
| 959 | + } | ||
| 952 | return _lineTooltipItem(dataBuilder(report)); | 960 | return _lineTooltipItem(dataBuilder(report)); |
| 953 | }).toList(); | 961 | }).toList(); |
| 954 | }, | 962 | }, |
| @@ -1016,7 +1024,7 @@ class _ChartSummaryData { | @@ -1016,7 +1024,7 @@ class _ChartSummaryData { | ||
| 1016 | } | 1024 | } |
| 1017 | 1025 | ||
| 1018 | factory _ChartSummaryData.quality(SleepReport report) { | 1026 | factory _ChartSummaryData.quality(SleepReport report) { |
| 1019 | - final score = report.qualityScore!; | 1027 | + final score = report.validQualityScore!; |
| 1020 | return _ChartSummaryData( | 1028 | return _ChartSummaryData( |
| 1021 | title: l10n.sleepQualityScore( | 1029 | title: l10n.sleepQualityScore( |
| 1022 | report.qualityLevel.shortText, | 1030 | report.qualityLevel.shortText, |
| @@ -1244,7 +1252,7 @@ class _HorizontalDashedGridPainter extends CustomPainter { | @@ -1244,7 +1252,7 @@ class _HorizontalDashedGridPainter extends CustomPainter { | ||
| 1244 | final paint = Paint() | 1252 | final paint = Paint() |
| 1245 | ..color = SleepWeekReportView.grid | 1253 | ..color = SleepWeekReportView.grid |
| 1246 | ..strokeWidth = 1; | 1254 | ..strokeWidth = 1; |
| 1247 | - final steps = ((maxY - minY) / interval).round(); | 1255 | + final steps = ((maxY - minY) / interval).ceil(); |
| 1248 | for (var index = 0; index <= steps; index++) { | 1256 | for (var index = 0; index <= steps; index++) { |
| 1249 | final y = size.height * (1 - index / steps); | 1257 | final y = size.height * (1 - index / steps); |
| 1250 | for (var x = 0.0; x < size.width; x += 4) { | 1258 | for (var x = 0.0; x < size.width; x += 4) { |
| @@ -1323,7 +1331,7 @@ class _RightAxisLabels extends StatelessWidget { | @@ -1323,7 +1331,7 @@ class _RightAxisLabels extends StatelessWidget { | ||
| 1323 | return IgnorePointer( | 1331 | return IgnorePointer( |
| 1324 | child: LayoutBuilder( | 1332 | child: LayoutBuilder( |
| 1325 | builder: (context, constraints) { | 1333 | builder: (context, constraints) { |
| 1326 | - final steps = ((maxY - minY) / interval).round(); | 1334 | + final steps = ((maxY - minY) / interval).ceil(); |
| 1327 | return Stack( | 1335 | return Stack( |
| 1328 | clipBehavior: Clip.none, | 1336 | clipBehavior: Clip.none, |
| 1329 | children: [ | 1337 | children: [ |
| @@ -1454,10 +1462,11 @@ class _ExtremeMetric extends StatelessWidget { | @@ -1454,10 +1462,11 @@ class _ExtremeMetric extends StatelessWidget { | ||
| 1454 | required Color color, | 1462 | required Color color, |
| 1455 | required SleepReport? report, | 1463 | required SleepReport? report, |
| 1456 | }) { | 1464 | }) { |
| 1465 | + final score = report?.validQualityScore; | ||
| 1457 | return _ExtremeMetric( | 1466 | return _ExtremeMetric( |
| 1458 | title: title, | 1467 | title: title, |
| 1459 | color: color, | 1468 | color: color, |
| 1460 | - value: report?.qualityScore == null ? '-' : '${report!.qualityScore}', | 1469 | + value: score == null ? '-' : '$score', |
| 1461 | unit: l10n.reportUnitScore, | 1470 | unit: l10n.reportUnitScore, |
| 1462 | date: report?.date, | 1471 | date: report?.date, |
| 1463 | ); | 1472 | ); |
| @@ -1468,7 +1477,8 @@ class _ExtremeMetric extends StatelessWidget { | @@ -1468,7 +1477,8 @@ class _ExtremeMetric extends StatelessWidget { | ||
| 1468 | required Color color, | 1477 | required Color color, |
| 1469 | required SleepReport? report, | 1478 | required SleepReport? report, |
| 1470 | }) { | 1479 | }) { |
| 1471 | - final start = report?.heartRate.sleepStart; | 1480 | + final start = |
| 1481 | + report?.hasSleepData == true ? report?.heartRate.sleepStart : null; | ||
| 1472 | return _ExtremeMetric( | 1482 | return _ExtremeMetric( |
| 1473 | title: title, | 1483 | title: title, |
| 1474 | color: color, | 1484 | color: color, |
-
Please register or login to post a comment