Commit 6810f12781a1da656446fb00fefdb376fa7ae5b7

Authored by 刘宏哲
1 parent ca0b4ce5

feat(app): bug fixed

... ... @@ -2,13 +2,13 @@ import 'dart:async';
import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
import 'package:doublefeel_flutter/core/error/app_error.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
... ... @@ -16,18 +16,19 @@ import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:get/get.dart';
import '../data/friends_repository.dart';
import '../dialog/edit_remark_dialog.dart';
import '../models/friend_health_data.dart';
class FriendsController extends GetxController {
FriendsController({
FriendsRepository? repository,
Future<void> Function(WatchAppOtherInfo info)? updateWatchOtherUserInfo,
}) : _repository =
repository ??
FriendsRepositoryImpl(
Get.find<FriendApi>(),
Get.find<UserPreferencesStorage>(),
),
}) : _repository = repository ??
FriendsRepositoryImpl(
Get.find<FriendApi>(),
Get.find<UserPreferencesStorage>(),
Get.find<UserAccountStorage>(),
),
_updateWatchOtherUserInfo = updateWatchOtherUserInfo ??
((info) => PlatformHostApi().refreshWatchAppAndWidgets());
... ... @@ -41,6 +42,7 @@ class FriendsController extends GetxController {
Future<void>? _friendsRequest;
bool _isPageVisible = false;
late final Worker _preferencesWorker;
late final Worker _realtimeStressSettingsWorker;
bool _showRealtimeStress = false;
bool get isFull => friends.length >= maxFriends;
... ... @@ -53,19 +55,28 @@ class FriendsController extends GetxController {
Get.find<UserPreferencesStorage>().preferences,
_onPreferencesChanged,
);
_realtimeStressSettingsWorker = ever<int>(
Get.find<UserAccountStorage>().realtimeStressSettingsVersion,
(_) => _onPreferencesChanged(
Get.find<UserPreferencesStorage>().preferences.value),
);
unawaited(refreshData());
}
@override
void onClose() {
_preferencesWorker.dispose();
_realtimeStressSettingsWorker.dispose();
super.onClose();
}
bool get _currentShowRealtimeStress {
final preferences = Get.find<UserPreferencesStorage>().preferences.value;
final userId = preferences.meUserInfo?.id;
return preferences.vipInfo?.isVip == true &&
(preferences.showRealtimeStress ?? true);
(userId == null ||
(Get.find<UserAccountStorage>().showRealtimeStress(userId) ??
true));
}
void _onPreferencesChanged(UserPreferences _) {
... ... @@ -115,28 +126,26 @@ class FriendsController extends GetxController {
}
Future<void> showEditRemarkDialog(FriendHealthData friend) async {
final result = await DialogUtils.showInputDialog(
InputDialogMetaData(
await Get.dialog<String>(
FriendEditRemarkDialog(
title: l10n.friendsEditRemarkTitle,
initialValue: friend.remark ?? '',
hintText: l10n.friendsEditRemarkHint,
confirmText: l10n.friendsSave,
maxLength: 6,
onConfirm: (remark) => _updateRemark(friend, remark),
),
barrierDismissible: true,
);
}
final remark = result.data?.trim();
if (result.action != DialogAction.confirm ||
remark == null ||
remark.isEmpty) {
return;
}
Future<bool> _updateRemark(FriendHealthData friend, String remark) async {
final userId = friend.userId;
if (userId == null) return;
if (userId == null) return false;
try {
await _repository.updateRemark(userId, remark);
await loadFriends();
return true;
} catch (error, stackTrace) {
if (error is AppError) {
AppToast.show(error.displayMessage);
... ... @@ -147,6 +156,7 @@ class FriendsController extends GetxController {
stackTrace,
);
}
return false;
}
}
... ...
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'
as api_models;
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
... ... @@ -36,10 +37,15 @@ class FriendListData {
}
class FriendsRepositoryImpl implements FriendsRepository {
const FriendsRepositoryImpl(this._friendApi, [this._userPreferencesStorage]);
const FriendsRepositoryImpl(
this._friendApi, [
this._userPreferencesStorage,
this._userAccountStorage,
]);
final FriendApi _friendApi;
final UserPreferencesStorage? _userPreferencesStorage;
final UserAccountStorage? _userAccountStorage;
@override
Future<List<FriendHealthData>> getFriends(
... ... @@ -160,8 +166,12 @@ class FriendsRepositoryImpl implements FriendsRepository {
(_userPreferencesStorage ?? Get.find<UserPreferencesStorage>())
.preferences
.value;
final userId = preferences.meUserInfo?.id;
return preferences.vipInfo?.isVip == true &&
(preferences.showRealtimeStress ?? true);
(userId == null ||
((_userAccountStorage ?? Get.find<UserAccountStorage>())
.showRealtimeStress(userId) ??
true));
}
api_models.FriendRealtimeStressItem? _latestRealtimeStress(
... ...
import 'package:doublefeel_flutter/core/theme/app_colors.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
class FriendEditRemarkDialog extends StatefulWidget {
const FriendEditRemarkDialog({
super.key,
required this.title,
required this.initialValue,
required this.hintText,
required this.confirmText,
required this.maxLength,
this.onConfirm,
});
final String title;
final String initialValue;
final String hintText;
final String confirmText;
final int maxLength;
final Future<bool> Function(String value)? onConfirm;
@override
State<FriendEditRemarkDialog> createState() => _FriendEditRemarkDialogState();
}
class _FriendEditRemarkDialogState extends State<FriendEditRemarkDialog> {
late final TextEditingController _controller;
bool _isSubmitting = false;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialValue);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Container(
width: 300.dp,
padding: EdgeInsets.fromLTRB(24.dp, 24.dp, 24.dp, 20.dp),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.dp),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 220.dp,
child: Text(
widget.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF141414),
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
),
SizedBox(height: 16.dp),
ValueListenableBuilder<TextEditingValue>(
valueListenable: _controller,
builder: (context, value, _) {
return Container(
width: double.infinity,
height: 52.dp,
padding: EdgeInsets.only(left: 22.dp, right: 22.dp),
decoration: BoxDecoration(
color: const Color(0xFFF3F3F3),
borderRadius: BorderRadius.circular(27.dp),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
autofocus: true,
maxLines: 1,
inputFormatters: [
LengthLimitingTextInputFormatter(widget.maxLength),
],
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.hintText,
hintStyle: const TextStyle(
color: AppColors.textTertiary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
isCollapsed: true,
),
style: const TextStyle(
color: Color(0xFF141414),
fontSize: 16,
fontWeight: FontWeight.w400,
),
),
),
Text(
'${value.text.characters.length}/${widget.maxLength}',
style: const TextStyle(
color: Color(0xFFD9D9D9),
fontSize: 16,
fontWeight: FontWeight.w400,
),
),
],
),
);
},
),
SizedBox(height: 24.dp),
ValueListenableBuilder<TextEditingValue>(
valueListenable: _controller,
builder: (context, value, _) {
final enabled = value.text.trim().isNotEmpty && !_isSubmitting;
return Opacity(
opacity: enabled ? 1 : 0.4,
child: GestureDetector(
onTap: enabled ? () => _confirm(value.text) : null,
behavior: HitTestBehavior.opaque,
child: Container(
width: 220.dp,
height: 48.dp,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(24.dp),
),
padding: EdgeInsets.symmetric(horizontal: 16.dp),
child: Text(
widget.confirmText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
),
),
);
},
),
],
),
),
);
}
Future<void> _confirm(String value) async {
final trimmed = value.trim();
if (trimmed.isEmpty || _isSubmitting) {
return;
}
setState(() {
_isSubmitting = true;
});
try {
final canClose =
await (widget.onConfirm?.call(trimmed) ?? Future<bool>.value(true));
if (canClose && mounted && Get.isDialogOpen == true) {
Get.back(result: trimmed);
}
} finally {
if (mounted) {
setState(() {
_isSubmitting = false;
});
}
}
}
}
... ...
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:get/get.dart';
... ... @@ -11,6 +12,7 @@ class PrivacySettingsBinding extends Bindings {
PrivacySettingsController(
Get.find<UserApi>(),
Get.find<UserPreferencesStorage>(),
Get.find<UserAccountStorage>(),
),
);
}
... ...
... ... @@ -2,19 +2,26 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/data/models/user/user_models.dart';
import 'package:get/get.dart';
class PrivacySettingsController extends GetxController {
PrivacySettingsController(this._userApi, this._userPreferencesStorage);
PrivacySettingsController(
this._userApi,
this._userPreferencesStorage,
this._userAccountStorage,
);
final UserApi _userApi;
final UserPreferencesStorage _userPreferencesStorage;
final UserAccountStorage _userAccountStorage;
final enableAddWithUCode = false.obs;
final isUpdatingEnableAddWithUCode = false.obs;
/// 页面文案是“禁止通过 ID 添加我”,因此该状态为 true 时表示禁止。
final disableAddWithUCode = false.obs;
final isUpdatingDisableAddWithUCode = false.obs;
final isVip = false.obs;
final showRealtimeStress = false.obs;
late final Worker _preferencesWorker;
... ... @@ -36,29 +43,32 @@ class PrivacySettingsController extends GetxController {
}
void _syncFromPreferences(UserPreferences preferences) {
enableAddWithUCode.value =
preferences.meUserInfo?.enableAddWithUCode == 1;
disableAddWithUCode.value =
preferences.meUserInfo?.enableAddWithUCode != 1;
isVip.value = preferences.vipInfo?.isVip == true;
showRealtimeStress.value =
isVip.value && (preferences.showRealtimeStress ?? true);
final userId = preferences.meUserInfo?.id;
showRealtimeStress.value = isVip.value &&
(userId == null
? true
: (_userAccountStorage.showRealtimeStress(userId) ?? true));
}
Future<void> updateEnableAddWithUCode(bool enabled) async {
if (isUpdatingEnableAddWithUCode.value) return;
Future<void> updateDisableAddWithUCode(bool disabled) async {
if (isUpdatingDisableAddWithUCode.value) return;
isUpdatingEnableAddWithUCode.value = true;
isUpdatingDisableAddWithUCode.value = true;
try {
final result = await _userApi.updateOwnerUserInfo(
enableAddWithUcode: enabled ? 0 : 1,
enableAddWithUcode: disabled ? 0 : 1,
);
if (result is AppSuccess<UserInfoResponse>) {
await _userPreferencesStorage.updateMeUserInfo(result.data);
enableAddWithUCode.value = result.data.enableAddWithUCode == null
? !enabled
: result.data.enableAddWithUCode == 1;
disableAddWithUCode.value = result.data.enableAddWithUCode == null
? disabled
: result.data.enableAddWithUCode != 1;
}
} finally {
isUpdatingEnableAddWithUCode.value = false;
isUpdatingDisableAddWithUCode.value = false;
}
}
... ... @@ -73,7 +83,10 @@ class PrivacySettingsController extends GetxController {
return;
}
await _userPreferencesStorage.updateShowRealtimeStress(enabled);
final userId = _userPreferencesStorage.preferences.value.meUserInfo?.id;
if (userId == null) return;
await _userAccountStorage.updateShowRealtimeStress(userId, enabled);
_syncFromPreferences(_userPreferencesStorage.preferences.value);
}
void executeBackLogic() {
... ...
... ... @@ -65,10 +65,10 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> {
Widget noAddByIdView(BuildContext context) {
return _settingRow(
title: context.l10n.privacySettingsDisableAddById,
value: controller.enableAddWithUCode,
onChanged: () => controller.isUpdatingEnableAddWithUCode.value
value: controller.disableAddWithUCode,
onChanged: () => controller.isUpdatingDisableAddWithUCode.value
? null
: controller.updateEnableAddWithUCode,
: controller.updateDisableAddWithUCode,
);
}
... ... @@ -126,7 +126,7 @@ class PrivacySettingsView extends GetView<PrivacySettingsController> {
scaleX: 52 / 51,
scaleY: 28 / 31,
child: CupertinoSwitch(
value: !value.value,
value: value.value,
activeTrackColor: AppColors.primary,
inactiveTrackColor: const Color(0xFFE5E5EA),
onChanged: onChanged(),
... ...
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// 账号级持久化存储。
... ... @@ -26,6 +27,13 @@ class UserAccountStorage {
static String _appleHealthUploadTestLastSyncTimeKey(int userId) =>
'apple_health_upload_test_last_sync_time_$userId';
/// 是否显示实时压力,按 userId 隔离,避免会话数据刷新时覆盖该偏好。
static String _showRealtimeStressKey(int userId) =>
'account_show_realtime_stress_$userId';
/// 在实时压力偏好更新后通知依赖它的页面刷新。
final realtimeStressSettingsVersion = 0.obs;
/// Onboarding 已全部完成的哨兵值
static const int _kOnboardingCompleted = -1;
... ... @@ -73,6 +81,17 @@ class UserAccountStorage {
int? getLastAddFriendBannerShowTime(int userId) =>
_prefs.getInt('last_add_friend_banner_show_time_$userId');
// ─── 实时压力展示偏好 ────────────────────────────────────────────────────
/// `null` 表示用户尚未设置,调用方应使用产品默认值。
bool? showRealtimeStress(int userId) =>
_prefs.getBool(_showRealtimeStressKey(userId));
Future<void> updateShowRealtimeStress(int userId, bool enabled) async {
await _prefs.setBool(_showRealtimeStressKey(userId), enabled);
realtimeStressSettingsVersion.value++;
}
// ─── Apple Health 上传记录 ────────────────────────────────────────────────
String? appleHealthUploadLocalRecordJson(int userId) =>
... ...
... ... @@ -122,10 +122,6 @@ class UserPreferencesStorage {
await _persist(preferences.value.copyWith(vipInfo: vipInfo));
}
Future<void> updateShowRealtimeStress(bool enabled) async {
await _persist(preferences.value.copyWith(showRealtimeStress: enabled));
}
Future<void> updateFromLogin({
required String accessToken,
UserInfoResponse? me,
... ...
... ... @@ -9,7 +9,6 @@ class UserPreferences {
this.accessToken = '',
this.rongcloudToken = '',
this.vipInfo,
this.showRealtimeStress,
});
final UserInfoResponse? meUserInfo;
... ... @@ -18,10 +17,6 @@ class UserPreferences {
final String rongcloudToken;
final UserPreferencesVipInfo? vipInfo;
/// `null` preserves the product default: enabled for members and disabled
/// for non-members.
final bool? showRealtimeStress;
static const empty = UserPreferences();
factory UserPreferences.fromJson(Map<String, dynamic> json) {
... ... @@ -40,7 +35,6 @@ class UserPreferences {
? null
: UserPreferencesVipInfo.fromJson(
json['vip_info'] as Map<String, dynamic>),
showRealtimeStress: json['show_realtime_stress'] as bool?,
);
}
... ... @@ -53,9 +47,6 @@ class UserPreferences {
val['access_token'] = accessToken;
val['rongcloud_token'] = rongcloudToken;
if (vipInfo != null) val['vip_info'] = vipInfo!.toJson();
if (showRealtimeStress != null) {
val['show_realtime_stress'] = showRealtimeStress;
}
return val;
}
... ... @@ -65,7 +56,6 @@ class UserPreferences {
String? accessToken,
String? rongcloudToken,
UserPreferencesVipInfo? vipInfo,
bool? showRealtimeStress,
bool clearPartner = false,
}) {
return UserPreferences(
... ... @@ -75,7 +65,6 @@ class UserPreferences {
accessToken: accessToken ?? this.accessToken,
rongcloudToken: rongcloudToken ?? this.rongcloudToken,
vipInfo: vipInfo ?? this.vipInfo,
showRealtimeStress: showRealtimeStress ?? this.showRealtimeStress,
);
}
}
... ...