Showing
13 changed files
with
230 additions
and
20 deletions
| @@ -19,8 +19,7 @@ class ActivityBurnReportLogic extends ReportPeriodLogic { | @@ -19,8 +19,7 @@ class ActivityBurnReportLogic extends ReportPeriodLogic { | ||
| 19 | ActivityBurnReportRepository? repository, | 19 | ActivityBurnReportRepository? repository, |
| 20 | }) : repository = repository ?? | 20 | }) : repository = repository ?? |
| 21 | ActivityBurnReportRepositoryImpl( | 21 | ActivityBurnReportRepositoryImpl( |
| 22 | - dataSource: | ||
| 23 | - ApiActivityBurnReportDataSource(_healthDataSource()), | 22 | + dataSource: ApiActivityBurnReportDataSource(_healthDataSource()), |
| 24 | ) { | 23 | ) { |
| 25 | targetUserId.value = initialTargetUserId; | 24 | targetUserId.value = initialTargetUserId; |
| 26 | } | 25 | } |
| @@ -52,7 +51,7 @@ class ActivityBurnReportLogic extends ReportPeriodLogic { | @@ -52,7 +51,7 @@ class ActivityBurnReportLogic extends ReportPeriodLogic { | ||
| 52 | @override | 51 | @override |
| 53 | Future<void> loadReport() async { | 52 | Future<void> loadReport() async { |
| 54 | isLoading.value = !isMySelf; | 53 | isLoading.value = !isMySelf; |
| 55 | - | 54 | + |
| 56 | try { | 55 | try { |
| 57 | if (selectedPeriod.value == ReportPeriod.week) { | 56 | if (selectedPeriod.value == ReportPeriod.week) { |
| 58 | weeklyReport.value = await repository.getWeeklyReport( | 57 | weeklyReport.value = await repository.getWeeklyReport( |
| @@ -60,10 +59,11 @@ class ActivityBurnReportLogic extends ReportPeriodLogic { | @@ -60,10 +59,11 @@ class ActivityBurnReportLogic extends ReportPeriodLogic { | ||
| 60 | targetUserId: targetUserId.value, | 59 | targetUserId: targetUserId.value, |
| 61 | ); | 60 | ); |
| 62 | } else if (selectedPeriod.value == ReportPeriod.month) { | 61 | } else if (selectedPeriod.value == ReportPeriod.month) { |
| 63 | - monthlyReport.value = await repository.getMonthlyReport( | 62 | + final result = await repository.getMonthlyReport( |
| 64 | monthStart, | 63 | monthStart, |
| 65 | targetUserId: targetUserId.value, | 64 | targetUserId: targetUserId.value, |
| 66 | ); | 65 | ); |
| 66 | + monthlyReport.value = result; | ||
| 67 | } else { | 67 | } else { |
| 68 | report.value = await repository.getDailyReport( | 68 | report.value = await repository.getDailyReport( |
| 69 | selectedDate.value, | 69 | selectedDate.value, |
| @@ -197,7 +197,10 @@ class CreateWatchThemeController extends GetxController { | @@ -197,7 +197,10 @@ class CreateWatchThemeController extends GetxController { | ||
| 197 | 197 | ||
| 198 | Future<void> showRenameStatusDialog(int index) async { | 198 | Future<void> showRenameStatusDialog(int index) async { |
| 199 | final result = await Get.dialog<String>( | 199 | final result = await Get.dialog<String>( |
| 200 | - WatchThemeRenameDialog(initialValue: customStatusNames[index]), | 200 | + WatchThemeRenameDialog( |
| 201 | + initialValue: customStatusNames[index], | ||
| 202 | + onConfirm: _validateStatusName, | ||
| 203 | + ), | ||
| 201 | barrierDismissible: true, | 204 | barrierDismissible: true, |
| 202 | ); | 205 | ); |
| 203 | if (result == null || result.trim().isEmpty) { | 206 | if (result == null || result.trim().isEmpty) { |
| @@ -206,6 +209,18 @@ class CreateWatchThemeController extends GetxController { | @@ -206,6 +209,18 @@ class CreateWatchThemeController extends GetxController { | ||
| 206 | customStatusNames[index] = result.trim(); | 209 | customStatusNames[index] = result.trim(); |
| 207 | } | 210 | } |
| 208 | 211 | ||
| 212 | + Future<bool> _validateStatusName(String value) async { | ||
| 213 | + final result = await _themeApi.checkTextContent(value); | ||
| 214 | + if (result case AppSuccess<ContentCheckTextResult>(data: final data) | ||
| 215 | + when data.isPass) { | ||
| 216 | + return true; | ||
| 217 | + } | ||
| 218 | + if (result is AppSuccess<ContentCheckTextResult>) { | ||
| 219 | + AppToast.show(l10n.watchThemeContentUnavailable); | ||
| 220 | + } | ||
| 221 | + return false; | ||
| 222 | + } | ||
| 223 | + | ||
| 209 | Future<void> handleCreateBack() async { | 224 | Future<void> handleCreateBack() async { |
| 210 | if (_creationCompleted) { | 225 | if (_creationCompleted) { |
| 211 | return; | 226 | return; |
| @@ -3,9 +3,14 @@ import 'package:get/get.dart'; | @@ -3,9 +3,14 @@ import 'package:get/get.dart'; | ||
| 3 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 3 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 4 | 4 | ||
| 5 | class WatchThemeRenameDialog extends StatefulWidget { | 5 | class WatchThemeRenameDialog extends StatefulWidget { |
| 6 | - const WatchThemeRenameDialog({super.key, required this.initialValue}); | 6 | + const WatchThemeRenameDialog({ |
| 7 | + super.key, | ||
| 8 | + required this.initialValue, | ||
| 9 | + this.onConfirm, | ||
| 10 | + }); | ||
| 7 | 11 | ||
| 8 | final String initialValue; | 12 | final String initialValue; |
| 13 | + final Future<bool> Function(String value)? onConfirm; | ||
| 9 | 14 | ||
| 10 | @override | 15 | @override |
| 11 | State<WatchThemeRenameDialog> createState() => _WatchThemeRenameDialogState(); | 16 | State<WatchThemeRenameDialog> createState() => _WatchThemeRenameDialogState(); |
| @@ -13,6 +18,7 @@ class WatchThemeRenameDialog extends StatefulWidget { | @@ -13,6 +18,7 @@ class WatchThemeRenameDialog extends StatefulWidget { | ||
| 13 | 18 | ||
| 14 | class _WatchThemeRenameDialogState extends State<WatchThemeRenameDialog> { | 19 | class _WatchThemeRenameDialogState extends State<WatchThemeRenameDialog> { |
| 15 | late final TextEditingController _controller; | 20 | late final TextEditingController _controller; |
| 21 | + bool _isSubmitting = false; | ||
| 16 | 22 | ||
| 17 | @override | 23 | @override |
| 18 | void initState() { | 24 | void initState() { |
| @@ -94,11 +100,11 @@ class _WatchThemeRenameDialogState extends State<WatchThemeRenameDialog> { | @@ -94,11 +100,11 @@ class _WatchThemeRenameDialogState extends State<WatchThemeRenameDialog> { | ||
| 94 | ValueListenableBuilder<TextEditingValue>( | 100 | ValueListenableBuilder<TextEditingValue>( |
| 95 | valueListenable: _controller, | 101 | valueListenable: _controller, |
| 96 | builder: (context, value, _) { | 102 | builder: (context, value, _) { |
| 97 | - final enabled = value.text.trim().isNotEmpty; | 103 | + final enabled = value.text.trim().isNotEmpty && !_isSubmitting; |
| 98 | return Opacity( | 104 | return Opacity( |
| 99 | opacity: enabled ? 1 : 0.4, | 105 | opacity: enabled ? 1 : 0.4, |
| 100 | child: GestureDetector( | 106 | child: GestureDetector( |
| 101 | - onTap: enabled ? () => Get.back(result: value.text) : null, | 107 | + onTap: enabled ? () => _confirm(value.text) : null, |
| 102 | child: Container( | 108 | child: Container( |
| 103 | width: 220, | 109 | width: 220, |
| 104 | height: 48, | 110 | height: 48, |
| @@ -125,6 +131,29 @@ class _WatchThemeRenameDialogState extends State<WatchThemeRenameDialog> { | @@ -125,6 +131,29 @@ class _WatchThemeRenameDialogState extends State<WatchThemeRenameDialog> { | ||
| 125 | ), | 131 | ), |
| 126 | ); | 132 | ); |
| 127 | } | 133 | } |
| 134 | + | ||
| 135 | + Future<void> _confirm(String value) async { | ||
| 136 | + final trimmed = value.trim(); | ||
| 137 | + if (trimmed.isEmpty || _isSubmitting) { | ||
| 138 | + return; | ||
| 139 | + } | ||
| 140 | + setState(() { | ||
| 141 | + _isSubmitting = true; | ||
| 142 | + }); | ||
| 143 | + try { | ||
| 144 | + final canClose = | ||
| 145 | + await (widget.onConfirm?.call(trimmed) ?? Future<bool>.value(true)); | ||
| 146 | + if (canClose) { | ||
| 147 | + Get.back(result: trimmed); | ||
| 148 | + } | ||
| 149 | + } finally { | ||
| 150 | + if (mounted) { | ||
| 151 | + setState(() { | ||
| 152 | + _isSubmitting = false; | ||
| 153 | + }); | ||
| 154 | + } | ||
| 155 | + } | ||
| 156 | + } | ||
| 128 | } | 157 | } |
| 129 | 158 | ||
| 130 | class WatchThemeConfirmDialog extends StatelessWidget { | 159 | class WatchThemeConfirmDialog extends StatelessWidget { |
| @@ -10,6 +10,20 @@ class ThemeApi { | @@ -10,6 +10,20 @@ class ThemeApi { | ||
| 10 | 10 | ||
| 11 | final DioClient _dioClient; | 11 | final DioClient _dioClient; |
| 12 | 12 | ||
| 13 | + Future<AppResult<ContentCheckTextResult>> checkTextContent(String text) { | ||
| 14 | + return safeCall( | ||
| 15 | + call: () async { | ||
| 16 | + final response = await _dioClient.dio.post( | ||
| 17 | + ApiPaths.contentCheckText, | ||
| 18 | + data: {'text': text}, | ||
| 19 | + ); | ||
| 20 | + return ContentCheckTextResult.fromJson( | ||
| 21 | + response.data as Map<String, dynamic>, | ||
| 22 | + ); | ||
| 23 | + }, | ||
| 24 | + ); | ||
| 25 | + } | ||
| 26 | + | ||
| 13 | Future<AppResult<void>> createTheme({ | 27 | Future<AppResult<void>> createTheme({ |
| 14 | required String themeName, | 28 | required String themeName, |
| 15 | required String energeticDescription, | 29 | required String energeticDescription, |
| @@ -153,3 +167,17 @@ class ThemeApi { | @@ -153,3 +167,17 @@ class ThemeApi { | ||
| 153 | // Keep compatibility with the original Swift ThemeEndpoint spelling. | 167 | // Keep compatibility with the original Swift ThemeEndpoint spelling. |
| 154 | Future<AppResult<void>> appplyTheme(int themeId) => applyTheme(themeId); | 168 | Future<AppResult<void>> appplyTheme(int themeId) => applyTheme(themeId); |
| 155 | } | 169 | } |
| 170 | + | ||
| 171 | +class ContentCheckTextResult { | ||
| 172 | + const ContentCheckTextResult({required this.suggestion}); | ||
| 173 | + | ||
| 174 | + final String suggestion; | ||
| 175 | + | ||
| 176 | + bool get isPass => suggestion == 'pass'; | ||
| 177 | + | ||
| 178 | + factory ContentCheckTextResult.fromJson(Map<String, dynamic> json) { | ||
| 179 | + return ContentCheckTextResult( | ||
| 180 | + suggestion: json['suggestion']?.toString() ?? '', | ||
| 181 | + ); | ||
| 182 | + } | ||
| 183 | +} |
| @@ -56,6 +56,7 @@ abstract final class ApiPaths { | @@ -56,6 +56,7 @@ abstract final class ApiPaths { | ||
| 56 | 56 | ||
| 57 | // Config | 57 | // Config |
| 58 | static const dynamicConfigList = '/client/doublefeel/dynamic_config/list/'; | 58 | static const dynamicConfigList = '/client/doublefeel/dynamic_config/list/'; |
| 59 | + static const contentCheckText = '/client/doublefeel/content_check/text/'; | ||
| 59 | 60 | ||
| 60 | // OBS | 61 | // OBS |
| 61 | static const obsTempToken = '/client/doublefeel/obs/temp_token/'; | 62 | static const obsTempToken = '/client/doublefeel/obs/temp_token/'; |
| @@ -576,17 +576,17 @@ class LocalHealthDataConvert { | @@ -576,17 +576,17 @@ class LocalHealthDataConvert { | ||
| 576 | move: (latest?.activeEnergyBurned ?? 0).ceil(), | 576 | move: (latest?.activeEnergyBurned ?? 0).ceil(), |
| 577 | standHours: latest?.appleStandHours ?? 0, | 577 | standHours: latest?.appleStandHours ?? 0, |
| 578 | exerciseSeconds: latest?.appleExerciseTime ?? 0, | 578 | exerciseSeconds: latest?.appleExerciseTime ?? 0, |
| 579 | - moveGoal: latest?.activeEnergyBurnedGoal ?? 480, | ||
| 580 | - standGoal: latest?.standHoursGoal ?? 8, | 579 | + moveGoal: latest?.activeEnergyBurnedGoal, |
| 580 | + standGoal: latest?.standHoursGoal, | ||
| 581 | exerciseGoalSeconds: latest?.exerciseTimeGoal, | 581 | exerciseGoalSeconds: latest?.exerciseTimeGoal, |
| 582 | ); | 582 | ); |
| 583 | } | 583 | } |
| 584 | 584 | ||
| 585 | static bool _hasActivityGoal(HealthKitRawActivityDataPoint item) { | 585 | static bool _hasActivityGoal(HealthKitRawActivityDataPoint item) { |
| 586 | - return item.activeEnergyBurnedGoal != null || | ||
| 587 | - item.appleMoveTimeGoal != null || | ||
| 588 | - item.standHoursGoal != null || | ||
| 589 | - item.exerciseTimeGoal != null; | 586 | + return _isPositive(item.activeEnergyBurnedGoal) || |
| 587 | + _isPositive(item.appleMoveTimeGoal) || | ||
| 588 | + _isPositive(item.standHoursGoal) || | ||
| 589 | + _isPositive(item.exerciseTimeGoal); | ||
| 590 | } | 590 | } |
| 591 | 591 | ||
| 592 | static HealthRawHrvStressPoint? _latestHrvByRawEndTime( | 592 | static HealthRawHrvStressPoint? _latestHrvByRawEndTime( |
| @@ -641,10 +641,42 @@ class LocalHealthDataConvert { | @@ -641,10 +641,42 @@ class LocalHealthDataConvert { | ||
| 641 | static _ActivityDaySummary? _latestActivitySummaryWithGoal( | 641 | static _ActivityDaySummary? _latestActivitySummaryWithGoal( |
| 642 | List<_ActivityDaySummary> daily, | 642 | List<_ActivityDaySummary> daily, |
| 643 | ) { | 643 | ) { |
| 644 | + num? moveGoal; | ||
| 645 | + num? standGoal; | ||
| 646 | + num? exerciseGoalSeconds; | ||
| 647 | + DateTime? latestGoalDay; | ||
| 644 | for (final item in daily.reversed) { | 648 | for (final item in daily.reversed) { |
| 645 | - if (item.hasGoal) return item; | 649 | + if (moveGoal == null && _isPositive(item.moveGoal)) { |
| 650 | + moveGoal = item.moveGoal; | ||
| 651 | + latestGoalDay ??= item.day; | ||
| 652 | + } | ||
| 653 | + if (standGoal == null && _isPositive(item.standGoal)) { | ||
| 654 | + standGoal = item.standGoal; | ||
| 655 | + latestGoalDay ??= item.day; | ||
| 656 | + } | ||
| 657 | + if (exerciseGoalSeconds == null && | ||
| 658 | + _isPositive(item.exerciseGoalSeconds)) { | ||
| 659 | + exerciseGoalSeconds = item.exerciseGoalSeconds; | ||
| 660 | + latestGoalDay ??= item.day; | ||
| 661 | + } | ||
| 662 | + if (moveGoal != null && | ||
| 663 | + standGoal != null && | ||
| 664 | + exerciseGoalSeconds != null) { | ||
| 665 | + break; | ||
| 666 | + } | ||
| 646 | } | 667 | } |
| 647 | - return null; | 668 | + if (moveGoal == null && standGoal == null && exerciseGoalSeconds == null) { |
| 669 | + return null; | ||
| 670 | + } | ||
| 671 | + return _ActivityDaySummary( | ||
| 672 | + day: latestGoalDay ?? daily.last.day, | ||
| 673 | + move: 0, | ||
| 674 | + standHours: 0, | ||
| 675 | + exerciseSeconds: 0, | ||
| 676 | + moveGoal: moveGoal, | ||
| 677 | + standGoal: standGoal, | ||
| 678 | + exerciseGoalSeconds: exerciseGoalSeconds, | ||
| 679 | + ); | ||
| 648 | } | 680 | } |
| 649 | 681 | ||
| 650 | static ActivityTargetInfo? _activityTargetInfo( | 682 | static ActivityTargetInfo? _activityTargetInfo( |
| @@ -945,6 +977,8 @@ class LocalHealthDataConvert { | @@ -945,6 +977,8 @@ class LocalHealthDataConvert { | ||
| 945 | if (values.isEmpty) return null; | 977 | if (values.isEmpty) return null; |
| 946 | return values.reduce(math.min); | 978 | return values.reduce(math.min); |
| 947 | } | 979 | } |
| 980 | + | ||
| 981 | + static bool _isPositive(num? value) => value != null && value > 0; | ||
| 948 | } | 982 | } |
| 949 | 983 | ||
| 950 | class _ActivityDaySummary { | 984 | class _ActivityDaySummary { |
| @@ -967,9 +1001,9 @@ class _ActivityDaySummary { | @@ -967,9 +1001,9 @@ class _ActivityDaySummary { | ||
| 967 | final num? exerciseGoalSeconds; | 1001 | final num? exerciseGoalSeconds; |
| 968 | 1002 | ||
| 969 | bool get hasGoal => | 1003 | bool get hasGoal => |
| 970 | - moveGoal != null || | ||
| 971 | - standGoal != null || | ||
| 972 | - exerciseGoalSeconds != null; | 1004 | + (moveGoal ?? 0) > 0 || |
| 1005 | + (standGoal ?? 0) > 0 || | ||
| 1006 | + (exerciseGoalSeconds ?? 0) > 0; | ||
| 973 | } | 1007 | } |
| 974 | 1008 | ||
| 975 | class _HrvDaySummary { | 1009 | class _HrvDaySummary { |
| @@ -171,10 +171,13 @@ class LocalHealthDataSource implements HealthDataSource { | @@ -171,10 +171,13 @@ class LocalHealthDataSource implements HealthDataSource { | ||
| 171 | startTime: queryStart, | 171 | startTime: queryStart, |
| 172 | endTime: queryEnd, | 172 | endTime: queryEnd, |
| 173 | ); | 173 | ); |
| 174 | + final resolvedActivity = _resolveActivityTarget(activity); | ||
| 174 | return AppSuccess( | 175 | return AppSuccess( |
| 175 | LocalHealthDataConvert.v2ActivityTarget( | 176 | LocalHealthDataConvert.v2ActivityTarget( |
| 176 | userId: queryUserId ?? 0, | 177 | userId: queryUserId ?? 0, |
| 177 | - activity: activity, | 178 | + activity: resolvedActivity == null |
| 179 | + ? const <HealthKitRawActivityDataPoint>[] | ||
| 180 | + : [resolvedActivity], | ||
| 178 | ), | 181 | ), |
| 179 | ); | 182 | ); |
| 180 | } catch (error) { | 183 | } catch (error) { |
| @@ -317,4 +320,53 @@ class LocalHealthDataSource implements HealthDataSource { | @@ -317,4 +320,53 @@ class LocalHealthDataSource implements HealthDataSource { | ||
| 317 | endTime: endTime, | 320 | endTime: endTime, |
| 318 | ); | 321 | ); |
| 319 | } | 322 | } |
| 323 | + | ||
| 324 | + HealthKitRawActivityDataPoint? _resolveActivityTarget( | ||
| 325 | + List<HealthKitRawActivityDataPoint> activity, | ||
| 326 | + ) { | ||
| 327 | + double? activeEnergyBurnedGoal; | ||
| 328 | + double? exerciseTimeGoal; | ||
| 329 | + double? standHoursGoal; | ||
| 330 | + var resolvedEndTime = 0; | ||
| 331 | + final reversed = [...activity] | ||
| 332 | + ..sort((a, b) => b.endTime.compareTo(a.endTime)); | ||
| 333 | + | ||
| 334 | + for (final item in reversed) { | ||
| 335 | + if (activeEnergyBurnedGoal == null && | ||
| 336 | + _isPositive(item.activeEnergyBurnedGoal)) { | ||
| 337 | + activeEnergyBurnedGoal = item.activeEnergyBurnedGoal; | ||
| 338 | + resolvedEndTime = | ||
| 339 | + resolvedEndTime < item.endTime ? item.endTime : resolvedEndTime; | ||
| 340 | + } | ||
| 341 | + if (exerciseTimeGoal == null && _isPositive(item.exerciseTimeGoal)) { | ||
| 342 | + exerciseTimeGoal = item.exerciseTimeGoal; | ||
| 343 | + resolvedEndTime = | ||
| 344 | + resolvedEndTime < item.endTime ? item.endTime : resolvedEndTime; | ||
| 345 | + } | ||
| 346 | + if (standHoursGoal == null && _isPositive(item.standHoursGoal)) { | ||
| 347 | + standHoursGoal = item.standHoursGoal; | ||
| 348 | + resolvedEndTime = | ||
| 349 | + resolvedEndTime < item.endTime ? item.endTime : resolvedEndTime; | ||
| 350 | + } | ||
| 351 | + if (activeEnergyBurnedGoal != null && | ||
| 352 | + exerciseTimeGoal != null && | ||
| 353 | + standHoursGoal != null) { | ||
| 354 | + break; | ||
| 355 | + } | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + if (activeEnergyBurnedGoal == null && | ||
| 359 | + exerciseTimeGoal == null && | ||
| 360 | + standHoursGoal == null) { | ||
| 361 | + return null; | ||
| 362 | + } | ||
| 363 | + return HealthKitRawActivityDataPoint( | ||
| 364 | + endTime: resolvedEndTime, | ||
| 365 | + activeEnergyBurnedGoal: activeEnergyBurnedGoal, | ||
| 366 | + exerciseTimeGoal: exerciseTimeGoal, | ||
| 367 | + standHoursGoal: standHoursGoal, | ||
| 368 | + ); | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + bool _isPositive(num? value) => value != null && value > 0; | ||
| 320 | } | 372 | } |
| @@ -627,6 +627,7 @@ | @@ -627,6 +627,7 @@ | ||
| 627 | "watchThemeRenameStatus": "Rename Status", | 627 | "watchThemeRenameStatus": "Rename Status", |
| 628 | "watchThemeEnterNickname": "Enter a name", | 628 | "watchThemeEnterNickname": "Enter a name", |
| 629 | "watchThemeSave": "Save", | 629 | "watchThemeSave": "Save", |
| 630 | + "watchThemeContentUnavailable": "This content is unavailable. Try another one.", | ||
| 630 | "watchThemeDialPreview": "Watch Face Preview", | 631 | "watchThemeDialPreview": "Watch Face Preview", |
| 631 | "watchThemeSwitchFriend": "Switch Friend", | 632 | "watchThemeSwitchFriend": "Switch Friend", |
| 632 | "watchThemeStatusPreview": "Status Preview", | 633 | "watchThemeStatusPreview": "Status Preview", |
| @@ -1020,6 +1020,7 @@ | @@ -1020,6 +1020,7 @@ | ||
| 1020 | "watchThemeRenameStatus": "修改状态名称", | 1020 | "watchThemeRenameStatus": "修改状态名称", |
| 1021 | "watchThemeEnterNickname": "请输入昵称", | 1021 | "watchThemeEnterNickname": "请输入昵称", |
| 1022 | "watchThemeSave": "保存", | 1022 | "watchThemeSave": "保存", |
| 1023 | + "watchThemeContentUnavailable": "这个内容不可用,换一个吧", | ||
| 1023 | "watchThemeDialPreview": "表盘预览", | 1024 | "watchThemeDialPreview": "表盘预览", |
| 1024 | "watchThemeSwitchFriend": "切换好友", | 1025 | "watchThemeSwitchFriend": "切换好友", |
| 1025 | "watchThemeStatusPreview": "状态预览", | 1026 | "watchThemeStatusPreview": "状态预览", |
| @@ -3717,6 +3717,12 @@ abstract class AppLocalizations { | @@ -3717,6 +3717,12 @@ abstract class AppLocalizations { | ||
| 3717 | /// **'保存'** | 3717 | /// **'保存'** |
| 3718 | String get watchThemeSave; | 3718 | String get watchThemeSave; |
| 3719 | 3719 | ||
| 3720 | + /// No description provided for @watchThemeContentUnavailable. | ||
| 3721 | + /// | ||
| 3722 | + /// In zh, this message translates to: | ||
| 3723 | + /// **'这个内容不可用,换一个吧'** | ||
| 3724 | + String get watchThemeContentUnavailable; | ||
| 3725 | + | ||
| 3720 | /// No description provided for @watchThemeDialPreview. | 3726 | /// No description provided for @watchThemeDialPreview. |
| 3721 | /// | 3727 | /// |
| 3722 | /// In zh, this message translates to: | 3728 | /// In zh, this message translates to: |
| @@ -2082,6 +2082,10 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -2082,6 +2082,10 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 2082 | String get watchThemeSave => 'Save'; | 2082 | String get watchThemeSave => 'Save'; |
| 2083 | 2083 | ||
| 2084 | @override | 2084 | @override |
| 2085 | + String get watchThemeContentUnavailable => | ||
| 2086 | + 'This content is unavailable. Try another one.'; | ||
| 2087 | + | ||
| 2088 | + @override | ||
| 2085 | String get watchThemeDialPreview => 'Watch Face Preview'; | 2089 | String get watchThemeDialPreview => 'Watch Face Preview'; |
| 2086 | 2090 | ||
| 2087 | @override | 2091 | @override |
| @@ -1988,6 +1988,9 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1988,6 +1988,9 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1988 | String get watchThemeSave => '保存'; | 1988 | String get watchThemeSave => '保存'; |
| 1989 | 1989 | ||
| 1990 | @override | 1990 | @override |
| 1991 | + String get watchThemeContentUnavailable => '这个内容不可用,换一个吧'; | ||
| 1992 | + | ||
| 1993 | + @override | ||
| 1991 | String get watchThemeDialPreview => '表盘预览'; | 1994 | String get watchThemeDialPreview => '表盘预览'; |
| 1992 | 1995 | ||
| 1993 | @override | 1996 | @override |
| @@ -485,6 +485,42 @@ void main() { | @@ -485,6 +485,42 @@ void main() { | ||
| 485 | expect(target.stand, 12); | 485 | expect(target.stand, 12); |
| 486 | expect(statistics.totalExercise, 1800); | 486 | expect(statistics.totalExercise, 1800); |
| 487 | expect(statistics.totalStand, 6); | 487 | expect(statistics.totalStand, 6); |
| 488 | + expect(statistics.activityTargetInfo?.move, 500); | ||
| 489 | + expect(statistics.activityTargetInfo?.exercise, 3600); | ||
| 490 | + expect(statistics.activityTargetInfo?.stand, 12); | ||
| 491 | + }); | ||
| 492 | + | ||
| 493 | + test('resolves activity goals independently from latest useful values', () { | ||
| 494 | + final firstDay = DateTime(2026, 7, 15); | ||
| 495 | + final secondDay = DateTime(2026, 7, 16); | ||
| 496 | + final activity = [ | ||
| 497 | + HealthKitRawActivityDataPoint( | ||
| 498 | + endTime: LocalHealthDataConvert.unixSeconds( | ||
| 499 | + firstDay.add(const Duration(hours: 12)), | ||
| 500 | + ), | ||
| 501 | + activeEnergyBurnedGoal: 0, | ||
| 502 | + exerciseTimeGoal: 3600, | ||
| 503 | + standHoursGoal: 12, | ||
| 504 | + ), | ||
| 505 | + HealthKitRawActivityDataPoint( | ||
| 506 | + endTime: LocalHealthDataConvert.unixSeconds( | ||
| 507 | + secondDay.add(const Duration(hours: 12)), | ||
| 508 | + ), | ||
| 509 | + activeEnergyBurnedGoal: 600, | ||
| 510 | + exerciseTimeGoal: 0, | ||
| 511 | + standHoursGoal: null, | ||
| 512 | + ), | ||
| 513 | + ]; | ||
| 514 | + | ||
| 515 | + final statistics = LocalHealthDataConvert.activityBurnStatistics( | ||
| 516 | + days: [firstDay, secondDay], | ||
| 517 | + previousDays: const [], | ||
| 518 | + activity: activity, | ||
| 519 | + heartRate: const [], | ||
| 520 | + sleepIntervals: const [], | ||
| 521 | + ); | ||
| 522 | + | ||
| 523 | + expect(statistics.activityTargetInfo?.move, 600); | ||
| 488 | expect(statistics.activityTargetInfo?.exercise, 3600); | 524 | expect(statistics.activityTargetInfo?.exercise, 3600); |
| 489 | expect(statistics.activityTargetInfo?.stand, 12); | 525 | expect(statistics.activityTargetInfo?.stand, 12); |
| 490 | }); | 526 | }); |
-
Please register or login to post a comment