Commit 31cbe47bfc816476dfcbd59be901e3cb015125c3

Authored by 常守达
1 parent c62867d9

feat(today): 好友主页

... ... @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
... ... @@ -283,6 +283,7 @@
66FBE5882FDA4F0F00F515B4 /* Frameworks */,
66FBE5892FDA4F0F00F515B4 /* Resources */,
66FBE5BE2FDA518D00F515B4 /* Embed Foundation Extensions */,
1F69C09D185F42507815E3C2 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
... ... @@ -364,6 +365,23 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1F69C09D185F42507815E3C2 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
4348DC7B38CB739C4ABA7DAC /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
... ...
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:get/get.dart';
import '../apple_health_upload/apple_health_upload_api.dart';
... ... @@ -53,6 +54,7 @@ void registerUserSessionDeps() {
Get.put(UserApi(dioClient), permanent: true);
Get.put(VipApi(dioClient), permanent: true);
Get.put(FriendApi(dioClient), permanent: true);
Get.put<ImService>(ImServiceStub(), permanent: true);
Get.put(
ThinkingDataService(Get.find<AppEnvironmentConfig>()),
... ...
import 'package:doublefeel_flutter/core/theme/app_colors.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/data/models/user/user_models.dart';
import 'package:doublefeel_flutter/r.dart';
... ... @@ -11,21 +12,18 @@ import '../controllers/account_settings_controller.dart';
class AccountSettingsView extends GetView<AccountSettingsController> {
const AccountSettingsView({super.key});
static const _background = Color(0xFFF5F2FF);
static const _danger = Color(0xFFFC4447);
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: _background,
systemNavigationBarColor: context.colors.backgroundPage,
systemNavigationBarIconBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: _background,
backgroundColor: context.colors.backgroundPage,
appBar: AppBar(
title: Text(
'账号设置',
... ... @@ -60,7 +58,7 @@ class AccountSettingsView extends GetView<AccountSettingsController> {
child: Text(
'注销账号',
style: TextStyle(
color: _danger,
color: context.colors.warning,
fontSize: 12.dp,
fontWeight: FontWeight.w400,
height: 1.25,
... ...
... ... @@ -12,13 +12,10 @@ import '../controllers/submit_feedback_controller.dart';
class SubmitFeedbackView extends GetView<SubmitFeedbackController> {
const SubmitFeedbackView({super.key});
static const _background = Color(0xFFF5F2FF);
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: _background,
appBar: AppBar(
backgroundColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
... ...
import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:get/get.dart';
/// 跳转到好友主页时需要传入的参数。
class FriendHomeArguments {
const FriendHomeArguments({
required this.userId,
this.name,
this.avatarUrl,
});
final int userId;
final String? name;
final String? avatarUrl;
}
class FriendHomeBinding extends Bindings {
@override
void dependencies() {
final args = Get.arguments as FriendHomeArguments;
final args = Get.arguments as FriendItem;
Get.lazyPut<TodayController>(
() => TodayController(
Get.find<UserApi>(),
Get.find<HealthApi>(),
Get.find<FriendApi>(),
Get.find<UserStateService>(),
Get.find<HealthKitUploadService>(),
Get.find<AppleHealthUploadTool>(),
friendUserId: args.userId, // 好友 userId,驱动 isFriend = true
friendInfo: args,
),
tag: 'friend_${args.userId}', // 动态 tag,支持多个好友同时在路由栈中
);
... ...
import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/views/tabs/today_tab.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../bindings/friend_home_binding.dart';
class FriendHomePage extends GetView<TodayController> {
const FriendHomePage({super.key});
FriendHomeArguments get _args => Get.arguments as FriendHomeArguments;
FriendItem get _args => Get.arguments as FriendItem;
/// 与 FriendHomeBinding 中注册的 tag 保持一致
@override
... ...
import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart';
... ... @@ -17,8 +18,8 @@ class HomeBinding extends Bindings {
Get.lazyPut<HomeController>(() => HomeController(), fenix: true);
Get.lazyPut<TodayController>(
() => TodayController(
Get.find<UserApi>(),
Get.find<HealthApi>(),
Get.find<FriendApi>(),
Get.find<UserStateService>(),
Get.find<HealthKitUploadService>(),
Get.find<AppleHealthUploadTool>(),
... ...
... ... @@ -5,15 +5,18 @@ import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_t
import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/home_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/today/friend_select_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.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/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:doublefeel_flutter/data/models/health/health_models.dart';
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
... ... @@ -46,26 +49,28 @@ class HrvAnnotation {
class TodayController extends GetxController {
TodayController(
this._userApi,
this._healthApi,
this._friendApi,
this._userStateService,
this._healthKitUploadService,
this._appleHealthUploadTool, {
this.friendUserId, // null = 自己,非 null = 好友
});
FriendItem? friendInfo, // null = 自己,非 null = 好友
}) : _initialFriendInfo = friendInfo;
final UserApi _userApi;
final HealthApi _healthApi;
final FriendApi _friendApi;
final UserStateService _userStateService;
final HealthKitUploadService _healthKitUploadService;
final AppleHealthUploadTool _appleHealthUploadTool;
final HealthKitHostApi _hostApi = HealthKitHostApi();
/// 好友的 userId;null 表示查看自己的数据,非 null 表示查看好友的数据。
final int? friendUserId;
/// 好友的信息;null 表示查看自己的数据,非 null 表示查看好友的数据。
final FriendItem? _initialFriendInfo;
final targetFriendInfo = Rxn<FriendItem>();
/// 是否正在查看好友数据。
bool get isFriend => friendUserId != null;
bool get isFriend => targetFriendInfo.value != null;
int? get friendUserId => targetFriendInfo.value?.friendUserId;
UserStateService get userStateService => _userStateService;
... ... @@ -140,6 +145,7 @@ class TodayController extends GetxController {
@override
void onInit() {
super.onInit();
targetFriendInfo.value = _initialFriendInfo;
final today = DateUtils.dateOnly(DateTime.now());
firstSelectableDay = DateTime(today.year - 1);
lastSelectableDay = today;
... ... @@ -497,11 +503,7 @@ class TodayController extends GetxController {
if (isFriend) {
Get.toNamed(
Routes.FRIEND_TREND,
arguments: FriendTrendArguments(
userId: 58,
name: 'xxa',
avatarUrl: 'asdfa',
),
arguments: getFriendTrendArguments(),
);
} else {
Get.find<HomeController>().openTrend(TrendType.hrv);
... ... @@ -510,14 +512,7 @@ class TodayController extends GetxController {
toTrendSleepPage() {
if (isFriend) {
Get.toNamed(
Routes.FRIEND_TREND,
arguments: FriendTrendArguments(
userId: 58,
name: 'xxa',
avatarUrl: 'asdfa',
),
);
Get.toNamed(Routes.FRIEND_TREND, arguments: getFriendTrendArguments());
} else {
Get.find<HomeController>().openTrend(TrendType.sleep);
}
... ... @@ -525,16 +520,61 @@ class TodayController extends GetxController {
toTrendActivityPage() {
if (isFriend) {
Get.toNamed(
Routes.FRIEND_TREND,
arguments: FriendTrendArguments(
userId: 58,
name: 'xxa',
avatarUrl: 'asdfa',
),
);
Get.toNamed(Routes.FRIEND_TREND, arguments: getFriendTrendArguments());
} else {
Get.find<HomeController>().openTrend(TrendType.activity);
}
}
getFriendTrendArguments() {
var value = targetFriendInfo.value;
if (value != null && value.friendUserId != null) {
return FriendTrendArguments(
userId: value.friendUserId!,
name: value.remarkName ?? '',
avatarUrl: value.avatar,
);
} else {
return null;
}
}
void selectFriend(FriendItem friendInfo) {
if (friendInfo.friendUserId != targetFriendInfo.value?.friendUserId) {
targetFriendInfo.value = friendInfo;
_clearRealTimeData();
loadDataForDate(selectedDate.value);
}
}
showFriendListBottomSheet() async {
_friendApi.friendList(false).then(
(res) {
switch (res) {
case AppSuccess(:final data):
friendsList.assignAll(data.list);
case AppFailure():
}
},
);
Get.bottomSheet(
DraggableScrollableSheet(
maxChildSize: 0.6,
initialChildSize: 0.6,
expand: false,
snap: true,
builder: (context, scrollController) {
return FriendSelectBottomSheet(
scrollController: scrollController, controller: this);
},
),
barrierColor: Colors.black.withValues(alpha: 0.7),
enableDrag: true,
isScrollControlled: true,
persistent: false,
);
}
RxList<FriendItem> friendsList = RxList.empty();
}
... ...
... ... @@ -21,7 +21,6 @@ class HomePage extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F2FF),
// 不使用系统 AppBar,Today 页自带状态栏适配
extendBody: true, // 让内容延伸到 bottomNavigationBar 下方
extendBodyBehindAppBar: true,
... ...
import 'package:cached_network_image/cached_network_image.dart';
import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart';
import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
... ... @@ -10,6 +9,7 @@ import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/data/models/user/user_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
... ... @@ -22,14 +22,12 @@ import '../../../../../core/theme/app_colors.dart';
class MyTab extends GetView<MyController> {
const MyTab({super.key});
static const _bgColor = Color(0xFFF5F2FF);
@override
Widget build(BuildContext context) {
final userPrefs = Get.find<UserPreferencesStorage>();
return Container(
color: _bgColor,
color: context.colors.backgroundPage,
child: SafeArea(
bottom: false,
child: Obx(() {
... ... @@ -80,7 +78,12 @@ class MyTab extends GetView<MyController> {
_SettingsRow(
title: 'Route List',
onTap: () {
Get.toNamed(Routes.ROUTE_LIST);
Get.toNamed(Routes.FRIEND_HOME,
arguments: FriendItem(
friendUserId: 61,
remarkName: '徐璐',
avatar:
'https://cdn3.didiapp.com/manager_upload/ab2abeecb14442b198bdaedc7c41f094.png'));
},
),
],
... ...
import 'package:cached_network_image/cached_network_image.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart';
import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
... ... @@ -16,6 +18,7 @@ import '../../widgets/today/today_hrv_ad_banner.dart';
import '../../widgets/today/today_hrv_chart_card.dart';
import '../../widgets/today/today_hrv_number_card.dart';
import '../../widgets/today/today_sleep_activity_cards.dart';
import '../../widgets/today/stress_status_explanation_bottom_sheet.dart';
import '../../widgets/today/premium_card.dart';
... ... @@ -83,7 +86,7 @@ class TodayTabBody extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
controller.isFriend
? _TopFriendBar()
? _TopFriendBar(controller)
: _TopDateBar(
title: _dateTitle(
context, controller.selectedDate.value),
... ... @@ -562,6 +565,10 @@ class _LatestHrvCard extends StatelessWidget {
class _TopFriendBar extends StatelessWidget {
final _topBarHeight = 48.0;
const _TopFriendBar(this.controller);
final TodayController controller;
@override
Widget build(BuildContext context) {
return SizedBox(
... ... @@ -572,7 +579,8 @@ class _TopFriendBar extends StatelessWidget {
children: [
IconButton(
highlightColor: Colors.transparent,
padding: EdgeInsets.zero,
padding: EdgeInsets.only(right: 2),
alignment: Alignment.centerRight,
onPressed: Get.back,
icon: Image.asset(
'assets/images/common/ic_nav_back.webp',
... ... @@ -583,17 +591,21 @@ class _TopFriendBar extends StatelessWidget {
Container(
width: 28,
height: 28,
margin: EdgeInsets.only(left: 4, right: 8),
margin: EdgeInsets.only(right: 4),
decoration: ShapeDecoration(
image: DecorationImage(
image: CachedNetworkImageProvider(
'${controller.targetFriendInfo.value?.avatar}'),
fit: BoxFit.cover,
),
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.78, color: Colors.white),
borderRadius: BorderRadius.circular(35.78),
),
),
child: CachedNetworkImage(imageUrl: 'imageUrl'),
),
Text(
'男朋友的状态',
'${controller.targetFriendInfo.value?.remarkName}的状态',
style: TextStyle(
color: Colors.black,
fontSize: 16,
... ... @@ -604,7 +616,9 @@ class _TopFriendBar extends StatelessWidget {
IconButton(
highlightColor: Colors.transparent,
padding: EdgeInsets.zero,
onPressed: Get.back,
onPressed: () {
controller.showFriendListBottomSheet();
},
icon: Image.asset(
'assets/images/common/ic_replace.png',
width: 20,
... ...
... ... @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/core/theme/app_colors.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
... ... @@ -156,6 +157,7 @@ class AccountSettingView extends GetView<MyController> {
Future<void> _logout() async {
await Get.find<UserStateService>().onLogout();
AppToast.show('已退出登录');
Get.offAllNamed(AppRoutes.login);
}
... ... @@ -164,6 +166,7 @@ class AccountSettingView extends GetView<MyController> {
if (deleteResult is! AppSuccess<void>) return;
await Get.find<UserStateService>().onLogout(callServerLogout: false);
AppToast.show('账号注销成功');
Get.offAllNamed(AppRoutes.login);
}
}
... ...
import 'package:cached_network_image/cached_network_image.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class FriendSelectBottomSheet extends StatelessWidget {
const FriendSelectBottomSheet({
super.key,
required this.scrollController,
required this.controller,
});
final ScrollController scrollController;
final TodayController controller;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Container(
decoration: BoxDecoration(
color: context.colors.backgroundPage,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
top: false,
child: Column(
children: [
SizedBox(
height: 64,
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: Text(
'选择好友',
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
Positioned(
left: 16,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: Get.back,
child: SizedBox(
width: 44,
height: 44,
child: Center(
child: Image.asset(
'assets/images/common/ic_close.png',
width: 20,
height: 20,
color: context.colors.chartPurple,
),
),
),
),
),
],
),
),
Expanded(
child: Obx(() {
var friendsList = controller.friendsList;
var currentSelectedId = controller.friendUserId;
return ListView.separated(
controller: scrollController,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 6, 16, 16),
itemCount: friendsList.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final friend = friendsList[index];
final isSelected = friend.friendUserId == currentSelectedId;
return _BottomSheetFriendRow(
friend: friend,
isSelected: isSelected,
onTap: () {
controller.selectFriend(friend);
},
);
},
);
}),
),
],
),
),
);
}
}
class _BottomSheetFriendRow extends StatelessWidget {
const _BottomSheetFriendRow({
required this.friend,
required this.isSelected,
required this.onTap,
});
final FriendItem friend;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: isSelected
? Border.all(color: const Color(0xFF845EEE), width: 1)
: null,
),
child: Row(
children: [
const SizedBox(width: 20),
// Avatar
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF845EEE).withValues(alpha: 0.3),
width: 0.8,
),
),
child: ClipOval(
child: CachedNetworkImage(
imageUrl: friend.avatar!,
width: 28,
height: 28,
fit: BoxFit.cover,
)),
),
const SizedBox(width: 8),
// Name
Expanded(
child: Text(
friend.remarkName ?? '未知好友',
style: const TextStyle(
color: Color(0xFF0F0F11),
fontSize: 14,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
// Checkbox
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 16),
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
isSelected ? const Color(0xFF845EEE) : Colors.transparent,
border: isSelected
? null
: Border.all(
color: const Color(0xFF0F0F11).withValues(alpha: 0.2),
width: 1.5,
),
),
child: isSelected
? const Icon(Icons.check, color: Colors.white, size: 12)
: null,
),
],
),
),
);
}
}
... ...
... ... @@ -24,9 +24,9 @@ class HrvMeasurementBottomSheet extends StatelessWidget {
final l10n = context.l10n;
return Container(
decoration: const BoxDecoration(
color: Color(0xFFF5F2FF),
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
decoration: BoxDecoration(
color: context.colors.backgroundPage,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
top: false,
... ...
... ... @@ -41,9 +41,9 @@ class HrvPrincipleExplanationBottomSheet extends StatelessWidget {
final l10n = context.l10n;
return Container(
decoration: const BoxDecoration(
color: Color(0xFFF5F2FF),
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
decoration: BoxDecoration(
color: context.colors.backgroundPage,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
top: false,
... ...
... ... @@ -10,8 +10,6 @@ class NoHealthDataPage extends StatelessWidget {
this.onHelp,
});
static const _backgroundColor = Color(0xFFF5F2FF);
static const _placeholderColor = Color(0xFFD9D9D9);
final VoidCallback? onRefresh;
... ... @@ -21,10 +19,10 @@ class NoHealthDataPage extends StatelessWidget {
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: AppTheme.systemUiOverlayStyle.copyWith(
systemNavigationBarColor: _backgroundColor,
systemNavigationBarColor: context.colors.backgroundPage,
),
child: Scaffold(
backgroundColor: _backgroundColor,
backgroundColor: context.colors.backgroundPage,
body: Column(
children: [
SafeArea(
... ...
... ... @@ -41,8 +41,8 @@ class RealtimeStressExplanationBottomSheet extends StatelessWidget {
final l10n = context.l10n;
return Container(
decoration: const BoxDecoration(
color: Color(0xFFF5F2FF),
decoration: BoxDecoration(
color: context.colors.backgroundPage,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
... ...
... ... @@ -41,8 +41,8 @@ class TodayFaqBottomSheet extends StatelessWidget {
final l10n = context.l10n;
return Container(
decoration: const BoxDecoration(
color: Color(0xFFF5F2FF),
decoration: BoxDecoration(
color: context.colors.backgroundPage,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
... ...
... ... @@ -28,9 +28,11 @@ class TodaySleepCard extends StatelessWidget {
(healthData?.hrAvg == null || healthData?.hrAvg == 0)
? '--'
: '${healthData?.hrAvg}';
double sleepDurationProgress =
((healthData?.sleepDurationScore() ?? 0) * 3.60);
double sleepScoreProgress = (healthData?.sleepScore ?? 0.0) * 3.6;
double sleepDurationProgress = sleepDuration == 0
? -1
: ((healthData?.sleepDurationScore() ?? 0) * 3.60);
double sleepScoreProgress =
sleepDuration == 0 ? -1 : (healthData?.sleepScore ?? 0.0) * 3.6;
return _TodaySummaryCard(
iconAsset: 'assets/images/common/ic_sleep_stroke.png',
... ...
import '../../../data/models/friend/friend_models.dart';
import '../../result/app_result.dart';
import '../../result/safe_call.dart';
import '../api_paths.dart';
import '../dio_client.dart';
class FriendApi {
FriendApi(this._dioClient);
final DioClient _dioClient;
Future<AppResult<void>> addFriend(String uniqueCode) {
return safeCall(
call: () async {
await _dioClient.dio.post(
ApiPaths.friends,
data: {'unique_code': uniqueCode},
);
},
);
}
Future<AppResult<FriendListResponse>> friendList(bool withHealthData) {
return safeCall(
call: () async {
final response = await _dioClient.dio.get(
ApiPaths.friends,
queryParameters: {'with_health_data': withHealthData ? 1 : 0},
);
return FriendListResponse.fromJson(
response.data as Map<String, dynamic>,
);
},
);
}
}
... ...
... ... @@ -63,4 +63,7 @@ abstract final class ApiPaths {
static const watchThemeList = '/client/doublefeel/theme/watch_theme/list/';
static const watchThemeActive =
'/client/doublefeel/theme/watch_theme/active/';
// friends
static const friends = '/client/doublefeel/health/v2/friends/';
}
... ...
// ─── Responses ───────────────────────────────────────────────────────────────
class FriendListResponse {
const FriendListResponse({this.list = const []});
final List<FriendItem> list;
factory FriendListResponse.fromJson(Map<String, dynamic> json) {
return FriendListResponse(
list: (json['list'] as List<dynamic>?)
?.map((e) => FriendItem.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
);
}
Map<String, dynamic> toJson() => {
'list': list.map((e) => e.toJson()).toList(),
};
}
class FriendItem {
const FriendItem({
this.id,
this.userId,
this.friendUserId,
this.remarkName,
this.showInDial,
this.state,
this.createTime,
this.updateTime,
this.avatar,
this.healthData,
});
final int? id;
final int? userId;
final int? friendUserId;
final String? remarkName;
/// 0 = hidden, 1 = shown in dial
final int? showInDial;
/// 1 = normal
final int? state;
final int? createTime;
final int? updateTime;
final String? avatar;
final FriendHealthData? healthData;
bool get isShowInDial => showInDial == 1;
factory FriendItem.fromJson(Map<String, dynamic> json) {
return FriendItem(
id: json['id'] as int?,
userId: json['user_id'] as int?,
friendUserId: json['friend_user_id'] as int?,
remarkName: json['remark_name'] as String?,
showInDial: json['show_in_dial'] as int?,
state: json['state'] as int?,
createTime: json['create_time'] as int?,
updateTime: json['update_time'] as int?,
avatar: json['avatar'] as String?,
healthData: json['health_data'] == null
? null
: FriendHealthData.fromJson(
json['health_data'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (id != null) val['id'] = id;
if (userId != null) val['user_id'] = userId;
if (friendUserId != null) val['friend_user_id'] = friendUserId;
if (remarkName != null) val['remark_name'] = remarkName;
if (showInDial != null) val['show_in_dial'] = showInDial;
if (state != null) val['state'] = state;
if (createTime != null) val['create_time'] = createTime;
if (updateTime != null) val['update_time'] = updateTime;
if (avatar != null) val['avatar'] = avatar;
if (healthData != null) val['health_data'] = healthData!.toJson();
return val;
}
}
class FriendHealthData {
const FriendHealthData({
this.hrvState,
this.realtimeStress,
this.sleepEvaluate,
this.totalSteps,
});
/// HRV state indicator
final int? hrvState;
/// Real-time stress data (may be an empty map `{}`)
final Map<String, dynamic>? realtimeStress;
/// Sleep quality score
final int? sleepEvaluate;
/// Total step count for the day (nullable)
final int? totalSteps;
factory FriendHealthData.fromJson(Map<String, dynamic> json) {
return FriendHealthData(
hrvState: json['hrv_state'] as int?,
realtimeStress: json['realtime_stress'] as Map<String, dynamic>?,
sleepEvaluate: json['sleep_evaluate'] as int?,
totalSteps: json['total_steps'] as int?,
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (hrvState != null) val['hrv_state'] = hrvState;
if (realtimeStress != null) val['realtime_stress'] = realtimeStress;
if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate;
if (totalSteps != null) val['total_steps'] = totalSteps;
return val;
}
}
... ...
... ... @@ -98,7 +98,9 @@ class V2HealthData {
factory V2HealthData.fromJson(Map<String, dynamic> json) {
return V2HealthData(
hrvAvg: _parseInt(json['hrv_avg']),
lastRestingHrValue: _parseInt(json['last_resting_hr_value']),
lastRestingHrValue: json['last_resting_hr_value'] == 0
? null
: _parseInt(json['last_resting_hr_value']),
hrAvg: _parseInt(json['hr_avg']),
move: _parseInt(json['move']),
exercise: _parseInt(json['exercise']),
... ...