Showing
14 changed files
with
297 additions
and
22 deletions
| @@ -9,6 +9,8 @@ import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | @@ -9,6 +9,8 @@ import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | ||
| 9 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 9 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 10 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 10 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 11 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 11 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| 12 | +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | ||
| 13 | +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | ||
| 12 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 14 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 13 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | 15 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; |
| 14 | import 'package:get/get.dart'; | 16 | import 'package:get/get.dart'; |
| @@ -21,7 +23,11 @@ class FriendsController extends GetxController { | @@ -21,7 +23,11 @@ class FriendsController extends GetxController { | ||
| 21 | FriendsRepository? repository, | 23 | FriendsRepository? repository, |
| 22 | Future<void> Function(WatchAppOtherInfo info)? updateWatchOtherUserInfo, | 24 | Future<void> Function(WatchAppOtherInfo info)? updateWatchOtherUserInfo, |
| 23 | }) : _repository = | 25 | }) : _repository = |
| 24 | - repository ?? FriendsRepositoryImpl(Get.find<FriendApi>()), | 26 | + repository ?? |
| 27 | + FriendsRepositoryImpl( | ||
| 28 | + Get.find<FriendApi>(), | ||
| 29 | + Get.find<UserPreferencesStorage>(), | ||
| 30 | + ), | ||
| 25 | _updateWatchOtherUserInfo = updateWatchOtherUserInfo ?? | 31 | _updateWatchOtherUserInfo = updateWatchOtherUserInfo ?? |
| 26 | ((info) => PlatformHostApi().refreshWatchAppAndWidgets()); | 32 | ((info) => PlatformHostApi().refreshWatchAppAndWidgets()); |
| 27 | 33 | ||
| @@ -34,12 +40,38 @@ class FriendsController extends GetxController { | @@ -34,12 +40,38 @@ class FriendsController extends GetxController { | ||
| 34 | final selfHealthData = Rxn<SelfFriendHealthData>(); | 40 | final selfHealthData = Rxn<SelfFriendHealthData>(); |
| 35 | Future<void>? _friendsRequest; | 41 | Future<void>? _friendsRequest; |
| 36 | bool _isPageVisible = false; | 42 | bool _isPageVisible = false; |
| 43 | + late final Worker _preferencesWorker; | ||
| 44 | + bool _showRealtimeStress = false; | ||
| 37 | 45 | ||
| 38 | bool get isFull => friends.length >= maxFriends; | 46 | bool get isFull => friends.length >= maxFriends; |
| 39 | 47 | ||
| 40 | @override | 48 | @override |
| 41 | void onInit() { | 49 | void onInit() { |
| 42 | super.onInit(); | 50 | super.onInit(); |
| 51 | + _showRealtimeStress = _currentShowRealtimeStress; | ||
| 52 | + _preferencesWorker = ever<UserPreferences>( | ||
| 53 | + Get.find<UserPreferencesStorage>().preferences, | ||
| 54 | + _onPreferencesChanged, | ||
| 55 | + ); | ||
| 56 | + unawaited(refreshData()); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + @override | ||
| 60 | + void onClose() { | ||
| 61 | + _preferencesWorker.dispose(); | ||
| 62 | + super.onClose(); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + bool get _currentShowRealtimeStress { | ||
| 66 | + final preferences = Get.find<UserPreferencesStorage>().preferences.value; | ||
| 67 | + return preferences.vipInfo?.isVip == true && | ||
| 68 | + (preferences.showRealtimeStress ?? true); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + void _onPreferencesChanged(UserPreferences _) { | ||
| 72 | + final showRealtimeStress = _currentShowRealtimeStress; | ||
| 73 | + if (showRealtimeStress == _showRealtimeStress) return; | ||
| 74 | + _showRealtimeStress = showRealtimeStress; | ||
| 43 | unawaited(refreshData()); | 75 | unawaited(refreshData()); |
| 44 | } | 76 | } |
| 45 | 77 |
| 1 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 1 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 2 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 2 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 3 | +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | ||
| 3 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart' | 4 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart' |
| 4 | as api_models; | 5 | as api_models; |
| 5 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 6 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 7 | +import 'package:get/get.dart'; | ||
| 6 | import 'package:intl/intl.dart'; | 8 | import 'package:intl/intl.dart'; |
| 7 | 9 | ||
| 8 | import '../models/friend_health_data.dart'; | 10 | import '../models/friend_health_data.dart'; |
| @@ -34,9 +36,10 @@ class FriendListData { | @@ -34,9 +36,10 @@ class FriendListData { | ||
| 34 | } | 36 | } |
| 35 | 37 | ||
| 36 | class FriendsRepositoryImpl implements FriendsRepository { | 38 | class FriendsRepositoryImpl implements FriendsRepository { |
| 37 | - const FriendsRepositoryImpl(this._friendApi); | 39 | + const FriendsRepositoryImpl(this._friendApi, [this._userPreferencesStorage]); |
| 38 | 40 | ||
| 39 | final FriendApi _friendApi; | 41 | final FriendApi _friendApi; |
| 42 | + final UserPreferencesStorage? _userPreferencesStorage; | ||
| 40 | 43 | ||
| 41 | @override | 44 | @override |
| 42 | Future<List<FriendHealthData>> getFriends( | 45 | Future<List<FriendHealthData>> getFriends( |
| @@ -96,6 +99,7 @@ class FriendsRepositoryImpl implements FriendsRepository { | @@ -96,6 +99,7 @@ class FriendsRepositoryImpl implements FriendsRepository { | ||
| 96 | final healthData = friend.healthData; | 99 | final healthData = friend.healthData; |
| 97 | final nickname = friend.friendNickname?.trim(); | 100 | final nickname = friend.friendNickname?.trim(); |
| 98 | final remark = friend.remarkName?.trim(); | 101 | final remark = friend.remarkName?.trim(); |
| 102 | + final stressData = _stressData(healthData); | ||
| 99 | 103 | ||
| 100 | return FriendHealthData( | 104 | return FriendHealthData( |
| 101 | friendItem: friend, | 105 | friendItem: friend, |
| @@ -105,13 +109,13 @@ class FriendsRepositoryImpl implements FriendsRepository { | @@ -105,13 +109,13 @@ class FriendsRepositoryImpl implements FriendsRepository { | ||
| 105 | ? l10n.friendsUnknownFriend | 109 | ? l10n.friendsUnknownFriend |
| 106 | : nickname, | 110 | : nickname, |
| 107 | remark: remark == null || remark.isEmpty ? null : remark, | 111 | remark: remark == null || remark.isEmpty ? null : remark, |
| 108 | - updatedAt: _updatedAt(healthData?.lastDataTime ?? friend.updateTime), | 112 | + updatedAt: _updatedAt(stressData.updatedAt), |
| 109 | sleepQualityScore: healthData?.sleepEvaluate, | 113 | sleepQualityScore: healthData?.sleepEvaluate, |
| 110 | steps: healthData?.totalSteps == null | 114 | steps: healthData?.totalSteps == null |
| 111 | ? null | 115 | ? null |
| 112 | : l10n.friendsStepCount(healthData!.totalSteps!), | 116 | : l10n.friendsStepCount(healthData!.totalSteps!), |
| 113 | - stressState: | ||
| 114 | - FriendStressState.fromValue(healthData?.comprehensiveStressState), | 117 | + stressState: stressData.state, |
| 118 | + stressValueText: stressData.valueText, | ||
| 115 | isOnWatchFace: friend.isShowInDial, | 119 | isOnWatchFace: friend.isShowInDial, |
| 116 | ); | 120 | ); |
| 117 | } | 121 | } |
| @@ -120,22 +124,85 @@ class FriendsRepositoryImpl implements FriendsRepository { | @@ -120,22 +124,85 @@ class FriendsRepositoryImpl implements FriendsRepository { | ||
| 120 | api_models.FriendHealthData? healthData, | 124 | api_models.FriendHealthData? healthData, |
| 121 | ) { | 125 | ) { |
| 122 | if (healthData == null) return null; | 126 | if (healthData == null) return null; |
| 127 | + final stressData = _stressData(healthData); | ||
| 123 | return SelfFriendHealthData( | 128 | return SelfFriendHealthData( |
| 124 | - updatedAt: _updatedAt(healthData.lastDataTime), | 129 | + updatedAt: _updatedAt(stressData.updatedAt), |
| 125 | sleepQualityScore: healthData.sleepEvaluate, | 130 | sleepQualityScore: healthData.sleepEvaluate, |
| 126 | steps: healthData.totalSteps == null | 131 | steps: healthData.totalSteps == null |
| 127 | ? null | 132 | ? null |
| 128 | : l10n.friendsStepCount(healthData.totalSteps!), | 133 | : l10n.friendsStepCount(healthData.totalSteps!), |
| 129 | - stressState: | ||
| 130 | - FriendStressState.fromValue(healthData.comprehensiveStressState), | 134 | + stressState: stressData.state, |
| 135 | + stressValueText: stressData.valueText, | ||
| 131 | ); | 136 | ); |
| 132 | } | 137 | } |
| 133 | 138 | ||
| 139 | + _FriendStressData _stressData(api_models.FriendHealthData? healthData) { | ||
| 140 | + if (healthData == null) return const _FriendStressData(); | ||
| 141 | + | ||
| 142 | + if (_showRealtimeStress) { | ||
| 143 | + final realtime = _latestRealtimeStress(healthData.realtimeStress); | ||
| 144 | + return _FriendStressData( | ||
| 145 | + state: FriendStressState.fromValue(realtime?.state), | ||
| 146 | + valueText: _valueText(realtime?.value, '%'), | ||
| 147 | + updatedAt: realtime?.time, | ||
| 148 | + ); | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + return _FriendStressData( | ||
| 152 | + state: FriendStressState.fromValue(healthData.hrvState), | ||
| 153 | + valueText: _valueText(healthData.latestHrv, 'ms'), | ||
| 154 | + updatedAt: healthData.lastDataTime, | ||
| 155 | + ); | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + bool get _showRealtimeStress { | ||
| 159 | + final preferences = | ||
| 160 | + (_userPreferencesStorage ?? Get.find<UserPreferencesStorage>()) | ||
| 161 | + .preferences | ||
| 162 | + .value; | ||
| 163 | + return preferences.vipInfo?.isVip == true && | ||
| 164 | + (preferences.showRealtimeStress ?? true); | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + api_models.FriendRealtimeStressItem? _latestRealtimeStress( | ||
| 168 | + List<api_models.FriendRealtimeStressItem>? samples, | ||
| 169 | + ) { | ||
| 170 | + if (samples == null || samples.isEmpty) return null; | ||
| 171 | + return samples.reduce( | ||
| 172 | + (latest, sample) => (sample.time ?? -1) > (latest.time ?? -1) | ||
| 173 | + ? sample | ||
| 174 | + : latest, | ||
| 175 | + ); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + String? _valueText(double? value, String unit) { | ||
| 179 | + if (value == null) return null; | ||
| 180 | + final text = value == value.truncateToDouble() | ||
| 181 | + ? value.toInt().toString() | ||
| 182 | + : value.toString(); | ||
| 183 | + return '$text$unit'; | ||
| 184 | + } | ||
| 185 | + | ||
| 134 | String _updatedAt(num? timestamp) { | 186 | String _updatedAt(num? timestamp) { |
| 135 | if (timestamp == null) return l10n.friendsWaitingForData; | 187 | if (timestamp == null) return l10n.friendsWaitingForData; |
| 136 | final milliseconds = | 188 | final milliseconds = |
| 137 | timestamp < 1000000000000 ? timestamp * 1000 : timestamp; | 189 | timestamp < 1000000000000 ? timestamp * 1000 : timestamp; |
| 138 | final time = DateTime.fromMillisecondsSinceEpoch(milliseconds.toInt()); | 190 | final time = DateTime.fromMillisecondsSinceEpoch(milliseconds.toInt()); |
| 139 | - return l10n.friendsUpdatedAt(DateFormat('HH:mm').format(time)); | 191 | + final formattedTime = DateFormat('HH:mm').format(time); |
| 192 | + return _showRealtimeStress | ||
| 193 | + ? l10n.friendsRealtimeStressUpdatedAt(formattedTime) | ||
| 194 | + : l10n.friendsHrvUpdatedAt(formattedTime); | ||
| 140 | } | 195 | } |
| 141 | } | 196 | } |
| 197 | + | ||
| 198 | +class _FriendStressData { | ||
| 199 | + const _FriendStressData({ | ||
| 200 | + this.state = FriendStressState.wait, | ||
| 201 | + this.valueText, | ||
| 202 | + this.updatedAt, | ||
| 203 | + }); | ||
| 204 | + | ||
| 205 | + final FriendStressState state; | ||
| 206 | + final String? valueText; | ||
| 207 | + final num? updatedAt; | ||
| 208 | +} |
| @@ -15,6 +15,7 @@ class FriendHealthData { | @@ -15,6 +15,7 @@ class FriendHealthData { | ||
| 15 | required this.sleepQualityScore, | 15 | required this.sleepQualityScore, |
| 16 | required this.steps, | 16 | required this.steps, |
| 17 | required this.stressState, | 17 | required this.stressState, |
| 18 | + this.stressValueText, | ||
| 18 | this.isOnWatchFace = false, | 19 | this.isOnWatchFace = false, |
| 19 | }); | 20 | }); |
| 20 | 21 | ||
| @@ -27,6 +28,7 @@ class FriendHealthData { | @@ -27,6 +28,7 @@ class FriendHealthData { | ||
| 27 | final int? sleepQualityScore; | 28 | final int? sleepQualityScore; |
| 28 | final String? steps; | 29 | final String? steps; |
| 29 | final FriendStressState stressState; | 30 | final FriendStressState stressState; |
| 31 | + final String? stressValueText; | ||
| 30 | final bool isOnWatchFace; | 32 | final bool isOnWatchFace; |
| 31 | 33 | ||
| 32 | String get displayName { | 34 | String get displayName { |
| @@ -45,6 +47,7 @@ class FriendHealthData { | @@ -45,6 +47,7 @@ class FriendHealthData { | ||
| 45 | int? sleepQualityScore, | 47 | int? sleepQualityScore, |
| 46 | String? steps, | 48 | String? steps, |
| 47 | FriendStressState? stressState, | 49 | FriendStressState? stressState, |
| 50 | + String? stressValueText, | ||
| 48 | bool? isOnWatchFace, | 51 | bool? isOnWatchFace, |
| 49 | }) { | 52 | }) { |
| 50 | return FriendHealthData( | 53 | return FriendHealthData( |
| @@ -57,6 +60,7 @@ class FriendHealthData { | @@ -57,6 +60,7 @@ class FriendHealthData { | ||
| 57 | sleepQualityScore: sleepQualityScore ?? this.sleepQualityScore, | 60 | sleepQualityScore: sleepQualityScore ?? this.sleepQualityScore, |
| 58 | steps: steps ?? this.steps, | 61 | steps: steps ?? this.steps, |
| 59 | stressState: stressState ?? this.stressState, | 62 | stressState: stressState ?? this.stressState, |
| 63 | + stressValueText: stressValueText ?? this.stressValueText, | ||
| 60 | isOnWatchFace: isOnWatchFace ?? this.isOnWatchFace, | 64 | isOnWatchFace: isOnWatchFace ?? this.isOnWatchFace, |
| 61 | ); | 65 | ); |
| 62 | } | 66 | } |
| @@ -68,10 +72,12 @@ class SelfFriendHealthData { | @@ -68,10 +72,12 @@ class SelfFriendHealthData { | ||
| 68 | required this.sleepQualityScore, | 72 | required this.sleepQualityScore, |
| 69 | required this.steps, | 73 | required this.steps, |
| 70 | required this.stressState, | 74 | required this.stressState, |
| 75 | + this.stressValueText, | ||
| 71 | }); | 76 | }); |
| 72 | 77 | ||
| 73 | final String updatedAt; | 78 | final String updatedAt; |
| 74 | final int? sleepQualityScore; | 79 | final int? sleepQualityScore; |
| 75 | final String? steps; | 80 | final String? steps; |
| 76 | final FriendStressState stressState; | 81 | final FriendStressState stressState; |
| 82 | + final String? stressValueText; | ||
| 77 | } | 83 | } |
| @@ -55,6 +55,7 @@ class FriendsTab extends GetView<FriendsController> { | @@ -55,6 +55,7 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 55 | steps: healthData?.steps, | 55 | steps: healthData?.steps, |
| 56 | stressState: | 56 | stressState: |
| 57 | healthData?.stressState ?? FriendStressState.wait, | 57 | healthData?.stressState ?? FriendStressState.wait, |
| 58 | + stressValueText: healthData?.stressValueText, | ||
| 58 | isLoading: isSelfInitialLoading, | 59 | isLoading: isSelfInitialLoading, |
| 59 | ); | 60 | ); |
| 60 | 61 | ||
| @@ -166,6 +167,7 @@ class FriendsTab extends GetView<FriendsController> { | @@ -166,6 +167,7 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 166 | sleepQualityScore: friend.sleepQualityScore, | 167 | sleepQualityScore: friend.sleepQualityScore, |
| 167 | steps: friend.steps, | 168 | steps: friend.steps, |
| 168 | stressState: friend.stressState, | 169 | stressState: friend.stressState, |
| 170 | + stressValueText: friend.stressValueText, | ||
| 169 | isOnWatchFace: friend.isOnWatchFace, | 171 | isOnWatchFace: friend.isOnWatchFace, |
| 170 | onTap: () => _openFriendHome(friend), | 172 | onTap: () => _openFriendHome(friend), |
| 171 | onMoreSelected: (action) { | 173 | onMoreSelected: (action) { |
| @@ -260,6 +262,7 @@ class _SelfHealthCard extends StatelessWidget { | @@ -260,6 +262,7 @@ class _SelfHealthCard extends StatelessWidget { | ||
| 260 | required this.sleepQualityScore, | 262 | required this.sleepQualityScore, |
| 261 | required this.steps, | 263 | required this.steps, |
| 262 | required this.stressState, | 264 | required this.stressState, |
| 265 | + required this.stressValueText, | ||
| 263 | required this.isLoading, | 266 | required this.isLoading, |
| 264 | }); | 267 | }); |
| 265 | 268 | ||
| @@ -269,6 +272,7 @@ class _SelfHealthCard extends StatelessWidget { | @@ -269,6 +272,7 @@ class _SelfHealthCard extends StatelessWidget { | ||
| 269 | final int? sleepQualityScore; | 272 | final int? sleepQualityScore; |
| 270 | final String? steps; | 273 | final String? steps; |
| 271 | final FriendStressState stressState; | 274 | final FriendStressState stressState; |
| 275 | + final String? stressValueText; | ||
| 272 | final bool isLoading; | 276 | final bool isLoading; |
| 273 | 277 | ||
| 274 | @override | 278 | @override |
| @@ -283,6 +287,7 @@ class _SelfHealthCard extends StatelessWidget { | @@ -283,6 +287,7 @@ class _SelfHealthCard extends StatelessWidget { | ||
| 283 | sleepQualityScore: sleepQualityScore, | 287 | sleepQualityScore: sleepQualityScore, |
| 284 | steps: steps, | 288 | steps: steps, |
| 285 | stressState: stressState, | 289 | stressState: stressState, |
| 290 | + stressValueText: stressValueText, | ||
| 286 | isSelf: true, | 291 | isSelf: true, |
| 287 | borderRadius: borderRadius, | 292 | borderRadius: borderRadius, |
| 288 | contentPadding: const EdgeInsets.fromLTRB(36, 20, 35, 20), | 293 | contentPadding: const EdgeInsets.fromLTRB(36, 20, 35, 20), |
| @@ -22,6 +22,7 @@ class FriendHealthCard extends StatelessWidget { | @@ -22,6 +22,7 @@ class FriendHealthCard extends StatelessWidget { | ||
| 22 | required this.sleepQualityScore, | 22 | required this.sleepQualityScore, |
| 23 | required this.steps, | 23 | required this.steps, |
| 24 | required this.stressState, | 24 | required this.stressState, |
| 25 | + this.stressValueText, | ||
| 25 | this.isSelf = false, | 26 | this.isSelf = false, |
| 26 | this.isOnWatchFace = false, | 27 | this.isOnWatchFace = false, |
| 27 | this.isSelected = false, | 28 | this.isSelected = false, |
| @@ -39,6 +40,7 @@ class FriendHealthCard extends StatelessWidget { | @@ -39,6 +40,7 @@ class FriendHealthCard extends StatelessWidget { | ||
| 39 | final int? sleepQualityScore; | 40 | final int? sleepQualityScore; |
| 40 | final String? steps; | 41 | final String? steps; |
| 41 | final FriendStressState stressState; | 42 | final FriendStressState stressState; |
| 43 | + final String? stressValueText; | ||
| 42 | final bool isSelf; | 44 | final bool isSelf; |
| 43 | final bool isOnWatchFace; | 45 | final bool isOnWatchFace; |
| 44 | final bool isSelected; | 46 | final bool isSelected; |
| @@ -92,6 +94,7 @@ class FriendHealthCard extends StatelessWidget { | @@ -92,6 +94,7 @@ class FriendHealthCard extends StatelessWidget { | ||
| 92 | Expanded( | 94 | Expanded( |
| 93 | child: _StatusFigure( | 95 | child: _StatusFigure( |
| 94 | stressState: stressState, | 96 | stressState: stressState, |
| 97 | + stressValueText: stressValueText, | ||
| 95 | ), | 98 | ), |
| 96 | ), | 99 | ), |
| 97 | const SizedBox(width: 18), | 100 | const SizedBox(width: 18), |
| @@ -332,9 +335,11 @@ class _AvatarFallback extends StatelessWidget { | @@ -332,9 +335,11 @@ class _AvatarFallback extends StatelessWidget { | ||
| 332 | class _StatusFigure extends StatelessWidget { | 335 | class _StatusFigure extends StatelessWidget { |
| 333 | const _StatusFigure({ | 336 | const _StatusFigure({ |
| 334 | required this.stressState, | 337 | required this.stressState, |
| 338 | + this.stressValueText, | ||
| 335 | }); | 339 | }); |
| 336 | 340 | ||
| 337 | final FriendStressState stressState; | 341 | final FriendStressState stressState; |
| 342 | + final String? stressValueText; | ||
| 338 | 343 | ||
| 339 | @override | 344 | @override |
| 340 | Widget build(BuildContext context) { | 345 | Widget build(BuildContext context) { |
| @@ -371,7 +376,9 @@ class _StatusFigure extends StatelessWidget { | @@ -371,7 +376,9 @@ class _StatusFigure extends StatelessWidget { | ||
| 371 | ), | 376 | ), |
| 372 | const SizedBox(height: 3), | 377 | const SizedBox(height: 3), |
| 373 | Text( | 378 | Text( |
| 374 | - stressState.label, | 379 | + stressState.hasData && stressValueText != null |
| 380 | + ? '${stressState.label}·$stressValueText' | ||
| 381 | + : stressState.label, | ||
| 375 | style: const TextStyle( | 382 | style: const TextStyle( |
| 376 | color: Color(0xFF0F0F11), | 383 | color: Color(0xFF0F0F11), |
| 377 | fontSize: 14, | 384 | fontSize: 14, |
| 1 | +import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | ||
| 1 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 3 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 2 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 4 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 3 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 5 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 6 | +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | ||
| 4 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | 7 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; |
| 5 | import 'package:get/get.dart'; | 8 | import 'package:get/get.dart'; |
| 6 | 9 | ||
| @@ -12,15 +15,34 @@ class PrivacySettingsController extends GetxController { | @@ -12,15 +15,34 @@ class PrivacySettingsController extends GetxController { | ||
| 12 | 15 | ||
| 13 | final enableAddWithUCode = false.obs; | 16 | final enableAddWithUCode = false.obs; |
| 14 | final isUpdatingEnableAddWithUCode = false.obs; | 17 | final isUpdatingEnableAddWithUCode = false.obs; |
| 18 | + final isVip = false.obs; | ||
| 19 | + final showRealtimeStress = false.obs; | ||
| 20 | + late final Worker _preferencesWorker; | ||
| 15 | 21 | ||
| 16 | @override | 22 | @override |
| 17 | void onInit() { | 23 | void onInit() { |
| 18 | - enableAddWithUCode.value = _userPreferencesStorage | ||
| 19 | - .preferences.value.meUserInfo?.enableAddWithUCode == | ||
| 20 | - 1; | 24 | + _syncFromPreferences(_userPreferencesStorage.preferences.value); |
| 25 | + _preferencesWorker = ever<UserPreferences>( | ||
| 26 | + _userPreferencesStorage.preferences, | ||
| 27 | + _syncFromPreferences, | ||
| 28 | + ); | ||
| 21 | super.onInit(); | 29 | super.onInit(); |
| 22 | } | 30 | } |
| 23 | 31 | ||
| 32 | + @override | ||
| 33 | + void onClose() { | ||
| 34 | + _preferencesWorker.dispose(); | ||
| 35 | + super.onClose(); | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + void _syncFromPreferences(UserPreferences preferences) { | ||
| 39 | + enableAddWithUCode.value = | ||
| 40 | + preferences.meUserInfo?.enableAddWithUCode == 1; | ||
| 41 | + isVip.value = preferences.vipInfo?.isVip == true; | ||
| 42 | + showRealtimeStress.value = | ||
| 43 | + isVip.value && (preferences.showRealtimeStress ?? true); | ||
| 44 | + } | ||
| 45 | + | ||
| 24 | Future<void> updateEnableAddWithUCode(bool enabled) async { | 46 | Future<void> updateEnableAddWithUCode(bool enabled) async { |
| 25 | if (isUpdatingEnableAddWithUCode.value) return; | 47 | if (isUpdatingEnableAddWithUCode.value) return; |
| 26 | 48 | ||
| @@ -40,6 +62,20 @@ class PrivacySettingsController extends GetxController { | @@ -40,6 +62,20 @@ class PrivacySettingsController extends GetxController { | ||
| 40 | } | 62 | } |
| 41 | } | 63 | } |
| 42 | 64 | ||
| 65 | + Future<void> updateShowRealtimeStress(bool enabled) async { | ||
| 66 | + final isVip = _userPreferencesStorage.preferences.value.vipInfo?.isVip == | ||
| 67 | + true; | ||
| 68 | + if (!isVip && enabled) { | ||
| 69 | + await Get.toNamed(Routes.PURCHASE, arguments: { | ||
| 70 | + IntentKeys.channelType: '隐私设置-显示实时压力', | ||
| 71 | + }); | ||
| 72 | + _syncFromPreferences(_userPreferencesStorage.preferences.value); | ||
| 73 | + return; | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + await _userPreferencesStorage.updateShowRealtimeStress(enabled); | ||
| 77 | + } | ||
| 78 | + | ||
| 43 | void executeBackLogic() { | 79 | void executeBackLogic() { |
| 44 | Get.back(); | 80 | Get.back(); |
| 45 | } | 81 | } |
| @@ -55,15 +55,52 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> { | @@ -55,15 +55,52 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> { | ||
| 55 | children: [ | 55 | children: [ |
| 56 | SizedBox(height: 16.dp), | 56 | SizedBox(height: 16.dp), |
| 57 | noAddByIdView(context), | 57 | noAddByIdView(context), |
| 58 | + SizedBox(height: 12.dp), | ||
| 59 | + showRealtimeStressView(context), | ||
| 58 | ], | 60 | ], |
| 59 | ), | 61 | ), |
| 60 | ); | 62 | ); |
| 61 | } | 63 | } |
| 62 | 64 | ||
| 63 | Widget noAddByIdView(BuildContext context) { | 65 | Widget noAddByIdView(BuildContext context) { |
| 66 | + return _settingRow( | ||
| 67 | + title: context.l10n.privacySettingsDisableAddById, | ||
| 68 | + value: controller.enableAddWithUCode, | ||
| 69 | + onChanged: () => controller.isUpdatingEnableAddWithUCode.value | ||
| 70 | + ? null | ||
| 71 | + : controller.updateEnableAddWithUCode, | ||
| 72 | + ); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + Widget showRealtimeStressView(BuildContext context) { | ||
| 76 | + return _settingRow( | ||
| 77 | + title: context.l10n.privacySettingsShowRealtimeStress, | ||
| 78 | + value: controller.showRealtimeStress, | ||
| 79 | + onChanged: () => controller.updateShowRealtimeStress, | ||
| 80 | + height: 56.dp, | ||
| 81 | + titleAccessory: () => controller.isVip.value | ||
| 82 | + ? const SizedBox.shrink() | ||
| 83 | + : SizedBox( | ||
| 84 | + width: 43.dp, | ||
| 85 | + height: 16.dp, | ||
| 86 | + child: Image.asset( | ||
| 87 | + 'assets/images/common/ic_pro_badge.webp', | ||
| 88 | + fit: BoxFit.fill, | ||
| 89 | + ), | ||
| 90 | + ), | ||
| 91 | + ); | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + Widget _settingRow({ | ||
| 95 | + required String title, | ||
| 96 | + required RxBool value, | ||
| 97 | + required ValueChanged<bool>? Function() onChanged, | ||
| 98 | + Widget Function()? titleAccessory, | ||
| 99 | + double? height, | ||
| 100 | + }) { | ||
| 64 | return Container( | 101 | return Container( |
| 65 | width: double.infinity, | 102 | width: double.infinity, |
| 66 | - height: 48.dp, | 103 | + height: height ?? 48.dp, |
| 67 | margin: EdgeInsets.symmetric(horizontal: 16.dp), | 104 | margin: EdgeInsets.symmetric(horizontal: 16.dp), |
| 68 | padding: EdgeInsets.symmetric(horizontal: 20.dp), | 105 | padding: EdgeInsets.symmetric(horizontal: 20.dp), |
| 69 | decoration: BoxDecoration( | 106 | decoration: BoxDecoration( |
| @@ -73,21 +110,27 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> { | @@ -73,21 +110,27 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> { | ||
| 73 | child: Row( | 110 | child: Row( |
| 74 | children: [ | 111 | children: [ |
| 75 | Text( | 112 | Text( |
| 76 | - context.l10n.privacySettingsDisableAddById, | 113 | + title, |
| 77 | style: const TextStyle( | 114 | style: const TextStyle( |
| 78 | fontSize: 14, | 115 | fontSize: 14, |
| 79 | color: AppColors.textPrimary, | 116 | color: AppColors.textPrimary, |
| 80 | ), | 117 | ), |
| 81 | ), | 118 | ), |
| 119 | + if (titleAccessory != null) ...[ | ||
| 120 | + SizedBox(width: 4.dp), | ||
| 121 | + Obx(titleAccessory), | ||
| 122 | + ], | ||
| 82 | const Spacer(), | 123 | const Spacer(), |
| 83 | Obx( | 124 | Obx( |
| 84 | - () => CupertinoSwitch( | ||
| 85 | - value: controller.enableAddWithUCode.value, | ||
| 86 | - activeTrackColor: AppColors.primary, | ||
| 87 | - inactiveTrackColor: const Color(0xFFE5E5EA), | ||
| 88 | - onChanged: controller.isUpdatingEnableAddWithUCode.value | ||
| 89 | - ? null | ||
| 90 | - : controller.updateEnableAddWithUCode, | 125 | + () => Transform.scale( |
| 126 | + scaleX: 52 / 51, | ||
| 127 | + scaleY: 28 / 31, | ||
| 128 | + child: CupertinoSwitch( | ||
| 129 | + value: value.value, | ||
| 130 | + activeTrackColor: AppColors.primary, | ||
| 131 | + inactiveTrackColor: const Color(0xFFE5E5EA), | ||
| 132 | + onChanged: onChanged(), | ||
| 133 | + ), | ||
| 91 | ), | 134 | ), |
| 92 | ), | 135 | ), |
| 93 | ], | 136 | ], |
| @@ -103,6 +103,10 @@ class UserPreferencesStorage { | @@ -103,6 +103,10 @@ class UserPreferencesStorage { | ||
| 103 | await _persist(preferences.value.copyWith(vipInfo: vipInfo)); | 103 | await _persist(preferences.value.copyWith(vipInfo: vipInfo)); |
| 104 | } | 104 | } |
| 105 | 105 | ||
| 106 | + Future<void> updateShowRealtimeStress(bool enabled) async { | ||
| 107 | + await _persist(preferences.value.copyWith(showRealtimeStress: enabled)); | ||
| 108 | + } | ||
| 109 | + | ||
| 106 | Future<void> updateFromLogin({ | 110 | Future<void> updateFromLogin({ |
| 107 | required String accessToken, | 111 | required String accessToken, |
| 108 | UserInfoResponse? me, | 112 | UserInfoResponse? me, |
| @@ -9,6 +9,7 @@ class UserPreferences { | @@ -9,6 +9,7 @@ class UserPreferences { | ||
| 9 | this.accessToken = '', | 9 | this.accessToken = '', |
| 10 | this.rongcloudToken = '', | 10 | this.rongcloudToken = '', |
| 11 | this.vipInfo, | 11 | this.vipInfo, |
| 12 | + this.showRealtimeStress, | ||
| 12 | }); | 13 | }); |
| 13 | 14 | ||
| 14 | final UserInfoResponse? meUserInfo; | 15 | final UserInfoResponse? meUserInfo; |
| @@ -17,6 +18,10 @@ class UserPreferences { | @@ -17,6 +18,10 @@ class UserPreferences { | ||
| 17 | final String rongcloudToken; | 18 | final String rongcloudToken; |
| 18 | final UserPreferencesVipInfo? vipInfo; | 19 | final UserPreferencesVipInfo? vipInfo; |
| 19 | 20 | ||
| 21 | + /// `null` preserves the product default: enabled for members and disabled | ||
| 22 | + /// for non-members. | ||
| 23 | + final bool? showRealtimeStress; | ||
| 24 | + | ||
| 20 | static const empty = UserPreferences(); | 25 | static const empty = UserPreferences(); |
| 21 | 26 | ||
| 22 | factory UserPreferences.fromJson(Map<String, dynamic> json) { | 27 | factory UserPreferences.fromJson(Map<String, dynamic> json) { |
| @@ -35,6 +40,7 @@ class UserPreferences { | @@ -35,6 +40,7 @@ class UserPreferences { | ||
| 35 | ? null | 40 | ? null |
| 36 | : UserPreferencesVipInfo.fromJson( | 41 | : UserPreferencesVipInfo.fromJson( |
| 37 | json['vip_info'] as Map<String, dynamic>), | 42 | json['vip_info'] as Map<String, dynamic>), |
| 43 | + showRealtimeStress: json['show_realtime_stress'] as bool?, | ||
| 38 | ); | 44 | ); |
| 39 | } | 45 | } |
| 40 | 46 | ||
| @@ -47,6 +53,9 @@ class UserPreferences { | @@ -47,6 +53,9 @@ class UserPreferences { | ||
| 47 | val['access_token'] = accessToken; | 53 | val['access_token'] = accessToken; |
| 48 | val['rongcloud_token'] = rongcloudToken; | 54 | val['rongcloud_token'] = rongcloudToken; |
| 49 | if (vipInfo != null) val['vip_info'] = vipInfo!.toJson(); | 55 | if (vipInfo != null) val['vip_info'] = vipInfo!.toJson(); |
| 56 | + if (showRealtimeStress != null) { | ||
| 57 | + val['show_realtime_stress'] = showRealtimeStress; | ||
| 58 | + } | ||
| 50 | return val; | 59 | return val; |
| 51 | } | 60 | } |
| 52 | 61 | ||
| @@ -56,6 +65,7 @@ class UserPreferences { | @@ -56,6 +65,7 @@ class UserPreferences { | ||
| 56 | String? accessToken, | 65 | String? accessToken, |
| 57 | String? rongcloudToken, | 66 | String? rongcloudToken, |
| 58 | UserPreferencesVipInfo? vipInfo, | 67 | UserPreferencesVipInfo? vipInfo, |
| 68 | + bool? showRealtimeStress, | ||
| 59 | bool clearPartner = false, | 69 | bool clearPartner = false, |
| 60 | }) { | 70 | }) { |
| 61 | return UserPreferences( | 71 | return UserPreferences( |
| @@ -65,6 +75,7 @@ class UserPreferences { | @@ -65,6 +75,7 @@ class UserPreferences { | ||
| 65 | accessToken: accessToken ?? this.accessToken, | 75 | accessToken: accessToken ?? this.accessToken, |
| 66 | rongcloudToken: rongcloudToken ?? this.rongcloudToken, | 76 | rongcloudToken: rongcloudToken ?? this.rongcloudToken, |
| 67 | vipInfo: vipInfo ?? this.vipInfo, | 77 | vipInfo: vipInfo ?? this.vipInfo, |
| 78 | + showRealtimeStress: showRealtimeStress ?? this.showRealtimeStress, | ||
| 68 | ); | 79 | ); |
| 69 | } | 80 | } |
| 70 | } | 81 | } |
| @@ -423,6 +423,8 @@ | @@ -423,6 +423,8 @@ | ||
| 423 | "friendsRemarkSuffix": " ({remark})", | 423 | "friendsRemarkSuffix": " ({remark})", |
| 424 | "friendsUnknownFriend": "Unknown friend", | 424 | "friendsUnknownFriend": "Unknown friend", |
| 425 | "friendsUpdatedAt": "Updated at {time}", | 425 | "friendsUpdatedAt": "Updated at {time}", |
| 426 | + "friendsRealtimeStressUpdatedAt": "Real-time stress updated at {time}", | ||
| 427 | + "friendsHrvUpdatedAt": "HRV updated at {time}", | ||
| 426 | "friendsStepCount": "{count} steps", | 428 | "friendsStepCount": "{count} steps", |
| 427 | "friendsStressAttention": "Stress alert", | 429 | "friendsStressAttention": "Stress alert", |
| 428 | "friendsWaitingForData": "Waiting for data", | 430 | "friendsWaitingForData": "Waiting for data", |
| @@ -456,6 +458,7 @@ | @@ -456,6 +458,7 @@ | ||
| 456 | "friendsDeleteConfirmAction": "Remove", | 458 | "friendsDeleteConfirmAction": "Remove", |
| 457 | "privacySettingsTitle": "Privacy Settings", | 459 | "privacySettingsTitle": "Privacy Settings", |
| 458 | "privacySettingsDisableAddById": "Don't allow others to add me by ID", | 460 | "privacySettingsDisableAddById": "Don't allow others to add me by ID", |
| 461 | + "privacySettingsShowRealtimeStress": "Show real-time stress", | ||
| 459 | "premiumActivatedTitle": "DoubleFeel Pro is now active", | 462 | "premiumActivatedTitle": "DoubleFeel Pro is now active", |
| 460 | "premiumActivatedDescription": "You can now monitor stress, sleep, and HRV in real time, build healthier habits, and share health updates with close contacts so the people who matter can stay informed.", | 463 | "premiumActivatedDescription": "You can now monitor stress, sleep, and HRV in real time, build healthier habits, and share health updates with close contacts so the people who matter can stay informed.", |
| 461 | "premiumActivatedContinue": "Continue", | 464 | "premiumActivatedContinue": "Continue", |
| @@ -771,6 +771,22 @@ | @@ -771,6 +771,22 @@ | ||
| 771 | } | 771 | } |
| 772 | } | 772 | } |
| 773 | }, | 773 | }, |
| 774 | + "friendsRealtimeStressUpdatedAt": "实时压力更新于{time}", | ||
| 775 | + "@friendsRealtimeStressUpdatedAt": { | ||
| 776 | + "placeholders": { | ||
| 777 | + "time": { | ||
| 778 | + "type": "String" | ||
| 779 | + } | ||
| 780 | + } | ||
| 781 | + }, | ||
| 782 | + "friendsHrvUpdatedAt": "HRV更新于{time}", | ||
| 783 | + "@friendsHrvUpdatedAt": { | ||
| 784 | + "placeholders": { | ||
| 785 | + "time": { | ||
| 786 | + "type": "String" | ||
| 787 | + } | ||
| 788 | + } | ||
| 789 | + }, | ||
| 774 | "friendsStepCount": "{count}步", | 790 | "friendsStepCount": "{count}步", |
| 775 | "@friendsStepCount": { | 791 | "@friendsStepCount": { |
| 776 | "placeholders": { | 792 | "placeholders": { |
| @@ -825,6 +841,7 @@ | @@ -825,6 +841,7 @@ | ||
| 825 | "friendsDeleteConfirmAction": "确认解除", | 841 | "friendsDeleteConfirmAction": "确认解除", |
| 826 | "privacySettingsTitle": "隐私设置", | 842 | "privacySettingsTitle": "隐私设置", |
| 827 | "privacySettingsDisableAddById": "不允许通过ID添加我", | 843 | "privacySettingsDisableAddById": "不允许通过ID添加我", |
| 844 | + "privacySettingsShowRealtimeStress": "显示实时压力", | ||
| 828 | "premiumActivatedTitle": "恭喜你已开通 DoubleFeel Pro", | 845 | "premiumActivatedTitle": "恭喜你已开通 DoubleFeel Pro", |
| 829 | "premiumActivatedDescription": "你现在可以实时监测压力、睡眠与 HRV,养成健康生活习惯,同时把关心分享给亲密联系人,让重要的人及时了解你的状态。", | 846 | "premiumActivatedDescription": "你现在可以实时监测压力、睡眠与 HRV,养成健康生活习惯,同时把关心分享给亲密联系人,让重要的人及时了解你的状态。", |
| 830 | "premiumActivatedContinue": "继续", | 847 | "premiumActivatedContinue": "继续", |
| @@ -2637,6 +2637,18 @@ abstract class AppLocalizations { | @@ -2637,6 +2637,18 @@ abstract class AppLocalizations { | ||
| 2637 | /// **'更新于{time}'** | 2637 | /// **'更新于{time}'** |
| 2638 | String friendsUpdatedAt(String time); | 2638 | String friendsUpdatedAt(String time); |
| 2639 | 2639 | ||
| 2640 | + /// No description provided for @friendsRealtimeStressUpdatedAt. | ||
| 2641 | + /// | ||
| 2642 | + /// In zh, this message translates to: | ||
| 2643 | + /// **'实时压力更新于{time}'** | ||
| 2644 | + String friendsRealtimeStressUpdatedAt(String time); | ||
| 2645 | + | ||
| 2646 | + /// No description provided for @friendsHrvUpdatedAt. | ||
| 2647 | + /// | ||
| 2648 | + /// In zh, this message translates to: | ||
| 2649 | + /// **'HRV更新于{time}'** | ||
| 2650 | + String friendsHrvUpdatedAt(String time); | ||
| 2651 | + | ||
| 2640 | /// No description provided for @friendsStepCount. | 2652 | /// No description provided for @friendsStepCount. |
| 2641 | /// | 2653 | /// |
| 2642 | /// In zh, this message translates to: | 2654 | /// In zh, this message translates to: |
| @@ -2835,6 +2847,12 @@ abstract class AppLocalizations { | @@ -2835,6 +2847,12 @@ abstract class AppLocalizations { | ||
| 2835 | /// **'不允许通过ID添加我'** | 2847 | /// **'不允许通过ID添加我'** |
| 2836 | String get privacySettingsDisableAddById; | 2848 | String get privacySettingsDisableAddById; |
| 2837 | 2849 | ||
| 2850 | + /// No description provided for @privacySettingsShowRealtimeStress. | ||
| 2851 | + /// | ||
| 2852 | + /// In zh, this message translates to: | ||
| 2853 | + /// **'显示实时压力'** | ||
| 2854 | + String get privacySettingsShowRealtimeStress; | ||
| 2855 | + | ||
| 2838 | /// No description provided for @premiumActivatedTitle. | 2856 | /// No description provided for @premiumActivatedTitle. |
| 2839 | /// | 2857 | /// |
| 2840 | /// In zh, this message translates to: | 2858 | /// In zh, this message translates to: |
| @@ -1467,6 +1467,16 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1467,6 +1467,16 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1467 | } | 1467 | } |
| 1468 | 1468 | ||
| 1469 | @override | 1469 | @override |
| 1470 | + String friendsRealtimeStressUpdatedAt(String time) { | ||
| 1471 | + return 'Real-time stress updated at $time'; | ||
| 1472 | + } | ||
| 1473 | + | ||
| 1474 | + @override | ||
| 1475 | + String friendsHrvUpdatedAt(String time) { | ||
| 1476 | + return 'HRV updated at $time'; | ||
| 1477 | + } | ||
| 1478 | + | ||
| 1479 | + @override | ||
| 1470 | String friendsStepCount(int count) { | 1480 | String friendsStepCount(int count) { |
| 1471 | return '$count steps'; | 1481 | return '$count steps'; |
| 1472 | } | 1482 | } |
| @@ -1576,6 +1586,9 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1576,6 +1586,9 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1576 | 'Don\'t allow others to add me by ID'; | 1586 | 'Don\'t allow others to add me by ID'; |
| 1577 | 1587 | ||
| 1578 | @override | 1588 | @override |
| 1589 | + String get privacySettingsShowRealtimeStress => 'Show real-time stress'; | ||
| 1590 | + | ||
| 1591 | + @override | ||
| 1579 | String get premiumActivatedTitle => 'DoubleFeel Pro is now active'; | 1592 | String get premiumActivatedTitle => 'DoubleFeel Pro is now active'; |
| 1580 | 1593 | ||
| 1581 | @override | 1594 | @override |
| @@ -1409,6 +1409,16 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1409,6 +1409,16 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1409 | } | 1409 | } |
| 1410 | 1410 | ||
| 1411 | @override | 1411 | @override |
| 1412 | + String friendsRealtimeStressUpdatedAt(String time) { | ||
| 1413 | + return '实时压力更新于$time'; | ||
| 1414 | + } | ||
| 1415 | + | ||
| 1416 | + @override | ||
| 1417 | + String friendsHrvUpdatedAt(String time) { | ||
| 1418 | + return 'HRV更新于$time'; | ||
| 1419 | + } | ||
| 1420 | + | ||
| 1421 | + @override | ||
| 1412 | String friendsStepCount(int count) { | 1422 | String friendsStepCount(int count) { |
| 1413 | return '$count步'; | 1423 | return '$count步'; |
| 1414 | } | 1424 | } |
| @@ -1514,6 +1524,9 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1514,6 +1524,9 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1514 | String get privacySettingsDisableAddById => '不允许通过ID添加我'; | 1524 | String get privacySettingsDisableAddById => '不允许通过ID添加我'; |
| 1515 | 1525 | ||
| 1516 | @override | 1526 | @override |
| 1527 | + String get privacySettingsShowRealtimeStress => '显示实时压力'; | ||
| 1528 | + | ||
| 1529 | + @override | ||
| 1517 | String get premiumActivatedTitle => '恭喜你已开通 DoubleFeel Pro'; | 1530 | String get premiumActivatedTitle => '恭喜你已开通 DoubleFeel Pro'; |
| 1518 | 1531 | ||
| 1519 | @override | 1532 | @override |
-
Please register or login to post a comment