Showing
27 changed files
with
809 additions
and
391 deletions
| @@ -317,215 +317,4 @@ class _SleepRange { | @@ -317,215 +317,4 @@ class _SleepRange { | ||
| 317 | 317 | ||
| 318 | final DateTime start; | 318 | final DateTime start; |
| 319 | final DateTime end; | 319 | final DateTime end; |
| 320 | -} | ||
| 321 | - | ||
| 322 | -class MockActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 323 | - const MockActivityBurnReportDataSource(); | ||
| 324 | - | ||
| 325 | - @override | ||
| 326 | - Future<ActivityBurnReport> fetchDailyReport( | ||
| 327 | - DateTime date, { | ||
| 328 | - int? targetUserId, | ||
| 329 | - }) async { | ||
| 330 | - await Future<void>.delayed(const Duration(milliseconds: 150)); | ||
| 331 | - | ||
| 332 | - if (date.day == 13) { | ||
| 333 | - return ActivityBurnReport.empty(date); | ||
| 334 | - } | ||
| 335 | - | ||
| 336 | - final start = DateTime(date.year, date.month, date.day); | ||
| 337 | - final points = <ActivityBurnHeartRatePoint>[ | ||
| 338 | - for (var i = 0; i < _mockBpms.length; i++) | ||
| 339 | - ActivityBurnHeartRatePoint( | ||
| 340 | - time: start.add(Duration(minutes: i * 12)), | ||
| 341 | - bpm: _mockBpms[i], | ||
| 342 | - ), | ||
| 343 | - ]; | ||
| 344 | - | ||
| 345 | - return ActivityBurnReport( | ||
| 346 | - date: date, | ||
| 347 | - activeEnergy: const ActivityBurnMetric(value: 188, goal: 500), | ||
| 348 | - exerciseMinutes: const ActivityBurnMetric(value: 15, goal: 30), | ||
| 349 | - standHours: const ActivityBurnMetric(value: 7, goal: 12), | ||
| 350 | - heartRate: ActivityBurnHeartRateSummary( | ||
| 351 | - startTime: start, | ||
| 352 | - endTime: start.add(const Duration(hours: 18)), | ||
| 353 | - sleepStartTime: start, | ||
| 354 | - sleepEndTime: start.add(const Duration(hours: 5)), | ||
| 355 | - userAge: null, | ||
| 356 | - points: points, | ||
| 357 | - ), | ||
| 358 | - ); | ||
| 359 | - } | ||
| 360 | - | ||
| 361 | - @override | ||
| 362 | - Future<WeeklyActivityBurnReport> fetchWeeklyReport( | ||
| 363 | - DateTime weekStart, { | ||
| 364 | - int? targetUserId, | ||
| 365 | - }) async { | ||
| 366 | - await Future<void>.delayed(const Duration(milliseconds: 150)); | ||
| 367 | - | ||
| 368 | - final normalizedStart = | ||
| 369 | - DateTime(weekStart.year, weekStart.month, weekStart.day); | ||
| 370 | - if (normalizedStart.day == 4) { | ||
| 371 | - return WeeklyActivityBurnReport.empty(normalizedStart); | ||
| 372 | - } | ||
| 373 | - | ||
| 374 | - final energy = [176, 217, 0, 0, 0, 0, 11]; | ||
| 375 | - final exercise = [12, 17, 0, 0, 0, 0, 0]; | ||
| 376 | - final stand = [7, 5, 0, 0, 0, 0, 0]; | ||
| 377 | - final days = <ActivityBurnReport>[]; | ||
| 378 | - | ||
| 379 | - for (var i = 0; i < 7; i++) { | ||
| 380 | - final date = normalizedStart.add(Duration(days: i)); | ||
| 381 | - if (energy[i] == 0 && exercise[i] == 0 && stand[i] == 0) { | ||
| 382 | - days.add(ActivityBurnReport.empty(date)); | ||
| 383 | - continue; | ||
| 384 | - } | ||
| 385 | - days.add( | ||
| 386 | - ActivityBurnReport( | ||
| 387 | - date: date, | ||
| 388 | - activeEnergy: ActivityBurnMetric(value: energy[i], goal: 200), | ||
| 389 | - exerciseMinutes: ActivityBurnMetric(value: exercise[i], goal: 15), | ||
| 390 | - standHours: ActivityBurnMetric(value: stand[i], goal: 4), | ||
| 391 | - ), | ||
| 392 | - ); | ||
| 393 | - } | ||
| 394 | - | ||
| 395 | - return WeeklyActivityBurnReport( | ||
| 396 | - weekStart: normalizedStart, | ||
| 397 | - weekEnd: normalizedStart.add(const Duration(days: 6)), | ||
| 398 | - days: days, | ||
| 399 | - ); | ||
| 400 | - } | ||
| 401 | - | ||
| 402 | - @override | ||
| 403 | - Future<MonthlyActivityBurnReport> fetchMonthlyReport( | ||
| 404 | - DateTime monthStart, { | ||
| 405 | - int? targetUserId, | ||
| 406 | - }) async { | ||
| 407 | - await Future<void>.delayed(const Duration(milliseconds: 150)); | ||
| 408 | - | ||
| 409 | - final start = DateTime(monthStart.year, monthStart.month); | ||
| 410 | - final end = DateTime(monthStart.year, monthStart.month + 1, 0); | ||
| 411 | - final energy = [ | ||
| 412 | - 245, | ||
| 413 | - 319, | ||
| 414 | - 207, | ||
| 415 | - 276, | ||
| 416 | - 303, | ||
| 417 | - 84, | ||
| 418 | - 358, | ||
| 419 | - 144, | ||
| 420 | - 285, | ||
| 421 | - 198, | ||
| 422 | - 342, | ||
| 423 | - 217, | ||
| 424 | - 108, | ||
| 425 | - ]; | ||
| 426 | - final exercise = [18, 20, 15, 17, 16, 6, 24, 8, 19, 12, 20, 17, 9]; | ||
| 427 | - final stand = [8, 9, 7, 8, 8, 3, 10, 5, 9, 6, 8, 5, 4]; | ||
| 428 | - final days = <ActivityBurnReport>[]; | ||
| 429 | - | ||
| 430 | - for (var i = 0; i < end.day; i++) { | ||
| 431 | - final date = start.add(Duration(days: i)); | ||
| 432 | - if (i >= energy.length) { | ||
| 433 | - days.add(ActivityBurnReport.empty(date)); | ||
| 434 | - continue; | ||
| 435 | - } | ||
| 436 | - days.add( | ||
| 437 | - ActivityBurnReport( | ||
| 438 | - date: date, | ||
| 439 | - activeEnergy: ActivityBurnMetric(value: energy[i], goal: 200), | ||
| 440 | - exerciseMinutes: ActivityBurnMetric(value: exercise[i], goal: 15), | ||
| 441 | - standHours: ActivityBurnMetric(value: stand[i], goal: 4), | ||
| 442 | - ), | ||
| 443 | - ); | ||
| 444 | - } | ||
| 445 | - | ||
| 446 | - return MonthlyActivityBurnReport( | ||
| 447 | - monthStart: start, | ||
| 448 | - monthEnd: end, | ||
| 449 | - days: days, | ||
| 450 | - ); | ||
| 451 | - } | ||
| 452 | -} | ||
| 453 | - | ||
| 454 | -const _mockBpms = [ | ||
| 455 | - 53.0, | ||
| 456 | - 56.0, | ||
| 457 | - 51.0, | ||
| 458 | - 58.0, | ||
| 459 | - 55.0, | ||
| 460 | - 60.0, | ||
| 461 | - 52.0, | ||
| 462 | - 59.0, | ||
| 463 | - 54.0, | ||
| 464 | - 61.0, | ||
| 465 | - 55.0, | ||
| 466 | - 63.0, | ||
| 467 | - 58.0, | ||
| 468 | - 66.0, | ||
| 469 | - 62.0, | ||
| 470 | - 70.0, | ||
| 471 | - 64.0, | ||
| 472 | - 74.0, | ||
| 473 | - 92.0, | ||
| 474 | - 108.0, | ||
| 475 | - 96.0, | ||
| 476 | - 124.0, | ||
| 477 | - 172.0, | ||
| 478 | - 136.0, | ||
| 479 | - 188.0, | ||
| 480 | - 151.0, | ||
| 481 | - 90.0, | ||
| 482 | - 82.0, | ||
| 483 | - 91.0, | ||
| 484 | - 88.0, | ||
| 485 | - 94.0, | ||
| 486 | - 90.0, | ||
| 487 | - 91.0, | ||
| 488 | - 89.0, | ||
| 489 | - 92.0, | ||
| 490 | - 87.0, | ||
| 491 | - 91.0, | ||
| 492 | - 114.0, | ||
| 493 | - 139.0, | ||
| 494 | - 120.0, | ||
| 495 | - 112.0, | ||
| 496 | - 95.0, | ||
| 497 | - 85.0, | ||
| 498 | - 78.0, | ||
| 499 | - 126.0, | ||
| 500 | - 93.0, | ||
| 501 | - 88.0, | ||
| 502 | - 104.0, | ||
| 503 | - 112.0, | ||
| 504 | - 105.0, | ||
| 505 | - 98.0, | ||
| 506 | - 87.0, | ||
| 507 | - 74.0, | ||
| 508 | - 61.0, | ||
| 509 | - 55.0, | ||
| 510 | - 66.0, | ||
| 511 | - 48.0, | ||
| 512 | - 62.0, | ||
| 513 | - 51.0, | ||
| 514 | - 43.0, | ||
| 515 | - 59.0, | ||
| 516 | - 47.0, | ||
| 517 | - 54.0, | ||
| 518 | - 40.0, | ||
| 519 | - 58.0, | ||
| 520 | - 46.0, | ||
| 521 | - 50.0, | ||
| 522 | - 44.0, | ||
| 523 | - 61.0, | ||
| 524 | - 57.0, | ||
| 525 | - 42.0, | ||
| 526 | - 55.0, | ||
| 527 | - 49.0, | ||
| 528 | - 60.0, | ||
| 529 | - 52.0, | ||
| 530 | - 64.0, | ||
| 531 | -]; | 320 | +} |
| @@ -7,9 +7,14 @@ class ActivityBurnMetric { | @@ -7,9 +7,14 @@ class ActivityBurnMetric { | ||
| 7 | final int value; | 7 | final int value; |
| 8 | final int goal; | 8 | final int goal; |
| 9 | 9 | ||
| 10 | + double get rawProgress { | ||
| 11 | + if (goal <= 0) return 0; | ||
| 12 | + return value / goal; | ||
| 13 | + } | ||
| 14 | + | ||
| 10 | double get progress { | 15 | double get progress { |
| 11 | if (goal <= 0) return 0; | 16 | if (goal <= 0) return 0; |
| 12 | - return (value / goal).clamp(0, 1).toDouble(); | 17 | + return rawProgress.clamp(0, 1).toDouble(); |
| 13 | } | 18 | } |
| 14 | } | 19 | } |
| 15 | 20 |
| @@ -21,11 +21,13 @@ class ActivityBurnReportView extends StatelessWidget { | @@ -21,11 +21,13 @@ class ActivityBurnReportView extends StatelessWidget { | ||
| 21 | required this.logic, | 21 | required this.logic, |
| 22 | required this.isVip, | 22 | required this.isVip, |
| 23 | required this.onSubscribe, | 23 | required this.onSubscribe, |
| 24 | + this.loadingIndicator = const ReportLoadingIndicator(), | ||
| 24 | }); | 25 | }); |
| 25 | 26 | ||
| 26 | final ActivityBurnReportLogic logic; | 27 | final ActivityBurnReportLogic logic; |
| 27 | final bool isVip; | 28 | final bool isVip; |
| 28 | final ValueChanged<String> onSubscribe; | 29 | final ValueChanged<String> onSubscribe; |
| 30 | + final Widget loadingIndicator; | ||
| 29 | 31 | ||
| 30 | @override | 32 | @override |
| 31 | Widget build(BuildContext context) { | 33 | Widget build(BuildContext context) { |
| @@ -59,7 +61,7 @@ class ActivityBurnReportView extends StatelessWidget { | @@ -59,7 +61,7 @@ class ActivityBurnReportView extends StatelessWidget { | ||
| 59 | child: Obx( | 61 | child: Obx( |
| 60 | () { | 62 | () { |
| 61 | if (logic.isLoading.value) { | 63 | if (logic.isLoading.value) { |
| 62 | - return const ReportLoadingIndicator(); | 64 | + return loadingIndicator; |
| 63 | } | 65 | } |
| 64 | return CustomScrollView( | 66 | return CustomScrollView( |
| 65 | physics: const ClampingScrollPhysics(), | 67 | physics: const ClampingScrollPhysics(), |
| @@ -93,9 +93,9 @@ class _RingProgress { | @@ -93,9 +93,9 @@ class _RingProgress { | ||
| 93 | stand = 0; | 93 | stand = 0; |
| 94 | 94 | ||
| 95 | factory _RingProgress.fromReport(ActivityBurnReport? report) => _RingProgress( | 95 | factory _RingProgress.fromReport(ActivityBurnReport? report) => _RingProgress( |
| 96 | - active: report?.activeEnergy?.progress ?? 0, | ||
| 97 | - exercise: report?.exerciseMinutes?.progress ?? 0, | ||
| 98 | - stand: report?.standHours?.progress ?? 0, | 96 | + active: report?.activeEnergy?.rawProgress ?? 0, |
| 97 | + exercise: report?.exerciseMinutes?.rawProgress ?? 0, | ||
| 98 | + stand: report?.standHours?.rawProgress ?? 0, | ||
| 99 | ); | 99 | ); |
| 100 | 100 | ||
| 101 | final double active; | 101 | final double active; |
| @@ -253,7 +253,16 @@ class _ActivityBurnRingPainter extends CustomPainter { | @@ -253,7 +253,16 @@ class _ActivityBurnRingPainter extends CustomPainter { | ||
| 253 | required double progress, | 253 | required double progress, |
| 254 | required double startAngle, | 254 | required double startAngle, |
| 255 | }) { | 255 | }) { |
| 256 | + if (progress <= 0) return; | ||
| 257 | + | ||
| 256 | final rect = Rect.fromCircle(center: center, radius: radius); | 258 | final rect = Rect.fromCircle(center: center, radius: radius); |
| 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; | ||
| 257 | 266 | ||
| 258 | final progressPaint = Paint() | 267 | final progressPaint = Paint() |
| 259 | ..color = progressColor | 268 | ..color = progressColor |
| @@ -261,16 +270,86 @@ class _ActivityBurnRingPainter extends CustomPainter { | @@ -261,16 +270,86 @@ class _ActivityBurnRingPainter extends CustomPainter { | ||
| 261 | ..strokeWidth = width | 270 | ..strokeWidth = width |
| 262 | ..strokeCap = StrokeCap.round; | 271 | ..strokeCap = StrokeCap.round; |
| 263 | 272 | ||
| 264 | - if (progress <= 0) return; | 273 | + final fullLapPaint = Paint() |
| 274 | + ..color = progressColor | ||
| 275 | + ..style = PaintingStyle.stroke | ||
| 276 | + ..strokeWidth = width | ||
| 277 | + ..strokeCap = StrokeCap.butt; | ||
| 278 | + | ||
| 279 | + if (baseProgress >= 1) { | ||
| 280 | + canvas.drawArc( | ||
| 281 | + rect, | ||
| 282 | + startAngle, | ||
| 283 | + math.pi * 2, | ||
| 284 | + false, | ||
| 285 | + fullLapPaint, | ||
| 286 | + ); | ||
| 287 | + } else { | ||
| 288 | + final baseEndAngle = startAngle + math.pi * 2 * baseProgress; | ||
| 289 | + _drawRingHeadShadow( | ||
| 290 | + canvas, | ||
| 291 | + center: center, | ||
| 292 | + radius: radius, | ||
| 293 | + width: width, | ||
| 294 | + angle: baseEndAngle, | ||
| 295 | + ); | ||
| 296 | + canvas.drawArc( | ||
| 297 | + rect, | ||
| 298 | + startAngle, | ||
| 299 | + math.pi * 2 * baseProgress, | ||
| 300 | + false, | ||
| 301 | + progressPaint, | ||
| 302 | + ); | ||
| 303 | + } | ||
| 304 | + | ||
| 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, | ||
| 314 | + ); | ||
| 265 | canvas.drawArc( | 315 | canvas.drawArc( |
| 266 | rect, | 316 | rect, |
| 267 | startAngle, | 317 | startAngle, |
| 268 | - math.pi * 2 * progress, | 318 | + math.pi * 2 * visibleOverflowLap, |
| 269 | false, | 319 | false, |
| 270 | progressPaint, | 320 | progressPaint, |
| 271 | ); | 321 | ); |
| 272 | } | 322 | } |
| 273 | 323 | ||
| 324 | + void _drawRingHeadShadow( | ||
| 325 | + Canvas canvas, { | ||
| 326 | + required Offset center, | ||
| 327 | + required double radius, | ||
| 328 | + required double width, | ||
| 329 | + required double angle, | ||
| 330 | + }) { | ||
| 331 | + final headCenter = Offset( | ||
| 332 | + center.dx + math.cos(angle) * radius, | ||
| 333 | + center.dy + math.sin(angle) * radius, | ||
| 334 | + ); | ||
| 335 | + final tangent = Offset(-math.sin(angle), math.cos(angle)); | ||
| 336 | + final ringClip = Path() | ||
| 337 | + ..fillType = PathFillType.evenOdd | ||
| 338 | + ..addOval(Rect.fromCircle(center: center, radius: radius + width / 2)) | ||
| 339 | + ..addOval(Rect.fromCircle(center: center, radius: radius - width / 2)); | ||
| 340 | + | ||
| 341 | + canvas.save(); | ||
| 342 | + canvas.clipPath(ringClip); | ||
| 343 | + canvas.drawCircle( | ||
| 344 | + headCenter + tangent * 2, | ||
| 345 | + width / 2, | ||
| 346 | + Paint() | ||
| 347 | + ..color = Colors.black.withValues(alpha: 0.36) | ||
| 348 | + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2), | ||
| 349 | + ); | ||
| 350 | + canvas.restore(); | ||
| 351 | + } | ||
| 352 | + | ||
| 274 | @override | 353 | @override |
| 275 | bool shouldRepaint(covariant _ActivityBurnRingPainter oldDelegate) { | 354 | bool shouldRepaint(covariant _ActivityBurnRingPainter oldDelegate) { |
| 276 | return oldDelegate.progress != progress || oldDelegate.hasData != hasData; | 355 | return oldDelegate.progress != progress || oldDelegate.hasData != hasData; |
| @@ -558,9 +558,11 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { | @@ -558,9 +558,11 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { | ||
| 558 | if (percent == null || !widget.report.hasData) { | 558 | if (percent == null || !widget.report.hasData) { |
| 559 | return context.l10n.activityComparedUnavailable; | 559 | return context.l10n.activityComparedUnavailable; |
| 560 | } | 560 | } |
| 561 | - if (percent == 0) return '与上周持平'; | ||
| 562 | - final direction = percent > 0 ? '多' : '少'; | ||
| 563 | - return '比上周$direction${percent.abs()}%'; | 561 | + if (percent == 0) return context.l10n.activitySameAsLastWeek; |
| 562 | + final absPercent = percent.abs(); | ||
| 563 | + return percent > 0 | ||
| 564 | + ? context.l10n.activityMoreThanLastWeek(absPercent) | ||
| 565 | + : context.l10n.activityLessThanLastWeek(absPercent); | ||
| 564 | } | 566 | } |
| 565 | 567 | ||
| 566 | double _barToY(int value, double maxY) { | 568 | double _barToY(int value, double maxY) { |
| @@ -43,13 +43,21 @@ class FriendsController extends GetxController { | @@ -43,13 +43,21 @@ class FriendsController extends GetxController { | ||
| 43 | unawaited(refreshData()); | 43 | unawaited(refreshData()); |
| 44 | } | 44 | } |
| 45 | 45 | ||
| 46 | - void markPageVisible() { | 46 | + void setPageVisible(bool visible) { |
| 47 | + if (visible) { | ||
| 48 | + _markPageVisible(); | ||
| 49 | + } else { | ||
| 50 | + _markPageHidden(); | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + void _markPageVisible() { | ||
| 47 | if (_isPageVisible) return; | 55 | if (_isPageVisible) return; |
| 48 | _isPageVisible = true; | 56 | _isPageVisible = true; |
| 49 | ta.track('enter_doublefeel_friend_page'); | 57 | ta.track('enter_doublefeel_friend_page'); |
| 50 | } | 58 | } |
| 51 | 59 | ||
| 52 | - void markPageHidden() { | 60 | + void _markPageHidden() { |
| 53 | _isPageVisible = false; | 61 | _isPageVisible = false; |
| 54 | } | 62 | } |
| 55 | 63 |
| @@ -110,7 +110,8 @@ class FriendsRepositoryImpl implements FriendsRepository { | @@ -110,7 +110,8 @@ class FriendsRepositoryImpl implements FriendsRepository { | ||
| 110 | steps: healthData?.totalSteps == null | 110 | steps: healthData?.totalSteps == null |
| 111 | ? null | 111 | ? null |
| 112 | : l10n.friendsStepCount(healthData!.totalSteps!), | 112 | : l10n.friendsStepCount(healthData!.totalSteps!), |
| 113 | - stressState: FriendStressState.fromValue(healthData?.hrvState), | 113 | + stressState: |
| 114 | + FriendStressState.fromValue(healthData?.comprehensiveStressState), | ||
| 114 | isOnWatchFace: friend.isShowInDial, | 115 | isOnWatchFace: friend.isShowInDial, |
| 115 | ); | 116 | ); |
| 116 | } | 117 | } |
| @@ -125,7 +126,8 @@ class FriendsRepositoryImpl implements FriendsRepository { | @@ -125,7 +126,8 @@ class FriendsRepositoryImpl implements FriendsRepository { | ||
| 125 | steps: healthData.totalSteps == null | 126 | steps: healthData.totalSteps == null |
| 126 | ? null | 127 | ? null |
| 127 | : l10n.friendsStepCount(healthData.totalSteps!), | 128 | : l10n.friendsStepCount(healthData.totalSteps!), |
| 128 | - stressState: FriendStressState.fromValue(healthData.hrvState), | 129 | + stressState: |
| 130 | + FriendStressState.fromValue(healthData.comprehensiveStressState), | ||
| 129 | ); | 131 | ); |
| 130 | } | 132 | } |
| 131 | 133 |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | import 'package:get/get.dart'; | 2 | import 'package:get/get.dart'; |
| 3 | +import 'package:lottie/lottie.dart'; | ||
| 3 | 4 | ||
| 4 | import '../../../../core/constants/intent_keys.dart'; | 5 | import '../../../../core/constants/intent_keys.dart'; |
| 5 | import '../../../../data/local/user_preferences_storage.dart'; | 6 | import '../../../../data/local/user_preferences_storage.dart'; |
| @@ -154,6 +155,23 @@ class _HealthTrendContentState extends State<HealthTrendContent> | @@ -154,6 +155,23 @@ class _HealthTrendContentState extends State<HealthTrendContent> | ||
| 154 | } | 155 | } |
| 155 | } | 156 | } |
| 156 | 157 | ||
| 158 | +class _HealthTrendLoadingIndicator extends StatelessWidget { | ||
| 159 | + const _HealthTrendLoadingIndicator(); | ||
| 160 | + | ||
| 161 | + @override | ||
| 162 | + Widget build(BuildContext context) { | ||
| 163 | + return Center( | ||
| 164 | + child: Lottie.asset( | ||
| 165 | + 'assets/lottie/loading.json', | ||
| 166 | + width: 130, | ||
| 167 | + height: 90, | ||
| 168 | + repeat: true, | ||
| 169 | + animate: true, | ||
| 170 | + ), | ||
| 171 | + ); | ||
| 172 | + } | ||
| 173 | +} | ||
| 174 | + | ||
| 157 | class _HrvTrendSection extends StatefulWidget { | 175 | class _HrvTrendSection extends StatefulWidget { |
| 158 | const _HrvTrendSection({ | 176 | const _HrvTrendSection({ |
| 159 | required this.query, | 177 | required this.query, |
| @@ -247,6 +265,7 @@ class _HrvTrendSectionState extends State<_HrvTrendSection> { | @@ -247,6 +265,7 @@ class _HrvTrendSectionState extends State<_HrvTrendSection> { | ||
| 247 | logic: _logic, | 265 | logic: _logic, |
| 248 | isVip: widget.isVip, | 266 | isVip: widget.isVip, |
| 249 | onSubscribe: widget.onSubscribe, | 267 | onSubscribe: widget.onSubscribe, |
| 268 | + loadingIndicator: const _HealthTrendLoadingIndicator(), | ||
| 250 | ); | 269 | ); |
| 251 | } | 270 | } |
| 252 | 271 | ||
| @@ -346,6 +365,7 @@ class _ActivityBurnTrendSectionState extends State<_ActivityBurnTrendSection> { | @@ -346,6 +365,7 @@ class _ActivityBurnTrendSectionState extends State<_ActivityBurnTrendSection> { | ||
| 346 | logic: _logic, | 365 | logic: _logic, |
| 347 | isVip: widget.isVip, | 366 | isVip: widget.isVip, |
| 348 | onSubscribe: widget.onSubscribe, | 367 | onSubscribe: widget.onSubscribe, |
| 368 | + loadingIndicator: const _HealthTrendLoadingIndicator(), | ||
| 349 | ); | 369 | ); |
| 350 | } | 370 | } |
| 351 | 371 | ||
| @@ -444,6 +464,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> { | @@ -444,6 +464,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> { | ||
| 444 | logic: _logic, | 464 | logic: _logic, |
| 445 | isVip: widget.isVip, | 465 | isVip: widget.isVip, |
| 446 | onSubscribe: widget.onSubscribe, | 466 | onSubscribe: widget.onSubscribe, |
| 467 | + loadingIndicator: const _HealthTrendLoadingIndicator(), | ||
| 447 | ); | 468 | ); |
| 448 | } | 469 | } |
| 449 | 470 |
| @@ -21,6 +21,11 @@ import '../../report_common/models/report_period.dart'; | @@ -21,6 +21,11 @@ import '../../report_common/models/report_period.dart'; | ||
| 21 | import 'today_controller.dart'; | 21 | import 'today_controller.dart'; |
| 22 | import 'trend/trend_controller.dart'; | 22 | import 'trend/trend_controller.dart'; |
| 23 | 23 | ||
| 24 | +enum TrendEntrySource { | ||
| 25 | + bottomTab, | ||
| 26 | + jump, | ||
| 27 | +} | ||
| 28 | + | ||
| 24 | class HomeController extends GetxController { | 29 | class HomeController extends GetxController { |
| 25 | static const trendTabIndex = 1; | 30 | static const trendTabIndex = 1; |
| 26 | static const friendsTabIndex = 2; | 31 | static const friendsTabIndex = 2; |
| @@ -136,23 +141,16 @@ class HomeController extends GetxController { | @@ -136,23 +141,16 @@ class HomeController extends GetxController { | ||
| 136 | int index, { | 141 | int index, { |
| 137 | TrendEntrySource trendEntrySource = TrendEntrySource.bottomTab, | 142 | TrendEntrySource trendEntrySource = TrendEntrySource.bottomTab, |
| 138 | }) { | 143 | }) { |
| 139 | - final previousIndex = selectedIndex.value; | ||
| 140 | selectedIndex.value = index; | 144 | selectedIndex.value = index; |
| 141 | - if (index == trendTabIndex) { | ||
| 142 | - final trendController = Get.find<TrendController>(); | ||
| 143 | - if (previousIndex != trendTabIndex) { | ||
| 144 | - trendController.markPageVisible(source: trendEntrySource); | ||
| 145 | - } else { | ||
| 146 | - trendController.updateEntrySource(trendEntrySource); | ||
| 147 | - } | ||
| 148 | - } else if (previousIndex == trendTabIndex && index != trendTabIndex) { | ||
| 149 | - Get.find<TrendController>().markPageHidden(); | ||
| 150 | - } | ||
| 151 | - if (index == friendsTabIndex && previousIndex != friendsTabIndex) { | ||
| 152 | - Get.find<FriendsController>().markPageVisible(); | ||
| 153 | - } else if (previousIndex == friendsTabIndex && index != friendsTabIndex) { | ||
| 154 | - Get.find<FriendsController>().markPageHidden(); | ||
| 155 | - } | 145 | + final trendController = Get.find<TrendController>(); |
| 146 | + final friendsController = Get.find<FriendsController>(); | ||
| 147 | + | ||
| 148 | + trendController.setPageVisible( | ||
| 149 | + index == trendTabIndex, | ||
| 150 | + resetQueryOnShow: trendEntrySource == TrendEntrySource.bottomTab, | ||
| 151 | + ); | ||
| 152 | + friendsController.setPageVisible(index == friendsTabIndex); | ||
| 153 | + | ||
| 156 | switch (index) { | 154 | switch (index) { |
| 157 | case 0: | 155 | case 0: |
| 158 | Get.find<TodayController>().refreshTab(); | 156 | Get.find<TodayController>().refreshTab(); |
| @@ -8,6 +8,7 @@ import '../../../../../core/result/app_result.dart'; | @@ -8,6 +8,7 @@ import '../../../../../core/result/app_result.dart'; | ||
| 8 | import '../../../../../data/models/friend/friend_models.dart'; | 8 | import '../../../../../data/models/friend/friend_models.dart'; |
| 9 | import '../../../health_trend/controllers/health_trend_analytics.dart'; | 9 | import '../../../health_trend/controllers/health_trend_analytics.dart'; |
| 10 | import '../../../health_trend/controllers/health_trend_control.dart'; | 10 | import '../../../health_trend/controllers/health_trend_control.dart'; |
| 11 | +import '../../../report_common/config/report_date_range_config.dart'; | ||
| 11 | import '../../../report_common/models/health_report_query.dart'; | 12 | import '../../../report_common/models/health_report_query.dart'; |
| 12 | import '../../../report_common/models/report_period.dart'; | 13 | import '../../../report_common/models/report_period.dart'; |
| 13 | import '../../widgets/trend/trend_friend_select_bottom_sheet.dart'; | 14 | import '../../widgets/trend/trend_friend_select_bottom_sheet.dart'; |
| @@ -20,11 +21,6 @@ enum TrendType { | @@ -20,11 +21,6 @@ enum TrendType { | ||
| 20 | int get tabIndex => index; | 21 | int get tabIndex => index; |
| 21 | } | 22 | } |
| 22 | 23 | ||
| 23 | -enum TrendEntrySource { | ||
| 24 | - bottomTab, | ||
| 25 | - jump, | ||
| 26 | -} | ||
| 27 | - | ||
| 28 | /// 趋势页顶层 Controller,仅负责: | 24 | /// 趋势页顶层 Controller,仅负责: |
| 29 | /// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex) | 25 | /// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex) |
| 30 | class TrendController extends GetxController with HealthTrendControl { | 26 | class TrendController extends GetxController with HealthTrendControl { |
| @@ -33,7 +29,6 @@ class TrendController extends GetxController with HealthTrendControl { | @@ -33,7 +29,6 @@ class TrendController extends GetxController with HealthTrendControl { | ||
| 33 | final FriendApi _friendApi; | 29 | final FriendApi _friendApi; |
| 34 | bool _isPageVisible = false; | 30 | bool _isPageVisible = false; |
| 35 | final refreshToken = 0.obs; | 31 | final refreshToken = 0.obs; |
| 36 | - final entrySource = TrendEntrySource.bottomTab.obs; | ||
| 37 | 32 | ||
| 38 | // 当前查看的用户。null 表示查看自己;非 null 表示查看指定用户。 | 33 | // 当前查看的用户。null 表示查看自己;非 null 表示查看指定用户。 |
| 39 | final targetUserId = RxnInt(); | 34 | final targetUserId = RxnInt(); |
| @@ -147,30 +142,34 @@ class TrendController extends GetxController with HealthTrendControl { | @@ -147,30 +142,34 @@ class TrendController extends GetxController with HealthTrendControl { | ||
| 147 | ); | 142 | ); |
| 148 | } | 143 | } |
| 149 | 144 | ||
| 150 | - void updateEntrySource(TrendEntrySource source) { | ||
| 151 | - entrySource.value = source; | ||
| 152 | - } | ||
| 153 | - | ||
| 154 | - void markPageVisible({ | ||
| 155 | - TrendEntrySource source = TrendEntrySource.bottomTab, | 145 | + void setPageVisible( |
| 146 | + bool visible, { | ||
| 147 | + bool resetQueryOnShow = false, | ||
| 156 | }) { | 148 | }) { |
| 157 | - updateEntrySource(source); | ||
| 158 | - if (source == TrendEntrySource.bottomTab) { | ||
| 159 | - _resetQueryForBottomTabEntry(); | 149 | + if (!visible) { |
| 150 | + _markPageHidden(); | ||
| 151 | + return; | ||
| 152 | + } | ||
| 153 | + if (resetQueryOnShow) { | ||
| 154 | + _resetToDefaultQuery(); | ||
| 160 | } | 155 | } |
| 156 | + _markPageVisible(); | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + void _markPageVisible() { | ||
| 161 | if (_isPageVisible) return; | 160 | if (_isPageVisible) return; |
| 162 | _isPageVisible = true; | 161 | _isPageVisible = true; |
| 163 | refreshToken.value++; | 162 | refreshToken.value++; |
| 164 | _trackEnterPage(); | 163 | _trackEnterPage(); |
| 165 | } | 164 | } |
| 166 | 165 | ||
| 167 | - void markPageHidden() { | 166 | + void _markPageHidden() { |
| 168 | _isPageVisible = false; | 167 | _isPageVisible = false; |
| 169 | } | 168 | } |
| 170 | 169 | ||
| 171 | - void _resetQueryForBottomTabEntry() { | 170 | + void _resetToDefaultQuery() { |
| 172 | changeType(TrendType.hrv.tabIndex); | 171 | changeType(TrendType.hrv.tabIndex); |
| 173 | - changeQuery(ReportPeriod.week, DateTime.now()); | 172 | + changeQuery(ReportPeriod.week, ReportDateRangeConfig.lastWeekStart()); |
| 174 | } | 173 | } |
| 175 | 174 | ||
| 176 | void _trackEnterPage() { | 175 | void _trackEnterPage() { |
| 1 | import 'package:cached_network_image/cached_network_image.dart'; | 1 | import 'package:cached_network_image/cached_network_image.dart'; |
| 2 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 2 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 3 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 3 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 4 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| 5 | 6 | ||
| @@ -55,11 +56,11 @@ class _HomeTrendHeader extends GetView<TrendController> { | @@ -55,11 +56,11 @@ class _HomeTrendHeader extends GetView<TrendController> { | ||
| 55 | child: Stack( | 56 | child: Stack( |
| 56 | alignment: Alignment.center, | 57 | alignment: Alignment.center, |
| 57 | children: [ | 58 | children: [ |
| 58 | - const Align( | 59 | + Align( |
| 59 | alignment: Alignment.centerLeft, | 60 | alignment: Alignment.centerLeft, |
| 60 | child: Text( | 61 | child: Text( |
| 61 | - '趋势', | ||
| 62 | - style: TextStyle( | 62 | + l10n.tabTrend, |
| 63 | + style: const TextStyle( | ||
| 63 | color: Color(0xFF0F0F11), | 64 | color: Color(0xFF0F0F11), |
| 64 | fontSize: 24, | 65 | fontSize: 24, |
| 65 | fontWeight: FontWeight.w600, | 66 | fontWeight: FontWeight.w600, |
| 1 | import 'package:fl_chart/fl_chart.dart'; | 1 | import 'package:fl_chart/fl_chart.dart'; |
| 2 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 2 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 3 | import 'package:get/get.dart'; | 4 | import 'package:get/get.dart'; |
| 4 | 5 | ||
| @@ -62,15 +63,16 @@ class _ActivitySummaryCard extends GetView<ActivityController> { | @@ -62,15 +63,16 @@ class _ActivitySummaryCard extends GetView<ActivityController> { | ||
| 62 | borderRadius: BorderRadius.circular(16), | 63 | borderRadius: BorderRadius.circular(16), |
| 63 | ), | 64 | ), |
| 64 | child: Obx(() { | 65 | child: Obx(() { |
| 65 | - final periodLabel = controller.currentPeriod.value.label; | 66 | + final periodLabel = |
| 67 | + _periodLabel(context, controller.currentPeriod.value); | ||
| 66 | return Column( | 68 | return Column( |
| 67 | crossAxisAlignment: CrossAxisAlignment.start, | 69 | crossAxisAlignment: CrossAxisAlignment.start, |
| 68 | children: [ | 70 | children: [ |
| 69 | Row( | 71 | Row( |
| 70 | children: [ | 72 | children: [ |
| 71 | - const Text( | ||
| 72 | - '活动消耗趋势', | ||
| 73 | - style: TextStyle( | 73 | + Text( |
| 74 | + context.l10n.activityTrendTitle, | ||
| 75 | + style: const TextStyle( | ||
| 74 | color: _h1, | 76 | color: _h1, |
| 75 | fontSize: 16, | 77 | fontSize: 16, |
| 76 | fontWeight: FontWeight.w600, | 78 | fontWeight: FontWeight.w600, |
| @@ -84,14 +86,14 @@ class _ActivitySummaryCard extends GetView<ActivityController> { | @@ -84,14 +86,14 @@ class _ActivitySummaryCard extends GetView<ActivityController> { | ||
| 84 | Row( | 86 | Row( |
| 85 | children: [ | 87 | children: [ |
| 86 | _StatItem( | 88 | _StatItem( |
| 87 | - label: '本$periodLabel总消耗', | 89 | + label: context.l10n.activityPeriodTotalBurn(periodLabel), |
| 88 | value: controller.totalBurn.value, | 90 | value: controller.totalBurn.value, |
| 89 | unit: 'kcal', | 91 | unit: 'kcal', |
| 90 | color: _activeColor, | 92 | color: _activeColor, |
| 91 | ), | 93 | ), |
| 92 | const SizedBox(width: 24), | 94 | const SizedBox(width: 24), |
| 93 | _StatItem( | 95 | _StatItem( |
| 94 | - label: '日均消耗', | 96 | + label: context.l10n.activityDailyAverageBurn, |
| 95 | value: controller.averageBurn.value, | 97 | value: controller.averageBurn.value, |
| 96 | unit: 'kcal', | 98 | unit: 'kcal', |
| 97 | color: const Color(0xFF3BD49D), | 99 | color: const Color(0xFF3BD49D), |
| @@ -103,6 +105,13 @@ class _ActivitySummaryCard extends GetView<ActivityController> { | @@ -103,6 +105,13 @@ class _ActivitySummaryCard extends GetView<ActivityController> { | ||
| 103 | }), | 105 | }), |
| 104 | ); | 106 | ); |
| 105 | } | 107 | } |
| 108 | + | ||
| 109 | + String _periodLabel(BuildContext context, TrendPeriod period) => | ||
| 110 | + switch (period) { | ||
| 111 | + TrendPeriod.week => context.l10n.reportPeriodWeek, | ||
| 112 | + TrendPeriod.month => context.l10n.reportPeriodMonth, | ||
| 113 | + TrendPeriod.year => context.l10n.reportPeriodYear, | ||
| 114 | + }; | ||
| 106 | } | 115 | } |
| 107 | 116 | ||
| 108 | class _StatItem extends StatelessWidget { | 117 | class _StatItem extends StatelessWidget { |
| @@ -173,11 +182,12 @@ class _ActivityChartCard extends GetView<ActivityController> { | @@ -173,11 +182,12 @@ class _ActivityChartCard extends GetView<ActivityController> { | ||
| 173 | ), | 182 | ), |
| 174 | child: Obx(() { | 183 | child: Obx(() { |
| 175 | final period = controller.currentPeriod.value; | 184 | final period = controller.currentPeriod.value; |
| 185 | + final periodLabel = _periodLabel(context, period); | ||
| 176 | return Column( | 186 | return Column( |
| 177 | crossAxisAlignment: CrossAxisAlignment.start, | 187 | crossAxisAlignment: CrossAxisAlignment.start, |
| 178 | children: [ | 188 | children: [ |
| 179 | Text( | 189 | Text( |
| 180 | - '活动消耗${period.label}趋势图', | 190 | + context.l10n.activityPeriodTrendChart(periodLabel), |
| 181 | style: const TextStyle( | 191 | style: const TextStyle( |
| 182 | color: _h1, | 192 | color: _h1, |
| 183 | fontSize: 14, | 193 | fontSize: 14, |
| @@ -195,6 +205,13 @@ class _ActivityChartCard extends GetView<ActivityController> { | @@ -195,6 +205,13 @@ class _ActivityChartCard extends GetView<ActivityController> { | ||
| 195 | ); | 205 | ); |
| 196 | } | 206 | } |
| 197 | 207 | ||
| 208 | + String _periodLabel(BuildContext context, TrendPeriod period) => | ||
| 209 | + switch (period) { | ||
| 210 | + TrendPeriod.week => context.l10n.reportPeriodWeek, | ||
| 211 | + TrendPeriod.month => context.l10n.reportPeriodMonth, | ||
| 212 | + TrendPeriod.year => context.l10n.reportPeriodYear, | ||
| 213 | + }; | ||
| 214 | + | ||
| 198 | Widget _buildChart() { | 215 | Widget _buildChart() { |
| 199 | if (controller.chartData.isEmpty) { | 216 | if (controller.chartData.isEmpty) { |
| 200 | return const Center( | 217 | return const Center( |
| @@ -239,10 +256,10 @@ class _ActivityChartCard extends GetView<ActivityController> { | @@ -239,10 +256,10 @@ class _ActivityChartCard extends GetView<ActivityController> { | ||
| 239 | ), | 256 | ), |
| 240 | borderData: FlBorderData(show: false), | 257 | borderData: FlBorderData(show: false), |
| 241 | titlesData: FlTitlesData( | 258 | titlesData: FlTitlesData( |
| 242 | - topTitles: const AxisTitles( | ||
| 243 | - sideTitles: SideTitles(showTitles: false)), | ||
| 244 | - rightTitles: const AxisTitles( | ||
| 245 | - sideTitles: SideTitles(showTitles: false)), | 259 | + topTitles: |
| 260 | + const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 261 | + rightTitles: | ||
| 262 | + const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 246 | leftTitles: AxisTitles( | 263 | leftTitles: AxisTitles( |
| 247 | sideTitles: SideTitles( | 264 | sideTitles: SideTitles( |
| 248 | showTitles: true, | 265 | showTitles: true, |
| 1 | import 'package:fl_chart/fl_chart.dart'; | 1 | import 'package:fl_chart/fl_chart.dart'; |
| 2 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 2 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 3 | import 'package:get/get.dart'; | 4 | import 'package:get/get.dart'; |
| 4 | 5 | ||
| @@ -65,15 +66,16 @@ class _HrvSummaryCard extends GetView<HrvController> { | @@ -65,15 +66,16 @@ class _HrvSummaryCard extends GetView<HrvController> { | ||
| 65 | borderRadius: BorderRadius.circular(16), | 66 | borderRadius: BorderRadius.circular(16), |
| 66 | ), | 67 | ), |
| 67 | child: Obx(() { | 68 | child: Obx(() { |
| 68 | - final periodLabel = controller.currentPeriod.value.label; | 69 | + final periodLabel = |
| 70 | + _periodLabel(context, controller.currentPeriod.value); | ||
| 69 | return Column( | 71 | return Column( |
| 70 | crossAxisAlignment: CrossAxisAlignment.start, | 72 | crossAxisAlignment: CrossAxisAlignment.start, |
| 71 | children: [ | 73 | children: [ |
| 72 | Row( | 74 | Row( |
| 73 | children: [ | 75 | children: [ |
| 74 | - const Text( | ||
| 75 | - 'HRV趋势', | ||
| 76 | - style: TextStyle( | 76 | + Text( |
| 77 | + context.l10n.hrvTrendTitle, | ||
| 78 | + style: const TextStyle( | ||
| 77 | color: _h1, | 79 | color: _h1, |
| 78 | fontSize: 16, | 80 | fontSize: 16, |
| 79 | fontWeight: FontWeight.w600, | 81 | fontWeight: FontWeight.w600, |
| @@ -87,14 +89,14 @@ class _HrvSummaryCard extends GetView<HrvController> { | @@ -87,14 +89,14 @@ class _HrvSummaryCard extends GetView<HrvController> { | ||
| 87 | Row( | 89 | Row( |
| 88 | children: [ | 90 | children: [ |
| 89 | _StatItem( | 91 | _StatItem( |
| 90 | - label: '本$periodLabel平均', | 92 | + label: context.l10n.hrvPeriodAverage(periodLabel), |
| 91 | value: controller.averageHrv.value, | 93 | value: controller.averageHrv.value, |
| 92 | unit: 'ms', | 94 | unit: 'ms', |
| 93 | color: _brandColor, | 95 | color: _brandColor, |
| 94 | ), | 96 | ), |
| 95 | const SizedBox(width: 24), | 97 | const SizedBox(width: 24), |
| 96 | _StatItem( | 98 | _StatItem( |
| 97 | - label: '较上$periodLabel', | 99 | + label: context.l10n.hrvComparedPreviousPeriod(periodLabel), |
| 98 | value: controller.changeHrv.value, | 100 | value: controller.changeHrv.value, |
| 99 | unit: 'ms', | 101 | unit: 'ms', |
| 100 | color: const Color(0xFF3BD49D), | 102 | color: const Color(0xFF3BD49D), |
| @@ -106,6 +108,13 @@ class _HrvSummaryCard extends GetView<HrvController> { | @@ -106,6 +108,13 @@ class _HrvSummaryCard extends GetView<HrvController> { | ||
| 106 | }), | 108 | }), |
| 107 | ); | 109 | ); |
| 108 | } | 110 | } |
| 111 | + | ||
| 112 | + String _periodLabel(BuildContext context, TrendPeriod period) => | ||
| 113 | + switch (period) { | ||
| 114 | + TrendPeriod.week => context.l10n.reportPeriodWeek, | ||
| 115 | + TrendPeriod.month => context.l10n.reportPeriodMonth, | ||
| 116 | + TrendPeriod.year => context.l10n.reportPeriodYear, | ||
| 117 | + }; | ||
| 109 | } | 118 | } |
| 110 | 119 | ||
| 111 | class _StatItem extends StatelessWidget { | 120 | class _StatItem extends StatelessWidget { |
| @@ -179,11 +188,12 @@ class _HrvChartCard extends GetView<HrvController> { | @@ -179,11 +188,12 @@ class _HrvChartCard extends GetView<HrvController> { | ||
| 179 | ), | 188 | ), |
| 180 | child: Obx(() { | 189 | child: Obx(() { |
| 181 | final period = controller.currentPeriod.value; | 190 | final period = controller.currentPeriod.value; |
| 191 | + final periodLabel = _periodLabel(context, period); | ||
| 182 | return Column( | 192 | return Column( |
| 183 | crossAxisAlignment: CrossAxisAlignment.start, | 193 | crossAxisAlignment: CrossAxisAlignment.start, |
| 184 | children: [ | 194 | children: [ |
| 185 | Text( | 195 | Text( |
| 186 | - 'HRV ${period.label}趋势图', | 196 | + context.l10n.hrvPeriodTrendChart(periodLabel), |
| 187 | style: const TextStyle( | 197 | style: const TextStyle( |
| 188 | color: _h1, | 198 | color: _h1, |
| 189 | fontSize: 14, | 199 | fontSize: 14, |
| @@ -201,6 +211,13 @@ class _HrvChartCard extends GetView<HrvController> { | @@ -201,6 +211,13 @@ class _HrvChartCard extends GetView<HrvController> { | ||
| 201 | ); | 211 | ); |
| 202 | } | 212 | } |
| 203 | 213 | ||
| 214 | + String _periodLabel(BuildContext context, TrendPeriod period) => | ||
| 215 | + switch (period) { | ||
| 216 | + TrendPeriod.week => context.l10n.reportPeriodWeek, | ||
| 217 | + TrendPeriod.month => context.l10n.reportPeriodMonth, | ||
| 218 | + TrendPeriod.year => context.l10n.reportPeriodYear, | ||
| 219 | + }; | ||
| 220 | + | ||
| 204 | Widget _buildChart(TrendPeriod period) { | 221 | Widget _buildChart(TrendPeriod period) { |
| 205 | if (controller.chartData.isEmpty) { | 222 | if (controller.chartData.isEmpty) { |
| 206 | return const Center( | 223 | return const Center( |
| 1 | import 'package:fl_chart/fl_chart.dart'; | 1 | import 'package:fl_chart/fl_chart.dart'; |
| 2 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 2 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 3 | import 'package:get/get.dart'; | 4 | import 'package:get/get.dart'; |
| 4 | 5 | ||
| @@ -62,15 +63,16 @@ class _SleepSummaryCard extends GetView<SleepController> { | @@ -62,15 +63,16 @@ class _SleepSummaryCard extends GetView<SleepController> { | ||
| 62 | borderRadius: BorderRadius.circular(16), | 63 | borderRadius: BorderRadius.circular(16), |
| 63 | ), | 64 | ), |
| 64 | child: Obx(() { | 65 | child: Obx(() { |
| 65 | - final periodLabel = controller.currentPeriod.value.label; | 66 | + final periodLabel = |
| 67 | + _periodLabel(context, controller.currentPeriod.value); | ||
| 66 | return Column( | 68 | return Column( |
| 67 | crossAxisAlignment: CrossAxisAlignment.start, | 69 | crossAxisAlignment: CrossAxisAlignment.start, |
| 68 | children: [ | 70 | children: [ |
| 69 | Row( | 71 | Row( |
| 70 | children: [ | 72 | children: [ |
| 71 | - const Text( | ||
| 72 | - '睡眠报告趋势', | ||
| 73 | - style: TextStyle( | 73 | + Text( |
| 74 | + context.l10n.sleepTrendTitle, | ||
| 75 | + style: const TextStyle( | ||
| 74 | color: _h1, | 76 | color: _h1, |
| 75 | fontSize: 16, | 77 | fontSize: 16, |
| 76 | fontWeight: FontWeight.w600, | 78 | fontWeight: FontWeight.w600, |
| @@ -84,14 +86,14 @@ class _SleepSummaryCard extends GetView<SleepController> { | @@ -84,14 +86,14 @@ class _SleepSummaryCard extends GetView<SleepController> { | ||
| 84 | Row( | 86 | Row( |
| 85 | children: [ | 87 | children: [ |
| 86 | _StatItem( | 88 | _StatItem( |
| 87 | - label: '本$periodLabel均睡眠', | 89 | + label: context.l10n.sleepPeriodAverageDuration(periodLabel), |
| 88 | value: controller.averageSleep.value, | 90 | value: controller.averageSleep.value, |
| 89 | unit: 'h', | 91 | unit: 'h', |
| 90 | color: _sleepColor, | 92 | color: _sleepColor, |
| 91 | ), | 93 | ), |
| 92 | const SizedBox(width: 24), | 94 | const SizedBox(width: 24), |
| 93 | _StatItem( | 95 | _StatItem( |
| 94 | - label: '深睡占比', | 96 | + label: context.l10n.sleepDeepSleepRatio, |
| 95 | value: controller.deepSleepRatio.value, | 97 | value: controller.deepSleepRatio.value, |
| 96 | unit: '%', | 98 | unit: '%', |
| 97 | color: const Color(0xFF845EEE), | 99 | color: const Color(0xFF845EEE), |
| @@ -103,6 +105,13 @@ class _SleepSummaryCard extends GetView<SleepController> { | @@ -103,6 +105,13 @@ class _SleepSummaryCard extends GetView<SleepController> { | ||
| 103 | }), | 105 | }), |
| 104 | ); | 106 | ); |
| 105 | } | 107 | } |
| 108 | + | ||
| 109 | + String _periodLabel(BuildContext context, TrendPeriod period) => | ||
| 110 | + switch (period) { | ||
| 111 | + TrendPeriod.week => context.l10n.reportPeriodWeek, | ||
| 112 | + TrendPeriod.month => context.l10n.reportPeriodMonth, | ||
| 113 | + TrendPeriod.year => context.l10n.reportPeriodYear, | ||
| 114 | + }; | ||
| 106 | } | 115 | } |
| 107 | 116 | ||
| 108 | class _StatItem extends StatelessWidget { | 117 | class _StatItem extends StatelessWidget { |
| @@ -170,11 +179,12 @@ class _SleepChartCard extends GetView<SleepController> { | @@ -170,11 +179,12 @@ class _SleepChartCard extends GetView<SleepController> { | ||
| 170 | ), | 179 | ), |
| 171 | child: Obx(() { | 180 | child: Obx(() { |
| 172 | final period = controller.currentPeriod.value; | 181 | final period = controller.currentPeriod.value; |
| 182 | + final periodLabel = _periodLabel(context, period); | ||
| 173 | return Column( | 183 | return Column( |
| 174 | crossAxisAlignment: CrossAxisAlignment.start, | 184 | crossAxisAlignment: CrossAxisAlignment.start, |
| 175 | children: [ | 185 | children: [ |
| 176 | Text( | 186 | Text( |
| 177 | - '睡眠时长${period.label}趋势图', | 187 | + context.l10n.sleepDurationPeriodTrendChart(periodLabel), |
| 178 | style: const TextStyle( | 188 | style: const TextStyle( |
| 179 | color: _h1, | 189 | color: _h1, |
| 180 | fontSize: 14, | 190 | fontSize: 14, |
| @@ -192,6 +202,13 @@ class _SleepChartCard extends GetView<SleepController> { | @@ -192,6 +202,13 @@ class _SleepChartCard extends GetView<SleepController> { | ||
| 192 | ); | 202 | ); |
| 193 | } | 203 | } |
| 194 | 204 | ||
| 205 | + String _periodLabel(BuildContext context, TrendPeriod period) => | ||
| 206 | + switch (period) { | ||
| 207 | + TrendPeriod.week => context.l10n.reportPeriodWeek, | ||
| 208 | + TrendPeriod.month => context.l10n.reportPeriodMonth, | ||
| 209 | + TrendPeriod.year => context.l10n.reportPeriodYear, | ||
| 210 | + }; | ||
| 211 | + | ||
| 195 | Widget _buildChart() { | 212 | Widget _buildChart() { |
| 196 | if (controller.chartData.isEmpty) { | 213 | if (controller.chartData.isEmpty) { |
| 197 | return const Center( | 214 | return const Center( |
| @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; | @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; | ||
| 2 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 2 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 3 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 3 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 4 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; | 4 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; |
| 5 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 5 | import 'package:flutter/material.dart'; | 6 | import 'package:flutter/material.dart'; |
| 6 | import 'package:get/get.dart'; | 7 | import 'package:get/get.dart'; |
| 7 | 8 | ||
| @@ -39,7 +40,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { | @@ -39,7 +40,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { | ||
| 39 | children: [ | 40 | children: [ |
| 40 | Center( | 41 | Center( |
| 41 | child: Text( | 42 | child: Text( |
| 42 | - '选择好友', | 43 | + context.l10n.friendsSelect, |
| 43 | style: TextStyle( | 44 | style: TextStyle( |
| 44 | color: context.colors.textPrimary, | 45 | color: context.colors.textPrimary, |
| 45 | fontSize: 16, | 46 | fontSize: 16, |
| @@ -89,8 +90,10 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { | @@ -89,8 +90,10 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { | ||
| 89 | return _BottomSheetUserRow( | 90 | return _BottomSheetUserRow( |
| 90 | name: selfNickname?.isNotEmpty == true | 91 | name: selfNickname?.isNotEmpty == true |
| 91 | ? selfNickname! | 92 | ? selfNickname! |
| 92 | - : '我', | ||
| 93 | - subtitle: selfNickname?.isNotEmpty == true ? '我' : null, | 93 | + : context.l10n.friendsMe, |
| 94 | + subtitle: selfNickname?.isNotEmpty == true | ||
| 95 | + ? context.l10n.friendsMe | ||
| 96 | + : null, | ||
| 94 | avatarUrl: self?.avatar, | 97 | avatarUrl: self?.avatar, |
| 95 | isSelected: currentSelectedId == null, | 98 | isSelected: currentSelectedId == null, |
| 96 | onTap: () { | 99 | onTap: () { |
| @@ -101,7 +104,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { | @@ -101,7 +104,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { | ||
| 101 | } | 104 | } |
| 102 | 105 | ||
| 103 | final friend = friendsList[index - 1]; | 106 | final friend = friendsList[index - 1]; |
| 104 | - final name = _friendName(friend); | 107 | + final name = _friendName(context, friend); |
| 105 | final nickname = friend.friendNickname?.trim(); | 108 | final nickname = friend.friendNickname?.trim(); |
| 106 | return _BottomSheetUserRow( | 109 | return _BottomSheetUserRow( |
| 107 | name: name, | 110 | name: name, |
| @@ -123,14 +126,14 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { | @@ -123,14 +126,14 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { | ||
| 123 | ); | 126 | ); |
| 124 | } | 127 | } |
| 125 | 128 | ||
| 126 | - String _friendName(FriendItem friend) { | 129 | + String _friendName(BuildContext context, FriendItem friend) { |
| 127 | final remark = friend.remarkName?.trim(); | 130 | final remark = friend.remarkName?.trim(); |
| 128 | if (remark?.isNotEmpty == true) return remark!; | 131 | if (remark?.isNotEmpty == true) return remark!; |
| 129 | 132 | ||
| 130 | final nickname = friend.friendNickname?.trim(); | 133 | final nickname = friend.friendNickname?.trim(); |
| 131 | if (nickname?.isNotEmpty == true) return nickname!; | 134 | if (nickname?.isNotEmpty == true) return nickname!; |
| 132 | 135 | ||
| 133 | - return '未知好友'; | 136 | + return context.l10n.friendsUnknownFriend; |
| 134 | } | 137 | } |
| 135 | } | 138 | } |
| 136 | 139 |
| @@ -18,11 +18,13 @@ class HrvReportView extends StatelessWidget { | @@ -18,11 +18,13 @@ class HrvReportView extends StatelessWidget { | ||
| 18 | required this.logic, | 18 | required this.logic, |
| 19 | required this.isVip, | 19 | required this.isVip, |
| 20 | required this.onSubscribe, | 20 | required this.onSubscribe, |
| 21 | + this.loadingIndicator = const ReportLoadingIndicator(), | ||
| 21 | }); | 22 | }); |
| 22 | 23 | ||
| 23 | final HrvReportLogic logic; | 24 | final HrvReportLogic logic; |
| 24 | final bool isVip; | 25 | final bool isVip; |
| 25 | final ValueChanged<String> onSubscribe; | 26 | final ValueChanged<String> onSubscribe; |
| 27 | + final Widget loadingIndicator; | ||
| 26 | 28 | ||
| 27 | @override | 29 | @override |
| 28 | Widget build(BuildContext context) { | 30 | Widget build(BuildContext context) { |
| @@ -71,7 +73,7 @@ class HrvReportView extends StatelessWidget { | @@ -71,7 +73,7 @@ class HrvReportView extends StatelessWidget { | ||
| 71 | child: Obx( | 73 | child: Obx( |
| 72 | () { | 74 | () { |
| 73 | if (logic.isLoading.value) { | 75 | if (logic.isLoading.value) { |
| 74 | - return const ReportLoadingIndicator(); | 76 | + return loadingIndicator; |
| 75 | } | 77 | } |
| 76 | return ListView( | 78 | return ListView( |
| 77 | physics: const ClampingScrollPhysics(), | 79 | physics: const ClampingScrollPhysics(), |
| 1 | +import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | ||
| 1 | import 'package:flutter/material.dart'; | 2 | import 'package:flutter/material.dart'; |
| 2 | 3 | ||
| 3 | class HrvDistributionBarSegment { | 4 | class HrvDistributionBarSegment { |
| @@ -47,7 +48,7 @@ class _HrvDistributionBarPainter extends CustomPainter { | @@ -47,7 +48,7 @@ class _HrvDistributionBarPainter extends CustomPainter { | ||
| 47 | final Color backgroundColor; | 48 | final Color backgroundColor; |
| 48 | final double radius; | 49 | final double radius; |
| 49 | static const borderWidth = 1.0; | 50 | static const borderWidth = 1.0; |
| 50 | - static const overlapHeight = 16.0; | 51 | + static var overlapHeight = 16.h; |
| 51 | 52 | ||
| 52 | @override | 53 | @override |
| 53 | void paint(Canvas canvas, Size size) { | 54 | void paint(Canvas canvas, Size size) { |
| @@ -57,7 +58,6 @@ class _HrvDistributionBarPainter extends CustomPainter { | @@ -57,7 +58,6 @@ class _HrvDistributionBarPainter extends CustomPainter { | ||
| 57 | ); | 58 | ); |
| 58 | canvas.save(); | 59 | canvas.save(); |
| 59 | canvas.clipRRect(clip); | 60 | canvas.clipRRect(clip); |
| 60 | - | ||
| 61 | if (segments.isEmpty) { | 61 | if (segments.isEmpty) { |
| 62 | _drawLayeredSegment( | 62 | _drawLayeredSegment( |
| 63 | canvas, | 63 | canvas, |
| @@ -68,7 +68,6 @@ class _HrvDistributionBarPainter extends CustomPainter { | @@ -68,7 +68,6 @@ class _HrvDistributionBarPainter extends CustomPainter { | ||
| 68 | canvas.restore(); | 68 | canvas.restore(); |
| 69 | return; | 69 | return; |
| 70 | } | 70 | } |
| 71 | - | ||
| 72 | final visibleHeights = _visibleSegmentHeightsFor(size.height, segments); | 71 | final visibleHeights = _visibleSegmentHeightsFor(size.height, segments); |
| 73 | var visibleTop = 0.0; | 72 | var visibleTop = 0.0; |
| 74 | for (var index = 0; index < segments.length; index++) { | 73 | for (var index = 0; index < segments.length; index++) { |
| @@ -467,32 +467,26 @@ class _TrendMetric extends StatelessWidget { | @@ -467,32 +467,26 @@ class _TrendMetric extends StatelessWidget { | ||
| 467 | } | 467 | } |
| 468 | 468 | ||
| 469 | String _weekComparisonText(BuildContext context, int difference) { | 469 | String _weekComparisonText(BuildContext context, int difference) { |
| 470 | - final isZh = Localizations.localeOf(context).languageCode == 'zh'; | ||
| 471 | return difference == 0 | 470 | return difference == 0 |
| 472 | - ? (isZh ? '与上周一致' : 'Same as last week') | 471 | + ? context.l10n.hrvSameAsLastWeek |
| 473 | : difference > 0 | 472 | : difference > 0 |
| 474 | ? context.l10n.hrvMoreDaysThanLastWeek(difference) | 473 | ? context.l10n.hrvMoreDaysThanLastWeek(difference) |
| 475 | : context.l10n.hrvFewerDaysThanLastWeek(difference.abs()); | 474 | : context.l10n.hrvFewerDaysThanLastWeek(difference.abs()); |
| 476 | } | 475 | } |
| 477 | 476 | ||
| 478 | String _monthComparisonText(BuildContext context, int difference) { | 477 | String _monthComparisonText(BuildContext context, int difference) { |
| 479 | - final isZh = Localizations.localeOf(context).languageCode == 'zh'; | ||
| 480 | if (difference == 0) { | 478 | if (difference == 0) { |
| 481 | - return isZh ? '与上月一致' : 'Same as last month'; | 479 | + return context.l10n.hrvSameAsLastMonth; |
| 482 | } | 480 | } |
| 483 | if (difference > 0) { | 481 | if (difference > 0) { |
| 484 | - return isZh | ||
| 485 | - ? '比上月多$difference天' | ||
| 486 | - : '$difference more days than last month'; | 482 | + return context.l10n.hrvMoreDaysThanLastMonth(difference); |
| 487 | } | 483 | } |
| 488 | - final count = difference.abs(); | ||
| 489 | - return isZh ? '比上月少$count天' : '$count fewer days than last month'; | 484 | + return context.l10n.hrvFewerDaysThanLastMonth(difference.abs()); |
| 490 | } | 485 | } |
| 491 | 486 | ||
| 492 | String _unavailableComparisonText(BuildContext context) { | 487 | String _unavailableComparisonText(BuildContext context) { |
| 493 | if (!isMonth) return context.l10n.hrvComparedLastWeekUnavailable; | 488 | if (!isMonth) return context.l10n.hrvComparedLastWeekUnavailable; |
| 494 | - final isZh = Localizations.localeOf(context).languageCode == 'zh'; | ||
| 495 | - return isZh ? '比上月少-天' : 'Compared with last month: -'; | 489 | + return context.l10n.hrvComparedLastMonthUnavailable; |
| 496 | } | 490 | } |
| 497 | 491 | ||
| 498 | Color _comparisonColor(int? difference) { | 492 | Color _comparisonColor(int? difference) { |
| @@ -518,13 +512,19 @@ class _StressAxisLabels extends StatelessWidget { | @@ -518,13 +512,19 @@ class _StressAxisLabels extends StatelessWidget { | ||
| 518 | 512 | ||
| 519 | @override | 513 | @override |
| 520 | Widget build(BuildContext context) { | 514 | Widget build(BuildContext context) { |
| 521 | - return const SizedBox( | 515 | + return SizedBox( |
| 522 | width: 14, | 516 | width: 14, |
| 523 | child: Column( | 517 | child: Column( |
| 524 | mainAxisAlignment: MainAxisAlignment.spaceBetween, | 518 | mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 525 | children: [ | 519 | children: [ |
| 526 | - RotatedBox(quarterTurns: 1, child: _StressAxisText('轻松')), | ||
| 527 | - RotatedBox(quarterTurns: 1, child: _StressAxisText('压力大')), | 520 | + RotatedBox( |
| 521 | + quarterTurns: 1, | ||
| 522 | + child: _StressAxisText(context.l10n.hrvRelaxedAxisLabel), | ||
| 523 | + ), | ||
| 524 | + RotatedBox( | ||
| 525 | + quarterTurns: 1, | ||
| 526 | + child: _StressAxisText(context.l10n.hrvStressedAxisLabel), | ||
| 527 | + ), | ||
| 528 | ], | 528 | ], |
| 529 | ), | 529 | ), |
| 530 | ); | 530 | ); |
| @@ -644,91 +644,91 @@ class _DistributionCard extends StatelessWidget { | @@ -644,91 +644,91 @@ class _DistributionCard extends StatelessWidget { | ||
| 644 | final HrvPeriodReport report; | 644 | final HrvPeriodReport report; |
| 645 | final bool showExample; | 645 | final bool showExample; |
| 646 | final VoidCallback? onTap; | 646 | final VoidCallback? onTap; |
| 647 | - static const _cardHeight = 425.0; | ||
| 648 | - static const _barRight = 45.0; | 647 | + static const _barRight = 25.0; |
| 649 | static const _barWidth = 42.0; | 648 | static const _barWidth = 42.0; |
| 650 | - static const _barHeight = 220.0; | ||
| 651 | 649 | ||
| 652 | @override | 650 | @override |
| 653 | Widget build(BuildContext context) { | 651 | Widget build(BuildContext context) { |
| 654 | return _Card( | 652 | return _Card( |
| 655 | onTap: onTap, | 653 | onTap: onTap, |
| 656 | padding: EdgeInsets.zero, | 654 | padding: EdgeInsets.zero, |
| 657 | - child: ConstrainedBox( | ||
| 658 | - constraints: const BoxConstraints(minHeight: _cardHeight), | ||
| 659 | - child: Stack( | 655 | + child: Padding( |
| 656 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 24), | ||
| 657 | + child: Column( | ||
| 658 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 660 | children: [ | 659 | children: [ |
| 661 | - Padding( | ||
| 662 | - padding: const EdgeInsets.fromLTRB(20, 20, 20, 24), | ||
| 663 | - child: Column( | ||
| 664 | - crossAxisAlignment: CrossAxisAlignment.start, | ||
| 665 | - children: [ | ||
| 666 | - _ReportTitle( | ||
| 667 | - title: HealthReportSubjectScope.titleOf( | ||
| 668 | - context, | ||
| 669 | - context.l10n.hrvStressDistribution, | ||
| 670 | - ), | ||
| 671 | - showExample: showExample, | ||
| 672 | - fontSize: 16, | ||
| 673 | - ), | ||
| 674 | - const SizedBox(height: 20), | ||
| 675 | - Text( | ||
| 676 | - context.l10n.hrvValidDays, | ||
| 677 | - style: const TextStyle( | ||
| 678 | - color: Color(0xFF78787D), | ||
| 679 | - fontSize: 12, | ||
| 680 | - ), | ||
| 681 | - ), | ||
| 682 | - const SizedBox(height: 2), | ||
| 683 | - _ValueWithUnit( | ||
| 684 | - value: '${report.validDays}', | ||
| 685 | - unit: context.l10n.reportUnitDay, | ||
| 686 | - ), | ||
| 687 | - const SizedBox(height: 28), | ||
| 688 | - SizedBox( | ||
| 689 | - width: 216, | ||
| 690 | - child: _DistributionGrid(report: report), | 660 | + _topView(context), |
| 661 | + const Divider(height: 1, color: Color(0xFFF3F3F3)), | ||
| 662 | + const SizedBox(height: 20), | ||
| 663 | + Row( | ||
| 664 | + children: [ | ||
| 665 | + Expanded( | ||
| 666 | + child: _ExtremeHrv( | ||
| 667 | + title: context.l10n.hrvLowest, | ||
| 668 | + day: report.minDay, | ||
| 691 | ), | 669 | ), |
| 692 | - const SizedBox(height: 20), | ||
| 693 | - const Divider(height: 1, color: Color(0xFFF3F3F3)), | ||
| 694 | - const SizedBox(height: 20), | ||
| 695 | - Row( | ||
| 696 | - children: [ | ||
| 697 | - Expanded( | ||
| 698 | - child: _ExtremeHrv( | ||
| 699 | - title: context.l10n.hrvLowest, | ||
| 700 | - day: report.minDay, | ||
| 701 | - ), | ||
| 702 | - ), | ||
| 703 | - Expanded( | ||
| 704 | - child: _ExtremeHrv( | ||
| 705 | - title: context.l10n.hrvHighest, | ||
| 706 | - day: report.maxDay, | ||
| 707 | - ), | ||
| 708 | - ), | ||
| 709 | - ], | 670 | + ), |
| 671 | + Expanded( | ||
| 672 | + child: _ExtremeHrv( | ||
| 673 | + title: context.l10n.hrvHighest, | ||
| 674 | + day: report.maxDay, | ||
| 710 | ), | 675 | ), |
| 711 | - ], | 676 | + ), |
| 677 | + ], | ||
| 678 | + ), | ||
| 679 | + ], | ||
| 680 | + ), | ||
| 681 | + ), | ||
| 682 | + ); | ||
| 683 | + } | ||
| 684 | + | ||
| 685 | + Widget _topView(BuildContext context) { | ||
| 686 | + return Stack( | ||
| 687 | + children: [ | ||
| 688 | + Positioned( | ||
| 689 | + child: Column( | ||
| 690 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 691 | + children: [ | ||
| 692 | + _ReportTitle( | ||
| 693 | + title: HealthReportSubjectScope.titleOf( | ||
| 694 | + context, | ||
| 695 | + context.l10n.hrvStressDistribution, | ||
| 712 | ), | 696 | ), |
| 697 | + showExample: showExample, | ||
| 698 | + fontSize: 16, | ||
| 713 | ), | 699 | ), |
| 714 | - Positioned( | ||
| 715 | - top: 0, | ||
| 716 | - right: _barRight, | ||
| 717 | - width: _barWidth, | ||
| 718 | - child: Container( | ||
| 719 | - alignment: Alignment.topCenter, | ||
| 720 | - padding: EdgeInsets.only(top: 50.h), | ||
| 721 | - child: SizedBox( | ||
| 722 | - height: _barHeight, | ||
| 723 | - child: IgnorePointer( | ||
| 724 | - child: _DistributionBar(report: report), | ||
| 725 | - ), | ||
| 726 | - ), | 700 | + const SizedBox(height: 20), |
| 701 | + Text( | ||
| 702 | + context.l10n.hrvValidDays, | ||
| 703 | + style: const TextStyle( | ||
| 704 | + color: Color(0xFF78787D), | ||
| 705 | + fontSize: 12, | ||
| 727 | ), | 706 | ), |
| 728 | ), | 707 | ), |
| 708 | + const SizedBox(height: 2), | ||
| 709 | + _ValueWithUnit( | ||
| 710 | + value: '${report.validDays}', | ||
| 711 | + unit: context.l10n.reportUnitDay, | ||
| 712 | + ), | ||
| 713 | + const SizedBox(height: 28), | ||
| 714 | + SizedBox( | ||
| 715 | + width: 216, | ||
| 716 | + child: _DistributionGrid(report: report), | ||
| 717 | + ), | ||
| 718 | + const SizedBox(height: 20), | ||
| 729 | ], | 719 | ], |
| 720 | + )), | ||
| 721 | + Positioned( | ||
| 722 | + top: 0, | ||
| 723 | + width: _barWidth, | ||
| 724 | + right: _barRight, | ||
| 725 | + bottom: 25.h, | ||
| 726 | + child: Container( | ||
| 727 | + alignment: Alignment.topCenter, | ||
| 728 | + padding: EdgeInsets.only(top: 25.h), | ||
| 729 | + child: IgnorePointer(child: _DistributionBar(report: report))), | ||
| 730 | ), | 730 | ), |
| 731 | - ), | 731 | + ], |
| 732 | ); | 732 | ); |
| 733 | } | 733 | } |
| 734 | } | 734 | } |
| @@ -859,7 +859,9 @@ class _ExtremeHrv extends StatelessWidget { | @@ -859,7 +859,9 @@ class _ExtremeHrv extends StatelessWidget { | ||
| 859 | unit: 'ms'), | 859 | unit: 'ms'), |
| 860 | const SizedBox(height: 2), | 860 | const SizedBox(height: 2), |
| 861 | Text( | 861 | Text( |
| 862 | - day == null ? '-月-日' : reportMonthDay(day!.date), | 862 | + day == null |
| 863 | + ? context.l10n.hrvEmptyMonthDayPlaceholder | ||
| 864 | + : reportMonthDay(day!.date), | ||
| 863 | style: const TextStyle(color: Color(0xFF78787D), fontSize: 12), | 865 | style: const TextStyle(color: Color(0xFF78787D), fontSize: 12), |
| 864 | ), | 866 | ), |
| 865 | ], | 867 | ], |
| @@ -22,11 +22,13 @@ class SleepReportView extends StatelessWidget { | @@ -22,11 +22,13 @@ class SleepReportView extends StatelessWidget { | ||
| 22 | required this.logic, | 22 | required this.logic, |
| 23 | required this.isVip, | 23 | required this.isVip, |
| 24 | required this.onSubscribe, | 24 | required this.onSubscribe, |
| 25 | + this.loadingIndicator = const ReportLoadingIndicator(), | ||
| 25 | }); | 26 | }); |
| 26 | 27 | ||
| 27 | final SleepReportLogic logic; | 28 | final SleepReportLogic logic; |
| 28 | final bool isVip; | 29 | final bool isVip; |
| 29 | final ValueChanged<String> onSubscribe; | 30 | final ValueChanged<String> onSubscribe; |
| 31 | + final Widget loadingIndicator; | ||
| 30 | 32 | ||
| 31 | @override | 33 | @override |
| 32 | Widget build(BuildContext context) { | 34 | Widget build(BuildContext context) { |
| @@ -60,7 +62,7 @@ class SleepReportView extends StatelessWidget { | @@ -60,7 +62,7 @@ class SleepReportView extends StatelessWidget { | ||
| 60 | child: Obx( | 62 | child: Obx( |
| 61 | () { | 63 | () { |
| 62 | if (logic.isLoading.value) { | 64 | if (logic.isLoading.value) { |
| 63 | - return const ReportLoadingIndicator(); | 65 | + return loadingIndicator; |
| 64 | } | 66 | } |
| 65 | return CustomScrollView( | 67 | return CustomScrollView( |
| 66 | physics: const ClampingScrollPhysics(), | 68 | physics: const ClampingScrollPhysics(), |
| @@ -377,7 +377,7 @@ class _TopMetric extends StatelessWidget { | @@ -377,7 +377,7 @@ class _TopMetric extends StatelessWidget { | ||
| 377 | Text( | 377 | Text( |
| 378 | trendText, | 378 | trendText, |
| 379 | style: const TextStyle( | 379 | style: const TextStyle( |
| 380 | - color: SleepWeekReportView.h3, | 380 | + color: SleepWeekReportView.h2, |
| 381 | fontSize: 12, | 381 | fontSize: 12, |
| 382 | fontWeight: FontWeight.w400, | 382 | fontWeight: FontWeight.w400, |
| 383 | height: 1.2, | 383 | height: 1.2, |
| @@ -1437,6 +1437,7 @@ class _ExtremeMetric extends StatelessWidget { | @@ -1437,6 +1437,7 @@ class _ExtremeMetric extends StatelessWidget { | ||
| 1437 | required this.unit, | 1437 | required this.unit, |
| 1438 | required this.date, | 1438 | required this.date, |
| 1439 | this.duration, | 1439 | this.duration, |
| 1440 | + this.isDurationMetric = false, | ||
| 1440 | }); | 1441 | }); |
| 1441 | 1442 | ||
| 1442 | factory _ExtremeMetric.duration({ | 1443 | factory _ExtremeMetric.duration({ |
| @@ -1450,10 +1451,11 @@ class _ExtremeMetric extends StatelessWidget { | @@ -1450,10 +1451,11 @@ class _ExtremeMetric extends StatelessWidget { | ||
| 1450 | color: color, | 1451 | color: color, |
| 1451 | value: duration == null ? '-' : '${duration.hoursPart}', | 1452 | value: duration == null ? '-' : '${duration.hoursPart}', |
| 1452 | unit: duration == null | 1453 | unit: duration == null |
| 1453 | - ? '${l10n.reportUnitHour} -${l10n.reportUnitMinute}' | 1454 | + ? '' |
| 1454 | : '${l10n.reportUnitHour}${duration.minutesPart}${l10n.reportUnitMinute}', | 1455 | : '${l10n.reportUnitHour}${duration.minutesPart}${l10n.reportUnitMinute}', |
| 1455 | date: report?.date, | 1456 | date: report?.date, |
| 1456 | duration: duration, | 1457 | duration: duration, |
| 1458 | + isDurationMetric: true, | ||
| 1457 | ); | 1459 | ); |
| 1458 | } | 1460 | } |
| 1459 | 1461 | ||
| @@ -1496,6 +1498,7 @@ class _ExtremeMetric extends StatelessWidget { | @@ -1496,6 +1498,7 @@ class _ExtremeMetric extends StatelessWidget { | ||
| 1496 | final String unit; | 1498 | final String unit; |
| 1497 | final DateTime? date; | 1499 | final DateTime? date; |
| 1498 | final SleepDuration? duration; | 1500 | final SleepDuration? duration; |
| 1501 | + final bool isDurationMetric; | ||
| 1499 | 1502 | ||
| 1500 | @override | 1503 | @override |
| 1501 | Widget build(BuildContext context) { | 1504 | Widget build(BuildContext context) { |
| @@ -1526,6 +1529,8 @@ class _ExtremeMetric extends StatelessWidget { | @@ -1526,6 +1529,8 @@ class _ExtremeMetric extends StatelessWidget { | ||
| 1526 | const SizedBox(height: 3), | 1529 | const SizedBox(height: 3), |
| 1527 | if (duration != null) | 1530 | if (duration != null) |
| 1528 | _ExtremeDurationValue(duration: duration!) | 1531 | _ExtremeDurationValue(duration: duration!) |
| 1532 | + else if (isDurationMetric) | ||
| 1533 | + const _ExtremeDurationPlaceholderValue() | ||
| 1529 | else | 1534 | else |
| 1530 | Row( | 1535 | Row( |
| 1531 | crossAxisAlignment: CrossAxisAlignment.end, | 1536 | crossAxisAlignment: CrossAxisAlignment.end, |
| @@ -1559,7 +1564,9 @@ class _ExtremeMetric extends StatelessWidget { | @@ -1559,7 +1564,9 @@ class _ExtremeMetric extends StatelessWidget { | ||
| 1559 | ), | 1564 | ), |
| 1560 | const SizedBox(height: 3), | 1565 | const SizedBox(height: 3), |
| 1561 | Text( | 1566 | Text( |
| 1562 | - date == null ? '-月-日 周-' : _dateText(context, date!), | 1567 | + date == null |
| 1568 | + ? context.l10n.sleepEmptyDateWithWeekday | ||
| 1569 | + : _dateText(context, date!), | ||
| 1563 | style: const TextStyle( | 1570 | style: const TextStyle( |
| 1564 | color: SleepWeekReportView.h2, | 1571 | color: SleepWeekReportView.h2, |
| 1565 | fontSize: 12, | 1572 | fontSize: 12, |
| @@ -1579,6 +1586,22 @@ class _ExtremeMetric extends StatelessWidget { | @@ -1579,6 +1586,22 @@ class _ExtremeMetric extends StatelessWidget { | ||
| 1579 | } | 1586 | } |
| 1580 | } | 1587 | } |
| 1581 | 1588 | ||
| 1589 | +class _ExtremeDurationPlaceholderValue extends StatelessWidget { | ||
| 1590 | + const _ExtremeDurationPlaceholderValue(); | ||
| 1591 | + | ||
| 1592 | + @override | ||
| 1593 | + Widget build(BuildContext context) { | ||
| 1594 | + return Row( | ||
| 1595 | + crossAxisAlignment: CrossAxisAlignment.end, | ||
| 1596 | + children: [ | ||
| 1597 | + _ExtremeDurationPart('-', context.l10n.reportUnitHour), | ||
| 1598 | + const SizedBox(width: 2), | ||
| 1599 | + _ExtremeDurationPart('-', context.l10n.reportUnitMinute), | ||
| 1600 | + ], | ||
| 1601 | + ); | ||
| 1602 | + } | ||
| 1603 | +} | ||
| 1604 | + | ||
| 1582 | class _ExtremeDurationValue extends StatelessWidget { | 1605 | class _ExtremeDurationValue extends StatelessWidget { |
| 1583 | const _ExtremeDurationValue({required this.duration}); | 1606 | const _ExtremeDurationValue({required this.duration}); |
| 1584 | 1607 | ||
| @@ -1589,14 +1612,28 @@ class _ExtremeDurationValue extends StatelessWidget { | @@ -1589,14 +1612,28 @@ class _ExtremeDurationValue extends StatelessWidget { | ||
| 1589 | return Row( | 1612 | return Row( |
| 1590 | crossAxisAlignment: CrossAxisAlignment.end, | 1613 | crossAxisAlignment: CrossAxisAlignment.end, |
| 1591 | children: [ | 1614 | children: [ |
| 1592 | - _part('${duration.hoursPart}', context.l10n.reportUnitHour), | 1615 | + _ExtremeDurationPart( |
| 1616 | + '${duration.hoursPart}', | ||
| 1617 | + context.l10n.reportUnitHour, | ||
| 1618 | + ), | ||
| 1593 | const SizedBox(width: 2), | 1619 | const SizedBox(width: 2), |
| 1594 | - _part('${duration.minutesPart}', context.l10n.reportUnitMinute), | 1620 | + _ExtremeDurationPart( |
| 1621 | + '${duration.minutesPart}', | ||
| 1622 | + context.l10n.reportUnitMinute, | ||
| 1623 | + ), | ||
| 1595 | ], | 1624 | ], |
| 1596 | ); | 1625 | ); |
| 1597 | } | 1626 | } |
| 1627 | +} | ||
| 1628 | + | ||
| 1629 | +class _ExtremeDurationPart extends StatelessWidget { | ||
| 1630 | + const _ExtremeDurationPart(this.value, this.unit); | ||
| 1598 | 1631 | ||
| 1599 | - Widget _part(String value, String unit) { | 1632 | + final String value; |
| 1633 | + final String unit; | ||
| 1634 | + | ||
| 1635 | + @override | ||
| 1636 | + Widget build(BuildContext context) { | ||
| 1600 | return Row( | 1637 | return Row( |
| 1601 | crossAxisAlignment: CrossAxisAlignment.end, | 1638 | crossAxisAlignment: CrossAxisAlignment.end, |
| 1602 | children: [ | 1639 | children: [ |
| @@ -8,7 +8,6 @@ import 'package:doublefeel_flutter/core/result/app_result.dart'; | @@ -8,7 +8,6 @@ import 'package:doublefeel_flutter/core/result/app_result.dart'; | ||
| 8 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 8 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 9 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | 9 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; |
| 10 | import 'package:get/get.dart'; | 10 | import 'package:get/get.dart'; |
| 11 | -import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 12 | 11 | ||
| 13 | import '../models/watch_theme_models.dart'; | 12 | import '../models/watch_theme_models.dart'; |
| 14 | 13 | ||
| @@ -95,7 +94,7 @@ class WatchThemeController extends GetxController { | @@ -95,7 +94,7 @@ class WatchThemeController extends GetxController { | ||
| 95 | 94 | ||
| 96 | Future<void> _toPremiumPage() async { | 95 | Future<void> _toPremiumPage() async { |
| 97 | await Get.toNamed(Routes.PURCHASE, arguments: { | 96 | await Get.toNamed(Routes.PURCHASE, arguments: { |
| 98 | - IntentKeys.channelType: l10n.watchThemePurchaseChannel, | 97 | + IntentKeys.channelType: "表盘主题", |
| 99 | }); | 98 | }); |
| 100 | await _refreshVip(); | 99 | await _refreshVip(); |
| 101 | } | 100 | } |
| @@ -106,6 +106,8 @@ class FriendItem { | @@ -106,6 +106,8 @@ class FriendItem { | ||
| 106 | class FriendHealthData { | 106 | class FriendHealthData { |
| 107 | const FriendHealthData({ | 107 | const FriendHealthData({ |
| 108 | this.hrvState, | 108 | this.hrvState, |
| 109 | + this.comprehensiveStressScore, | ||
| 110 | + this.comprehensiveStressState, | ||
| 109 | this.latestHrv, | 111 | this.latestHrv, |
| 110 | this.realtimeStress, | 112 | this.realtimeStress, |
| 111 | this.sleepEvaluate, | 113 | this.sleepEvaluate, |
| @@ -116,6 +118,12 @@ class FriendHealthData { | @@ -116,6 +118,12 @@ class FriendHealthData { | ||
| 116 | /// HRV state indicator | 118 | /// HRV state indicator |
| 117 | final int? hrvState; | 119 | final int? hrvState; |
| 118 | 120 | ||
| 121 | + /// Comprehensive stress score. | ||
| 122 | + final int? comprehensiveStressScore; | ||
| 123 | + | ||
| 124 | + /// Comprehensive stress state indicator. | ||
| 125 | + final int? comprehensiveStressState; | ||
| 126 | + | ||
| 119 | /// Latest HRV value used by the friend health card. | 127 | /// Latest HRV value used by the friend health card. |
| 120 | final double? latestHrv; | 128 | final double? latestHrv; |
| 121 | 129 | ||
| @@ -134,6 +142,8 @@ class FriendHealthData { | @@ -134,6 +142,8 @@ class FriendHealthData { | ||
| 134 | factory FriendHealthData.fromJson(Map<String, dynamic> json) { | 142 | factory FriendHealthData.fromJson(Map<String, dynamic> json) { |
| 135 | return FriendHealthData( | 143 | return FriendHealthData( |
| 136 | hrvState: _parseInt(json['hrv_state']), | 144 | hrvState: _parseInt(json['hrv_state']), |
| 145 | + comprehensiveStressScore: _parseInt(json['comprehensive_stress_score']), | ||
| 146 | + comprehensiveStressState: _parseInt(json['comprehensive_stress_state']), | ||
| 137 | latestHrv: _parseDouble(json['latest_hrv']), | 147 | latestHrv: _parseDouble(json['latest_hrv']), |
| 138 | realtimeStress: json['realtime_stress'] as Map<String, dynamic>?, | 148 | realtimeStress: json['realtime_stress'] as Map<String, dynamic>?, |
| 139 | sleepEvaluate: _parseInt(json['sleep_evaluate']), | 149 | sleepEvaluate: _parseInt(json['sleep_evaluate']), |
| @@ -145,6 +155,12 @@ class FriendHealthData { | @@ -145,6 +155,12 @@ class FriendHealthData { | ||
| 145 | Map<String, dynamic> toJson() { | 155 | Map<String, dynamic> toJson() { |
| 146 | final val = <String, dynamic>{}; | 156 | final val = <String, dynamic>{}; |
| 147 | if (hrvState != null) val['hrv_state'] = hrvState; | 157 | if (hrvState != null) val['hrv_state'] = hrvState; |
| 158 | + if (comprehensiveStressScore != null) { | ||
| 159 | + val['comprehensive_stress_score'] = comprehensiveStressScore; | ||
| 160 | + } | ||
| 161 | + if (comprehensiveStressState != null) { | ||
| 162 | + val['comprehensive_stress_state'] = comprehensiveStressState; | ||
| 163 | + } | ||
| 148 | if (latestHrv != null) val['latest_hrv'] = latestHrv; | 164 | if (latestHrv != null) val['latest_hrv'] = latestHrv; |
| 149 | if (realtimeStress != null) val['realtime_stress'] = realtimeStress; | 165 | if (realtimeStress != null) val['realtime_stress'] = realtimeStress; |
| 150 | if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate; | 166 | if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate; |
| @@ -345,7 +345,15 @@ | @@ -345,7 +345,15 @@ | ||
| 345 | "hrvSameAsLastWeek": "Same as last week", | 345 | "hrvSameAsLastWeek": "Same as last week", |
| 346 | "hrvMoreDaysThanLastWeek": "{count} more days than last week", | 346 | "hrvMoreDaysThanLastWeek": "{count} more days than last week", |
| 347 | "hrvFewerDaysThanLastWeek": "{count} fewer days than last week", | 347 | "hrvFewerDaysThanLastWeek": "{count} fewer days than last week", |
| 348 | + "hrvComparedLastMonthUnavailable": "Compared with last month: -", | ||
| 349 | + "hrvSameAsLastMonth": "Same as last month", | ||
| 350 | + "hrvMoreDaysThanLastMonth": "{count} more days than last month", | ||
| 351 | + "hrvFewerDaysThanLastMonth": "{count} fewer days than last month", | ||
| 348 | "hrvUnlockNow": "Unlock Now", | 352 | "hrvUnlockNow": "Unlock Now", |
| 353 | + "hrvTrendTitle": "HRV Trend", | ||
| 354 | + "hrvPeriodAverage": "This {period} average", | ||
| 355 | + "hrvComparedPreviousPeriod": "vs previous {period}", | ||
| 356 | + "hrvPeriodTrendChart": "HRV {period} Trend Chart", | ||
| 349 | "activityTotalBurn": "Total Activity Burn", | 357 | "activityTotalBurn": "Total Activity Burn", |
| 350 | "activityExerciseTotalDuration": "Total Exercise Time", | 358 | "activityExerciseTotalDuration": "Total Exercise Time", |
| 351 | "activityStandTotalDuration": "Total Stand Time", | 359 | "activityStandTotalDuration": "Total Stand Time", |
| @@ -359,9 +367,16 @@ | @@ -359,9 +367,16 @@ | ||
| 359 | "activityComparedLastMonth": "34% less than last month", | 367 | "activityComparedLastMonth": "34% less than last month", |
| 360 | "activityComparedUnavailable": "Compared with last week: -", | 368 | "activityComparedUnavailable": "Compared with last week: -", |
| 361 | "activityComparedLastMonthUnavailable": "Compared with last month: -", | 369 | "activityComparedLastMonthUnavailable": "Compared with last month: -", |
| 370 | + "activitySameAsLastWeek": "Same as last week", | ||
| 371 | + "activityMoreThanLastWeek": "{percent}% more than last week", | ||
| 372 | + "activityLessThanLastWeek": "{percent}% less than last week", | ||
| 362 | "activitySameAsLastMonth": "Same as last month", | 373 | "activitySameAsLastMonth": "Same as last month", |
| 363 | "activityMoreThanLastMonth": "{percent}% more than last month", | 374 | "activityMoreThanLastMonth": "{percent}% more than last month", |
| 364 | "activityLessThanLastMonth": "{percent}% less than last month", | 375 | "activityLessThanLastMonth": "{percent}% less than last month", |
| 376 | + "activityTrendTitle": "Activity Burn Trend", | ||
| 377 | + "activityPeriodTotalBurn": "This {period} total burn", | ||
| 378 | + "activityDailyAverageBurn": "Daily average burn", | ||
| 379 | + "activityPeriodTrendChart": "Activity Burn {period} Trend Chart", | ||
| 365 | "activityMove": "Move", | 380 | "activityMove": "Move", |
| 366 | "activityExercise": "Exercise", | 381 | "activityExercise": "Exercise", |
| 367 | "activityStand": "Stand", | 382 | "activityStand": "Stand", |
| @@ -389,6 +404,11 @@ | @@ -389,6 +404,11 @@ | ||
| 389 | "sleepTarget": "Target", | 404 | "sleepTarget": "Target", |
| 390 | "sleepHighest": "Highest", | 405 | "sleepHighest": "Highest", |
| 391 | "sleepLowest": "Lowest", | 406 | "sleepLowest": "Lowest", |
| 407 | + "sleepTrendTitle": "Sleep Report Trend", | ||
| 408 | + "sleepPeriodAverageDuration": "This {period} average sleep", | ||
| 409 | + "sleepDeepSleepRatio": "Deep sleep ratio", | ||
| 410 | + "sleepDurationPeriodTrendChart": "Sleep Duration {period} Trend Chart", | ||
| 411 | + "sleepEmptyDateWithWeekday": "-", | ||
| 392 | "sleepQualityDescription": "DoubleFeel calculates your daily sleep quality score from sleep duration, sleep stages, deep sleep and recovery, nighttime heart rate, and HRV changes.\nThis score helps you understand your recovery and sleep performance more clearly.", | 412 | "sleepQualityDescription": "DoubleFeel calculates your daily sleep quality score from sleep duration, sleep stages, deep sleep and recovery, nighttime heart rate, and HRV changes.\nThis score helps you understand your recovery and sleep performance more clearly.", |
| 393 | "sleepQualityAttentionRange": "<60 pts", | 413 | "sleepQualityAttentionRange": "<60 pts", |
| 394 | "sleepQualityNormalRange": "60–85 pts", | 414 | "sleepQualityNormalRange": "60–85 pts", |
| @@ -531,7 +531,50 @@ | @@ -531,7 +531,50 @@ | ||
| 531 | } | 531 | } |
| 532 | } | 532 | } |
| 533 | }, | 533 | }, |
| 534 | + "hrvComparedLastMonthUnavailable": "比上月少-天", | ||
| 535 | + "hrvSameAsLastMonth": "与上月一致", | ||
| 536 | + "hrvMoreDaysThanLastMonth": "比上月多{count}天", | ||
| 537 | + "@hrvMoreDaysThanLastMonth": { | ||
| 538 | + "placeholders": { | ||
| 539 | + "count": { | ||
| 540 | + "type": "int" | ||
| 541 | + } | ||
| 542 | + } | ||
| 543 | + }, | ||
| 544 | + "hrvFewerDaysThanLastMonth": "比上月少{count}天", | ||
| 545 | + "@hrvFewerDaysThanLastMonth": { | ||
| 546 | + "placeholders": { | ||
| 547 | + "count": { | ||
| 548 | + "type": "int" | ||
| 549 | + } | ||
| 550 | + } | ||
| 551 | + }, | ||
| 534 | "hrvUnlockNow": "立即解锁", | 552 | "hrvUnlockNow": "立即解锁", |
| 553 | + "hrvTrendTitle": "HRV趋势", | ||
| 554 | + "hrvPeriodAverage": "本{period}平均", | ||
| 555 | + "@hrvPeriodAverage": { | ||
| 556 | + "placeholders": { | ||
| 557 | + "period": { | ||
| 558 | + "type": "String" | ||
| 559 | + } | ||
| 560 | + } | ||
| 561 | + }, | ||
| 562 | + "hrvComparedPreviousPeriod": "较上{period}", | ||
| 563 | + "@hrvComparedPreviousPeriod": { | ||
| 564 | + "placeholders": { | ||
| 565 | + "period": { | ||
| 566 | + "type": "String" | ||
| 567 | + } | ||
| 568 | + } | ||
| 569 | + }, | ||
| 570 | + "hrvPeriodTrendChart": "HRV {period}趋势图", | ||
| 571 | + "@hrvPeriodTrendChart": { | ||
| 572 | + "placeholders": { | ||
| 573 | + "period": { | ||
| 574 | + "type": "String" | ||
| 575 | + } | ||
| 576 | + } | ||
| 577 | + }, | ||
| 535 | "activityTotalBurn": "活动总消耗", | 578 | "activityTotalBurn": "活动总消耗", |
| 536 | "activityExerciseTotalDuration": "锻炼总时长", | 579 | "activityExerciseTotalDuration": "锻炼总时长", |
| 537 | "activityStandTotalDuration": "站立总时长", | 580 | "activityStandTotalDuration": "站立总时长", |
| @@ -545,6 +588,23 @@ | @@ -545,6 +588,23 @@ | ||
| 545 | "activityComparedLastMonth": "比上月少34%", | 588 | "activityComparedLastMonth": "比上月少34%", |
| 546 | "activityComparedUnavailable": "比上周-", | 589 | "activityComparedUnavailable": "比上周-", |
| 547 | "activityComparedLastMonthUnavailable": "比上月-", | 590 | "activityComparedLastMonthUnavailable": "比上月-", |
| 591 | + "activitySameAsLastWeek": "与上周持平", | ||
| 592 | + "activityMoreThanLastWeek": "比上周多{percent}%", | ||
| 593 | + "@activityMoreThanLastWeek": { | ||
| 594 | + "placeholders": { | ||
| 595 | + "percent": { | ||
| 596 | + "type": "int" | ||
| 597 | + } | ||
| 598 | + } | ||
| 599 | + }, | ||
| 600 | + "activityLessThanLastWeek": "比上周少{percent}%", | ||
| 601 | + "@activityLessThanLastWeek": { | ||
| 602 | + "placeholders": { | ||
| 603 | + "percent": { | ||
| 604 | + "type": "int" | ||
| 605 | + } | ||
| 606 | + } | ||
| 607 | + }, | ||
| 548 | "activitySameAsLastMonth": "与上月持平", | 608 | "activitySameAsLastMonth": "与上月持平", |
| 549 | "activityMoreThanLastMonth": "比上月多{percent}%", | 609 | "activityMoreThanLastMonth": "比上月多{percent}%", |
| 550 | "@activityMoreThanLastMonth": { | 610 | "@activityMoreThanLastMonth": { |
| @@ -562,6 +622,24 @@ | @@ -562,6 +622,24 @@ | ||
| 562 | } | 622 | } |
| 563 | } | 623 | } |
| 564 | }, | 624 | }, |
| 625 | + "activityTrendTitle": "活动消耗趋势", | ||
| 626 | + "activityPeriodTotalBurn": "本{period}总消耗", | ||
| 627 | + "@activityPeriodTotalBurn": { | ||
| 628 | + "placeholders": { | ||
| 629 | + "period": { | ||
| 630 | + "type": "String" | ||
| 631 | + } | ||
| 632 | + } | ||
| 633 | + }, | ||
| 634 | + "activityDailyAverageBurn": "日均消耗", | ||
| 635 | + "activityPeriodTrendChart": "活动消耗{period}趋势图", | ||
| 636 | + "@activityPeriodTrendChart": { | ||
| 637 | + "placeholders": { | ||
| 638 | + "period": { | ||
| 639 | + "type": "String" | ||
| 640 | + } | ||
| 641 | + } | ||
| 642 | + }, | ||
| 565 | "activityMove": "活动", | 643 | "activityMove": "活动", |
| 566 | "activityExercise": "锻炼", | 644 | "activityExercise": "锻炼", |
| 567 | "activityStand": "站立", | 645 | "activityStand": "站立", |
| @@ -626,6 +704,25 @@ | @@ -626,6 +704,25 @@ | ||
| 626 | "sleepTarget": "目标", | 704 | "sleepTarget": "目标", |
| 627 | "sleepHighest": "最高", | 705 | "sleepHighest": "最高", |
| 628 | "sleepLowest": "最低", | 706 | "sleepLowest": "最低", |
| 707 | + "sleepTrendTitle": "睡眠报告趋势", | ||
| 708 | + "sleepPeriodAverageDuration": "本{period}均睡眠", | ||
| 709 | + "@sleepPeriodAverageDuration": { | ||
| 710 | + "placeholders": { | ||
| 711 | + "period": { | ||
| 712 | + "type": "String" | ||
| 713 | + } | ||
| 714 | + } | ||
| 715 | + }, | ||
| 716 | + "sleepDeepSleepRatio": "深睡占比", | ||
| 717 | + "sleepDurationPeriodTrendChart": "睡眠时长{period}趋势图", | ||
| 718 | + "@sleepDurationPeriodTrendChart": { | ||
| 719 | + "placeholders": { | ||
| 720 | + "period": { | ||
| 721 | + "type": "String" | ||
| 722 | + } | ||
| 723 | + } | ||
| 724 | + }, | ||
| 725 | + "sleepEmptyDateWithWeekday": "-月-日 周-", | ||
| 629 | "sleepQualityDescription": "DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。", | 726 | "sleepQualityDescription": "DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。", |
| 630 | "sleepQualityAttentionRange": "<60分", | 727 | "sleepQualityAttentionRange": "<60分", |
| 631 | "sleepQualityNormalRange": "60~85分", | 728 | "sleepQualityNormalRange": "60~85分", |
| @@ -2169,12 +2169,60 @@ abstract class AppLocalizations { | @@ -2169,12 +2169,60 @@ abstract class AppLocalizations { | ||
| 2169 | /// **'比上周少{count}天'** | 2169 | /// **'比上周少{count}天'** |
| 2170 | String hrvFewerDaysThanLastWeek(int count); | 2170 | String hrvFewerDaysThanLastWeek(int count); |
| 2171 | 2171 | ||
| 2172 | + /// No description provided for @hrvComparedLastMonthUnavailable. | ||
| 2173 | + /// | ||
| 2174 | + /// In zh, this message translates to: | ||
| 2175 | + /// **'比上月少-天'** | ||
| 2176 | + String get hrvComparedLastMonthUnavailable; | ||
| 2177 | + | ||
| 2178 | + /// No description provided for @hrvSameAsLastMonth. | ||
| 2179 | + /// | ||
| 2180 | + /// In zh, this message translates to: | ||
| 2181 | + /// **'与上月一致'** | ||
| 2182 | + String get hrvSameAsLastMonth; | ||
| 2183 | + | ||
| 2184 | + /// No description provided for @hrvMoreDaysThanLastMonth. | ||
| 2185 | + /// | ||
| 2186 | + /// In zh, this message translates to: | ||
| 2187 | + /// **'比上月多{count}天'** | ||
| 2188 | + String hrvMoreDaysThanLastMonth(int count); | ||
| 2189 | + | ||
| 2190 | + /// No description provided for @hrvFewerDaysThanLastMonth. | ||
| 2191 | + /// | ||
| 2192 | + /// In zh, this message translates to: | ||
| 2193 | + /// **'比上月少{count}天'** | ||
| 2194 | + String hrvFewerDaysThanLastMonth(int count); | ||
| 2195 | + | ||
| 2172 | /// No description provided for @hrvUnlockNow. | 2196 | /// No description provided for @hrvUnlockNow. |
| 2173 | /// | 2197 | /// |
| 2174 | /// In zh, this message translates to: | 2198 | /// In zh, this message translates to: |
| 2175 | /// **'立即解锁'** | 2199 | /// **'立即解锁'** |
| 2176 | String get hrvUnlockNow; | 2200 | String get hrvUnlockNow; |
| 2177 | 2201 | ||
| 2202 | + /// No description provided for @hrvTrendTitle. | ||
| 2203 | + /// | ||
| 2204 | + /// In zh, this message translates to: | ||
| 2205 | + /// **'HRV趋势'** | ||
| 2206 | + String get hrvTrendTitle; | ||
| 2207 | + | ||
| 2208 | + /// No description provided for @hrvPeriodAverage. | ||
| 2209 | + /// | ||
| 2210 | + /// In zh, this message translates to: | ||
| 2211 | + /// **'本{period}平均'** | ||
| 2212 | + String hrvPeriodAverage(String period); | ||
| 2213 | + | ||
| 2214 | + /// No description provided for @hrvComparedPreviousPeriod. | ||
| 2215 | + /// | ||
| 2216 | + /// In zh, this message translates to: | ||
| 2217 | + /// **'较上{period}'** | ||
| 2218 | + String hrvComparedPreviousPeriod(String period); | ||
| 2219 | + | ||
| 2220 | + /// No description provided for @hrvPeriodTrendChart. | ||
| 2221 | + /// | ||
| 2222 | + /// In zh, this message translates to: | ||
| 2223 | + /// **'HRV {period}趋势图'** | ||
| 2224 | + String hrvPeriodTrendChart(String period); | ||
| 2225 | + | ||
| 2178 | /// No description provided for @activityTotalBurn. | 2226 | /// No description provided for @activityTotalBurn. |
| 2179 | /// | 2227 | /// |
| 2180 | /// In zh, this message translates to: | 2228 | /// In zh, this message translates to: |
| @@ -2253,6 +2301,24 @@ abstract class AppLocalizations { | @@ -2253,6 +2301,24 @@ abstract class AppLocalizations { | ||
| 2253 | /// **'比上月-'** | 2301 | /// **'比上月-'** |
| 2254 | String get activityComparedLastMonthUnavailable; | 2302 | String get activityComparedLastMonthUnavailable; |
| 2255 | 2303 | ||
| 2304 | + /// No description provided for @activitySameAsLastWeek. | ||
| 2305 | + /// | ||
| 2306 | + /// In zh, this message translates to: | ||
| 2307 | + /// **'与上周持平'** | ||
| 2308 | + String get activitySameAsLastWeek; | ||
| 2309 | + | ||
| 2310 | + /// No description provided for @activityMoreThanLastWeek. | ||
| 2311 | + /// | ||
| 2312 | + /// In zh, this message translates to: | ||
| 2313 | + /// **'比上周多{percent}%'** | ||
| 2314 | + String activityMoreThanLastWeek(int percent); | ||
| 2315 | + | ||
| 2316 | + /// No description provided for @activityLessThanLastWeek. | ||
| 2317 | + /// | ||
| 2318 | + /// In zh, this message translates to: | ||
| 2319 | + /// **'比上周少{percent}%'** | ||
| 2320 | + String activityLessThanLastWeek(int percent); | ||
| 2321 | + | ||
| 2256 | /// No description provided for @activitySameAsLastMonth. | 2322 | /// No description provided for @activitySameAsLastMonth. |
| 2257 | /// | 2323 | /// |
| 2258 | /// In zh, this message translates to: | 2324 | /// In zh, this message translates to: |
| @@ -2271,6 +2337,30 @@ abstract class AppLocalizations { | @@ -2271,6 +2337,30 @@ abstract class AppLocalizations { | ||
| 2271 | /// **'比上月少{percent}%'** | 2337 | /// **'比上月少{percent}%'** |
| 2272 | String activityLessThanLastMonth(int percent); | 2338 | String activityLessThanLastMonth(int percent); |
| 2273 | 2339 | ||
| 2340 | + /// No description provided for @activityTrendTitle. | ||
| 2341 | + /// | ||
| 2342 | + /// In zh, this message translates to: | ||
| 2343 | + /// **'活动消耗趋势'** | ||
| 2344 | + String get activityTrendTitle; | ||
| 2345 | + | ||
| 2346 | + /// No description provided for @activityPeriodTotalBurn. | ||
| 2347 | + /// | ||
| 2348 | + /// In zh, this message translates to: | ||
| 2349 | + /// **'本{period}总消耗'** | ||
| 2350 | + String activityPeriodTotalBurn(String period); | ||
| 2351 | + | ||
| 2352 | + /// No description provided for @activityDailyAverageBurn. | ||
| 2353 | + /// | ||
| 2354 | + /// In zh, this message translates to: | ||
| 2355 | + /// **'日均消耗'** | ||
| 2356 | + String get activityDailyAverageBurn; | ||
| 2357 | + | ||
| 2358 | + /// No description provided for @activityPeriodTrendChart. | ||
| 2359 | + /// | ||
| 2360 | + /// In zh, this message translates to: | ||
| 2361 | + /// **'活动消耗{period}趋势图'** | ||
| 2362 | + String activityPeriodTrendChart(String period); | ||
| 2363 | + | ||
| 2274 | /// No description provided for @activityMove. | 2364 | /// No description provided for @activityMove. |
| 2275 | /// | 2365 | /// |
| 2276 | /// In zh, this message translates to: | 2366 | /// In zh, this message translates to: |
| @@ -2433,6 +2523,36 @@ abstract class AppLocalizations { | @@ -2433,6 +2523,36 @@ abstract class AppLocalizations { | ||
| 2433 | /// **'最低'** | 2523 | /// **'最低'** |
| 2434 | String get sleepLowest; | 2524 | String get sleepLowest; |
| 2435 | 2525 | ||
| 2526 | + /// No description provided for @sleepTrendTitle. | ||
| 2527 | + /// | ||
| 2528 | + /// In zh, this message translates to: | ||
| 2529 | + /// **'睡眠报告趋势'** | ||
| 2530 | + String get sleepTrendTitle; | ||
| 2531 | + | ||
| 2532 | + /// No description provided for @sleepPeriodAverageDuration. | ||
| 2533 | + /// | ||
| 2534 | + /// In zh, this message translates to: | ||
| 2535 | + /// **'本{period}均睡眠'** | ||
| 2536 | + String sleepPeriodAverageDuration(String period); | ||
| 2537 | + | ||
| 2538 | + /// No description provided for @sleepDeepSleepRatio. | ||
| 2539 | + /// | ||
| 2540 | + /// In zh, this message translates to: | ||
| 2541 | + /// **'深睡占比'** | ||
| 2542 | + String get sleepDeepSleepRatio; | ||
| 2543 | + | ||
| 2544 | + /// No description provided for @sleepDurationPeriodTrendChart. | ||
| 2545 | + /// | ||
| 2546 | + /// In zh, this message translates to: | ||
| 2547 | + /// **'睡眠时长{period}趋势图'** | ||
| 2548 | + String sleepDurationPeriodTrendChart(String period); | ||
| 2549 | + | ||
| 2550 | + /// No description provided for @sleepEmptyDateWithWeekday. | ||
| 2551 | + /// | ||
| 2552 | + /// In zh, this message translates to: | ||
| 2553 | + /// **'-月-日 周-'** | ||
| 2554 | + String get sleepEmptyDateWithWeekday; | ||
| 2555 | + | ||
| 2436 | /// No description provided for @sleepQualityDescription. | 2556 | /// No description provided for @sleepQualityDescription. |
| 2437 | /// | 2557 | /// |
| 2438 | /// In zh, this message translates to: | 2558 | /// In zh, this message translates to: |
| @@ -1196,9 +1196,43 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1196,9 +1196,43 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1196 | } | 1196 | } |
| 1197 | 1197 | ||
| 1198 | @override | 1198 | @override |
| 1199 | + String get hrvComparedLastMonthUnavailable => 'Compared with last month: -'; | ||
| 1200 | + | ||
| 1201 | + @override | ||
| 1202 | + String get hrvSameAsLastMonth => 'Same as last month'; | ||
| 1203 | + | ||
| 1204 | + @override | ||
| 1205 | + String hrvMoreDaysThanLastMonth(int count) { | ||
| 1206 | + return '$count more days than last month'; | ||
| 1207 | + } | ||
| 1208 | + | ||
| 1209 | + @override | ||
| 1210 | + String hrvFewerDaysThanLastMonth(int count) { | ||
| 1211 | + return '$count fewer days than last month'; | ||
| 1212 | + } | ||
| 1213 | + | ||
| 1214 | + @override | ||
| 1199 | String get hrvUnlockNow => 'Unlock Now'; | 1215 | String get hrvUnlockNow => 'Unlock Now'; |
| 1200 | 1216 | ||
| 1201 | @override | 1217 | @override |
| 1218 | + String get hrvTrendTitle => 'HRV Trend'; | ||
| 1219 | + | ||
| 1220 | + @override | ||
| 1221 | + String hrvPeriodAverage(String period) { | ||
| 1222 | + return 'This $period average'; | ||
| 1223 | + } | ||
| 1224 | + | ||
| 1225 | + @override | ||
| 1226 | + String hrvComparedPreviousPeriod(String period) { | ||
| 1227 | + return 'vs previous $period'; | ||
| 1228 | + } | ||
| 1229 | + | ||
| 1230 | + @override | ||
| 1231 | + String hrvPeriodTrendChart(String period) { | ||
| 1232 | + return 'HRV $period Trend Chart'; | ||
| 1233 | + } | ||
| 1234 | + | ||
| 1235 | + @override | ||
| 1202 | String get activityTotalBurn => 'Total Activity Burn'; | 1236 | String get activityTotalBurn => 'Total Activity Burn'; |
| 1203 | 1237 | ||
| 1204 | @override | 1238 | @override |
| @@ -1239,6 +1273,19 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1239,6 +1273,19 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1239 | 'Compared with last month: -'; | 1273 | 'Compared with last month: -'; |
| 1240 | 1274 | ||
| 1241 | @override | 1275 | @override |
| 1276 | + String get activitySameAsLastWeek => 'Same as last week'; | ||
| 1277 | + | ||
| 1278 | + @override | ||
| 1279 | + String activityMoreThanLastWeek(int percent) { | ||
| 1280 | + return '$percent% more than last week'; | ||
| 1281 | + } | ||
| 1282 | + | ||
| 1283 | + @override | ||
| 1284 | + String activityLessThanLastWeek(int percent) { | ||
| 1285 | + return '$percent% less than last week'; | ||
| 1286 | + } | ||
| 1287 | + | ||
| 1288 | + @override | ||
| 1242 | String get activitySameAsLastMonth => 'Same as last month'; | 1289 | String get activitySameAsLastMonth => 'Same as last month'; |
| 1243 | 1290 | ||
| 1244 | @override | 1291 | @override |
| @@ -1252,6 +1299,22 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1252,6 +1299,22 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1252 | } | 1299 | } |
| 1253 | 1300 | ||
| 1254 | @override | 1301 | @override |
| 1302 | + String get activityTrendTitle => 'Activity Burn Trend'; | ||
| 1303 | + | ||
| 1304 | + @override | ||
| 1305 | + String activityPeriodTotalBurn(String period) { | ||
| 1306 | + return 'This $period total burn'; | ||
| 1307 | + } | ||
| 1308 | + | ||
| 1309 | + @override | ||
| 1310 | + String get activityDailyAverageBurn => 'Daily average burn'; | ||
| 1311 | + | ||
| 1312 | + @override | ||
| 1313 | + String activityPeriodTrendChart(String period) { | ||
| 1314 | + return 'Activity Burn $period Trend Chart'; | ||
| 1315 | + } | ||
| 1316 | + | ||
| 1317 | + @override | ||
| 1255 | String get activityMove => 'Move'; | 1318 | String get activityMove => 'Move'; |
| 1256 | 1319 | ||
| 1257 | @override | 1320 | @override |
| @@ -1341,6 +1404,25 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1341,6 +1404,25 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1341 | String get sleepLowest => 'Lowest'; | 1404 | String get sleepLowest => 'Lowest'; |
| 1342 | 1405 | ||
| 1343 | @override | 1406 | @override |
| 1407 | + String get sleepTrendTitle => 'Sleep Report Trend'; | ||
| 1408 | + | ||
| 1409 | + @override | ||
| 1410 | + String sleepPeriodAverageDuration(String period) { | ||
| 1411 | + return 'This $period average sleep'; | ||
| 1412 | + } | ||
| 1413 | + | ||
| 1414 | + @override | ||
| 1415 | + String get sleepDeepSleepRatio => 'Deep sleep ratio'; | ||
| 1416 | + | ||
| 1417 | + @override | ||
| 1418 | + String sleepDurationPeriodTrendChart(String period) { | ||
| 1419 | + return 'Sleep Duration $period Trend Chart'; | ||
| 1420 | + } | ||
| 1421 | + | ||
| 1422 | + @override | ||
| 1423 | + String get sleepEmptyDateWithWeekday => '-'; | ||
| 1424 | + | ||
| 1425 | + @override | ||
| 1344 | String get sleepQualityDescription => | 1426 | String get sleepQualityDescription => |
| 1345 | 'DoubleFeel calculates your daily sleep quality score from sleep duration, sleep stages, deep sleep and recovery, nighttime heart rate, and HRV changes.\nThis score helps you understand your recovery and sleep performance more clearly.'; | 1427 | 'DoubleFeel calculates your daily sleep quality score from sleep duration, sleep stages, deep sleep and recovery, nighttime heart rate, and HRV changes.\nThis score helps you understand your recovery and sleep performance more clearly.'; |
| 1346 | 1428 |
| @@ -1132,9 +1132,43 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1132,9 +1132,43 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1132 | } | 1132 | } |
| 1133 | 1133 | ||
| 1134 | @override | 1134 | @override |
| 1135 | + String get hrvComparedLastMonthUnavailable => '比上月少-天'; | ||
| 1136 | + | ||
| 1137 | + @override | ||
| 1138 | + String get hrvSameAsLastMonth => '与上月一致'; | ||
| 1139 | + | ||
| 1140 | + @override | ||
| 1141 | + String hrvMoreDaysThanLastMonth(int count) { | ||
| 1142 | + return '比上月多$count天'; | ||
| 1143 | + } | ||
| 1144 | + | ||
| 1145 | + @override | ||
| 1146 | + String hrvFewerDaysThanLastMonth(int count) { | ||
| 1147 | + return '比上月少$count天'; | ||
| 1148 | + } | ||
| 1149 | + | ||
| 1150 | + @override | ||
| 1135 | String get hrvUnlockNow => '立即解锁'; | 1151 | String get hrvUnlockNow => '立即解锁'; |
| 1136 | 1152 | ||
| 1137 | @override | 1153 | @override |
| 1154 | + String get hrvTrendTitle => 'HRV趋势'; | ||
| 1155 | + | ||
| 1156 | + @override | ||
| 1157 | + String hrvPeriodAverage(String period) { | ||
| 1158 | + return '本$period平均'; | ||
| 1159 | + } | ||
| 1160 | + | ||
| 1161 | + @override | ||
| 1162 | + String hrvComparedPreviousPeriod(String period) { | ||
| 1163 | + return '较上$period'; | ||
| 1164 | + } | ||
| 1165 | + | ||
| 1166 | + @override | ||
| 1167 | + String hrvPeriodTrendChart(String period) { | ||
| 1168 | + return 'HRV $period趋势图'; | ||
| 1169 | + } | ||
| 1170 | + | ||
| 1171 | + @override | ||
| 1138 | String get activityTotalBurn => '活动总消耗'; | 1172 | String get activityTotalBurn => '活动总消耗'; |
| 1139 | 1173 | ||
| 1140 | @override | 1174 | @override |
| @@ -1174,6 +1208,19 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1174,6 +1208,19 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1174 | String get activityComparedLastMonthUnavailable => '比上月-'; | 1208 | String get activityComparedLastMonthUnavailable => '比上月-'; |
| 1175 | 1209 | ||
| 1176 | @override | 1210 | @override |
| 1211 | + String get activitySameAsLastWeek => '与上周持平'; | ||
| 1212 | + | ||
| 1213 | + @override | ||
| 1214 | + String activityMoreThanLastWeek(int percent) { | ||
| 1215 | + return '比上周多$percent%'; | ||
| 1216 | + } | ||
| 1217 | + | ||
| 1218 | + @override | ||
| 1219 | + String activityLessThanLastWeek(int percent) { | ||
| 1220 | + return '比上周少$percent%'; | ||
| 1221 | + } | ||
| 1222 | + | ||
| 1223 | + @override | ||
| 1177 | String get activitySameAsLastMonth => '与上月持平'; | 1224 | String get activitySameAsLastMonth => '与上月持平'; |
| 1178 | 1225 | ||
| 1179 | @override | 1226 | @override |
| @@ -1187,6 +1234,22 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1187,6 +1234,22 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1187 | } | 1234 | } |
| 1188 | 1235 | ||
| 1189 | @override | 1236 | @override |
| 1237 | + String get activityTrendTitle => '活动消耗趋势'; | ||
| 1238 | + | ||
| 1239 | + @override | ||
| 1240 | + String activityPeriodTotalBurn(String period) { | ||
| 1241 | + return '本$period总消耗'; | ||
| 1242 | + } | ||
| 1243 | + | ||
| 1244 | + @override | ||
| 1245 | + String get activityDailyAverageBurn => '日均消耗'; | ||
| 1246 | + | ||
| 1247 | + @override | ||
| 1248 | + String activityPeriodTrendChart(String period) { | ||
| 1249 | + return '活动消耗$period趋势图'; | ||
| 1250 | + } | ||
| 1251 | + | ||
| 1252 | + @override | ||
| 1190 | String get activityMove => '活动'; | 1253 | String get activityMove => '活动'; |
| 1191 | 1254 | ||
| 1192 | @override | 1255 | @override |
| @@ -1276,6 +1339,25 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1276,6 +1339,25 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1276 | String get sleepLowest => '最低'; | 1339 | String get sleepLowest => '最低'; |
| 1277 | 1340 | ||
| 1278 | @override | 1341 | @override |
| 1342 | + String get sleepTrendTitle => '睡眠报告趋势'; | ||
| 1343 | + | ||
| 1344 | + @override | ||
| 1345 | + String sleepPeriodAverageDuration(String period) { | ||
| 1346 | + return '本$period均睡眠'; | ||
| 1347 | + } | ||
| 1348 | + | ||
| 1349 | + @override | ||
| 1350 | + String get sleepDeepSleepRatio => '深睡占比'; | ||
| 1351 | + | ||
| 1352 | + @override | ||
| 1353 | + String sleepDurationPeriodTrendChart(String period) { | ||
| 1354 | + return '睡眠时长$period趋势图'; | ||
| 1355 | + } | ||
| 1356 | + | ||
| 1357 | + @override | ||
| 1358 | + String get sleepEmptyDateWithWeekday => '-月-日 周-'; | ||
| 1359 | + | ||
| 1360 | + @override | ||
| 1279 | String get sleepQualityDescription => | 1361 | String get sleepQualityDescription => |
| 1280 | 'DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。'; | 1362 | 'DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。'; |
| 1281 | 1363 |
-
Please register or login to post a comment