Showing
7 changed files
with
206 additions
and
128 deletions
| @@ -257,12 +257,10 @@ class _ActivityBurnRingPainter extends CustomPainter { | @@ -257,12 +257,10 @@ class _ActivityBurnRingPainter extends CustomPainter { | ||
| 257 | 257 | ||
| 258 | final rect = Rect.fromCircle(center: center, radius: radius); | 258 | final rect = Rect.fromCircle(center: center, radius: radius); |
| 259 | final safeProgress = math.max(0, progress); | 259 | final safeProgress = math.max(0, progress); |
| 260 | - final hasOverflow = safeProgress > 1; | ||
| 261 | - final baseProgress = hasOverflow ? 1.0 : safeProgress; | ||
| 262 | - final overflowProgress = safeProgress - 1; | ||
| 263 | - final overflowLap = overflowProgress % 1; | ||
| 264 | - final visibleOverflowLap = | ||
| 265 | - hasOverflow && overflowLap == 0 ? 1.0 : overflowLap; | 260 | + final hasCompletedLap = safeProgress >= 1; |
| 261 | + final remainder = safeProgress % 1; | ||
| 262 | + final visibleProgress = hasCompletedLap ? remainder : safeProgress; | ||
| 263 | + final isExactLap = hasCompletedLap && remainder == 0; | ||
| 266 | 264 | ||
| 267 | final progressPaint = Paint() | 265 | final progressPaint = Paint() |
| 268 | ..color = progressColor | 266 | ..color = progressColor |
| @@ -276,7 +274,9 @@ class _ActivityBurnRingPainter extends CustomPainter { | @@ -276,7 +274,9 @@ class _ActivityBurnRingPainter extends CustomPainter { | ||
| 276 | ..strokeWidth = width | 274 | ..strokeWidth = width |
| 277 | ..strokeCap = StrokeCap.butt; | 275 | ..strokeCap = StrokeCap.butt; |
| 278 | 276 | ||
| 279 | - if (baseProgress >= 1) { | 277 | + // Bottom layer: once a lap is complete, keep a solid full ring below the |
| 278 | + // animated current lap. | ||
| 279 | + if (hasCompletedLap) { | ||
| 280 | canvas.drawArc( | 280 | canvas.drawArc( |
| 281 | rect, | 281 | rect, |
| 282 | startAngle, | 282 | startAngle, |
| @@ -284,40 +284,64 @@ class _ActivityBurnRingPainter extends CustomPainter { | @@ -284,40 +284,64 @@ class _ActivityBurnRingPainter extends CustomPainter { | ||
| 284 | false, | 284 | false, |
| 285 | fullLapPaint, | 285 | fullLapPaint, |
| 286 | ); | 286 | ); |
| 287 | - } else { | ||
| 288 | - final baseEndAngle = startAngle + math.pi * 2 * baseProgress; | ||
| 289 | - _drawRingHeadShadow( | 287 | + } |
| 288 | + | ||
| 289 | + final endAngle = startAngle + math.pi * 2 * visibleProgress; | ||
| 290 | + | ||
| 291 | + // Head layer: its shadow is painted first, then the colored current lap. | ||
| 292 | + // For an exact lap there is no partial arc, so paint a colored head at the | ||
| 293 | + // 12 o'clock endpoint instead. | ||
| 294 | + _drawRingHeadShadow( | ||
| 295 | + canvas, | ||
| 296 | + center: center, | ||
| 297 | + radius: radius, | ||
| 298 | + width: width, | ||
| 299 | + angle: endAngle, | ||
| 300 | + forwardOnly: isExactLap, | ||
| 301 | + ); | ||
| 302 | + if (isExactLap) { | ||
| 303 | + _drawRingEndCap( | ||
| 290 | canvas, | 304 | canvas, |
| 291 | center: center, | 305 | center: center, |
| 292 | radius: radius, | 306 | radius: radius, |
| 293 | width: width, | 307 | width: width, |
| 294 | - angle: baseEndAngle, | 308 | + angle: endAngle, |
| 309 | + color: progressColor, | ||
| 295 | ); | 310 | ); |
| 311 | + } else { | ||
| 296 | canvas.drawArc( | 312 | canvas.drawArc( |
| 297 | rect, | 313 | rect, |
| 298 | startAngle, | 314 | startAngle, |
| 299 | - math.pi * 2 * baseProgress, | 315 | + math.pi * 2 * visibleProgress, |
| 300 | false, | 316 | false, |
| 301 | progressPaint, | 317 | progressPaint, |
| 302 | ); | 318 | ); |
| 303 | } | 319 | } |
| 320 | + } | ||
| 304 | 321 | ||
| 305 | - if (!hasOverflow || visibleOverflowLap <= 0) return; | ||
| 306 | - | ||
| 307 | - final overflowEndAngle = startAngle + math.pi * 2 * visibleOverflowLap; | ||
| 308 | - _drawRingHeadShadow( | ||
| 309 | - canvas, | ||
| 310 | - center: center, | ||
| 311 | - radius: radius, | ||
| 312 | - width: width, | ||
| 313 | - angle: overflowEndAngle, | 322 | + void _drawRingEndCap( |
| 323 | + Canvas canvas, { | ||
| 324 | + required Offset center, | ||
| 325 | + required double radius, | ||
| 326 | + required double width, | ||
| 327 | + required double angle, | ||
| 328 | + required Color color, | ||
| 329 | + }) { | ||
| 330 | + final headCenter = Offset( | ||
| 331 | + center.dx + math.cos(angle) * radius, | ||
| 332 | + center.dy + math.sin(angle) * radius, | ||
| 314 | ); | 333 | ); |
| 334 | + // Only paint the forward half of the cap. The completed ring below already | ||
| 335 | + // provides the trailing half; using a full circle makes the head look like | ||
| 336 | + // a separate colored dot at exactly 1.0x, 2.0x, etc. | ||
| 315 | canvas.drawArc( | 337 | canvas.drawArc( |
| 316 | - rect, | ||
| 317 | - startAngle, | ||
| 318 | - math.pi * 2 * visibleOverflowLap, | ||
| 319 | - false, | ||
| 320 | - progressPaint, | 338 | + Rect.fromCircle(center: headCenter, radius: width / 2), |
| 339 | + angle, | ||
| 340 | + math.pi, | ||
| 341 | + true, | ||
| 342 | + Paint() | ||
| 343 | + ..color = color | ||
| 344 | + ..style = PaintingStyle.fill, | ||
| 321 | ); | 345 | ); |
| 322 | } | 346 | } |
| 323 | 347 | ||
| @@ -327,6 +351,7 @@ class _ActivityBurnRingPainter extends CustomPainter { | @@ -327,6 +351,7 @@ class _ActivityBurnRingPainter extends CustomPainter { | ||
| 327 | required double radius, | 351 | required double radius, |
| 328 | required double width, | 352 | required double width, |
| 329 | required double angle, | 353 | required double angle, |
| 354 | + bool forwardOnly = false, | ||
| 330 | }) { | 355 | }) { |
| 331 | final headCenter = Offset( | 356 | final headCenter = Offset( |
| 332 | center.dx + math.cos(angle) * radius, | 357 | center.dx + math.cos(angle) * radius, |
| @@ -340,6 +365,29 @@ class _ActivityBurnRingPainter extends CustomPainter { | @@ -340,6 +365,29 @@ class _ActivityBurnRingPainter extends CustomPainter { | ||
| 340 | 365 | ||
| 341 | canvas.save(); | 366 | canvas.save(); |
| 342 | canvas.clipPath(ringClip); | 367 | canvas.clipPath(ringClip); |
| 368 | + if (forwardOnly) { | ||
| 369 | + final radial = Offset(math.cos(angle), math.sin(angle)); | ||
| 370 | + final extent = width * 2; | ||
| 371 | + final forwardClip = Path() | ||
| 372 | + ..moveTo( | ||
| 373 | + headCenter.dx - radial.dx * extent, | ||
| 374 | + headCenter.dy - radial.dy * extent, | ||
| 375 | + ) | ||
| 376 | + ..lineTo( | ||
| 377 | + headCenter.dx + radial.dx * extent, | ||
| 378 | + headCenter.dy + radial.dy * extent, | ||
| 379 | + ) | ||
| 380 | + ..lineTo( | ||
| 381 | + headCenter.dx + radial.dx * extent + tangent.dx * extent, | ||
| 382 | + headCenter.dy + radial.dy * extent + tangent.dy * extent, | ||
| 383 | + ) | ||
| 384 | + ..lineTo( | ||
| 385 | + headCenter.dx - radial.dx * extent + tangent.dx * extent, | ||
| 386 | + headCenter.dy - radial.dy * extent + tangent.dy * extent, | ||
| 387 | + ) | ||
| 388 | + ..close(); | ||
| 389 | + canvas.clipPath(forwardClip); | ||
| 390 | + } | ||
| 343 | canvas.drawCircle( | 391 | canvas.drawCircle( |
| 344 | headCenter + tangent * 2, | 392 | headCenter + tangent * 2, |
| 345 | width / 2, | 393 | width / 2, |
| @@ -27,7 +27,7 @@ class PrivacySettingsController extends GetxController { | @@ -27,7 +27,7 @@ class PrivacySettingsController extends GetxController { | ||
| 27 | isUpdatingEnableAddWithUCode.value = true; | 27 | isUpdatingEnableAddWithUCode.value = true; |
| 28 | try { | 28 | try { |
| 29 | final result = await _userApi.updateOwnerUserInfo( | 29 | final result = await _userApi.updateOwnerUserInfo( |
| 30 | - enableAddWithUcode: enabled ? 0 : 1, | 30 | + enableAddWithUcode: enabled ? 1 : 0, |
| 31 | ); | 31 | ); |
| 32 | if (result is AppSuccess<UserInfoResponse>) { | 32 | if (result is AppSuccess<UserInfoResponse>) { |
| 33 | await _userPreferencesStorage.updateMeUserInfo(result.data); | 33 | await _userPreferencesStorage.updateMeUserInfo(result.data); |
| @@ -97,6 +97,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -97,6 +97,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 97 | averageDurationSeconds: data.avgSleepDuration, | 97 | averageDurationSeconds: data.avgSleepDuration, |
| 98 | targetDurationSeconds: data.sleepTargetDuration, | 98 | targetDurationSeconds: data.sleepTargetDuration, |
| 99 | averageScore: data.avgSleepScore, | 99 | averageScore: data.avgSleepScore, |
| 100 | + averageSleepEvaluate: data.avgSleepEvaluate?.toInt(), | ||
| 100 | previousAverageDurationSeconds: data.qoqAvgSleepDuration, | 101 | previousAverageDurationSeconds: data.qoqAvgSleepDuration, |
| 101 | previousAverageScore: data.qoqSleepScore, | 102 | previousAverageScore: data.qoqSleepScore, |
| 102 | earliestSleepOverride: _sleepInfoReport( | 103 | earliestSleepOverride: _sleepInfoReport( |
| @@ -141,6 +142,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -141,6 +142,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 141 | averageDurationSeconds: data.avgSleepDuration, | 142 | averageDurationSeconds: data.avgSleepDuration, |
| 142 | targetDurationSeconds: data.sleepTargetDuration, | 143 | targetDurationSeconds: data.sleepTargetDuration, |
| 143 | averageScore: data.avgSleepScore, | 144 | averageScore: data.avgSleepScore, |
| 145 | + averageSleepEvaluate: data.avgSleepEvaluate?.toInt(), | ||
| 144 | previousAverageDurationSeconds: data.qoqAvgSleepDuration, | 146 | previousAverageDurationSeconds: data.qoqAvgSleepDuration, |
| 145 | previousAverageScore: data.qoqSleepScore, | 147 | previousAverageScore: data.qoqSleepScore, |
| 146 | earliestSleepOverride: _sleepInfoReport( | 148 | earliestSleepOverride: _sleepInfoReport( |
| @@ -183,6 +185,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -183,6 +185,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 183 | ? report.duration | 185 | ? report.duration |
| 184 | : SleepDuration(minutes: durationSeconds ~/ 60), | 186 | : SleepDuration(minutes: durationSeconds ~/ 60), |
| 185 | qualityScore: score?.toInt() ?? report.qualityScore, | 187 | qualityScore: score?.toInt() ?? report.qualityScore, |
| 188 | + sleepEvaluate: report.sleepEvaluate, | ||
| 186 | heartRate: report.heartRate, | 189 | heartRate: report.heartRate, |
| 187 | ); | 190 | ); |
| 188 | } | 191 | } |
| @@ -202,6 +205,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -202,6 +205,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 202 | date: date, | 205 | date: date, |
| 203 | duration: report.duration, | 206 | duration: report.duration, |
| 204 | qualityScore: report.qualityScore, | 207 | qualityScore: report.qualityScore, |
| 208 | + sleepEvaluate: report.sleepEvaluate, | ||
| 205 | heartRate: SleepHeartRateSummary( | 209 | heartRate: SleepHeartRateSummary( |
| 206 | averageBpm: heartRate.averageBpm, | 210 | averageBpm: heartRate.averageBpm, |
| 207 | maxBpm: heartRate.maxBpm, | 211 | maxBpm: heartRate.maxBpm, |
| @@ -260,9 +264,11 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -260,9 +264,11 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 260 | timeKey: trend?.timeKey, | 264 | timeKey: trend?.timeKey, |
| 261 | totalTime: trend?.totalTime ?? data.avgSleepDuration, | 265 | totalTime: trend?.totalTime ?? data.avgSleepDuration, |
| 262 | score: trend?.score ?? data.avgSleepScore, | 266 | score: trend?.score ?? data.avgSleepScore, |
| 267 | + sleepEvaluate: trend?.sleepEvaluate ?? data.avgSleepEvaluate, | ||
| 263 | ); | 268 | ); |
| 264 | if (dailyTrend.totalTime == null && | 269 | if (dailyTrend.totalTime == null && |
| 265 | dailyTrend.score == null && | 270 | dailyTrend.score == null && |
| 271 | + dailyTrend.sleepEvaluate == null && | ||
| 266 | asleepTime == null && | 272 | asleepTime == null && |
| 267 | points.isEmpty && | 273 | points.isEmpty && |
| 268 | data.avgHr == null && | 274 | data.avgHr == null && |
| @@ -364,6 +370,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | @@ -364,6 +370,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { | ||
| 364 | sleepDurationSeconds: sleepDurationSeconds, | 370 | sleepDurationSeconds: sleepDurationSeconds, |
| 365 | targetDurationSeconds: targetDurationSeconds, | 371 | targetDurationSeconds: targetDurationSeconds, |
| 366 | qualityScore: trend?.score?.toInt(), | 372 | qualityScore: trend?.score?.toInt(), |
| 373 | + sleepEvaluate: trend?.sleepEvaluate?.toInt(), | ||
| 367 | heartRate: _heartRateSummary( | 374 | heartRate: _heartRateSummary( |
| 368 | points, | 375 | points, |
| 369 | asleepTime, | 376 | asleepTime, |
| @@ -479,6 +486,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | @@ -479,6 +486,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | ||
| 479 | sleepDurationSeconds: 405 * 60, | 486 | sleepDurationSeconds: 405 * 60, |
| 480 | targetDurationSeconds: 28800, | 487 | targetDurationSeconds: 28800, |
| 481 | qualityScore: 86, | 488 | qualityScore: 86, |
| 489 | + sleepEvaluate: 1, | ||
| 482 | heartRate: _buildHeartRateSummary( | 490 | heartRate: _buildHeartRateSummary( |
| 483 | points: points, | 491 | points: points, |
| 484 | sleepStart: sleepStart, | 492 | sleepStart: sleepStart, |
| @@ -530,6 +538,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | @@ -530,6 +538,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | ||
| 530 | qualityScore: [82, 88, 0, 79, 91, 86, 84][i] == 0 | 538 | qualityScore: [82, 88, 0, 79, 91, 86, 84][i] == 0 |
| 531 | ? null | 539 | ? null |
| 532 | : [82, 88, 0, 79, 91, 86, 84][i], | 540 | : [82, 88, 0, 79, 91, 86, 84][i], |
| 541 | + sleepEvaluate: [2, 1, null, 2, 1, 1, 2][i], | ||
| 533 | heartRate: _buildHeartRateSummary( | 542 | heartRate: _buildHeartRateSummary( |
| 534 | points: points, | 543 | points: points, |
| 535 | sleepStart: sleepStart, | 544 | sleepStart: sleepStart, |
| @@ -543,6 +552,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | @@ -543,6 +552,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | ||
| 543 | weekStart: normalizedStart, | 552 | weekStart: normalizedStart, |
| 544 | weekEnd: normalizedStart.add(const Duration(days: 6)), | 553 | weekEnd: normalizedStart.add(const Duration(days: 6)), |
| 545 | days: days, | 554 | days: days, |
| 555 | + averageSleepEvaluate: 1, | ||
| 546 | ); | 556 | ); |
| 547 | } | 557 | } |
| 548 | 558 | ||
| @@ -626,6 +636,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | @@ -626,6 +636,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | ||
| 626 | date: date, | 636 | date: date, |
| 627 | duration: SleepDuration(minutes: durationMinutes[i]), | 637 | duration: SleepDuration(minutes: durationMinutes[i]), |
| 628 | qualityScore: qualityScores[i], | 638 | qualityScore: qualityScores[i], |
| 639 | + sleepEvaluate: qualityScores[i] > 85 ? 1 : 2, | ||
| 629 | heartRate: _buildHeartRateSummary( | 640 | heartRate: _buildHeartRateSummary( |
| 630 | points: points, | 641 | points: points, |
| 631 | sleepStart: sleepStart, | 642 | sleepStart: sleepStart, |
| @@ -639,6 +650,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | @@ -639,6 +650,7 @@ class MockSleepReportDataSource implements SleepReportDataSource { | ||
| 639 | monthStart: normalizedStart, | 650 | monthStart: normalizedStart, |
| 640 | monthEnd: monthEnd, | 651 | monthEnd: monthEnd, |
| 641 | days: days, | 652 | days: days, |
| 653 | + averageSleepEvaluate: 1, | ||
| 642 | ); | 654 | ); |
| 643 | } | 655 | } |
| 644 | } | 656 | } |
| @@ -48,13 +48,6 @@ extension SleepQualityLevelText on SleepQualityLevel { | @@ -48,13 +48,6 @@ extension SleepQualityLevelText on SleepQualityLevel { | ||
| 48 | } | 48 | } |
| 49 | } | 49 | } |
| 50 | 50 | ||
| 51 | -SleepQualityLevel sleepQualityLevelFromScore(int? score) { | ||
| 52 | - if (score == null) return SleepQualityLevel.unknown; | ||
| 53 | - if (score > 85) return SleepQualityLevel.excellent; | ||
| 54 | - if (score >= 60) return SleepQualityLevel.good; | ||
| 55 | - return SleepQualityLevel.poor; | ||
| 56 | -} | ||
| 57 | - | ||
| 58 | SleepQualityLevel sleepQualityLevelFromSleepEvaluate(int? sleepEvaluate) { | 51 | SleepQualityLevel sleepQualityLevelFromSleepEvaluate(int? sleepEvaluate) { |
| 59 | return switch (sleepEvaluate) { | 52 | return switch (sleepEvaluate) { |
| 60 | 1 => SleepQualityLevel.excellent, | 53 | 1 => SleepQualityLevel.excellent, |
| @@ -168,6 +161,7 @@ class SleepReport { | @@ -168,6 +161,7 @@ class SleepReport { | ||
| 168 | final num? sleepDurationSeconds; | 161 | final num? sleepDurationSeconds; |
| 169 | final num? targetDurationSeconds; | 162 | final num? targetDurationSeconds; |
| 170 | final int? qualityScore; | 163 | final int? qualityScore; |
| 164 | + final int? sleepEvaluate; | ||
| 171 | final SleepHeartRateSummary heartRate; | 165 | final SleepHeartRateSummary heartRate; |
| 172 | 166 | ||
| 173 | const SleepReport({ | 167 | const SleepReport({ |
| @@ -176,6 +170,7 @@ class SleepReport { | @@ -176,6 +170,7 @@ class SleepReport { | ||
| 176 | this.sleepDurationSeconds, | 170 | this.sleepDurationSeconds, |
| 177 | this.targetDurationSeconds, | 171 | this.targetDurationSeconds, |
| 178 | this.qualityScore, | 172 | this.qualityScore, |
| 173 | + this.sleepEvaluate, | ||
| 179 | this.heartRate = const SleepHeartRateSummary(), | 174 | this.heartRate = const SleepHeartRateSummary(), |
| 180 | }); | 175 | }); |
| 181 | 176 | ||
| @@ -189,7 +184,7 @@ class SleepReport { | @@ -189,7 +184,7 @@ class SleepReport { | ||
| 189 | hasSleepData ? heartRate : const SleepHeartRateSummary(); | 184 | hasSleepData ? heartRate : const SleepHeartRateSummary(); |
| 190 | 185 | ||
| 191 | SleepQualityLevel get qualityLevel => | 186 | SleepQualityLevel get qualityLevel => |
| 192 | - sleepQualityLevelFromScore(validQualityScore); | 187 | + sleepQualityLevelFromSleepEvaluate(sleepEvaluate); |
| 193 | 188 | ||
| 194 | factory SleepReport.empty(DateTime date) { | 189 | factory SleepReport.empty(DateTime date) { |
| 195 | return SleepReport(date: date); | 190 | return SleepReport(date: date); |
| @@ -204,6 +199,7 @@ class SleepReport { | @@ -204,6 +199,7 @@ class SleepReport { | ||
| 204 | sleepDurationSeconds: json['sleepDurationSeconds'] as num?, | 199 | sleepDurationSeconds: json['sleepDurationSeconds'] as num?, |
| 205 | targetDurationSeconds: json['targetDurationSeconds'] as num?, | 200 | targetDurationSeconds: json['targetDurationSeconds'] as num?, |
| 206 | qualityScore: json['qualityScore'] as int?, | 201 | qualityScore: json['qualityScore'] as int?, |
| 202 | + sleepEvaluate: json['sleepEvaluate'] as int?, | ||
| 207 | heartRate: json['heartRate'] == null | 203 | heartRate: json['heartRate'] == null |
| 208 | ? const SleepHeartRateSummary() | 204 | ? const SleepHeartRateSummary() |
| 209 | : SleepHeartRateSummary.fromJson( | 205 | : SleepHeartRateSummary.fromJson( |
| @@ -218,6 +214,7 @@ class SleepReport { | @@ -218,6 +214,7 @@ class SleepReport { | ||
| 218 | 'sleepDurationSeconds': sleepDurationSeconds, | 214 | 'sleepDurationSeconds': sleepDurationSeconds, |
| 219 | 'targetDurationSeconds': targetDurationSeconds, | 215 | 'targetDurationSeconds': targetDurationSeconds, |
| 220 | 'qualityScore': qualityScore, | 216 | 'qualityScore': qualityScore, |
| 217 | + 'sleepEvaluate': sleepEvaluate, | ||
| 221 | 'heartRate': heartRate.toJson(), | 218 | 'heartRate': heartRate.toJson(), |
| 222 | }; | 219 | }; |
| 223 | } | 220 | } |
| @@ -230,6 +227,7 @@ class WeeklySleepReport { | @@ -230,6 +227,7 @@ class WeeklySleepReport { | ||
| 230 | this.averageDurationSeconds, | 227 | this.averageDurationSeconds, |
| 231 | this.targetDurationSeconds, | 228 | this.targetDurationSeconds, |
| 232 | this.averageScore, | 229 | this.averageScore, |
| 230 | + this.averageSleepEvaluate, | ||
| 233 | this.previousAverageDurationSeconds, | 231 | this.previousAverageDurationSeconds, |
| 234 | this.previousAverageScore, | 232 | this.previousAverageScore, |
| 235 | this.earliestSleepOverride, | 233 | this.earliestSleepOverride, |
| @@ -244,6 +242,7 @@ class WeeklySleepReport { | @@ -244,6 +242,7 @@ class WeeklySleepReport { | ||
| 244 | final num? averageDurationSeconds; | 242 | final num? averageDurationSeconds; |
| 245 | final num? targetDurationSeconds; | 243 | final num? targetDurationSeconds; |
| 246 | final num? averageScore; | 244 | final num? averageScore; |
| 245 | + final int? averageSleepEvaluate; | ||
| 247 | final num? previousAverageDurationSeconds; | 246 | final num? previousAverageDurationSeconds; |
| 248 | final num? previousAverageScore; | 247 | final num? previousAverageScore; |
| 249 | final SleepReport? earliestSleepOverride; | 248 | final SleepReport? earliestSleepOverride; |
| @@ -294,7 +293,7 @@ class WeeklySleepReport { | @@ -294,7 +293,7 @@ class WeeklySleepReport { | ||
| 294 | } | 293 | } |
| 295 | 294 | ||
| 296 | SleepQualityLevel get averageQualityLevel => | 295 | SleepQualityLevel get averageQualityLevel => |
| 297 | - sleepQualityLevelFromScore(averageQualityScore); | 296 | + sleepQualityLevelFromSleepEvaluate(averageSleepEvaluate); |
| 298 | 297 | ||
| 299 | double? get durationComparisonPercent => _comparisonPercent( | 298 | double? get durationComparisonPercent => _comparisonPercent( |
| 300 | _computedAverageDurationSeconds, | 299 | _computedAverageDurationSeconds, |
| @@ -360,6 +359,7 @@ class MonthlySleepReport { | @@ -360,6 +359,7 @@ class MonthlySleepReport { | ||
| 360 | this.averageDurationSeconds, | 359 | this.averageDurationSeconds, |
| 361 | this.targetDurationSeconds, | 360 | this.targetDurationSeconds, |
| 362 | this.averageScore, | 361 | this.averageScore, |
| 362 | + this.averageSleepEvaluate, | ||
| 363 | this.previousAverageDurationSeconds, | 363 | this.previousAverageDurationSeconds, |
| 364 | this.previousAverageScore, | 364 | this.previousAverageScore, |
| 365 | this.earliestSleepOverride, | 365 | this.earliestSleepOverride, |
| @@ -374,6 +374,7 @@ class MonthlySleepReport { | @@ -374,6 +374,7 @@ class MonthlySleepReport { | ||
| 374 | final num? averageDurationSeconds; | 374 | final num? averageDurationSeconds; |
| 375 | final num? targetDurationSeconds; | 375 | final num? targetDurationSeconds; |
| 376 | final num? averageScore; | 376 | final num? averageScore; |
| 377 | + final int? averageSleepEvaluate; | ||
| 377 | final num? previousAverageDurationSeconds; | 378 | final num? previousAverageDurationSeconds; |
| 378 | final num? previousAverageScore; | 379 | final num? previousAverageScore; |
| 379 | final SleepReport? earliestSleepOverride; | 380 | final SleepReport? earliestSleepOverride; |
| @@ -424,7 +425,7 @@ class MonthlySleepReport { | @@ -424,7 +425,7 @@ class MonthlySleepReport { | ||
| 424 | } | 425 | } |
| 425 | 426 | ||
| 426 | SleepQualityLevel get averageQualityLevel => | 427 | SleepQualityLevel get averageQualityLevel => |
| 427 | - sleepQualityLevelFromScore(averageQualityScore); | 428 | + sleepQualityLevelFromSleepEvaluate(averageSleepEvaluate); |
| 428 | 429 | ||
| 429 | double? get durationComparisonPercent => _comparisonPercent( | 430 | double? get durationComparisonPercent => _comparisonPercent( |
| 430 | _computedAverageDurationSeconds, | 431 | _computedAverageDurationSeconds, |
| @@ -520,21 +520,6 @@ class _DurationChartState extends State<_DurationChart> { | @@ -520,21 +520,6 @@ class _DurationChartState extends State<_DurationChart> { | ||
| 520 | alignment: Alignment.center, | 520 | alignment: Alignment.center, |
| 521 | clipBehavior: Clip.none, | 521 | clipBehavior: Clip.none, |
| 522 | children: [ | 522 | children: [ |
| 523 | - if (_selectedIndex != null && _selectedOffset != null) | ||
| 524 | - Positioned.fill( | ||
| 525 | - child: LayoutBuilder( | ||
| 526 | - builder: (context, constraints) => | ||
| 527 | - ChartSelectionLineOverlay( | ||
| 528 | - offset: Offset.zero, | ||
| 529 | - color: SleepWeekReportView.h3, | ||
| 530 | - bottomTitleHeight: _ChartPlotFrame.bottomTitleHeight, | ||
| 531 | - plotLeft: 0, | ||
| 532 | - plotRight: 0, | ||
| 533 | - lineX: _selectedOffset!.dx, | ||
| 534 | - lineTop: _fixedTooltipBottom, | ||
| 535 | - ), | ||
| 536 | - ), | ||
| 537 | - ), | ||
| 538 | _ChartPlotFrame( | 523 | _ChartPlotFrame( |
| 539 | maxY: _maxY, | 524 | maxY: _maxY, |
| 540 | interval: 2, | 525 | interval: 2, |
| @@ -566,15 +551,25 @@ class _DurationChartState extends State<_DurationChart> { | @@ -566,15 +551,25 @@ class _DurationChartState extends State<_DurationChart> { | ||
| 566 | ), | 551 | ), |
| 567 | onPointerUp: (_) => _onSelected(null, null), | 552 | onPointerUp: (_) => _onSelected(null, null), |
| 568 | onPointerCancel: (_) => _onSelected(null, null), | 553 | onPointerCancel: (_) => _onSelected(null, null), |
| 569 | - child: BarChart( | ||
| 570 | - _chartData( | ||
| 571 | - constraints.maxWidth, | ||
| 572 | - constraints.maxHeight, | ||
| 573 | - ), | ||
| 574 | - ), | 554 | + child: BarChart(_chartData(constraints.maxWidth)), |
| 575 | ), | 555 | ), |
| 576 | ), | 556 | ), |
| 577 | ), | 557 | ), |
| 558 | + if (_selectedIndex != null && _selectedOffset != null) | ||
| 559 | + Positioned.fill( | ||
| 560 | + child: LayoutBuilder( | ||
| 561 | + builder: (context, constraints) => | ||
| 562 | + ChartSelectionLineOverlay( | ||
| 563 | + offset: Offset.zero, | ||
| 564 | + color: SleepWeekReportView.h3, | ||
| 565 | + bottomTitleHeight: _ChartPlotFrame.bottomTitleHeight, | ||
| 566 | + plotLeft: 0, | ||
| 567 | + plotRight: 0, | ||
| 568 | + lineX: _selectedOffset!.dx, | ||
| 569 | + lineTop: -_barTooltipMargin, | ||
| 570 | + ), | ||
| 571 | + ), | ||
| 572 | + ), | ||
| 578 | if (!hasData) const _WaitingDataText(), | 573 | if (!hasData) const _WaitingDataText(), |
| 579 | ], | 574 | ], |
| 580 | ), | 575 | ), |
| @@ -583,7 +578,7 @@ class _DurationChartState extends State<_DurationChart> { | @@ -583,7 +578,7 @@ class _DurationChartState extends State<_DurationChart> { | ||
| 583 | ); | 578 | ); |
| 584 | } | 579 | } |
| 585 | 580 | ||
| 586 | - BarChartData _chartData(double plotWidth, double chartHeight) { | 581 | + BarChartData _chartData(double plotWidth) { |
| 587 | return BarChartData( | 582 | return BarChartData( |
| 588 | minY: 0, | 583 | minY: 0, |
| 589 | maxY: _maxY, | 584 | maxY: _maxY, |
| @@ -624,11 +619,7 @@ class _DurationChartState extends State<_DurationChart> { | @@ -624,11 +619,7 @@ class _DurationChartState extends State<_DurationChart> { | ||
| 624 | ), | 619 | ), |
| 625 | ), | 620 | ), |
| 626 | BarChartRodData( | 621 | BarChartRodData( |
| 627 | - toY: _tooltipAnchorValue( | ||
| 628 | - maxY: _maxY, | ||
| 629 | - chartHeight: chartHeight, | ||
| 630 | - tooltipMargin: _barTooltipMargin, | ||
| 631 | - ), | 622 | + toY: _maxY, |
| 632 | width: 0, | 623 | width: 0, |
| 633 | color: Colors.transparent, | 624 | color: Colors.transparent, |
| 634 | ), | 625 | ), |
| @@ -738,21 +729,6 @@ class _QualityChartState extends State<_QualityChart> { | @@ -738,21 +729,6 @@ class _QualityChartState extends State<_QualityChart> { | ||
| 738 | alignment: Alignment.center, | 729 | alignment: Alignment.center, |
| 739 | clipBehavior: Clip.none, | 730 | clipBehavior: Clip.none, |
| 740 | children: [ | 731 | children: [ |
| 741 | - if (_selectedIndex != null && _selectedOffset != null) | ||
| 742 | - Positioned.fill( | ||
| 743 | - child: LayoutBuilder( | ||
| 744 | - builder: (context, constraints) => | ||
| 745 | - ChartSelectionLineOverlay( | ||
| 746 | - offset: Offset.zero, | ||
| 747 | - color: SleepWeekReportView.h3, | ||
| 748 | - bottomTitleHeight: _ChartPlotFrame.bottomTitleHeight, | ||
| 749 | - plotLeft: 0, | ||
| 750 | - plotRight: 0, | ||
| 751 | - lineX: _selectedOffset!.dx, | ||
| 752 | - lineTop: _fixedTooltipBottom, | ||
| 753 | - ), | ||
| 754 | - ), | ||
| 755 | - ), | ||
| 756 | _ChartPlotFrame( | 732 | _ChartPlotFrame( |
| 757 | maxY: 100, | 733 | maxY: 100, |
| 758 | interval: 25, | 734 | interval: 25, |
| @@ -778,15 +754,25 @@ class _QualityChartState extends State<_QualityChart> { | @@ -778,15 +754,25 @@ class _QualityChartState extends State<_QualityChart> { | ||
| 778 | ), | 754 | ), |
| 779 | onPointerUp: (_) => _onSelected(null, null), | 755 | onPointerUp: (_) => _onSelected(null, null), |
| 780 | onPointerCancel: (_) => _onSelected(null, null), | 756 | onPointerCancel: (_) => _onSelected(null, null), |
| 781 | - child: BarChart( | ||
| 782 | - _chartData( | ||
| 783 | - constraints.maxWidth, | ||
| 784 | - constraints.maxHeight, | ||
| 785 | - ), | ||
| 786 | - ), | 757 | + child: BarChart(_chartData(constraints.maxWidth)), |
| 787 | ), | 758 | ), |
| 788 | ), | 759 | ), |
| 789 | ), | 760 | ), |
| 761 | + if (_selectedIndex != null && _selectedOffset != null) | ||
| 762 | + Positioned.fill( | ||
| 763 | + child: LayoutBuilder( | ||
| 764 | + builder: (context, constraints) => | ||
| 765 | + ChartSelectionLineOverlay( | ||
| 766 | + offset: Offset.zero, | ||
| 767 | + color: SleepWeekReportView.h3, | ||
| 768 | + bottomTitleHeight: _ChartPlotFrame.bottomTitleHeight, | ||
| 769 | + plotLeft: 0, | ||
| 770 | + plotRight: 0, | ||
| 771 | + lineX: _selectedOffset!.dx, | ||
| 772 | + lineTop: -_barTooltipMargin, | ||
| 773 | + ), | ||
| 774 | + ), | ||
| 775 | + ), | ||
| 790 | if (!hasData) const _WaitingDataText(), | 776 | if (!hasData) const _WaitingDataText(), |
| 791 | ], | 777 | ], |
| 792 | ), | 778 | ), |
| @@ -795,7 +781,7 @@ class _QualityChartState extends State<_QualityChart> { | @@ -795,7 +781,7 @@ class _QualityChartState extends State<_QualityChart> { | ||
| 795 | ); | 781 | ); |
| 796 | } | 782 | } |
| 797 | 783 | ||
| 798 | - BarChartData _chartData(double plotWidth, double chartHeight) { | 784 | + BarChartData _chartData(double plotWidth) { |
| 799 | return BarChartData( | 785 | return BarChartData( |
| 800 | minY: 0, | 786 | minY: 0, |
| 801 | maxY: 100, | 787 | maxY: 100, |
| @@ -835,11 +821,7 @@ class _QualityChartState extends State<_QualityChart> { | @@ -835,11 +821,7 @@ class _QualityChartState extends State<_QualityChart> { | ||
| 835 | ), | 821 | ), |
| 836 | ), | 822 | ), |
| 837 | BarChartRodData( | 823 | BarChartRodData( |
| 838 | - toY: _tooltipAnchorValue( | ||
| 839 | - maxY: 100, | ||
| 840 | - chartHeight: chartHeight, | ||
| 841 | - tooltipMargin: _barTooltipMargin, | ||
| 842 | - ), | 824 | + toY: 100, |
| 843 | width: 0, | 825 | width: 0, |
| 844 | color: Colors.transparent, | 826 | color: Colors.transparent, |
| 845 | ), | 827 | ), |
| @@ -964,14 +946,14 @@ class _BedtimeChartState extends State<_BedtimeChart> { | @@ -964,14 +946,14 @@ class _BedtimeChartState extends State<_BedtimeChart> { | ||
| 964 | constraints.maxWidth, | 946 | constraints.maxWidth, |
| 965 | _selectedDayIndex!, | 947 | _selectedDayIndex!, |
| 966 | ), | 948 | ), |
| 967 | - lineTop: _fixedTooltipBottom, | 949 | + lineTop: _bedtimeChartTopInset, |
| 968 | ), | 950 | ), |
| 969 | ), | 951 | ), |
| 970 | _ChartPlotFrame( | 952 | _ChartPlotFrame( |
| 971 | maxY: axis.maxHour.toDouble(), | 953 | maxY: axis.maxHour.toDouble(), |
| 972 | minY: axis.minHour.toDouble(), | 954 | minY: axis.minHour.toDouble(), |
| 973 | interval: 1, | 955 | interval: 1, |
| 974 | - topInset: 16, | 956 | + topInset: _bedtimeChartTopInset, |
| 975 | insetChildHorizontally: false, | 957 | insetChildHorizontally: false, |
| 976 | rightLabel: _timeAxisLabel, | 958 | rightLabel: _timeAxisLabel, |
| 977 | child: LayoutBuilder( | 959 | child: LayoutBuilder( |
| @@ -991,7 +973,6 @@ class _BedtimeChartState extends State<_BedtimeChart> { | @@ -991,7 +973,6 @@ class _BedtimeChartState extends State<_BedtimeChart> { | ||
| 991 | _chartData( | 973 | _chartData( |
| 992 | axis, | 974 | axis, |
| 993 | constraints.maxWidth, | 975 | constraints.maxWidth, |
| 994 | - constraints.maxHeight, | ||
| 995 | ), | 976 | ), |
| 996 | ), | 977 | ), |
| 997 | ), | 978 | ), |
| @@ -1015,7 +996,6 @@ class _BedtimeChartState extends State<_BedtimeChart> { | @@ -1015,7 +996,6 @@ class _BedtimeChartState extends State<_BedtimeChart> { | ||
| 1015 | LineChartData _chartData( | 996 | LineChartData _chartData( |
| 1016 | _BedtimeAxis axis, | 997 | _BedtimeAxis axis, |
| 1017 | double chartWidth, | 998 | double chartWidth, |
| 1018 | - double chartHeight, | ||
| 1019 | ) { | 999 | ) { |
| 1020 | final points = <FlSpot>[ | 1000 | final points = <FlSpot>[ |
| 1021 | for (var i = 0; i < widget.days.length; i++) | 1001 | for (var i = 0; i < widget.days.length; i++) |
| @@ -1049,16 +1029,7 @@ class _BedtimeChartState extends State<_BedtimeChart> { | @@ -1049,16 +1029,7 @@ class _BedtimeChartState extends State<_BedtimeChart> { | ||
| 1049 | ); | 1029 | ); |
| 1050 | final anchorData = LineChartBarData( | 1030 | final anchorData = LineChartBarData( |
| 1051 | spots: [ | 1031 | spots: [ |
| 1052 | - for (final point in points) | ||
| 1053 | - FlSpot( | ||
| 1054 | - point.x, | ||
| 1055 | - _tooltipAnchorValue( | ||
| 1056 | - maxY: _tooltipChartMaxY, | ||
| 1057 | - chartHeight: chartHeight, | ||
| 1058 | - tooltipMargin: _tooltipMargin, | ||
| 1059 | - chartTopInset: 16, | ||
| 1060 | - ), | ||
| 1061 | - ), | 1032 | + for (final point in points) FlSpot(point.x, _tooltipChartMaxY), |
| 1062 | ], | 1033 | ], |
| 1063 | color: Colors.transparent, | 1034 | color: Colors.transparent, |
| 1064 | barWidth: 0, | 1035 | barWidth: 0, |
| @@ -1160,22 +1131,9 @@ const _weekBarGap = 28.0; | @@ -1160,22 +1131,9 @@ const _weekBarGap = 28.0; | ||
| 1160 | const _monthHorizontalInsets = 46.0; | 1131 | const _monthHorizontalInsets = 46.0; |
| 1161 | 1132 | ||
| 1162 | const _tooltipChartMaxY = 110.0; | 1133 | const _tooltipChartMaxY = 110.0; |
| 1163 | -const _tooltipMargin = 16.0; | 1134 | +const _bedtimeTooltipMargin = 0.0; |
| 1164 | const _barTooltipMargin = 6.0; | 1135 | const _barTooltipMargin = 6.0; |
| 1165 | -const _fixedTooltipBottom = 64.0; | ||
| 1166 | - | ||
| 1167 | -double _tooltipAnchorValue({ | ||
| 1168 | - required double maxY, | ||
| 1169 | - required double chartHeight, | ||
| 1170 | - required double tooltipMargin, | ||
| 1171 | - double chartTopInset = 0, | ||
| 1172 | -}) { | ||
| 1173 | - final plotHeight = chartHeight - _ChartPlotFrame.bottomTitleHeight; | ||
| 1174 | - if (plotHeight <= 0) return maxY; | ||
| 1175 | - final anchorY = (_fixedTooltipBottom - chartTopInset + tooltipMargin) | ||
| 1176 | - .clamp(0, plotHeight); | ||
| 1177 | - return (maxY * (1 - anchorY / plotHeight)).clamp(0, maxY); | ||
| 1178 | -} | 1136 | +const _bedtimeChartTopInset = 16.0; |
| 1179 | 1137 | ||
| 1180 | double _barGroupsSpace(double plotWidth, int count, bool compact) { | 1138 | double _barGroupsSpace(double plotWidth, int count, bool compact) { |
| 1181 | if (count <= 1) return 0; | 1139 | if (count <= 1) return 0; |
| @@ -1351,7 +1309,7 @@ LineTouchTooltipData _lineTooltipData({ | @@ -1351,7 +1309,7 @@ LineTouchTooltipData _lineTooltipData({ | ||
| 1351 | tooltipBorder: BorderSide.none, | 1309 | tooltipBorder: BorderSide.none, |
| 1352 | getTooltipColor: (_) => SleepWeekReportView.grid, | 1310 | getTooltipColor: (_) => SleepWeekReportView.grid, |
| 1353 | tooltipPadding: const EdgeInsets.fromLTRB(8, 7, 8, 6), | 1311 | tooltipPadding: const EdgeInsets.fromLTRB(8, 7, 8, 6), |
| 1354 | - tooltipMargin: 16, | 1312 | + tooltipMargin: _bedtimeTooltipMargin, |
| 1355 | fitInsideHorizontally: true, | 1313 | fitInsideHorizontally: true, |
| 1356 | fitInsideVertically: false, | 1314 | fitInsideVertically: false, |
| 1357 | showOnTopOfTheChartBoxArea: false, | 1315 | showOnTopOfTheChartBoxArea: false, |
| @@ -127,8 +127,8 @@ class FriendHealthData { | @@ -127,8 +127,8 @@ class FriendHealthData { | ||
| 127 | /// Latest HRV value used by the friend health card. | 127 | /// Latest HRV value used by the friend health card. |
| 128 | final double? latestHrv; | 128 | final double? latestHrv; |
| 129 | 129 | ||
| 130 | - /// Real-time stress data (may be an empty map `{}`) | ||
| 131 | - final Map<String, dynamic>? realtimeStress; | 130 | + /// Real-time stress samples returned by the API (may be an empty list `[]`). |
| 131 | + final List<FriendRealtimeStressItem>? realtimeStress; | ||
| 132 | 132 | ||
| 133 | /// Sleep quality score | 133 | /// Sleep quality score |
| 134 | final int? sleepEvaluate; | 134 | final int? sleepEvaluate; |
| @@ -145,7 +145,13 @@ class FriendHealthData { | @@ -145,7 +145,13 @@ class FriendHealthData { | ||
| 145 | comprehensiveStressScore: _parseInt(json['comprehensive_stress_score']), | 145 | comprehensiveStressScore: _parseInt(json['comprehensive_stress_score']), |
| 146 | comprehensiveStressState: _parseInt(json['comprehensive_stress_state']), | 146 | comprehensiveStressState: _parseInt(json['comprehensive_stress_state']), |
| 147 | latestHrv: _parseDouble(json['latest_hrv']), | 147 | latestHrv: _parseDouble(json['latest_hrv']), |
| 148 | - realtimeStress: json['realtime_stress'] as Map<String, dynamic>?, | 148 | + realtimeStress: (json['realtime_stress'] as List<dynamic>?) |
| 149 | + ?.map( | ||
| 150 | + (item) => FriendRealtimeStressItem.fromJson( | ||
| 151 | + item as Map<String, dynamic>, | ||
| 152 | + ), | ||
| 153 | + ) | ||
| 154 | + .toList(), | ||
| 149 | sleepEvaluate: _parseInt(json['sleep_evaluate']), | 155 | sleepEvaluate: _parseInt(json['sleep_evaluate']), |
| 150 | totalSteps: _parseInt(json['total_steps']), | 156 | totalSteps: _parseInt(json['total_steps']), |
| 151 | lastDataTime: _parseNum(json['last_data_time']), | 157 | lastDataTime: _parseNum(json['last_data_time']), |
| @@ -162,7 +168,10 @@ class FriendHealthData { | @@ -162,7 +168,10 @@ class FriendHealthData { | ||
| 162 | val['comprehensive_stress_state'] = comprehensiveStressState; | 168 | val['comprehensive_stress_state'] = comprehensiveStressState; |
| 163 | } | 169 | } |
| 164 | if (latestHrv != null) val['latest_hrv'] = latestHrv; | 170 | if (latestHrv != null) val['latest_hrv'] = latestHrv; |
| 165 | - if (realtimeStress != null) val['realtime_stress'] = realtimeStress; | 171 | + if (realtimeStress != null) { |
| 172 | + val['realtime_stress'] = | ||
| 173 | + realtimeStress!.map((item) => item.toJson()).toList(); | ||
| 174 | + } | ||
| 166 | if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate; | 175 | if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate; |
| 167 | if (totalSteps != null) val['total_steps'] = totalSteps; | 176 | if (totalSteps != null) val['total_steps'] = totalSteps; |
| 168 | if (lastDataTime != null) val['last_data_time'] = lastDataTime; | 177 | if (lastDataTime != null) val['last_data_time'] = lastDataTime; |
| @@ -170,6 +179,45 @@ class FriendHealthData { | @@ -170,6 +179,45 @@ class FriendHealthData { | ||
| 170 | } | 179 | } |
| 171 | } | 180 | } |
| 172 | 181 | ||
| 182 | +class FriendRealtimeStressItem { | ||
| 183 | + const FriendRealtimeStressItem({ | ||
| 184 | + this.time, | ||
| 185 | + this.value, | ||
| 186 | + this.state, | ||
| 187 | + this.isAsleep, | ||
| 188 | + }); | ||
| 189 | + | ||
| 190 | + /// Unix timestamp in seconds. | ||
| 191 | + final num? time; | ||
| 192 | + | ||
| 193 | + /// Real-time stress score. | ||
| 194 | + final double? value; | ||
| 195 | + | ||
| 196 | + /// Stress state indicator. | ||
| 197 | + final int? state; | ||
| 198 | + | ||
| 199 | + /// Whether the sample was recorded while asleep (0 = no, 1 = yes). | ||
| 200 | + final int? isAsleep; | ||
| 201 | + | ||
| 202 | + factory FriendRealtimeStressItem.fromJson(Map<String, dynamic> json) { | ||
| 203 | + return FriendRealtimeStressItem( | ||
| 204 | + time: _parseNum(json['time']), | ||
| 205 | + value: _parseDouble(json['value']), | ||
| 206 | + state: _parseInt(json['state']), | ||
| 207 | + isAsleep: _parseInt(json['is_asleep']), | ||
| 208 | + ); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + Map<String, dynamic> toJson() { | ||
| 212 | + final val = <String, dynamic>{}; | ||
| 213 | + if (time != null) val['time'] = time; | ||
| 214 | + if (value != null) val['value'] = value; | ||
| 215 | + if (state != null) val['state'] = state; | ||
| 216 | + if (isAsleep != null) val['is_asleep'] = isAsleep; | ||
| 217 | + return val; | ||
| 218 | + } | ||
| 219 | +} | ||
| 220 | + | ||
| 173 | int? _parseInt(dynamic value) { | 221 | int? _parseInt(dynamic value) { |
| 174 | if (value == null) return null; | 222 | if (value == null) return null; |
| 175 | if (value is int) return value; | 223 | if (value is int) return value; |
| @@ -190,20 +190,31 @@ class AsleepTimeTrendList { | @@ -190,20 +190,31 @@ class AsleepTimeTrendList { | ||
| 190 | } | 190 | } |
| 191 | 191 | ||
| 192 | class SleepTrendList { | 192 | class SleepTrendList { |
| 193 | - SleepTrendList({this.timeKey, this.totalTime, this.score}); | 193 | + SleepTrendList({ |
| 194 | + this.timeKey, | ||
| 195 | + this.totalTime, | ||
| 196 | + this.score, | ||
| 197 | + this.sleepEvaluate, | ||
| 198 | + }); | ||
| 194 | 199 | ||
| 195 | factory SleepTrendList.fromJson(Map<String, dynamic> json) => SleepTrendList( | 200 | factory SleepTrendList.fromJson(Map<String, dynamic> json) => SleepTrendList( |
| 196 | timeKey: json['time_key'], | 201 | timeKey: json['time_key'], |
| 197 | totalTime: _parseNum(json['total_time']), | 202 | totalTime: _parseNum(json['total_time']), |
| 198 | score: _parseNum(json['score']), | 203 | score: _parseNum(json['score']), |
| 204 | + sleepEvaluate: _parseNum(json['sleep_evaluate']), | ||
| 199 | ); | 205 | ); |
| 200 | 206 | ||
| 201 | Object? timeKey; | 207 | Object? timeKey; |
| 202 | num? totalTime; | 208 | num? totalTime; |
| 203 | num? score; | 209 | num? score; |
| 210 | + num? sleepEvaluate; | ||
| 204 | 211 | ||
| 205 | - Map<String, dynamic> toJson() => | ||
| 206 | - {'time_key': timeKey, 'total_time': totalTime, 'score': score}; | 212 | + Map<String, dynamic> toJson() => { |
| 213 | + 'time_key': timeKey, | ||
| 214 | + 'total_time': totalTime, | ||
| 215 | + 'score': score, | ||
| 216 | + 'sleep_evaluate': sleepEvaluate, | ||
| 217 | + }; | ||
| 207 | } | 218 | } |
| 208 | 219 | ||
| 209 | num? _parseNum(Object? value) { | 220 | num? _parseNum(Object? value) { |
-
Please register or login to post a comment