Commit 26c97b6e080f42e60f52e1ca23d4cc62eeaf62a9

Authored by 刘宏哲
1 parent f9956c3e

feat(app): update ui

@@ -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