Commit c8044b546a999ee92afd87a081ac8e3728bcad9a

Authored by 刘宏哲
1 parent 7293cdd4

feat(app): update ui

Showing 27 changed files with 809 additions and 391 deletions
... ... @@ -317,215 +317,4 @@ class _SleepRange {
final DateTime start;
final DateTime end;
}
class MockActivityBurnReportDataSource implements ActivityBurnReportDataSource {
const MockActivityBurnReportDataSource();
@override
Future<ActivityBurnReport> fetchDailyReport(
DateTime date, {
int? targetUserId,
}) async {
await Future<void>.delayed(const Duration(milliseconds: 150));
if (date.day == 13) {
return ActivityBurnReport.empty(date);
}
final start = DateTime(date.year, date.month, date.day);
final points = <ActivityBurnHeartRatePoint>[
for (var i = 0; i < _mockBpms.length; i++)
ActivityBurnHeartRatePoint(
time: start.add(Duration(minutes: i * 12)),
bpm: _mockBpms[i],
),
];
return ActivityBurnReport(
date: date,
activeEnergy: const ActivityBurnMetric(value: 188, goal: 500),
exerciseMinutes: const ActivityBurnMetric(value: 15, goal: 30),
standHours: const ActivityBurnMetric(value: 7, goal: 12),
heartRate: ActivityBurnHeartRateSummary(
startTime: start,
endTime: start.add(const Duration(hours: 18)),
sleepStartTime: start,
sleepEndTime: start.add(const Duration(hours: 5)),
userAge: null,
points: points,
),
);
}
@override
Future<WeeklyActivityBurnReport> fetchWeeklyReport(
DateTime weekStart, {
int? targetUserId,
}) async {
await Future<void>.delayed(const Duration(milliseconds: 150));
final normalizedStart =
DateTime(weekStart.year, weekStart.month, weekStart.day);
if (normalizedStart.day == 4) {
return WeeklyActivityBurnReport.empty(normalizedStart);
}
final energy = [176, 217, 0, 0, 0, 0, 11];
final exercise = [12, 17, 0, 0, 0, 0, 0];
final stand = [7, 5, 0, 0, 0, 0, 0];
final days = <ActivityBurnReport>[];
for (var i = 0; i < 7; i++) {
final date = normalizedStart.add(Duration(days: i));
if (energy[i] == 0 && exercise[i] == 0 && stand[i] == 0) {
days.add(ActivityBurnReport.empty(date));
continue;
}
days.add(
ActivityBurnReport(
date: date,
activeEnergy: ActivityBurnMetric(value: energy[i], goal: 200),
exerciseMinutes: ActivityBurnMetric(value: exercise[i], goal: 15),
standHours: ActivityBurnMetric(value: stand[i], goal: 4),
),
);
}
return WeeklyActivityBurnReport(
weekStart: normalizedStart,
weekEnd: normalizedStart.add(const Duration(days: 6)),
days: days,
);
}
@override
Future<MonthlyActivityBurnReport> fetchMonthlyReport(
DateTime monthStart, {
int? targetUserId,
}) async {
await Future<void>.delayed(const Duration(milliseconds: 150));
final start = DateTime(monthStart.year, monthStart.month);
final end = DateTime(monthStart.year, monthStart.month + 1, 0);
final energy = [
245,
319,
207,
276,
303,
84,
358,
144,
285,
198,
342,
217,
108,
];
final exercise = [18, 20, 15, 17, 16, 6, 24, 8, 19, 12, 20, 17, 9];
final stand = [8, 9, 7, 8, 8, 3, 10, 5, 9, 6, 8, 5, 4];
final days = <ActivityBurnReport>[];
for (var i = 0; i < end.day; i++) {
final date = start.add(Duration(days: i));
if (i >= energy.length) {
days.add(ActivityBurnReport.empty(date));
continue;
}
days.add(
ActivityBurnReport(
date: date,
activeEnergy: ActivityBurnMetric(value: energy[i], goal: 200),
exerciseMinutes: ActivityBurnMetric(value: exercise[i], goal: 15),
standHours: ActivityBurnMetric(value: stand[i], goal: 4),
),
);
}
return MonthlyActivityBurnReport(
monthStart: start,
monthEnd: end,
days: days,
);
}
}
const _mockBpms = [
53.0,
56.0,
51.0,
58.0,
55.0,
60.0,
52.0,
59.0,
54.0,
61.0,
55.0,
63.0,
58.0,
66.0,
62.0,
70.0,
64.0,
74.0,
92.0,
108.0,
96.0,
124.0,
172.0,
136.0,
188.0,
151.0,
90.0,
82.0,
91.0,
88.0,
94.0,
90.0,
91.0,
89.0,
92.0,
87.0,
91.0,
114.0,
139.0,
120.0,
112.0,
95.0,
85.0,
78.0,
126.0,
93.0,
88.0,
104.0,
112.0,
105.0,
98.0,
87.0,
74.0,
61.0,
55.0,
66.0,
48.0,
62.0,
51.0,
43.0,
59.0,
47.0,
54.0,
40.0,
58.0,
46.0,
50.0,
44.0,
61.0,
57.0,
42.0,
55.0,
49.0,
60.0,
52.0,
64.0,
];
}
\ No newline at end of file
... ...
... ... @@ -7,9 +7,14 @@ class ActivityBurnMetric {
final int value;
final int goal;
double get rawProgress {
if (goal <= 0) return 0;
return value / goal;
}
double get progress {
if (goal <= 0) return 0;
return (value / goal).clamp(0, 1).toDouble();
return rawProgress.clamp(0, 1).toDouble();
}
}
... ...
... ... @@ -21,11 +21,13 @@ class ActivityBurnReportView extends StatelessWidget {
required this.logic,
required this.isVip,
required this.onSubscribe,
this.loadingIndicator = const ReportLoadingIndicator(),
});
final ActivityBurnReportLogic logic;
final bool isVip;
final ValueChanged<String> onSubscribe;
final Widget loadingIndicator;
@override
Widget build(BuildContext context) {
... ... @@ -59,7 +61,7 @@ class ActivityBurnReportView extends StatelessWidget {
child: Obx(
() {
if (logic.isLoading.value) {
return const ReportLoadingIndicator();
return loadingIndicator;
}
return CustomScrollView(
physics: const ClampingScrollPhysics(),
... ...
... ... @@ -93,9 +93,9 @@ class _RingProgress {
stand = 0;
factory _RingProgress.fromReport(ActivityBurnReport? report) => _RingProgress(
active: report?.activeEnergy?.progress ?? 0,
exercise: report?.exerciseMinutes?.progress ?? 0,
stand: report?.standHours?.progress ?? 0,
active: report?.activeEnergy?.rawProgress ?? 0,
exercise: report?.exerciseMinutes?.rawProgress ?? 0,
stand: report?.standHours?.rawProgress ?? 0,
);
final double active;
... ... @@ -253,7 +253,16 @@ class _ActivityBurnRingPainter extends CustomPainter {
required double progress,
required double startAngle,
}) {
if (progress <= 0) return;
final rect = Rect.fromCircle(center: center, radius: radius);
final safeProgress = math.max(0, progress);
final hasOverflow = safeProgress > 1;
final baseProgress = hasOverflow ? 1.0 : safeProgress;
final overflowProgress = safeProgress - 1;
final overflowLap = overflowProgress % 1;
final visibleOverflowLap =
hasOverflow && overflowLap == 0 ? 1.0 : overflowLap;
final progressPaint = Paint()
..color = progressColor
... ... @@ -261,16 +270,86 @@ class _ActivityBurnRingPainter extends CustomPainter {
..strokeWidth = width
..strokeCap = StrokeCap.round;
if (progress <= 0) return;
final fullLapPaint = Paint()
..color = progressColor
..style = PaintingStyle.stroke
..strokeWidth = width
..strokeCap = StrokeCap.butt;
if (baseProgress >= 1) {
canvas.drawArc(
rect,
startAngle,
math.pi * 2,
false,
fullLapPaint,
);
} else {
final baseEndAngle = startAngle + math.pi * 2 * baseProgress;
_drawRingHeadShadow(
canvas,
center: center,
radius: radius,
width: width,
angle: baseEndAngle,
);
canvas.drawArc(
rect,
startAngle,
math.pi * 2 * baseProgress,
false,
progressPaint,
);
}
if (!hasOverflow || visibleOverflowLap <= 0) return;
final overflowEndAngle = startAngle + math.pi * 2 * visibleOverflowLap;
_drawRingHeadShadow(
canvas,
center: center,
radius: radius,
width: width,
angle: overflowEndAngle,
);
canvas.drawArc(
rect,
startAngle,
math.pi * 2 * progress,
math.pi * 2 * visibleOverflowLap,
false,
progressPaint,
);
}
void _drawRingHeadShadow(
Canvas canvas, {
required Offset center,
required double radius,
required double width,
required double angle,
}) {
final headCenter = Offset(
center.dx + math.cos(angle) * radius,
center.dy + math.sin(angle) * radius,
);
final tangent = Offset(-math.sin(angle), math.cos(angle));
final ringClip = Path()
..fillType = PathFillType.evenOdd
..addOval(Rect.fromCircle(center: center, radius: radius + width / 2))
..addOval(Rect.fromCircle(center: center, radius: radius - width / 2));
canvas.save();
canvas.clipPath(ringClip);
canvas.drawCircle(
headCenter + tangent * 2,
width / 2,
Paint()
..color = Colors.black.withValues(alpha: 0.36)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2),
);
canvas.restore();
}
@override
bool shouldRepaint(covariant _ActivityBurnRingPainter oldDelegate) {
return oldDelegate.progress != progress || oldDelegate.hasData != hasData;
... ...
... ... @@ -558,9 +558,11 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> {
if (percent == null || !widget.report.hasData) {
return context.l10n.activityComparedUnavailable;
}
if (percent == 0) return '与上周持平';
final direction = percent > 0 ? '多' : '少';
return '比上周$direction${percent.abs()}%';
if (percent == 0) return context.l10n.activitySameAsLastWeek;
final absPercent = percent.abs();
return percent > 0
? context.l10n.activityMoreThanLastWeek(absPercent)
: context.l10n.activityLessThanLastWeek(absPercent);
}
double _barToY(int value, double maxY) {
... ...
... ... @@ -43,13 +43,21 @@ class FriendsController extends GetxController {
unawaited(refreshData());
}
void markPageVisible() {
void setPageVisible(bool visible) {
if (visible) {
_markPageVisible();
} else {
_markPageHidden();
}
}
void _markPageVisible() {
if (_isPageVisible) return;
_isPageVisible = true;
ta.track('enter_doublefeel_friend_page');
}
void markPageHidden() {
void _markPageHidden() {
_isPageVisible = false;
}
... ...
... ... @@ -110,7 +110,8 @@ class FriendsRepositoryImpl implements FriendsRepository {
steps: healthData?.totalSteps == null
? null
: l10n.friendsStepCount(healthData!.totalSteps!),
stressState: FriendStressState.fromValue(healthData?.hrvState),
stressState:
FriendStressState.fromValue(healthData?.comprehensiveStressState),
isOnWatchFace: friend.isShowInDial,
);
}
... ... @@ -125,7 +126,8 @@ class FriendsRepositoryImpl implements FriendsRepository {
steps: healthData.totalSteps == null
? null
: l10n.friendsStepCount(healthData.totalSteps!),
stressState: FriendStressState.fromValue(healthData.hrvState),
stressState:
FriendStressState.fromValue(healthData.comprehensiveStressState),
);
}
... ...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../../core/constants/intent_keys.dart';
import '../../../../data/local/user_preferences_storage.dart';
... ... @@ -154,6 +155,23 @@ class _HealthTrendContentState extends State<HealthTrendContent>
}
}
class _HealthTrendLoadingIndicator extends StatelessWidget {
const _HealthTrendLoadingIndicator();
@override
Widget build(BuildContext context) {
return Center(
child: Lottie.asset(
'assets/lottie/loading.json',
width: 130,
height: 90,
repeat: true,
animate: true,
),
);
}
}
class _HrvTrendSection extends StatefulWidget {
const _HrvTrendSection({
required this.query,
... ... @@ -247,6 +265,7 @@ class _HrvTrendSectionState extends State<_HrvTrendSection> {
logic: _logic,
isVip: widget.isVip,
onSubscribe: widget.onSubscribe,
loadingIndicator: const _HealthTrendLoadingIndicator(),
);
}
... ... @@ -346,6 +365,7 @@ class _ActivityBurnTrendSectionState extends State<_ActivityBurnTrendSection> {
logic: _logic,
isVip: widget.isVip,
onSubscribe: widget.onSubscribe,
loadingIndicator: const _HealthTrendLoadingIndicator(),
);
}
... ... @@ -444,6 +464,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> {
logic: _logic,
isVip: widget.isVip,
onSubscribe: widget.onSubscribe,
loadingIndicator: const _HealthTrendLoadingIndicator(),
);
}
... ...
... ... @@ -21,6 +21,11 @@ import '../../report_common/models/report_period.dart';
import 'today_controller.dart';
import 'trend/trend_controller.dart';
enum TrendEntrySource {
bottomTab,
jump,
}
class HomeController extends GetxController {
static const trendTabIndex = 1;
static const friendsTabIndex = 2;
... ... @@ -136,23 +141,16 @@ class HomeController extends GetxController {
int index, {
TrendEntrySource trendEntrySource = TrendEntrySource.bottomTab,
}) {
final previousIndex = selectedIndex.value;
selectedIndex.value = index;
if (index == trendTabIndex) {
final trendController = Get.find<TrendController>();
if (previousIndex != trendTabIndex) {
trendController.markPageVisible(source: trendEntrySource);
} else {
trendController.updateEntrySource(trendEntrySource);
}
} else if (previousIndex == trendTabIndex && index != trendTabIndex) {
Get.find<TrendController>().markPageHidden();
}
if (index == friendsTabIndex && previousIndex != friendsTabIndex) {
Get.find<FriendsController>().markPageVisible();
} else if (previousIndex == friendsTabIndex && index != friendsTabIndex) {
Get.find<FriendsController>().markPageHidden();
}
final trendController = Get.find<TrendController>();
final friendsController = Get.find<FriendsController>();
trendController.setPageVisible(
index == trendTabIndex,
resetQueryOnShow: trendEntrySource == TrendEntrySource.bottomTab,
);
friendsController.setPageVisible(index == friendsTabIndex);
switch (index) {
case 0:
Get.find<TodayController>().refreshTab();
... ...
... ... @@ -8,6 +8,7 @@ import '../../../../../core/result/app_result.dart';
import '../../../../../data/models/friend/friend_models.dart';
import '../../../health_trend/controllers/health_trend_analytics.dart';
import '../../../health_trend/controllers/health_trend_control.dart';
import '../../../report_common/config/report_date_range_config.dart';
import '../../../report_common/models/health_report_query.dart';
import '../../../report_common/models/report_period.dart';
import '../../widgets/trend/trend_friend_select_bottom_sheet.dart';
... ... @@ -20,11 +21,6 @@ enum TrendType {
int get tabIndex => index;
}
enum TrendEntrySource {
bottomTab,
jump,
}
/// 趋势页顶层 Controller,仅负责:
/// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex)
class TrendController extends GetxController with HealthTrendControl {
... ... @@ -33,7 +29,6 @@ class TrendController extends GetxController with HealthTrendControl {
final FriendApi _friendApi;
bool _isPageVisible = false;
final refreshToken = 0.obs;
final entrySource = TrendEntrySource.bottomTab.obs;
// 当前查看的用户。null 表示查看自己;非 null 表示查看指定用户。
final targetUserId = RxnInt();
... ... @@ -147,30 +142,34 @@ class TrendController extends GetxController with HealthTrendControl {
);
}
void updateEntrySource(TrendEntrySource source) {
entrySource.value = source;
}
void markPageVisible({
TrendEntrySource source = TrendEntrySource.bottomTab,
void setPageVisible(
bool visible, {
bool resetQueryOnShow = false,
}) {
updateEntrySource(source);
if (source == TrendEntrySource.bottomTab) {
_resetQueryForBottomTabEntry();
if (!visible) {
_markPageHidden();
return;
}
if (resetQueryOnShow) {
_resetToDefaultQuery();
}
_markPageVisible();
}
void _markPageVisible() {
if (_isPageVisible) return;
_isPageVisible = true;
refreshToken.value++;
_trackEnterPage();
}
void markPageHidden() {
void _markPageHidden() {
_isPageVisible = false;
}
void _resetQueryForBottomTabEntry() {
void _resetToDefaultQuery() {
changeType(TrendType.hrv.tabIndex);
changeQuery(ReportPeriod.week, DateTime.now());
changeQuery(ReportPeriod.week, ReportDateRangeConfig.lastWeekStart());
}
void _trackEnterPage() {
... ...
import 'package:cached_network_image/cached_network_image.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -55,11 +56,11 @@ class _HomeTrendHeader extends GetView<TrendController> {
child: Stack(
alignment: Alignment.center,
children: [
const Align(
Align(
alignment: Alignment.centerLeft,
child: Text(
'趋势',
style: TextStyle(
l10n.tabTrend,
style: const TextStyle(
color: Color(0xFF0F0F11),
fontSize: 24,
fontWeight: FontWeight.w600,
... ...
import 'package:fl_chart/fl_chart.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -62,15 +63,16 @@ class _ActivitySummaryCard extends GetView<ActivityController> {
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final periodLabel = controller.currentPeriod.value.label;
final periodLabel =
_periodLabel(context, controller.currentPeriod.value);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'活动消耗趋势',
style: TextStyle(
Text(
context.l10n.activityTrendTitle,
style: const TextStyle(
color: _h1,
fontSize: 16,
fontWeight: FontWeight.w600,
... ... @@ -84,14 +86,14 @@ class _ActivitySummaryCard extends GetView<ActivityController> {
Row(
children: [
_StatItem(
label: '本$periodLabel总消耗',
label: context.l10n.activityPeriodTotalBurn(periodLabel),
value: controller.totalBurn.value,
unit: 'kcal',
color: _activeColor,
),
const SizedBox(width: 24),
_StatItem(
label: '日均消耗',
label: context.l10n.activityDailyAverageBurn,
value: controller.averageBurn.value,
unit: 'kcal',
color: const Color(0xFF3BD49D),
... ... @@ -103,6 +105,13 @@ class _ActivitySummaryCard extends GetView<ActivityController> {
}),
);
}
String _periodLabel(BuildContext context, TrendPeriod period) =>
switch (period) {
TrendPeriod.week => context.l10n.reportPeriodWeek,
TrendPeriod.month => context.l10n.reportPeriodMonth,
TrendPeriod.year => context.l10n.reportPeriodYear,
};
}
class _StatItem extends StatelessWidget {
... ... @@ -173,11 +182,12 @@ class _ActivityChartCard extends GetView<ActivityController> {
),
child: Obx(() {
final period = controller.currentPeriod.value;
final periodLabel = _periodLabel(context, period);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'活动消耗${period.label}趋势图',
context.l10n.activityPeriodTrendChart(periodLabel),
style: const TextStyle(
color: _h1,
fontSize: 14,
... ... @@ -195,6 +205,13 @@ class _ActivityChartCard extends GetView<ActivityController> {
);
}
String _periodLabel(BuildContext context, TrendPeriod period) =>
switch (period) {
TrendPeriod.week => context.l10n.reportPeriodWeek,
TrendPeriod.month => context.l10n.reportPeriodMonth,
TrendPeriod.year => context.l10n.reportPeriodYear,
};
Widget _buildChart() {
if (controller.chartData.isEmpty) {
return const Center(
... ... @@ -239,10 +256,10 @@ class _ActivityChartCard extends GetView<ActivityController> {
),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
... ...
import 'package:fl_chart/fl_chart.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -65,15 +66,16 @@ class _HrvSummaryCard extends GetView<HrvController> {
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final periodLabel = controller.currentPeriod.value.label;
final periodLabel =
_periodLabel(context, controller.currentPeriod.value);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'HRV趋势',
style: TextStyle(
Text(
context.l10n.hrvTrendTitle,
style: const TextStyle(
color: _h1,
fontSize: 16,
fontWeight: FontWeight.w600,
... ... @@ -87,14 +89,14 @@ class _HrvSummaryCard extends GetView<HrvController> {
Row(
children: [
_StatItem(
label: '本$periodLabel平均',
label: context.l10n.hrvPeriodAverage(periodLabel),
value: controller.averageHrv.value,
unit: 'ms',
color: _brandColor,
),
const SizedBox(width: 24),
_StatItem(
label: '较上$periodLabel',
label: context.l10n.hrvComparedPreviousPeriod(periodLabel),
value: controller.changeHrv.value,
unit: 'ms',
color: const Color(0xFF3BD49D),
... ... @@ -106,6 +108,13 @@ class _HrvSummaryCard extends GetView<HrvController> {
}),
);
}
String _periodLabel(BuildContext context, TrendPeriod period) =>
switch (period) {
TrendPeriod.week => context.l10n.reportPeriodWeek,
TrendPeriod.month => context.l10n.reportPeriodMonth,
TrendPeriod.year => context.l10n.reportPeriodYear,
};
}
class _StatItem extends StatelessWidget {
... ... @@ -179,11 +188,12 @@ class _HrvChartCard extends GetView<HrvController> {
),
child: Obx(() {
final period = controller.currentPeriod.value;
final periodLabel = _periodLabel(context, period);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'HRV ${period.label}趋势图',
context.l10n.hrvPeriodTrendChart(periodLabel),
style: const TextStyle(
color: _h1,
fontSize: 14,
... ... @@ -201,6 +211,13 @@ class _HrvChartCard extends GetView<HrvController> {
);
}
String _periodLabel(BuildContext context, TrendPeriod period) =>
switch (period) {
TrendPeriod.week => context.l10n.reportPeriodWeek,
TrendPeriod.month => context.l10n.reportPeriodMonth,
TrendPeriod.year => context.l10n.reportPeriodYear,
};
Widget _buildChart(TrendPeriod period) {
if (controller.chartData.isEmpty) {
return const Center(
... ...
import 'package:fl_chart/fl_chart.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -62,15 +63,16 @@ class _SleepSummaryCard extends GetView<SleepController> {
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final periodLabel = controller.currentPeriod.value.label;
final periodLabel =
_periodLabel(context, controller.currentPeriod.value);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'睡眠报告趋势',
style: TextStyle(
Text(
context.l10n.sleepTrendTitle,
style: const TextStyle(
color: _h1,
fontSize: 16,
fontWeight: FontWeight.w600,
... ... @@ -84,14 +86,14 @@ class _SleepSummaryCard extends GetView<SleepController> {
Row(
children: [
_StatItem(
label: '本$periodLabel均睡眠',
label: context.l10n.sleepPeriodAverageDuration(periodLabel),
value: controller.averageSleep.value,
unit: 'h',
color: _sleepColor,
),
const SizedBox(width: 24),
_StatItem(
label: '深睡占比',
label: context.l10n.sleepDeepSleepRatio,
value: controller.deepSleepRatio.value,
unit: '%',
color: const Color(0xFF845EEE),
... ... @@ -103,6 +105,13 @@ class _SleepSummaryCard extends GetView<SleepController> {
}),
);
}
String _periodLabel(BuildContext context, TrendPeriod period) =>
switch (period) {
TrendPeriod.week => context.l10n.reportPeriodWeek,
TrendPeriod.month => context.l10n.reportPeriodMonth,
TrendPeriod.year => context.l10n.reportPeriodYear,
};
}
class _StatItem extends StatelessWidget {
... ... @@ -170,11 +179,12 @@ class _SleepChartCard extends GetView<SleepController> {
),
child: Obx(() {
final period = controller.currentPeriod.value;
final periodLabel = _periodLabel(context, period);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'睡眠时长${period.label}趋势图',
context.l10n.sleepDurationPeriodTrendChart(periodLabel),
style: const TextStyle(
color: _h1,
fontSize: 14,
... ... @@ -192,6 +202,13 @@ class _SleepChartCard extends GetView<SleepController> {
);
}
String _periodLabel(BuildContext context, TrendPeriod period) =>
switch (period) {
TrendPeriod.week => context.l10n.reportPeriodWeek,
TrendPeriod.month => context.l10n.reportPeriodMonth,
TrendPeriod.year => context.l10n.reportPeriodYear,
};
Widget _buildChart() {
if (controller.chartData.isEmpty) {
return const Center(
... ...
... ... @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -39,7 +40,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
children: [
Center(
child: Text(
'选择好友',
context.l10n.friendsSelect,
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
... ... @@ -89,8 +90,10 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
return _BottomSheetUserRow(
name: selfNickname?.isNotEmpty == true
? selfNickname!
: '我',
subtitle: selfNickname?.isNotEmpty == true ? '我' : null,
: context.l10n.friendsMe,
subtitle: selfNickname?.isNotEmpty == true
? context.l10n.friendsMe
: null,
avatarUrl: self?.avatar,
isSelected: currentSelectedId == null,
onTap: () {
... ... @@ -101,7 +104,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
}
final friend = friendsList[index - 1];
final name = _friendName(friend);
final name = _friendName(context, friend);
final nickname = friend.friendNickname?.trim();
return _BottomSheetUserRow(
name: name,
... ... @@ -123,14 +126,14 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
);
}
String _friendName(FriendItem friend) {
String _friendName(BuildContext context, FriendItem friend) {
final remark = friend.remarkName?.trim();
if (remark?.isNotEmpty == true) return remark!;
final nickname = friend.friendNickname?.trim();
if (nickname?.isNotEmpty == true) return nickname!;
return '未知好友';
return context.l10n.friendsUnknownFriend;
}
}
... ...
... ... @@ -18,11 +18,13 @@ class HrvReportView extends StatelessWidget {
required this.logic,
required this.isVip,
required this.onSubscribe,
this.loadingIndicator = const ReportLoadingIndicator(),
});
final HrvReportLogic logic;
final bool isVip;
final ValueChanged<String> onSubscribe;
final Widget loadingIndicator;
@override
Widget build(BuildContext context) {
... ... @@ -71,7 +73,7 @@ class HrvReportView extends StatelessWidget {
child: Obx(
() {
if (logic.isLoading.value) {
return const ReportLoadingIndicator();
return loadingIndicator;
}
return ListView(
physics: const ClampingScrollPhysics(),
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
class HrvDistributionBarSegment {
... ... @@ -47,7 +48,7 @@ class _HrvDistributionBarPainter extends CustomPainter {
final Color backgroundColor;
final double radius;
static const borderWidth = 1.0;
static const overlapHeight = 16.0;
static var overlapHeight = 16.h;
@override
void paint(Canvas canvas, Size size) {
... ... @@ -57,7 +58,6 @@ class _HrvDistributionBarPainter extends CustomPainter {
);
canvas.save();
canvas.clipRRect(clip);
if (segments.isEmpty) {
_drawLayeredSegment(
canvas,
... ... @@ -68,7 +68,6 @@ class _HrvDistributionBarPainter extends CustomPainter {
canvas.restore();
return;
}
final visibleHeights = _visibleSegmentHeightsFor(size.height, segments);
var visibleTop = 0.0;
for (var index = 0; index < segments.length; index++) {
... ...
... ... @@ -467,32 +467,26 @@ class _TrendMetric extends StatelessWidget {
}
String _weekComparisonText(BuildContext context, int difference) {
final isZh = Localizations.localeOf(context).languageCode == 'zh';
return difference == 0
? (isZh ? '与上周一致' : 'Same as last week')
? context.l10n.hrvSameAsLastWeek
: difference > 0
? context.l10n.hrvMoreDaysThanLastWeek(difference)
: context.l10n.hrvFewerDaysThanLastWeek(difference.abs());
}
String _monthComparisonText(BuildContext context, int difference) {
final isZh = Localizations.localeOf(context).languageCode == 'zh';
if (difference == 0) {
return isZh ? '与上月一致' : 'Same as last month';
return context.l10n.hrvSameAsLastMonth;
}
if (difference > 0) {
return isZh
? '比上月多$difference天'
: '$difference more days than last month';
return context.l10n.hrvMoreDaysThanLastMonth(difference);
}
final count = difference.abs();
return isZh ? '比上月少$count天' : '$count fewer days than last month';
return context.l10n.hrvFewerDaysThanLastMonth(difference.abs());
}
String _unavailableComparisonText(BuildContext context) {
if (!isMonth) return context.l10n.hrvComparedLastWeekUnavailable;
final isZh = Localizations.localeOf(context).languageCode == 'zh';
return isZh ? '比上月少-天' : 'Compared with last month: -';
return context.l10n.hrvComparedLastMonthUnavailable;
}
Color _comparisonColor(int? difference) {
... ... @@ -518,13 +512,19 @@ class _StressAxisLabels extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const SizedBox(
return SizedBox(
width: 14,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RotatedBox(quarterTurns: 1, child: _StressAxisText('轻松')),
RotatedBox(quarterTurns: 1, child: _StressAxisText('压力大')),
RotatedBox(
quarterTurns: 1,
child: _StressAxisText(context.l10n.hrvRelaxedAxisLabel),
),
RotatedBox(
quarterTurns: 1,
child: _StressAxisText(context.l10n.hrvStressedAxisLabel),
),
],
),
);
... ... @@ -644,91 +644,91 @@ class _DistributionCard extends StatelessWidget {
final HrvPeriodReport report;
final bool showExample;
final VoidCallback? onTap;
static const _cardHeight = 425.0;
static const _barRight = 45.0;
static const _barRight = 25.0;
static const _barWidth = 42.0;
static const _barHeight = 220.0;
@override
Widget build(BuildContext context) {
return _Card(
onTap: onTap,
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: _cardHeight),
child: Stack(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ReportTitle(
title: HealthReportSubjectScope.titleOf(
context,
context.l10n.hrvStressDistribution,
),
showExample: showExample,
fontSize: 16,
),
const SizedBox(height: 20),
Text(
context.l10n.hrvValidDays,
style: const TextStyle(
color: Color(0xFF78787D),
fontSize: 12,
),
),
const SizedBox(height: 2),
_ValueWithUnit(
value: '${report.validDays}',
unit: context.l10n.reportUnitDay,
),
const SizedBox(height: 28),
SizedBox(
width: 216,
child: _DistributionGrid(report: report),
_topView(context),
const Divider(height: 1, color: Color(0xFFF3F3F3)),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _ExtremeHrv(
title: context.l10n.hrvLowest,
day: report.minDay,
),
const SizedBox(height: 20),
const Divider(height: 1, color: Color(0xFFF3F3F3)),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _ExtremeHrv(
title: context.l10n.hrvLowest,
day: report.minDay,
),
),
Expanded(
child: _ExtremeHrv(
title: context.l10n.hrvHighest,
day: report.maxDay,
),
),
],
),
Expanded(
child: _ExtremeHrv(
title: context.l10n.hrvHighest,
day: report.maxDay,
),
],
),
],
),
],
),
),
);
}
Widget _topView(BuildContext context) {
return Stack(
children: [
Positioned(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ReportTitle(
title: HealthReportSubjectScope.titleOf(
context,
context.l10n.hrvStressDistribution,
),
showExample: showExample,
fontSize: 16,
),
Positioned(
top: 0,
right: _barRight,
width: _barWidth,
child: Container(
alignment: Alignment.topCenter,
padding: EdgeInsets.only(top: 50.h),
child: SizedBox(
height: _barHeight,
child: IgnorePointer(
child: _DistributionBar(report: report),
),
),
const SizedBox(height: 20),
Text(
context.l10n.hrvValidDays,
style: const TextStyle(
color: Color(0xFF78787D),
fontSize: 12,
),
),
const SizedBox(height: 2),
_ValueWithUnit(
value: '${report.validDays}',
unit: context.l10n.reportUnitDay,
),
const SizedBox(height: 28),
SizedBox(
width: 216,
child: _DistributionGrid(report: report),
),
const SizedBox(height: 20),
],
)),
Positioned(
top: 0,
width: _barWidth,
right: _barRight,
bottom: 25.h,
child: Container(
alignment: Alignment.topCenter,
padding: EdgeInsets.only(top: 25.h),
child: IgnorePointer(child: _DistributionBar(report: report))),
),
),
],
);
}
}
... ... @@ -859,7 +859,9 @@ class _ExtremeHrv extends StatelessWidget {
unit: 'ms'),
const SizedBox(height: 2),
Text(
day == null ? '-月-日' : reportMonthDay(day!.date),
day == null
? context.l10n.hrvEmptyMonthDayPlaceholder
: reportMonthDay(day!.date),
style: const TextStyle(color: Color(0xFF78787D), fontSize: 12),
),
],
... ...
... ... @@ -22,11 +22,13 @@ class SleepReportView extends StatelessWidget {
required this.logic,
required this.isVip,
required this.onSubscribe,
this.loadingIndicator = const ReportLoadingIndicator(),
});
final SleepReportLogic logic;
final bool isVip;
final ValueChanged<String> onSubscribe;
final Widget loadingIndicator;
@override
Widget build(BuildContext context) {
... ... @@ -60,7 +62,7 @@ class SleepReportView extends StatelessWidget {
child: Obx(
() {
if (logic.isLoading.value) {
return const ReportLoadingIndicator();
return loadingIndicator;
}
return CustomScrollView(
physics: const ClampingScrollPhysics(),
... ...
... ... @@ -377,7 +377,7 @@ class _TopMetric extends StatelessWidget {
Text(
trendText,
style: const TextStyle(
color: SleepWeekReportView.h3,
color: SleepWeekReportView.h2,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.2,
... ... @@ -1437,6 +1437,7 @@ class _ExtremeMetric extends StatelessWidget {
required this.unit,
required this.date,
this.duration,
this.isDurationMetric = false,
});
factory _ExtremeMetric.duration({
... ... @@ -1450,10 +1451,11 @@ class _ExtremeMetric extends StatelessWidget {
color: color,
value: duration == null ? '-' : '${duration.hoursPart}',
unit: duration == null
? '${l10n.reportUnitHour} -${l10n.reportUnitMinute}'
? ''
: '${l10n.reportUnitHour}${duration.minutesPart}${l10n.reportUnitMinute}',
date: report?.date,
duration: duration,
isDurationMetric: true,
);
}
... ... @@ -1496,6 +1498,7 @@ class _ExtremeMetric extends StatelessWidget {
final String unit;
final DateTime? date;
final SleepDuration? duration;
final bool isDurationMetric;
@override
Widget build(BuildContext context) {
... ... @@ -1526,6 +1529,8 @@ class _ExtremeMetric extends StatelessWidget {
const SizedBox(height: 3),
if (duration != null)
_ExtremeDurationValue(duration: duration!)
else if (isDurationMetric)
const _ExtremeDurationPlaceholderValue()
else
Row(
crossAxisAlignment: CrossAxisAlignment.end,
... ... @@ -1559,7 +1564,9 @@ class _ExtremeMetric extends StatelessWidget {
),
const SizedBox(height: 3),
Text(
date == null ? '-月-日 周-' : _dateText(context, date!),
date == null
? context.l10n.sleepEmptyDateWithWeekday
: _dateText(context, date!),
style: const TextStyle(
color: SleepWeekReportView.h2,
fontSize: 12,
... ... @@ -1579,6 +1586,22 @@ class _ExtremeMetric extends StatelessWidget {
}
}
class _ExtremeDurationPlaceholderValue extends StatelessWidget {
const _ExtremeDurationPlaceholderValue();
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_ExtremeDurationPart('-', context.l10n.reportUnitHour),
const SizedBox(width: 2),
_ExtremeDurationPart('-', context.l10n.reportUnitMinute),
],
);
}
}
class _ExtremeDurationValue extends StatelessWidget {
const _ExtremeDurationValue({required this.duration});
... ... @@ -1589,14 +1612,28 @@ class _ExtremeDurationValue extends StatelessWidget {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_part('${duration.hoursPart}', context.l10n.reportUnitHour),
_ExtremeDurationPart(
'${duration.hoursPart}',
context.l10n.reportUnitHour,
),
const SizedBox(width: 2),
_part('${duration.minutesPart}', context.l10n.reportUnitMinute),
_ExtremeDurationPart(
'${duration.minutesPart}',
context.l10n.reportUnitMinute,
),
],
);
}
}
class _ExtremeDurationPart extends StatelessWidget {
const _ExtremeDurationPart(this.value, this.unit);
Widget _part(String value, String unit) {
final String value;
final String unit;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
... ...
... ... @@ -8,7 +8,6 @@ import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:get/get.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import '../models/watch_theme_models.dart';
... ... @@ -95,7 +94,7 @@ class WatchThemeController extends GetxController {
Future<void> _toPremiumPage() async {
await Get.toNamed(Routes.PURCHASE, arguments: {
IntentKeys.channelType: l10n.watchThemePurchaseChannel,
IntentKeys.channelType: "表盘主题",
});
await _refreshVip();
}
... ...
... ... @@ -106,6 +106,8 @@ class FriendItem {
class FriendHealthData {
const FriendHealthData({
this.hrvState,
this.comprehensiveStressScore,
this.comprehensiveStressState,
this.latestHrv,
this.realtimeStress,
this.sleepEvaluate,
... ... @@ -116,6 +118,12 @@ class FriendHealthData {
/// HRV state indicator
final int? hrvState;
/// Comprehensive stress score.
final int? comprehensiveStressScore;
/// Comprehensive stress state indicator.
final int? comprehensiveStressState;
/// Latest HRV value used by the friend health card.
final double? latestHrv;
... ... @@ -134,6 +142,8 @@ class FriendHealthData {
factory FriendHealthData.fromJson(Map<String, dynamic> json) {
return FriendHealthData(
hrvState: _parseInt(json['hrv_state']),
comprehensiveStressScore: _parseInt(json['comprehensive_stress_score']),
comprehensiveStressState: _parseInt(json['comprehensive_stress_state']),
latestHrv: _parseDouble(json['latest_hrv']),
realtimeStress: json['realtime_stress'] as Map<String, dynamic>?,
sleepEvaluate: _parseInt(json['sleep_evaluate']),
... ... @@ -145,6 +155,12 @@ class FriendHealthData {
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (hrvState != null) val['hrv_state'] = hrvState;
if (comprehensiveStressScore != null) {
val['comprehensive_stress_score'] = comprehensiveStressScore;
}
if (comprehensiveStressState != null) {
val['comprehensive_stress_state'] = comprehensiveStressState;
}
if (latestHrv != null) val['latest_hrv'] = latestHrv;
if (realtimeStress != null) val['realtime_stress'] = realtimeStress;
if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate;
... ...
... ... @@ -345,7 +345,15 @@
"hrvSameAsLastWeek": "Same as last week",
"hrvMoreDaysThanLastWeek": "{count} more days than last week",
"hrvFewerDaysThanLastWeek": "{count} fewer days than last week",
"hrvComparedLastMonthUnavailable": "Compared with last month: -",
"hrvSameAsLastMonth": "Same as last month",
"hrvMoreDaysThanLastMonth": "{count} more days than last month",
"hrvFewerDaysThanLastMonth": "{count} fewer days than last month",
"hrvUnlockNow": "Unlock Now",
"hrvTrendTitle": "HRV Trend",
"hrvPeriodAverage": "This {period} average",
"hrvComparedPreviousPeriod": "vs previous {period}",
"hrvPeriodTrendChart": "HRV {period} Trend Chart",
"activityTotalBurn": "Total Activity Burn",
"activityExerciseTotalDuration": "Total Exercise Time",
"activityStandTotalDuration": "Total Stand Time",
... ... @@ -359,9 +367,16 @@
"activityComparedLastMonth": "34% less than last month",
"activityComparedUnavailable": "Compared with last week: -",
"activityComparedLastMonthUnavailable": "Compared with last month: -",
"activitySameAsLastWeek": "Same as last week",
"activityMoreThanLastWeek": "{percent}% more than last week",
"activityLessThanLastWeek": "{percent}% less than last week",
"activitySameAsLastMonth": "Same as last month",
"activityMoreThanLastMonth": "{percent}% more than last month",
"activityLessThanLastMonth": "{percent}% less than last month",
"activityTrendTitle": "Activity Burn Trend",
"activityPeriodTotalBurn": "This {period} total burn",
"activityDailyAverageBurn": "Daily average burn",
"activityPeriodTrendChart": "Activity Burn {period} Trend Chart",
"activityMove": "Move",
"activityExercise": "Exercise",
"activityStand": "Stand",
... ... @@ -389,6 +404,11 @@
"sleepTarget": "Target",
"sleepHighest": "Highest",
"sleepLowest": "Lowest",
"sleepTrendTitle": "Sleep Report Trend",
"sleepPeriodAverageDuration": "This {period} average sleep",
"sleepDeepSleepRatio": "Deep sleep ratio",
"sleepDurationPeriodTrendChart": "Sleep Duration {period} Trend Chart",
"sleepEmptyDateWithWeekday": "-",
"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.",
"sleepQualityAttentionRange": "<60 pts",
"sleepQualityNormalRange": "60–85 pts",
... ...
... ... @@ -531,7 +531,50 @@
}
}
},
"hrvComparedLastMonthUnavailable": "比上月少-天",
"hrvSameAsLastMonth": "与上月一致",
"hrvMoreDaysThanLastMonth": "比上月多{count}天",
"@hrvMoreDaysThanLastMonth": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"hrvFewerDaysThanLastMonth": "比上月少{count}天",
"@hrvFewerDaysThanLastMonth": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"hrvUnlockNow": "立即解锁",
"hrvTrendTitle": "HRV趋势",
"hrvPeriodAverage": "本{period}平均",
"@hrvPeriodAverage": {
"placeholders": {
"period": {
"type": "String"
}
}
},
"hrvComparedPreviousPeriod": "较上{period}",
"@hrvComparedPreviousPeriod": {
"placeholders": {
"period": {
"type": "String"
}
}
},
"hrvPeriodTrendChart": "HRV {period}趋势图",
"@hrvPeriodTrendChart": {
"placeholders": {
"period": {
"type": "String"
}
}
},
"activityTotalBurn": "活动总消耗",
"activityExerciseTotalDuration": "锻炼总时长",
"activityStandTotalDuration": "站立总时长",
... ... @@ -545,6 +588,23 @@
"activityComparedLastMonth": "比上月少34%",
"activityComparedUnavailable": "比上周-",
"activityComparedLastMonthUnavailable": "比上月-",
"activitySameAsLastWeek": "与上周持平",
"activityMoreThanLastWeek": "比上周多{percent}%",
"@activityMoreThanLastWeek": {
"placeholders": {
"percent": {
"type": "int"
}
}
},
"activityLessThanLastWeek": "比上周少{percent}%",
"@activityLessThanLastWeek": {
"placeholders": {
"percent": {
"type": "int"
}
}
},
"activitySameAsLastMonth": "与上月持平",
"activityMoreThanLastMonth": "比上月多{percent}%",
"@activityMoreThanLastMonth": {
... ... @@ -562,6 +622,24 @@
}
}
},
"activityTrendTitle": "活动消耗趋势",
"activityPeriodTotalBurn": "本{period}总消耗",
"@activityPeriodTotalBurn": {
"placeholders": {
"period": {
"type": "String"
}
}
},
"activityDailyAverageBurn": "日均消耗",
"activityPeriodTrendChart": "活动消耗{period}趋势图",
"@activityPeriodTrendChart": {
"placeholders": {
"period": {
"type": "String"
}
}
},
"activityMove": "活动",
"activityExercise": "锻炼",
"activityStand": "站立",
... ... @@ -626,6 +704,25 @@
"sleepTarget": "目标",
"sleepHighest": "最高",
"sleepLowest": "最低",
"sleepTrendTitle": "睡眠报告趋势",
"sleepPeriodAverageDuration": "本{period}均睡眠",
"@sleepPeriodAverageDuration": {
"placeholders": {
"period": {
"type": "String"
}
}
},
"sleepDeepSleepRatio": "深睡占比",
"sleepDurationPeriodTrendChart": "睡眠时长{period}趋势图",
"@sleepDurationPeriodTrendChart": {
"placeholders": {
"period": {
"type": "String"
}
}
},
"sleepEmptyDateWithWeekday": "-月-日 周-",
"sleepQualityDescription": "DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。",
"sleepQualityAttentionRange": "<60分",
"sleepQualityNormalRange": "60~85分",
... ...
... ... @@ -2169,12 +2169,60 @@ abstract class AppLocalizations {
/// **'比上周少{count}天'**
String hrvFewerDaysThanLastWeek(int count);
/// No description provided for @hrvComparedLastMonthUnavailable.
///
/// In zh, this message translates to:
/// **'比上月少-天'**
String get hrvComparedLastMonthUnavailable;
/// No description provided for @hrvSameAsLastMonth.
///
/// In zh, this message translates to:
/// **'与上月一致'**
String get hrvSameAsLastMonth;
/// No description provided for @hrvMoreDaysThanLastMonth.
///
/// In zh, this message translates to:
/// **'比上月多{count}天'**
String hrvMoreDaysThanLastMonth(int count);
/// No description provided for @hrvFewerDaysThanLastMonth.
///
/// In zh, this message translates to:
/// **'比上月少{count}天'**
String hrvFewerDaysThanLastMonth(int count);
/// No description provided for @hrvUnlockNow.
///
/// In zh, this message translates to:
/// **'立即解锁'**
String get hrvUnlockNow;
/// No description provided for @hrvTrendTitle.
///
/// In zh, this message translates to:
/// **'HRV趋势'**
String get hrvTrendTitle;
/// No description provided for @hrvPeriodAverage.
///
/// In zh, this message translates to:
/// **'本{period}平均'**
String hrvPeriodAverage(String period);
/// No description provided for @hrvComparedPreviousPeriod.
///
/// In zh, this message translates to:
/// **'较上{period}'**
String hrvComparedPreviousPeriod(String period);
/// No description provided for @hrvPeriodTrendChart.
///
/// In zh, this message translates to:
/// **'HRV {period}趋势图'**
String hrvPeriodTrendChart(String period);
/// No description provided for @activityTotalBurn.
///
/// In zh, this message translates to:
... ... @@ -2253,6 +2301,24 @@ abstract class AppLocalizations {
/// **'比上月-'**
String get activityComparedLastMonthUnavailable;
/// No description provided for @activitySameAsLastWeek.
///
/// In zh, this message translates to:
/// **'与上周持平'**
String get activitySameAsLastWeek;
/// No description provided for @activityMoreThanLastWeek.
///
/// In zh, this message translates to:
/// **'比上周多{percent}%'**
String activityMoreThanLastWeek(int percent);
/// No description provided for @activityLessThanLastWeek.
///
/// In zh, this message translates to:
/// **'比上周少{percent}%'**
String activityLessThanLastWeek(int percent);
/// No description provided for @activitySameAsLastMonth.
///
/// In zh, this message translates to:
... ... @@ -2271,6 +2337,30 @@ abstract class AppLocalizations {
/// **'比上月少{percent}%'**
String activityLessThanLastMonth(int percent);
/// No description provided for @activityTrendTitle.
///
/// In zh, this message translates to:
/// **'活动消耗趋势'**
String get activityTrendTitle;
/// No description provided for @activityPeriodTotalBurn.
///
/// In zh, this message translates to:
/// **'本{period}总消耗'**
String activityPeriodTotalBurn(String period);
/// No description provided for @activityDailyAverageBurn.
///
/// In zh, this message translates to:
/// **'日均消耗'**
String get activityDailyAverageBurn;
/// No description provided for @activityPeriodTrendChart.
///
/// In zh, this message translates to:
/// **'活动消耗{period}趋势图'**
String activityPeriodTrendChart(String period);
/// No description provided for @activityMove.
///
/// In zh, this message translates to:
... ... @@ -2433,6 +2523,36 @@ abstract class AppLocalizations {
/// **'最低'**
String get sleepLowest;
/// No description provided for @sleepTrendTitle.
///
/// In zh, this message translates to:
/// **'睡眠报告趋势'**
String get sleepTrendTitle;
/// No description provided for @sleepPeriodAverageDuration.
///
/// In zh, this message translates to:
/// **'本{period}均睡眠'**
String sleepPeriodAverageDuration(String period);
/// No description provided for @sleepDeepSleepRatio.
///
/// In zh, this message translates to:
/// **'深睡占比'**
String get sleepDeepSleepRatio;
/// No description provided for @sleepDurationPeriodTrendChart.
///
/// In zh, this message translates to:
/// **'睡眠时长{period}趋势图'**
String sleepDurationPeriodTrendChart(String period);
/// No description provided for @sleepEmptyDateWithWeekday.
///
/// In zh, this message translates to:
/// **'-月-日 周-'**
String get sleepEmptyDateWithWeekday;
/// No description provided for @sleepQualityDescription.
///
/// In zh, this message translates to:
... ...
... ... @@ -1196,9 +1196,43 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get hrvComparedLastMonthUnavailable => 'Compared with last month: -';
@override
String get hrvSameAsLastMonth => 'Same as last month';
@override
String hrvMoreDaysThanLastMonth(int count) {
return '$count more days than last month';
}
@override
String hrvFewerDaysThanLastMonth(int count) {
return '$count fewer days than last month';
}
@override
String get hrvUnlockNow => 'Unlock Now';
@override
String get hrvTrendTitle => 'HRV Trend';
@override
String hrvPeriodAverage(String period) {
return 'This $period average';
}
@override
String hrvComparedPreviousPeriod(String period) {
return 'vs previous $period';
}
@override
String hrvPeriodTrendChart(String period) {
return 'HRV $period Trend Chart';
}
@override
String get activityTotalBurn => 'Total Activity Burn';
@override
... ... @@ -1239,6 +1273,19 @@ class AppLocalizationsEn extends AppLocalizations {
'Compared with last month: -';
@override
String get activitySameAsLastWeek => 'Same as last week';
@override
String activityMoreThanLastWeek(int percent) {
return '$percent% more than last week';
}
@override
String activityLessThanLastWeek(int percent) {
return '$percent% less than last week';
}
@override
String get activitySameAsLastMonth => 'Same as last month';
@override
... ... @@ -1252,6 +1299,22 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get activityTrendTitle => 'Activity Burn Trend';
@override
String activityPeriodTotalBurn(String period) {
return 'This $period total burn';
}
@override
String get activityDailyAverageBurn => 'Daily average burn';
@override
String activityPeriodTrendChart(String period) {
return 'Activity Burn $period Trend Chart';
}
@override
String get activityMove => 'Move';
@override
... ... @@ -1341,6 +1404,25 @@ class AppLocalizationsEn extends AppLocalizations {
String get sleepLowest => 'Lowest';
@override
String get sleepTrendTitle => 'Sleep Report Trend';
@override
String sleepPeriodAverageDuration(String period) {
return 'This $period average sleep';
}
@override
String get sleepDeepSleepRatio => 'Deep sleep ratio';
@override
String sleepDurationPeriodTrendChart(String period) {
return 'Sleep Duration $period Trend Chart';
}
@override
String get sleepEmptyDateWithWeekday => '-';
@override
String get 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.';
... ...
... ... @@ -1132,9 +1132,43 @@ class AppLocalizationsZh extends AppLocalizations {
}
@override
String get hrvComparedLastMonthUnavailable => '比上月少-天';
@override
String get hrvSameAsLastMonth => '与上月一致';
@override
String hrvMoreDaysThanLastMonth(int count) {
return '比上月多$count天';
}
@override
String hrvFewerDaysThanLastMonth(int count) {
return '比上月少$count天';
}
@override
String get hrvUnlockNow => '立即解锁';
@override
String get hrvTrendTitle => 'HRV趋势';
@override
String hrvPeriodAverage(String period) {
return '本$period平均';
}
@override
String hrvComparedPreviousPeriod(String period) {
return '较上$period';
}
@override
String hrvPeriodTrendChart(String period) {
return 'HRV $period趋势图';
}
@override
String get activityTotalBurn => '活动总消耗';
@override
... ... @@ -1174,6 +1208,19 @@ class AppLocalizationsZh extends AppLocalizations {
String get activityComparedLastMonthUnavailable => '比上月-';
@override
String get activitySameAsLastWeek => '与上周持平';
@override
String activityMoreThanLastWeek(int percent) {
return '比上周多$percent%';
}
@override
String activityLessThanLastWeek(int percent) {
return '比上周少$percent%';
}
@override
String get activitySameAsLastMonth => '与上月持平';
@override
... ... @@ -1187,6 +1234,22 @@ class AppLocalizationsZh extends AppLocalizations {
}
@override
String get activityTrendTitle => '活动消耗趋势';
@override
String activityPeriodTotalBurn(String period) {
return '本$period总消耗';
}
@override
String get activityDailyAverageBurn => '日均消耗';
@override
String activityPeriodTrendChart(String period) {
return '活动消耗$period趋势图';
}
@override
String get activityMove => '活动';
@override
... ... @@ -1276,6 +1339,25 @@ class AppLocalizationsZh extends AppLocalizations {
String get sleepLowest => '最低';
@override
String get sleepTrendTitle => '睡眠报告趋势';
@override
String sleepPeriodAverageDuration(String period) {
return '本$period均睡眠';
}
@override
String get sleepDeepSleepRatio => '深睡占比';
@override
String sleepDurationPeriodTrendChart(String period) {
return '睡眠时长$period趋势图';
}
@override
String get sleepEmptyDateWithWeekday => '-月-日 周-';
@override
String get sleepQualityDescription =>
'DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。';
... ...