Showing
9 changed files
with
285 additions
and
55 deletions
| @@ -2,13 +2,13 @@ import 'dart:async'; | @@ -2,13 +2,13 @@ import 'dart:async'; | ||
| 2 | 2 | ||
| 3 | import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; | 3 | import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; |
| 4 | import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; | 4 | import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; |
| 5 | -import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart'; | ||
| 6 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | 5 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; |
| 7 | import 'package:doublefeel_flutter/core/error/app_error.dart'; | 6 | import 'package:doublefeel_flutter/core/error/app_error.dart'; |
| 8 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 7 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 9 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 8 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 10 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 9 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 11 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 10 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| 11 | +import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; | ||
| 12 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 12 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 13 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | 13 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; |
| 14 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 14 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| @@ -16,18 +16,19 @@ import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | @@ -16,18 +16,19 @@ import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | ||
| 16 | import 'package:get/get.dart'; | 16 | import 'package:get/get.dart'; |
| 17 | 17 | ||
| 18 | import '../data/friends_repository.dart'; | 18 | import '../data/friends_repository.dart'; |
| 19 | +import '../dialog/edit_remark_dialog.dart'; | ||
| 19 | import '../models/friend_health_data.dart'; | 20 | import '../models/friend_health_data.dart'; |
| 20 | 21 | ||
| 21 | class FriendsController extends GetxController { | 22 | class FriendsController extends GetxController { |
| 22 | FriendsController({ | 23 | FriendsController({ |
| 23 | FriendsRepository? repository, | 24 | FriendsRepository? repository, |
| 24 | Future<void> Function(WatchAppOtherInfo info)? updateWatchOtherUserInfo, | 25 | Future<void> Function(WatchAppOtherInfo info)? updateWatchOtherUserInfo, |
| 25 | - }) : _repository = | ||
| 26 | - repository ?? | ||
| 27 | - FriendsRepositoryImpl( | ||
| 28 | - Get.find<FriendApi>(), | ||
| 29 | - Get.find<UserPreferencesStorage>(), | ||
| 30 | - ), | 26 | + }) : _repository = repository ?? |
| 27 | + FriendsRepositoryImpl( | ||
| 28 | + Get.find<FriendApi>(), | ||
| 29 | + Get.find<UserPreferencesStorage>(), | ||
| 30 | + Get.find<UserAccountStorage>(), | ||
| 31 | + ), | ||
| 31 | _updateWatchOtherUserInfo = updateWatchOtherUserInfo ?? | 32 | _updateWatchOtherUserInfo = updateWatchOtherUserInfo ?? |
| 32 | ((info) => PlatformHostApi().refreshWatchAppAndWidgets()); | 33 | ((info) => PlatformHostApi().refreshWatchAppAndWidgets()); |
| 33 | 34 | ||
| @@ -41,6 +42,7 @@ class FriendsController extends GetxController { | @@ -41,6 +42,7 @@ class FriendsController extends GetxController { | ||
| 41 | Future<void>? _friendsRequest; | 42 | Future<void>? _friendsRequest; |
| 42 | bool _isPageVisible = false; | 43 | bool _isPageVisible = false; |
| 43 | late final Worker _preferencesWorker; | 44 | late final Worker _preferencesWorker; |
| 45 | + late final Worker _realtimeStressSettingsWorker; | ||
| 44 | bool _showRealtimeStress = false; | 46 | bool _showRealtimeStress = false; |
| 45 | 47 | ||
| 46 | bool get isFull => friends.length >= maxFriends; | 48 | bool get isFull => friends.length >= maxFriends; |
| @@ -53,19 +55,28 @@ class FriendsController extends GetxController { | @@ -53,19 +55,28 @@ class FriendsController extends GetxController { | ||
| 53 | Get.find<UserPreferencesStorage>().preferences, | 55 | Get.find<UserPreferencesStorage>().preferences, |
| 54 | _onPreferencesChanged, | 56 | _onPreferencesChanged, |
| 55 | ); | 57 | ); |
| 58 | + _realtimeStressSettingsWorker = ever<int>( | ||
| 59 | + Get.find<UserAccountStorage>().realtimeStressSettingsVersion, | ||
| 60 | + (_) => _onPreferencesChanged( | ||
| 61 | + Get.find<UserPreferencesStorage>().preferences.value), | ||
| 62 | + ); | ||
| 56 | unawaited(refreshData()); | 63 | unawaited(refreshData()); |
| 57 | } | 64 | } |
| 58 | 65 | ||
| 59 | @override | 66 | @override |
| 60 | void onClose() { | 67 | void onClose() { |
| 61 | _preferencesWorker.dispose(); | 68 | _preferencesWorker.dispose(); |
| 69 | + _realtimeStressSettingsWorker.dispose(); | ||
| 62 | super.onClose(); | 70 | super.onClose(); |
| 63 | } | 71 | } |
| 64 | 72 | ||
| 65 | bool get _currentShowRealtimeStress { | 73 | bool get _currentShowRealtimeStress { |
| 66 | final preferences = Get.find<UserPreferencesStorage>().preferences.value; | 74 | final preferences = Get.find<UserPreferencesStorage>().preferences.value; |
| 75 | + final userId = preferences.meUserInfo?.id; | ||
| 67 | return preferences.vipInfo?.isVip == true && | 76 | return preferences.vipInfo?.isVip == true && |
| 68 | - (preferences.showRealtimeStress ?? true); | 77 | + (userId == null || |
| 78 | + (Get.find<UserAccountStorage>().showRealtimeStress(userId) ?? | ||
| 79 | + true)); | ||
| 69 | } | 80 | } |
| 70 | 81 | ||
| 71 | void _onPreferencesChanged(UserPreferences _) { | 82 | void _onPreferencesChanged(UserPreferences _) { |
| @@ -115,28 +126,26 @@ class FriendsController extends GetxController { | @@ -115,28 +126,26 @@ class FriendsController extends GetxController { | ||
| 115 | } | 126 | } |
| 116 | 127 | ||
| 117 | Future<void> showEditRemarkDialog(FriendHealthData friend) async { | 128 | Future<void> showEditRemarkDialog(FriendHealthData friend) async { |
| 118 | - final result = await DialogUtils.showInputDialog( | ||
| 119 | - InputDialogMetaData( | 129 | + await Get.dialog<String>( |
| 130 | + FriendEditRemarkDialog( | ||
| 120 | title: l10n.friendsEditRemarkTitle, | 131 | title: l10n.friendsEditRemarkTitle, |
| 121 | initialValue: friend.remark ?? '', | 132 | initialValue: friend.remark ?? '', |
| 122 | hintText: l10n.friendsEditRemarkHint, | 133 | hintText: l10n.friendsEditRemarkHint, |
| 123 | confirmText: l10n.friendsSave, | 134 | confirmText: l10n.friendsSave, |
| 124 | maxLength: 6, | 135 | maxLength: 6, |
| 136 | + onConfirm: (remark) => _updateRemark(friend, remark), | ||
| 125 | ), | 137 | ), |
| 138 | + barrierDismissible: true, | ||
| 126 | ); | 139 | ); |
| 140 | + } | ||
| 127 | 141 | ||
| 128 | - final remark = result.data?.trim(); | ||
| 129 | - if (result.action != DialogAction.confirm || | ||
| 130 | - remark == null || | ||
| 131 | - remark.isEmpty) { | ||
| 132 | - return; | ||
| 133 | - } | ||
| 134 | - | 142 | + Future<bool> _updateRemark(FriendHealthData friend, String remark) async { |
| 135 | final userId = friend.userId; | 143 | final userId = friend.userId; |
| 136 | - if (userId == null) return; | 144 | + if (userId == null) return false; |
| 137 | try { | 145 | try { |
| 138 | await _repository.updateRemark(userId, remark); | 146 | await _repository.updateRemark(userId, remark); |
| 139 | await loadFriends(); | 147 | await loadFriends(); |
| 148 | + return true; | ||
| 140 | } catch (error, stackTrace) { | 149 | } catch (error, stackTrace) { |
| 141 | if (error is AppError) { | 150 | if (error is AppError) { |
| 142 | AppToast.show(error.displayMessage); | 151 | AppToast.show(error.displayMessage); |
| @@ -147,6 +156,7 @@ class FriendsController extends GetxController { | @@ -147,6 +156,7 @@ class FriendsController extends GetxController { | ||
| 147 | stackTrace, | 156 | stackTrace, |
| 148 | ); | 157 | ); |
| 149 | } | 158 | } |
| 159 | + return false; | ||
| 150 | } | 160 | } |
| 151 | } | 161 | } |
| 152 | 162 |
| 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/local/user_preferences_storage.dart'; |
| 4 | +import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; | ||
| 4 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart' | 5 | import 'package:doublefeel_flutter/data/models/friend/friend_models.dart' |
| 5 | as api_models; | 6 | as api_models; |
| 6 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 7 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| @@ -36,10 +37,15 @@ class FriendListData { | @@ -36,10 +37,15 @@ class FriendListData { | ||
| 36 | } | 37 | } |
| 37 | 38 | ||
| 38 | class FriendsRepositoryImpl implements FriendsRepository { | 39 | class FriendsRepositoryImpl implements FriendsRepository { |
| 39 | - const FriendsRepositoryImpl(this._friendApi, [this._userPreferencesStorage]); | 40 | + const FriendsRepositoryImpl( |
| 41 | + this._friendApi, [ | ||
| 42 | + this._userPreferencesStorage, | ||
| 43 | + this._userAccountStorage, | ||
| 44 | + ]); | ||
| 40 | 45 | ||
| 41 | final FriendApi _friendApi; | 46 | final FriendApi _friendApi; |
| 42 | final UserPreferencesStorage? _userPreferencesStorage; | 47 | final UserPreferencesStorage? _userPreferencesStorage; |
| 48 | + final UserAccountStorage? _userAccountStorage; | ||
| 43 | 49 | ||
| 44 | @override | 50 | @override |
| 45 | Future<List<FriendHealthData>> getFriends( | 51 | Future<List<FriendHealthData>> getFriends( |
| @@ -160,8 +166,12 @@ class FriendsRepositoryImpl implements FriendsRepository { | @@ -160,8 +166,12 @@ class FriendsRepositoryImpl implements FriendsRepository { | ||
| 160 | (_userPreferencesStorage ?? Get.find<UserPreferencesStorage>()) | 166 | (_userPreferencesStorage ?? Get.find<UserPreferencesStorage>()) |
| 161 | .preferences | 167 | .preferences |
| 162 | .value; | 168 | .value; |
| 169 | + final userId = preferences.meUserInfo?.id; | ||
| 163 | return preferences.vipInfo?.isVip == true && | 170 | return preferences.vipInfo?.isVip == true && |
| 164 | - (preferences.showRealtimeStress ?? true); | 171 | + (userId == null || |
| 172 | + ((_userAccountStorage ?? Get.find<UserAccountStorage>()) | ||
| 173 | + .showRealtimeStress(userId) ?? | ||
| 174 | + true)); | ||
| 165 | } | 175 | } |
| 166 | 176 | ||
| 167 | api_models.FriendRealtimeStressItem? _latestRealtimeStress( | 177 | api_models.FriendRealtimeStressItem? _latestRealtimeStress( |
| 1 | +import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/core/util/size_extensions.dart'; | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:flutter/services.dart'; | ||
| 5 | +import 'package:get/get.dart'; | ||
| 6 | + | ||
| 7 | +class FriendEditRemarkDialog extends StatefulWidget { | ||
| 8 | + const FriendEditRemarkDialog({ | ||
| 9 | + super.key, | ||
| 10 | + required this.title, | ||
| 11 | + required this.initialValue, | ||
| 12 | + required this.hintText, | ||
| 13 | + required this.confirmText, | ||
| 14 | + required this.maxLength, | ||
| 15 | + this.onConfirm, | ||
| 16 | + }); | ||
| 17 | + | ||
| 18 | + final String title; | ||
| 19 | + final String initialValue; | ||
| 20 | + final String hintText; | ||
| 21 | + final String confirmText; | ||
| 22 | + final int maxLength; | ||
| 23 | + final Future<bool> Function(String value)? onConfirm; | ||
| 24 | + | ||
| 25 | + @override | ||
| 26 | + State<FriendEditRemarkDialog> createState() => _FriendEditRemarkDialogState(); | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +class _FriendEditRemarkDialogState extends State<FriendEditRemarkDialog> { | ||
| 30 | + late final TextEditingController _controller; | ||
| 31 | + bool _isSubmitting = false; | ||
| 32 | + | ||
| 33 | + @override | ||
| 34 | + void initState() { | ||
| 35 | + super.initState(); | ||
| 36 | + _controller = TextEditingController(text: widget.initialValue); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @override | ||
| 40 | + void dispose() { | ||
| 41 | + _controller.dispose(); | ||
| 42 | + super.dispose(); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @override | ||
| 46 | + Widget build(BuildContext context) { | ||
| 47 | + return Dialog( | ||
| 48 | + backgroundColor: Colors.transparent, | ||
| 49 | + insetPadding: EdgeInsets.zero, | ||
| 50 | + child: Container( | ||
| 51 | + width: 300.dp, | ||
| 52 | + padding: EdgeInsets.fromLTRB(24.dp, 24.dp, 24.dp, 20.dp), | ||
| 53 | + decoration: BoxDecoration( | ||
| 54 | + color: Colors.white, | ||
| 55 | + borderRadius: BorderRadius.circular(24.dp), | ||
| 56 | + ), | ||
| 57 | + child: Column( | ||
| 58 | + mainAxisSize: MainAxisSize.min, | ||
| 59 | + children: [ | ||
| 60 | + SizedBox( | ||
| 61 | + width: 220.dp, | ||
| 62 | + child: Text( | ||
| 63 | + widget.title, | ||
| 64 | + maxLines: 1, | ||
| 65 | + overflow: TextOverflow.ellipsis, | ||
| 66 | + textAlign: TextAlign.center, | ||
| 67 | + style: const TextStyle( | ||
| 68 | + color: Color(0xFF141414), | ||
| 69 | + fontSize: 18, | ||
| 70 | + fontWeight: FontWeight.w600, | ||
| 71 | + height: 1.25, | ||
| 72 | + ), | ||
| 73 | + ), | ||
| 74 | + ), | ||
| 75 | + SizedBox(height: 16.dp), | ||
| 76 | + ValueListenableBuilder<TextEditingValue>( | ||
| 77 | + valueListenable: _controller, | ||
| 78 | + builder: (context, value, _) { | ||
| 79 | + return Container( | ||
| 80 | + width: double.infinity, | ||
| 81 | + height: 52.dp, | ||
| 82 | + padding: EdgeInsets.only(left: 22.dp, right: 22.dp), | ||
| 83 | + decoration: BoxDecoration( | ||
| 84 | + color: const Color(0xFFF3F3F3), | ||
| 85 | + borderRadius: BorderRadius.circular(27.dp), | ||
| 86 | + ), | ||
| 87 | + child: Row( | ||
| 88 | + children: [ | ||
| 89 | + Expanded( | ||
| 90 | + child: TextField( | ||
| 91 | + controller: _controller, | ||
| 92 | + autofocus: true, | ||
| 93 | + maxLines: 1, | ||
| 94 | + inputFormatters: [ | ||
| 95 | + LengthLimitingTextInputFormatter(widget.maxLength), | ||
| 96 | + ], | ||
| 97 | + decoration: InputDecoration( | ||
| 98 | + border: InputBorder.none, | ||
| 99 | + hintText: widget.hintText, | ||
| 100 | + hintStyle: const TextStyle( | ||
| 101 | + color: AppColors.textTertiary, | ||
| 102 | + fontSize: 16, | ||
| 103 | + fontWeight: FontWeight.w400, | ||
| 104 | + ), | ||
| 105 | + isCollapsed: true, | ||
| 106 | + ), | ||
| 107 | + style: const TextStyle( | ||
| 108 | + color: Color(0xFF141414), | ||
| 109 | + fontSize: 16, | ||
| 110 | + fontWeight: FontWeight.w400, | ||
| 111 | + ), | ||
| 112 | + ), | ||
| 113 | + ), | ||
| 114 | + Text( | ||
| 115 | + '${value.text.characters.length}/${widget.maxLength}', | ||
| 116 | + style: const TextStyle( | ||
| 117 | + color: Color(0xFFD9D9D9), | ||
| 118 | + fontSize: 16, | ||
| 119 | + fontWeight: FontWeight.w400, | ||
| 120 | + ), | ||
| 121 | + ), | ||
| 122 | + ], | ||
| 123 | + ), | ||
| 124 | + ); | ||
| 125 | + }, | ||
| 126 | + ), | ||
| 127 | + SizedBox(height: 24.dp), | ||
| 128 | + ValueListenableBuilder<TextEditingValue>( | ||
| 129 | + valueListenable: _controller, | ||
| 130 | + builder: (context, value, _) { | ||
| 131 | + final enabled = value.text.trim().isNotEmpty && !_isSubmitting; | ||
| 132 | + return Opacity( | ||
| 133 | + opacity: enabled ? 1 : 0.4, | ||
| 134 | + child: GestureDetector( | ||
| 135 | + onTap: enabled ? () => _confirm(value.text) : null, | ||
| 136 | + behavior: HitTestBehavior.opaque, | ||
| 137 | + child: Container( | ||
| 138 | + width: 220.dp, | ||
| 139 | + height: 48.dp, | ||
| 140 | + alignment: Alignment.center, | ||
| 141 | + decoration: BoxDecoration( | ||
| 142 | + color: AppColors.primary, | ||
| 143 | + borderRadius: BorderRadius.circular(24.dp), | ||
| 144 | + ), | ||
| 145 | + padding: EdgeInsets.symmetric(horizontal: 16.dp), | ||
| 146 | + child: Text( | ||
| 147 | + widget.confirmText, | ||
| 148 | + maxLines: 1, | ||
| 149 | + overflow: TextOverflow.ellipsis, | ||
| 150 | + textAlign: TextAlign.center, | ||
| 151 | + style: const TextStyle( | ||
| 152 | + color: Colors.white, | ||
| 153 | + fontSize: 14, | ||
| 154 | + fontWeight: FontWeight.w500, | ||
| 155 | + height: 1.2, | ||
| 156 | + ), | ||
| 157 | + ), | ||
| 158 | + ), | ||
| 159 | + ), | ||
| 160 | + ); | ||
| 161 | + }, | ||
| 162 | + ), | ||
| 163 | + ], | ||
| 164 | + ), | ||
| 165 | + ), | ||
| 166 | + ); | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + Future<void> _confirm(String value) async { | ||
| 170 | + final trimmed = value.trim(); | ||
| 171 | + if (trimmed.isEmpty || _isSubmitting) { | ||
| 172 | + return; | ||
| 173 | + } | ||
| 174 | + setState(() { | ||
| 175 | + _isSubmitting = true; | ||
| 176 | + }); | ||
| 177 | + try { | ||
| 178 | + final canClose = | ||
| 179 | + await (widget.onConfirm?.call(trimmed) ?? Future<bool>.value(true)); | ||
| 180 | + if (canClose && mounted && Get.isDialogOpen == true) { | ||
| 181 | + Get.back(result: trimmed); | ||
| 182 | + } | ||
| 183 | + } finally { | ||
| 184 | + if (mounted) { | ||
| 185 | + setState(() { | ||
| 186 | + _isSubmitting = false; | ||
| 187 | + }); | ||
| 188 | + } | ||
| 189 | + } | ||
| 190 | + } | ||
| 191 | +} |
| 1 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 1 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 2 | +import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; | ||
| 2 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 3 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 3 | import 'package:get/get.dart'; | 4 | import 'package:get/get.dart'; |
| 4 | 5 | ||
| @@ -11,6 +12,7 @@ class PrivacySettingsBinding extends Bindings { | @@ -11,6 +12,7 @@ class PrivacySettingsBinding extends Bindings { | ||
| 11 | PrivacySettingsController( | 12 | PrivacySettingsController( |
| 12 | Get.find<UserApi>(), | 13 | Get.find<UserApi>(), |
| 13 | Get.find<UserPreferencesStorage>(), | 14 | Get.find<UserPreferencesStorage>(), |
| 15 | + Get.find<UserAccountStorage>(), | ||
| 14 | ), | 16 | ), |
| 15 | ); | 17 | ); |
| 16 | } | 18 | } |
| @@ -2,19 +2,26 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | @@ -2,19 +2,26 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | ||
| 2 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | 2 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; |
| 3 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 3 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 4 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 4 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 5 | +import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; | ||
| 5 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 6 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 6 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | 7 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; |
| 7 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | 8 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; |
| 8 | import 'package:get/get.dart'; | 9 | import 'package:get/get.dart'; |
| 9 | 10 | ||
| 10 | class PrivacySettingsController extends GetxController { | 11 | class PrivacySettingsController extends GetxController { |
| 11 | - PrivacySettingsController(this._userApi, this._userPreferencesStorage); | 12 | + PrivacySettingsController( |
| 13 | + this._userApi, | ||
| 14 | + this._userPreferencesStorage, | ||
| 15 | + this._userAccountStorage, | ||
| 16 | + ); | ||
| 12 | 17 | ||
| 13 | final UserApi _userApi; | 18 | final UserApi _userApi; |
| 14 | final UserPreferencesStorage _userPreferencesStorage; | 19 | final UserPreferencesStorage _userPreferencesStorage; |
| 20 | + final UserAccountStorage _userAccountStorage; | ||
| 15 | 21 | ||
| 16 | - final enableAddWithUCode = false.obs; | ||
| 17 | - final isUpdatingEnableAddWithUCode = false.obs; | 22 | + /// 页面文案是“禁止通过 ID 添加我”,因此该状态为 true 时表示禁止。 |
| 23 | + final disableAddWithUCode = false.obs; | ||
| 24 | + final isUpdatingDisableAddWithUCode = false.obs; | ||
| 18 | final isVip = false.obs; | 25 | final isVip = false.obs; |
| 19 | final showRealtimeStress = false.obs; | 26 | final showRealtimeStress = false.obs; |
| 20 | late final Worker _preferencesWorker; | 27 | late final Worker _preferencesWorker; |
| @@ -36,29 +43,32 @@ class PrivacySettingsController extends GetxController { | @@ -36,29 +43,32 @@ class PrivacySettingsController extends GetxController { | ||
| 36 | } | 43 | } |
| 37 | 44 | ||
| 38 | void _syncFromPreferences(UserPreferences preferences) { | 45 | void _syncFromPreferences(UserPreferences preferences) { |
| 39 | - enableAddWithUCode.value = | ||
| 40 | - preferences.meUserInfo?.enableAddWithUCode == 1; | 46 | + disableAddWithUCode.value = |
| 47 | + preferences.meUserInfo?.enableAddWithUCode != 1; | ||
| 41 | isVip.value = preferences.vipInfo?.isVip == true; | 48 | isVip.value = preferences.vipInfo?.isVip == true; |
| 42 | - showRealtimeStress.value = | ||
| 43 | - isVip.value && (preferences.showRealtimeStress ?? true); | 49 | + final userId = preferences.meUserInfo?.id; |
| 50 | + showRealtimeStress.value = isVip.value && | ||
| 51 | + (userId == null | ||
| 52 | + ? true | ||
| 53 | + : (_userAccountStorage.showRealtimeStress(userId) ?? true)); | ||
| 44 | } | 54 | } |
| 45 | 55 | ||
| 46 | - Future<void> updateEnableAddWithUCode(bool enabled) async { | ||
| 47 | - if (isUpdatingEnableAddWithUCode.value) return; | 56 | + Future<void> updateDisableAddWithUCode(bool disabled) async { |
| 57 | + if (isUpdatingDisableAddWithUCode.value) return; | ||
| 48 | 58 | ||
| 49 | - isUpdatingEnableAddWithUCode.value = true; | 59 | + isUpdatingDisableAddWithUCode.value = true; |
| 50 | try { | 60 | try { |
| 51 | final result = await _userApi.updateOwnerUserInfo( | 61 | final result = await _userApi.updateOwnerUserInfo( |
| 52 | - enableAddWithUcode: enabled ? 0 : 1, | 62 | + enableAddWithUcode: disabled ? 0 : 1, |
| 53 | ); | 63 | ); |
| 54 | if (result is AppSuccess<UserInfoResponse>) { | 64 | if (result is AppSuccess<UserInfoResponse>) { |
| 55 | await _userPreferencesStorage.updateMeUserInfo(result.data); | 65 | await _userPreferencesStorage.updateMeUserInfo(result.data); |
| 56 | - enableAddWithUCode.value = result.data.enableAddWithUCode == null | ||
| 57 | - ? !enabled | ||
| 58 | - : result.data.enableAddWithUCode == 1; | 66 | + disableAddWithUCode.value = result.data.enableAddWithUCode == null |
| 67 | + ? disabled | ||
| 68 | + : result.data.enableAddWithUCode != 1; | ||
| 59 | } | 69 | } |
| 60 | } finally { | 70 | } finally { |
| 61 | - isUpdatingEnableAddWithUCode.value = false; | 71 | + isUpdatingDisableAddWithUCode.value = false; |
| 62 | } | 72 | } |
| 63 | } | 73 | } |
| 64 | 74 | ||
| @@ -73,7 +83,10 @@ class PrivacySettingsController extends GetxController { | @@ -73,7 +83,10 @@ class PrivacySettingsController extends GetxController { | ||
| 73 | return; | 83 | return; |
| 74 | } | 84 | } |
| 75 | 85 | ||
| 76 | - await _userPreferencesStorage.updateShowRealtimeStress(enabled); | 86 | + final userId = _userPreferencesStorage.preferences.value.meUserInfo?.id; |
| 87 | + if (userId == null) return; | ||
| 88 | + await _userAccountStorage.updateShowRealtimeStress(userId, enabled); | ||
| 89 | + _syncFromPreferences(_userPreferencesStorage.preferences.value); | ||
| 77 | } | 90 | } |
| 78 | 91 | ||
| 79 | void executeBackLogic() { | 92 | void executeBackLogic() { |
| @@ -65,10 +65,10 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> { | @@ -65,10 +65,10 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> { | ||
| 65 | Widget noAddByIdView(BuildContext context) { | 65 | Widget noAddByIdView(BuildContext context) { |
| 66 | return _settingRow( | 66 | return _settingRow( |
| 67 | title: context.l10n.privacySettingsDisableAddById, | 67 | title: context.l10n.privacySettingsDisableAddById, |
| 68 | - value: controller.enableAddWithUCode, | ||
| 69 | - onChanged: () => controller.isUpdatingEnableAddWithUCode.value | 68 | + value: controller.disableAddWithUCode, |
| 69 | + onChanged: () => controller.isUpdatingDisableAddWithUCode.value | ||
| 70 | ? null | 70 | ? null |
| 71 | - : controller.updateEnableAddWithUCode, | 71 | + : controller.updateDisableAddWithUCode, |
| 72 | ); | 72 | ); |
| 73 | } | 73 | } |
| 74 | 74 | ||
| @@ -126,7 +126,7 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> { | @@ -126,7 +126,7 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> { | ||
| 126 | scaleX: 52 / 51, | 126 | scaleX: 52 / 51, |
| 127 | scaleY: 28 / 31, | 127 | scaleY: 28 / 31, |
| 128 | child: CupertinoSwitch( | 128 | child: CupertinoSwitch( |
| 129 | - value: !value.value, | 129 | + value: value.value, |
| 130 | activeTrackColor: AppColors.primary, | 130 | activeTrackColor: AppColors.primary, |
| 131 | inactiveTrackColor: const Color(0xFFE5E5EA), | 131 | inactiveTrackColor: const Color(0xFFE5E5EA), |
| 132 | onChanged: onChanged(), | 132 | onChanged: onChanged(), |
| 1 | +import 'package:get/get.dart'; | ||
| 1 | import 'package:shared_preferences/shared_preferences.dart'; | 2 | import 'package:shared_preferences/shared_preferences.dart'; |
| 2 | 3 | ||
| 3 | /// 账号级持久化存储。 | 4 | /// 账号级持久化存储。 |
| @@ -26,6 +27,13 @@ class UserAccountStorage { | @@ -26,6 +27,13 @@ class UserAccountStorage { | ||
| 26 | static String _appleHealthUploadTestLastSyncTimeKey(int userId) => | 27 | static String _appleHealthUploadTestLastSyncTimeKey(int userId) => |
| 27 | 'apple_health_upload_test_last_sync_time_$userId'; | 28 | 'apple_health_upload_test_last_sync_time_$userId'; |
| 28 | 29 | ||
| 30 | + /// 是否显示实时压力,按 userId 隔离,避免会话数据刷新时覆盖该偏好。 | ||
| 31 | + static String _showRealtimeStressKey(int userId) => | ||
| 32 | + 'account_show_realtime_stress_$userId'; | ||
| 33 | + | ||
| 34 | + /// 在实时压力偏好更新后通知依赖它的页面刷新。 | ||
| 35 | + final realtimeStressSettingsVersion = 0.obs; | ||
| 36 | + | ||
| 29 | /// Onboarding 已全部完成的哨兵值 | 37 | /// Onboarding 已全部完成的哨兵值 |
| 30 | static const int _kOnboardingCompleted = -1; | 38 | static const int _kOnboardingCompleted = -1; |
| 31 | 39 | ||
| @@ -73,6 +81,17 @@ class UserAccountStorage { | @@ -73,6 +81,17 @@ class UserAccountStorage { | ||
| 73 | int? getLastAddFriendBannerShowTime(int userId) => | 81 | int? getLastAddFriendBannerShowTime(int userId) => |
| 74 | _prefs.getInt('last_add_friend_banner_show_time_$userId'); | 82 | _prefs.getInt('last_add_friend_banner_show_time_$userId'); |
| 75 | 83 | ||
| 84 | + // ─── 实时压力展示偏好 ──────────────────────────────────────────────────── | ||
| 85 | + | ||
| 86 | + /// `null` 表示用户尚未设置,调用方应使用产品默认值。 | ||
| 87 | + bool? showRealtimeStress(int userId) => | ||
| 88 | + _prefs.getBool(_showRealtimeStressKey(userId)); | ||
| 89 | + | ||
| 90 | + Future<void> updateShowRealtimeStress(int userId, bool enabled) async { | ||
| 91 | + await _prefs.setBool(_showRealtimeStressKey(userId), enabled); | ||
| 92 | + realtimeStressSettingsVersion.value++; | ||
| 93 | + } | ||
| 94 | + | ||
| 76 | // ─── Apple Health 上传记录 ──────────────────────────────────────────────── | 95 | // ─── Apple Health 上传记录 ──────────────────────────────────────────────── |
| 77 | 96 | ||
| 78 | String? appleHealthUploadLocalRecordJson(int userId) => | 97 | String? appleHealthUploadLocalRecordJson(int userId) => |
| @@ -122,10 +122,6 @@ class UserPreferencesStorage { | @@ -122,10 +122,6 @@ class UserPreferencesStorage { | ||
| 122 | await _persist(preferences.value.copyWith(vipInfo: vipInfo)); | 122 | await _persist(preferences.value.copyWith(vipInfo: vipInfo)); |
| 123 | } | 123 | } |
| 124 | 124 | ||
| 125 | - Future<void> updateShowRealtimeStress(bool enabled) async { | ||
| 126 | - await _persist(preferences.value.copyWith(showRealtimeStress: enabled)); | ||
| 127 | - } | ||
| 128 | - | ||
| 129 | Future<void> updateFromLogin({ | 125 | Future<void> updateFromLogin({ |
| 130 | required String accessToken, | 126 | required String accessToken, |
| 131 | UserInfoResponse? me, | 127 | UserInfoResponse? me, |
| @@ -9,7 +9,6 @@ class UserPreferences { | @@ -9,7 +9,6 @@ class UserPreferences { | ||
| 9 | this.accessToken = '', | 9 | this.accessToken = '', |
| 10 | this.rongcloudToken = '', | 10 | this.rongcloudToken = '', |
| 11 | this.vipInfo, | 11 | this.vipInfo, |
| 12 | - this.showRealtimeStress, | ||
| 13 | }); | 12 | }); |
| 14 | 13 | ||
| 15 | final UserInfoResponse? meUserInfo; | 14 | final UserInfoResponse? meUserInfo; |
| @@ -18,10 +17,6 @@ class UserPreferences { | @@ -18,10 +17,6 @@ class UserPreferences { | ||
| 18 | final String rongcloudToken; | 17 | final String rongcloudToken; |
| 19 | final UserPreferencesVipInfo? vipInfo; | 18 | final UserPreferencesVipInfo? vipInfo; |
| 20 | 19 | ||
| 21 | - /// `null` preserves the product default: enabled for members and disabled | ||
| 22 | - /// for non-members. | ||
| 23 | - final bool? showRealtimeStress; | ||
| 24 | - | ||
| 25 | static const empty = UserPreferences(); | 20 | static const empty = UserPreferences(); |
| 26 | 21 | ||
| 27 | factory UserPreferences.fromJson(Map<String, dynamic> json) { | 22 | factory UserPreferences.fromJson(Map<String, dynamic> json) { |
| @@ -40,7 +35,6 @@ class UserPreferences { | @@ -40,7 +35,6 @@ class UserPreferences { | ||
| 40 | ? null | 35 | ? null |
| 41 | : UserPreferencesVipInfo.fromJson( | 36 | : UserPreferencesVipInfo.fromJson( |
| 42 | json['vip_info'] as Map<String, dynamic>), | 37 | json['vip_info'] as Map<String, dynamic>), |
| 43 | - showRealtimeStress: json['show_realtime_stress'] as bool?, | ||
| 44 | ); | 38 | ); |
| 45 | } | 39 | } |
| 46 | 40 | ||
| @@ -53,9 +47,6 @@ class UserPreferences { | @@ -53,9 +47,6 @@ class UserPreferences { | ||
| 53 | val['access_token'] = accessToken; | 47 | val['access_token'] = accessToken; |
| 54 | val['rongcloud_token'] = rongcloudToken; | 48 | val['rongcloud_token'] = rongcloudToken; |
| 55 | if (vipInfo != null) val['vip_info'] = vipInfo!.toJson(); | 49 | if (vipInfo != null) val['vip_info'] = vipInfo!.toJson(); |
| 56 | - if (showRealtimeStress != null) { | ||
| 57 | - val['show_realtime_stress'] = showRealtimeStress; | ||
| 58 | - } | ||
| 59 | return val; | 50 | return val; |
| 60 | } | 51 | } |
| 61 | 52 | ||
| @@ -65,7 +56,6 @@ class UserPreferences { | @@ -65,7 +56,6 @@ class UserPreferences { | ||
| 65 | String? accessToken, | 56 | String? accessToken, |
| 66 | String? rongcloudToken, | 57 | String? rongcloudToken, |
| 67 | UserPreferencesVipInfo? vipInfo, | 58 | UserPreferencesVipInfo? vipInfo, |
| 68 | - bool? showRealtimeStress, | ||
| 69 | bool clearPartner = false, | 59 | bool clearPartner = false, |
| 70 | }) { | 60 | }) { |
| 71 | return UserPreferences( | 61 | return UserPreferences( |
| @@ -75,7 +65,6 @@ class UserPreferences { | @@ -75,7 +65,6 @@ class UserPreferences { | ||
| 75 | accessToken: accessToken ?? this.accessToken, | 65 | accessToken: accessToken ?? this.accessToken, |
| 76 | rongcloudToken: rongcloudToken ?? this.rongcloudToken, | 66 | rongcloudToken: rongcloudToken ?? this.rongcloudToken, |
| 77 | vipInfo: vipInfo ?? this.vipInfo, | 67 | vipInfo: vipInfo ?? this.vipInfo, |
| 78 | - showRealtimeStress: showRealtimeStress ?? this.showRealtimeStress, | ||
| 79 | ); | 68 | ); |
| 80 | } | 69 | } |
| 81 | } | 70 | } |
-
Please register or login to post a comment