Commit dd1f741d8f15a217bf787501580a713dbf1a1a63

Authored by 权海
1 parent fe7e25a8

feat(ui):首页互动入口

1 import 'package:doublefeel_flutter/app/modules/home/widgets/df_tab_bar.dart'; 1 import 'package:doublefeel_flutter/app/modules/home/widgets/df_tab_bar.dart';
  2 +import 'package:doublefeel_flutter/app/modules/interact/widgets/friend_interaction_entry.dart';
2 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 3 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
3 import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; 4 import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
4 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 5 import 'package:doublefeel_flutter/core/util/app_toast.dart';
@@ -32,52 +33,51 @@ class FriendsTab extends GetView<FriendsController> { @@ -32,52 +33,51 @@ class FriendsTab extends GetView<FriendsController> {
32 Expanded( 33 Expanded(
33 child: ColoredBox( 34 child: ColoredBox(
34 color: const Color(0xFFF5F2FF), 35 color: const Color(0xFFF5F2FF),
35 - child: Obx(  
36 - () {  
37 - final friends = controller.friends;  
38 - final hasFriends = friends.isNotEmpty;  
39 - final isFull = controller.isFull;  
40 - final healthData = controller.selfHealthData.value;  
41 - final currentUser =  
42 - userPreferences.preferences.value.meUserInfo;  
43 - final isSelfInitialLoading =  
44 - controller.isLoading.value && healthData == null;  
45 - final isFriendsInitialLoading =  
46 - controller.isLoading.value && !hasFriends;  
47 - final selfHealthCard = _SelfHealthCard(  
48 - name: currentUser?.nickname?.trim().isNotEmpty == true  
49 - ? currentUser!.nickname!.trim()  
50 - : '-',  
51 - avatarUrl: currentUser?.avatar,  
52 - updatedAt: healthData?.updatedAt ??  
53 - context.l10n.friendsWaitingForData,  
54 - sleepQualityScore: healthData?.sleepQualityScore,  
55 - steps: healthData?.steps,  
56 - activeCalories: healthData?.activeCalories,  
57 - stressState:  
58 - healthData?.stressState ?? FriendStressState.wait,  
59 - stressValueText: healthData?.stressValueText,  
60 - isLoading: isSelfInitialLoading,  
61 - ); 36 + child: Obx(() {
  37 + final friends = controller.friends;
  38 + final hasFriends = friends.isNotEmpty;
  39 + final isFull = controller.isFull;
  40 + final healthData = controller.selfHealthData.value;
  41 + final currentUser =
  42 + userPreferences.preferences.value.meUserInfo;
  43 + final isSelfInitialLoading =
  44 + controller.isLoading.value && healthData == null;
  45 + final isFriendsInitialLoading =
  46 + controller.isLoading.value && !hasFriends;
  47 + final selfHealthCard = _SelfHealthCard(
  48 + name: currentUser?.nickname?.trim().isNotEmpty == true
  49 + ? currentUser!.nickname!.trim()
  50 + : '-',
  51 + avatarUrl: currentUser?.avatar,
  52 + updatedAt:
  53 + healthData?.updatedAt ??
  54 + context.l10n.friendsWaitingForData,
  55 + sleepQualityScore: healthData?.sleepQualityScore,
  56 + steps: healthData?.steps,
  57 + activeCalories: healthData?.activeCalories,
  58 + stressState:
  59 + healthData?.stressState ?? FriendStressState.wait,
  60 + stressValueText: healthData?.stressValueText,
  61 + isLoading: isSelfInitialLoading,
  62 + );
62 63
63 - if (isFriendsInitialLoading) {  
64 - return _buildLoadingFriendsView(context, selfHealthCard);  
65 - }  
66 - if (!hasFriends) {  
67 - return _buildEmptyFriendsView(  
68 - context,  
69 - selfHealthCard,  
70 - isFull: isFull,  
71 - );  
72 - }  
73 - return _buildFriendsListView( 64 + if (isFriendsInitialLoading) {
  65 + return _buildLoadingFriendsView(context, selfHealthCard);
  66 + }
  67 + if (!hasFriends) {
  68 + return _buildEmptyFriendsView(
74 context, 69 context,
75 selfHealthCard, 70 selfHealthCard,
76 - friends: friends,  
77 isFull: isFull, 71 isFull: isFull,
78 ); 72 );
79 - },  
80 - ), 73 + }
  74 + return _buildFriendsListView(
  75 + context,
  76 + selfHealthCard,
  77 + friends: friends,
  78 + isFull: isFull,
  79 + );
  80 + }),
81 ), 81 ),
82 ), 82 ),
83 ], 83 ],
@@ -86,24 +86,13 @@ class FriendsTab extends GetView<FriendsController> { @@ -86,24 +86,13 @@ class FriendsTab extends GetView<FriendsController> {
86 ); 86 );
87 } 87 }
88 88
89 - Widget _buildLoadingFriendsView(  
90 - BuildContext context,  
91 - Widget selfHealthCard,  
92 - ) { 89 + Widget _buildLoadingFriendsView(BuildContext context, Widget selfHealthCard) {
93 return RefreshIndicator( 90 return RefreshIndicator(
94 onRefresh: controller.refreshData, 91 onRefresh: controller.refreshData,
95 child: ListView( 92 child: ListView(
96 physics: const AlwaysScrollableScrollPhysics(), 93 physics: const AlwaysScrollableScrollPhysics(),
97 - padding: EdgeInsets.fromLTRB(  
98 - 0,  
99 - 0,  
100 - 0,  
101 - _emptyListBottomPadding(context),  
102 - ),  
103 - children: [  
104 - selfHealthCard,  
105 - const _FriendsLoadingIndicator(),  
106 - ], 94 + padding: EdgeInsets.fromLTRB(0, 0, 0, _emptyListBottomPadding(context)),
  95 + children: [selfHealthCard, const _FriendsLoadingIndicator()],
107 ), 96 ),
108 ); 97 );
109 } 98 }
@@ -117,12 +106,7 @@ class FriendsTab extends GetView<FriendsController> { @@ -117,12 +106,7 @@ class FriendsTab extends GetView<FriendsController> {
117 onRefresh: controller.refreshData, 106 onRefresh: controller.refreshData,
118 child: ListView( 107 child: ListView(
119 physics: const AlwaysScrollableScrollPhysics(), 108 physics: const AlwaysScrollableScrollPhysics(),
120 - padding: EdgeInsets.fromLTRB(  
121 - 0,  
122 - 0,  
123 - 0,  
124 - _emptyListBottomPadding(context),  
125 - ), 109 + padding: EdgeInsets.fromLTRB(0, 0, 0, _emptyListBottomPadding(context)),
126 children: [ 110 children: [
127 selfHealthCard, 111 selfHealthCard,
128 _EmptyFriendsView( 112 _EmptyFriendsView(
@@ -225,6 +209,11 @@ class FriendsTab extends GetView<FriendsController> { @@ -225,6 +209,11 @@ class FriendsTab extends GetView<FriendsController> {
225 ], 209 ],
226 ), 210 ),
227 ), 211 ),
  212 + Positioned(
  213 + right: 16,
  214 + bottom: _floatingAddButtonBottom(context) + 52,
  215 + child: const FriendInteractionFloatingEntry(),
  216 + ),
228 ], 217 ],
229 ); 218 );
230 } 219 }
@@ -238,8 +227,9 @@ class FriendsTab extends GetView<FriendsController> { @@ -238,8 +227,9 @@ class FriendsTab extends GetView<FriendsController> {
238 } 227 }
239 228
240 double _tabBarAvoidanceBottom(BuildContext context) { 229 double _tabBarAvoidanceBottom(BuildContext context) {
241 - final bottomInset =  
242 - Theme.of(context).platform == TargetPlatform.iOS ? 34.0 : 0.0; 230 + final bottomInset = Theme.of(context).platform == TargetPlatform.iOS
  231 + ? 34.0
  232 + : 0.0;
243 return bottomInset + DfTabBar.height + DfTabBar.bottomMargin; 233 return bottomInset + DfTabBar.height + DfTabBar.bottomMargin;
244 } 234 }
245 235
@@ -265,10 +255,7 @@ class FriendsTab extends GetView<FriendsController> { @@ -265,10 +255,7 @@ class FriendsTab extends GetView<FriendsController> {
265 } 255 }
266 256
267 void _openFriendHome(FriendHealthData friend) { 257 void _openFriendHome(FriendHealthData friend) {
268 - Get.toNamed(  
269 - Routes.FRIEND_HOME,  
270 - arguments: friend.friendItem,  
271 - ); 258 + Get.toNamed(Routes.FRIEND_HOME, arguments: friend.friendItem);
272 } 259 }
273 260
274 void _handleFriendAction(FriendCardAction action, FriendHealthData friend) { 261 void _handleFriendAction(FriendCardAction action, FriendHealthData friend) {
@@ -386,11 +373,7 @@ class _FriendsHeader extends StatelessWidget { @@ -386,11 +373,7 @@ class _FriendsHeader extends StatelessWidget {
386 onPressed: () { 373 onPressed: () {
387 _navigatorToPrivacySettings(context); 374 _navigatorToPrivacySettings(context);
388 }, 375 },
389 - icon: Image.asset(  
390 - R.assetsImagesSettings,  
391 - width: 24,  
392 - height: 24,  
393 - ), 376 + icon: Image.asset(R.assetsImagesSettings, width: 24, height: 24),
394 padding: EdgeInsets.zero, 377 padding: EdgeInsets.zero,
395 constraints: const BoxConstraints.tightFor(width: 40, height: 40), 378 constraints: const BoxConstraints.tightFor(width: 40, height: 40),
396 ), 379 ),
@@ -406,10 +389,7 @@ _navigatorToPrivacySettings(BuildContext context) { @@ -406,10 +389,7 @@ _navigatorToPrivacySettings(BuildContext context) {
406 } 389 }
407 390
408 class _EmptyFriendsView extends StatelessWidget { 391 class _EmptyFriendsView extends StatelessWidget {
409 - const _EmptyFriendsView({  
410 - required this.enabled,  
411 - this.onTap,  
412 - }); 392 + const _EmptyFriendsView({required this.enabled, this.onTap});
413 393
414 final bool enabled; 394 final bool enabled;
415 final VoidCallback? onTap; 395 final VoidCallback? onTap;
@@ -441,10 +421,7 @@ class _EmptyFriendsView extends StatelessWidget { @@ -441,10 +421,7 @@ class _EmptyFriendsView extends StatelessWidget {
441 ), 421 ),
442 ), 422 ),
443 const SizedBox(height: 12), 423 const SizedBox(height: 12),
444 - _AddFriendButton(  
445 - enabled: enabled,  
446 - onTap: onTap,  
447 - ), 424 + _AddFriendButton(enabled: enabled, onTap: onTap),
448 ], 425 ],
449 ), 426 ),
450 ); 427 );
@@ -471,52 +448,53 @@ class _AddFriendButton extends StatelessWidget { @@ -471,52 +448,53 @@ class _AddFriendButton extends StatelessWidget {
471 onTap: onTap, 448 onTap: onTap,
472 behavior: HitTestBehavior.opaque, 449 behavior: HitTestBehavior.opaque,
473 child: SizedBox( 450 child: SizedBox(
474 - width: 240,  
475 - height: 48,  
476 - child: Stack(  
477 - children: [  
478 - Container(  
479 - width: double.infinity,  
480 - height: double.infinity, 451 + width: 240,
  452 + height: 48,
  453 + child: Stack(
  454 + children: [
  455 + Container(
  456 + width: double.infinity,
  457 + height: double.infinity,
  458 + decoration: BoxDecoration(
  459 + color: Colors.white,
  460 + borderRadius: BorderRadius.circular(24),
  461 + ),
  462 + ),
  463 + Opacity(
  464 + opacity: enabled ? 1 : 0.4,
  465 + child: Container(
  466 + width: 240,
  467 + height: 48,
481 decoration: BoxDecoration( 468 decoration: BoxDecoration(
482 - color: Colors.white, 469 + color: const Color(0xFF845EEE),
483 borderRadius: BorderRadius.circular(24), 470 borderRadius: BorderRadius.circular(24),
484 ), 471 ),
485 - ),  
486 - Opacity(  
487 - opacity: enabled ? 1 : 0.4,  
488 - child: Container(  
489 - width: 240,  
490 - height: 48,  
491 - decoration: BoxDecoration(  
492 - color: const Color(0xFF845EEE),  
493 - borderRadius: BorderRadius.circular(24),  
494 - ),  
495 - child: Row(  
496 - mainAxisAlignment: MainAxisAlignment.center,  
497 - children: [  
498 - Image.asset(  
499 - 'assets/images/common/ic_add.png',  
500 - width: 18,  
501 - height: 18, 472 + child: Row(
  473 + mainAxisAlignment: MainAxisAlignment.center,
  474 + children: [
  475 + Image.asset(
  476 + 'assets/images/common/ic_add.png',
  477 + width: 18,
  478 + height: 18,
  479 + color: Colors.white,
  480 + ),
  481 + const SizedBox(width: 6),
  482 + Text(
  483 + _buttonText(context),
  484 + style: const TextStyle(
502 color: Colors.white, 485 color: Colors.white,
  486 + fontSize: 16,
  487 + fontWeight: FontWeight.w600,
  488 + height: 1.4,
503 ), 489 ),
504 - const SizedBox(width: 6),  
505 - Text(  
506 - _buttonText(context),  
507 - style: const TextStyle(  
508 - color: Colors.white,  
509 - fontSize: 16,  
510 - fontWeight: FontWeight.w600,  
511 - height: 1.4,  
512 - ),  
513 - ),  
514 - ],  
515 - ), 490 + ),
  491 + ],
516 ), 492 ),
517 - )  
518 - ],  
519 - )), 493 + ),
  494 + ),
  495 + ],
  496 + ),
  497 + ),
520 ), 498 ),
521 ); 499 );
522 } 500 }
@@ -54,6 +54,7 @@ class AppHomeController extends GetxController { @@ -54,6 +54,7 @@ class AppHomeController extends GetxController {
54 } 54 }
55 55
56 bool get hasWatchedFriend => watchedFriend.value != null; 56 bool get hasWatchedFriend => watchedFriend.value != null;
  57 + bool get hasAnyFriend => _friendsController.friends.isNotEmpty;
57 bool get isShowingSelf => selectedTargetId.value == selfTodayTag; 58 bool get isShowingSelf => selectedTargetId.value == selfTodayTag;
58 String get selfName { 59 String get selfName {
59 final info = _userPreferencesStorage.preferences.value.meUserInfo; 60 final info = _userPreferencesStorage.preferences.value.meUserInfo;
@@ -42,6 +42,7 @@ class AppHomeTab extends GetView<AppHomeController> { @@ -42,6 +42,7 @@ class AppHomeTab extends GetView<AppHomeController> {
42 stressCard: showPeopleSwitcher 42 stressCard: showPeopleSwitcher
43 ? AppHomeStressCard(controller: todayController) 43 ? AppHomeStressCard(controller: todayController)
44 : null, 44 : null,
  45 + showInteractionEntry: controller.hasAnyFriend,
45 ); 46 );
46 } 47 }
47 48
1 import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart'; 1 import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart';
  2 +import 'package:doublefeel_flutter/app/modules/interact/widgets/friend_interaction_entry.dart';
2 import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; 3 import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart';
3 import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_bottom_slogan.dart'; 4 import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_bottom_slogan.dart';
4 import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; 5 import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart';
@@ -40,6 +41,7 @@ class TodayTabBody extends StatelessWidget { @@ -40,6 +41,7 @@ class TodayTabBody extends StatelessWidget {
40 this.useHomeHeader = false, 41 this.useHomeHeader = false,
41 this.headerFooter, 42 this.headerFooter,
42 this.stressCard, 43 this.stressCard,
  44 + this.showInteractionEntry = false,
43 }); 45 });
44 46
45 final TodayController controller; 47 final TodayController controller;
@@ -55,6 +57,8 @@ class TodayTabBody extends StatelessWidget { @@ -55,6 +57,8 @@ class TodayTabBody extends StatelessWidget {
55 /// The card starts below the week calendar so [headerFooter] can overlap it. 57 /// The card starts below the week calendar so [headerFooter] can overlap it.
56 final Widget? stressCard; 58 final Widget? stressCard;
57 59
  60 + final bool showInteractionEntry;
  61 +
58 final _weekCalendarHeight = 58.0; 62 final _weekCalendarHeight = 58.0;
59 final _topContentHeight = /*_topBarHeight + _weekCalendarHeight*/ 106.0; 63 final _topContentHeight = /*_topBarHeight + _weekCalendarHeight*/ 106.0;
60 static const _peopleSwitcherAspectRatio = 252 / 1029; 64 static const _peopleSwitcherAspectRatio = 252 / 1029;
@@ -156,6 +160,12 @@ class TodayTabBody extends StatelessWidget { @@ -156,6 +160,12 @@ class TodayTabBody extends StatelessWidget {
156 ), 160 ),
157 ), 161 ),
158 ), 162 ),
  163 + if (controller.isFriend || showInteractionEntry)
  164 + const Positioned(
  165 + right: 16,
  166 + bottom: 92,
  167 + child: FriendInteractionFloatingEntry(),
  168 + ),
159 ], 169 ],
160 ), 170 ),
161 ); 171 );
1 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 1 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
  2 +import 'package:doublefeel_flutter/app/modules/interact/widgets/friend_interaction_entry.dart';
2 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 3 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
4 import 'package:get/get.dart'; 5 import 'package:get/get.dart';
@@ -21,7 +22,9 @@ class TodayHrvNumberCard extends StatelessWidget { @@ -21,7 +22,9 @@ class TodayHrvNumberCard extends StatelessWidget {
21 ), 22 ),
22 child: Obx(() { 23 child: Obx(() {
23 var sameDay = DateUtils.isSameDay( 24 var sameDay = DateUtils.isSameDay(
24 - controller.selectedDate.value, controller.lastSelectableDay.value); 25 + controller.selectedDate.value,
  26 + controller.lastSelectableDay.value,
  27 + );
25 28
26 var otherHrv = (sameDay 29 var otherHrv = (sameDay
27 ? context.l10n.todaySAverageHrv 30 ? context.l10n.todaySAverageHrv
@@ -39,6 +42,7 @@ class TodayHrvNumberCard extends StatelessWidget { @@ -39,6 +42,7 @@ class TodayHrvNumberCard extends StatelessWidget {
39 : otherHrv, 42 : otherHrv,
40 value: hrvAvg > 0 ? hrvAvg.toString() : '-', 43 value: hrvAvg > 0 ? hrvAvg.toString() : '-',
41 unit: 'ms', 44 unit: 'ms',
  45 + interactionEnabled: controller.isFriend && hrvAvg > 0,
42 ), 46 ),
43 ), 47 ),
44 48
@@ -46,12 +50,18 @@ class TodayHrvNumberCard extends StatelessWidget { @@ -46,12 +50,18 @@ class TodayHrvNumberCard extends StatelessWidget {
46 child: _NumberItem( 50 child: _NumberItem(
47 label: controller.isFriend 51 label: controller.isFriend
48 ? context.l10n.reportOtherPossessiveTitle( 52 ? context.l10n.reportOtherPossessiveTitle(
49 - context.l10n.restingHeartRate) 53 + context.l10n.restingHeartRate,
  54 + )
50 : context.l10n.restingHeartRate, 55 : context.l10n.restingHeartRate,
51 - value: controller.v2HealthData.value?.lastRestingHrValue 56 + value:
  57 + controller.v2HealthData.value?.lastRestingHrValue
52 ?.toString() ?? 58 ?.toString() ??
53 '-', 59 '-',
54 unit: 'bpm', 60 unit: 'bpm',
  61 + interactionEnabled:
  62 + controller.isFriend &&
  63 + (controller.v2HealthData.value?.lastRestingHrValue ?? 0) >
  64 + 0,
55 ), 65 ),
56 ), 66 ),
57 ], 67 ],
@@ -65,11 +75,13 @@ class _NumberItem extends StatelessWidget { @@ -65,11 +75,13 @@ class _NumberItem extends StatelessWidget {
65 final String label; 75 final String label;
66 final String value; 76 final String value;
67 final String unit; 77 final String unit;
  78 + final bool? interactionEnabled;
68 79
69 const _NumberItem({ 80 const _NumberItem({
70 required this.label, 81 required this.label,
71 required this.value, 82 required this.value,
72 required this.unit, 83 required this.unit,
  84 + this.interactionEnabled,
73 }); 85 });
74 86
75 @override 87 @override
@@ -79,13 +91,23 @@ class _NumberItem extends StatelessWidget { @@ -79,13 +91,23 @@ class _NumberItem extends StatelessWidget {
79 child: Column( 91 child: Column(
80 crossAxisAlignment: CrossAxisAlignment.start, 92 crossAxisAlignment: CrossAxisAlignment.start,
81 children: [ 93 children: [
82 - Text(  
83 - label,  
84 - style: TextStyle(  
85 - fontSize: 12,  
86 - color: context.colors.textTertiary,  
87 - fontWeight: FontWeight.w400,  
88 - ), 94 + Row(
  95 + children: [
  96 + Expanded(
  97 + child: Text(
  98 + label,
  99 + maxLines: 1,
  100 + overflow: TextOverflow.ellipsis,
  101 + style: TextStyle(
  102 + fontSize: 12,
  103 + color: context.colors.textTertiary,
  104 + fontWeight: FontWeight.w400,
  105 + ),
  106 + ),
  107 + ),
  108 + if (interactionEnabled != null)
  109 + FriendInteractionButton(enabled: interactionEnabled!),
  110 + ],
89 ), 111 ),
90 const SizedBox(height: 6), 112 const SizedBox(height: 6),
91 Row( 113 Row(
1 import 'package:doublefeel_flutter/app/utils/platform_compact.dart'; 1 import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
  2 +import 'package:doublefeel_flutter/app/modules/interact/widgets/friend_interaction_entry.dart';
2 import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/arc_progress_widget.dart'; 3 import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/arc_progress_widget.dart';
3 import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/combine.dart'; 4 import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/combine.dart';
4 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 5 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
@@ -28,29 +29,31 @@ class TodaySleepCard extends StatelessWidget { @@ -28,29 +29,31 @@ class TodaySleepCard extends StatelessWidget {
28 Color sleepStateColor = sleepDuration == 0 29 Color sleepStateColor = sleepDuration == 0
29 ? context.colors.textPrimary 30 ? context.colors.textPrimary
30 : healthData?.sleepStateColor(context) ?? 31 : healthData?.sleepStateColor(context) ??
31 - context.colors.textPrimary;  
32 - String sleepStateStr =  
33 - sleepDuration == 0 ? '-' : healthData?.sleepStateStr() ?? '-'; 32 + context.colors.textPrimary;
  33 + String sleepStateStr = sleepDuration == 0
  34 + ? '-'
  35 + : healthData?.sleepStateStr() ?? '-';
34 String sleepAverageHeartRate = 36 String sleepAverageHeartRate =
35 (healthData?.hrAvg == null || healthData?.hrAvg == 0) 37 (healthData?.hrAvg == null || healthData?.hrAvg == 0)
36 - ? '-'  
37 - : '${healthData?.hrAvg}'; 38 + ? '-'
  39 + : '${healthData?.hrAvg}';
38 double sleepDurationProgress = 40 double sleepDurationProgress =
39 (activityTarget?.sleepTargetDuration ?? 28800) == 0 || 41 (activityTarget?.sleepTargetDuration ?? 28800) == 0 ||
40 - sleepDuration == 0  
41 - ? -1  
42 - : ((healthData?.sleepDuration ?? 0) /  
43 - (activityTarget?.sleepTargetDuration ?? 28800) *  
44 - 360);  
45 - double sleepScoreProgress =  
46 - sleepDuration == 0 ? -1 : (healthData?.sleepScore ?? 0.0) * 3.6; 42 + sleepDuration == 0
  43 + ? -1
  44 + : ((healthData?.sleepDuration ?? 0) /
  45 + (activityTarget?.sleepTargetDuration ?? 28800) *
  46 + 360);
  47 + double sleepScoreProgress = sleepDuration == 0
  48 + ? -1
  49 + : (healthData?.sleepScore ?? 0.0) * 3.6;
47 50
48 double? sleepDurationRatio = 51 double? sleepDurationRatio =
49 (activityTarget?.sleepTargetDuration ?? 28800) == 0 || 52 (activityTarget?.sleepTargetDuration ?? 28800) == 0 ||
50 - sleepDuration == 0  
51 - ? null  
52 - : ((healthData?.sleepDuration ?? 0) /  
53 - (activityTarget?.sleepTargetDuration ?? 28800)); 53 + sleepDuration == 0
  54 + ? null
  55 + : ((healthData?.sleepDuration ?? 0) /
  56 + (activityTarget?.sleepTargetDuration ?? 28800));
54 double? sleepScoreRatio = sleepDuration == 0 57 double? sleepScoreRatio = sleepDuration == 0
55 ? null 58 ? null
56 : ((healthData?.sleepScore ?? 0.0) / 100.0); 59 : ((healthData?.sleepScore ?? 0.0) / 100.0);
@@ -62,50 +65,51 @@ class TodaySleepCard extends StatelessWidget { @@ -62,50 +65,51 @@ class TodaySleepCard extends StatelessWidget {
62 actionText: l10n.viewSleepReport, 65 actionText: l10n.viewSleepReport,
63 metrics: [ 66 metrics: [
64 _MetricBlock( 67 _MetricBlock(
65 - label: l10n.duration,  
66 - labelColor: context.colors.primary,  
67 - value: (sleepDuration > 0)  
68 - ? Text.rich(  
69 - TextSpan(  
70 - style: TextStyle(  
71 - color: context.colors.textPrimary,  
72 - fontSize: 16,  
73 - fontWeight: FontWeight.w600,  
74 - ),  
75 - children: [  
76 - if (sleepHours > 0) ...[  
77 - TextSpan(text: '$sleepHours'),  
78 - TextSpan(  
79 - text: l10n.sleepDailyUnitHour,  
80 - style: TextStyle(  
81 - color: context.colors.textSecondary,  
82 - fontSize: 12,  
83 - fontWeight: FontWeight.w400,  
84 - ),  
85 - )  
86 - ],  
87 - if (sleepMinutes > 0) ...[  
88 - TextSpan(text: '$sleepMinutes'),  
89 - TextSpan(  
90 - text: l10n.sleepDailyUnitMinute,  
91 - style: TextStyle(  
92 - color: context.colors.textSecondary,  
93 - fontSize: 12,  
94 - fontWeight: FontWeight.w400,  
95 - ),  
96 - ),  
97 - ]  
98 - ],  
99 - ),  
100 - )  
101 - : Text(  
102 - '-', 68 + label: l10n.duration,
  69 + labelColor: context.colors.primary,
  70 + value: (sleepDuration > 0)
  71 + ? Text.rich(
  72 + TextSpan(
103 style: TextStyle( 73 style: TextStyle(
104 color: context.colors.textPrimary, 74 color: context.colors.textPrimary,
105 fontSize: 16, 75 fontSize: 16,
106 fontWeight: FontWeight.w600, 76 fontWeight: FontWeight.w600,
107 ), 77 ),
108 - )), 78 + children: [
  79 + if (sleepHours > 0) ...[
  80 + TextSpan(text: '$sleepHours'),
  81 + TextSpan(
  82 + text: l10n.sleepDailyUnitHour,
  83 + style: TextStyle(
  84 + color: context.colors.textSecondary,
  85 + fontSize: 12,
  86 + fontWeight: FontWeight.w400,
  87 + ),
  88 + ),
  89 + ],
  90 + if (sleepMinutes > 0) ...[
  91 + TextSpan(text: '$sleepMinutes'),
  92 + TextSpan(
  93 + text: l10n.sleepDailyUnitMinute,
  94 + style: TextStyle(
  95 + color: context.colors.textSecondary,
  96 + fontSize: 12,
  97 + fontWeight: FontWeight.w400,
  98 + ),
  99 + ),
  100 + ],
  101 + ],
  102 + ),
  103 + )
  104 + : Text(
  105 + '-',
  106 + style: TextStyle(
  107 + color: context.colors.textPrimary,
  108 + fontSize: 16,
  109 + fontWeight: FontWeight.w600,
  110 + ),
  111 + ),
  112 + ),
109 _MetricBlock( 113 _MetricBlock(
110 label: l10n.quality, 114 label: l10n.quality,
111 labelColor: const Color(0xFF7B9BFB), 115 labelColor: const Color(0xFF7B9BFB),
@@ -192,8 +196,10 @@ class TodaySleepCard extends StatelessWidget { @@ -192,8 +196,10 @@ class TodaySleepCard extends StatelessWidget {
192 ], 196 ],
193 centerCircleSizeRatio: 0.3, 197 centerCircleSizeRatio: 0.3,
194 gapRatio: 0.1, 198 gapRatio: 0.1,
195 - interpolatedColorRatio: 1), 199 + interpolatedColorRatio: 1,
  200 + ),
196 ), 201 ),
  202 + interactionEnabled: controller.isFriend && sleepDuration > 0,
197 ); 203 );
198 }), 204 }),
199 ); 205 );
@@ -252,9 +258,7 @@ class TodayActivityCard extends StatelessWidget { @@ -252,9 +258,7 @@ class TodayActivityCard extends StatelessWidget {
252 fontSize: 16, 258 fontSize: 16,
253 fontWeight: FontWeight.w600, 259 fontWeight: FontWeight.w600,
254 ), 260 ),
255 - children: [  
256 - const TextSpan(text: '-'),  
257 - ], 261 + children: [const TextSpan(text: '-')],
258 ), 262 ),
259 ); 263 );
260 } 264 }
@@ -290,20 +294,18 @@ class TodayActivityCard extends StatelessWidget { @@ -290,20 +294,18 @@ class TodayActivityCard extends StatelessWidget {
290 fontSize: 16, 294 fontSize: 16,
291 fontWeight: FontWeight.w600, 295 fontWeight: FontWeight.w600,
292 ), 296 ),
293 - children: [  
294 - const TextSpan(text: '-'),  
295 - ], 297 + children: [const TextSpan(text: '-')],
296 ), 298 ),
297 ); 299 );
298 } 300 }
299 double activityProgress = 301 double activityProgress =
300 (activity > 0 && (activityTarget?.move ?? 0) > 0) 302 (activity > 0 && (activityTarget?.move ?? 0) > 0)
301 - ? (activity / (activityTarget?.move ?? 0)) * 360  
302 - : -1; 303 + ? (activity / (activityTarget?.move ?? 0)) * 360
  304 + : -1;
303 double exerciseProgress = 305 double exerciseProgress =
304 (exercise > 0 && (activityTarget?.exercise ?? 0) > 0) 306 (exercise > 0 && (activityTarget?.exercise ?? 0) > 0)
305 - ? (exercise / (activityTarget?.exercise ?? 0)) * 360  
306 - : -1; 307 + ? (exercise / (activityTarget?.exercise ?? 0)) * 360
  308 + : -1;
307 double standProgress = (stand > 0 && (activityTarget?.stand ?? 0) > 0) 309 double standProgress = (stand > 0 && (activityTarget?.stand ?? 0) > 0)
308 ? (stand / (activityTarget?.stand ?? 0)) * 360 310 ? (stand / (activityTarget?.stand ?? 0)) * 360
309 : -1; 311 : -1;
@@ -312,8 +314,8 @@ class TodayActivityCard extends StatelessWidget { @@ -312,8 +314,8 @@ class TodayActivityCard extends StatelessWidget {
312 : null; 314 : null;
313 double? exerciseRatio = 315 double? exerciseRatio =
314 (exercise > 0 && (activityTarget?.exercise ?? 0) > 0) 316 (exercise > 0 && (activityTarget?.exercise ?? 0) > 0)
315 - ? (exercise / (activityTarget?.exercise ?? 0))  
316 - : null; 317 + ? (exercise / (activityTarget?.exercise ?? 0))
  318 + : null;
317 double? standRatio = (stand > 0 && (activityTarget?.stand ?? 0) > 0) 319 double? standRatio = (stand > 0 && (activityTarget?.stand ?? 0) > 0)
318 ? (stand / (activityTarget?.stand ?? 0)) 320 ? (stand / (activityTarget?.stand ?? 0))
319 : null; 321 : null;
@@ -405,7 +407,7 @@ class TodayActivityCard extends StatelessWidget { @@ -405,7 +407,7 @@ class TodayActivityCard extends StatelessWidget {
405 sweepAngles: [ 407 sweepAngles: [
406 activityProgress, 408 activityProgress,
407 exerciseProgress, 409 exerciseProgress,
408 - standProgress 410 + standProgress,
409 ], 411 ],
410 backgroundColors: [ 412 backgroundColors: [
411 context.colors.chartPink.withOpacity(0.4), 413 context.colors.chartPink.withOpacity(0.4),
@@ -413,20 +415,17 @@ class TodayActivityCard extends StatelessWidget { @@ -413,20 +415,17 @@ class TodayActivityCard extends StatelessWidget {
413 context.colors.chartBlue.withOpacity(0.4), 415 context.colors.chartBlue.withOpacity(0.4),
414 ], 416 ],
415 gradientColors: [ 417 gradientColors: [
416 - [  
417 - context.colors.chartPink,  
418 - ],  
419 - [  
420 - context.colors.chartGreen,  
421 - ],  
422 - [  
423 - context.colors.chartBlue,  
424 - ], 418 + [context.colors.chartPink],
  419 + [context.colors.chartGreen],
  420 + [context.colors.chartBlue],
425 ], 421 ],
426 ), 422 ),
427 ], 423 ],
428 ), 424 ),
429 ), 425 ),
  426 + interactionEnabled:
  427 + controller.isFriend &&
  428 + (activity > 0 || exercise > 0 || stand > 0),
430 ); 429 );
431 }), 430 }),
432 ); 431 );
@@ -441,6 +440,7 @@ class _TodaySummaryCard extends StatelessWidget { @@ -441,6 +440,7 @@ class _TodaySummaryCard extends StatelessWidget {
441 required this.actionText, 440 required this.actionText,
442 required this.metrics, 441 required this.metrics,
443 required this.rightWidget, 442 required this.rightWidget,
  443 + this.interactionEnabled,
444 }); 444 });
445 445
446 final String iconAsset; 446 final String iconAsset;
@@ -450,6 +450,7 @@ class _TodaySummaryCard extends StatelessWidget { @@ -450,6 +450,7 @@ class _TodaySummaryCard extends StatelessWidget {
450 final List<Widget> metrics; 450 final List<Widget> metrics;
451 451
452 final Widget rightWidget; 452 final Widget rightWidget;
  453 + final bool? interactionEnabled;
453 454
454 @override 455 @override
455 Widget build(BuildContext context) { 456 Widget build(BuildContext context) {
@@ -466,12 +467,7 @@ class _TodaySummaryCard extends StatelessWidget { @@ -466,12 +467,7 @@ class _TodaySummaryCard extends StatelessWidget {
466 children: [ 467 children: [
467 Row( 468 Row(
468 children: [ 469 children: [
469 - Image.asset(  
470 - iconAsset,  
471 - width: 16,  
472 - height: 16,  
473 - color: iconColor,  
474 - ), 470 + Image.asset(iconAsset, width: 16, height: 16, color: iconColor),
475 const SizedBox(width: 4), 471 const SizedBox(width: 4),
476 Text( 472 Text(
477 title, 473 title,
@@ -481,6 +477,10 @@ class _TodaySummaryCard extends StatelessWidget { @@ -481,6 +477,10 @@ class _TodaySummaryCard extends StatelessWidget {
481 fontWeight: FontWeight.w600, 477 fontWeight: FontWeight.w600,
482 ), 478 ),
483 ), 479 ),
  480 + if (interactionEnabled != null) ...[
  481 + const SizedBox(width: 8),
  482 + FriendInteractionButton(enabled: interactionEnabled!),
  483 + ],
484 const Spacer(), 484 const Spacer(),
485 Text( 485 Text(
486 actionText, 486 actionText,
@@ -510,7 +510,7 @@ class _TodaySummaryCard extends StatelessWidget { @@ -510,7 +510,7 @@ class _TodaySummaryCard extends StatelessWidget {
510 ], 510 ],
511 ), 511 ),
512 ), 512 ),
513 - rightWidget 513 + rightWidget,
514 ], 514 ],
515 ), 515 ),
516 ], 516 ],
  1 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
  2 +import 'package:flutter/material.dart';
  3 +import 'package:get/get.dart';
  4 +
  5 +class FriendInteractionButton extends StatelessWidget {
  6 + const FriendInteractionButton({
  7 + super.key,
  8 + required this.enabled,
  9 + this.size = 24,
  10 + });
  11 +
  12 + final bool enabled;
  13 + final double size;
  14 +
  15 + @override
  16 + Widget build(BuildContext context) => Opacity(
  17 + opacity: enabled ? 1 : 0.4,
  18 + child: GestureDetector(
  19 + behavior: HitTestBehavior.opaque,
  20 + onTap: enabled ? _openInteraction : null,
  21 + child: SizedBox(
  22 + width: size,
  23 + height: size,
  24 + child: DecoratedBox(
  25 + decoration: const BoxDecoration(
  26 + color: Color(0xFF845EEE),
  27 + shape: BoxShape.circle,
  28 + ),
  29 + child: Padding(
  30 + padding: EdgeInsets.all(size / 8),
  31 + child: Image.asset('assets/images/interact/icon_stick.png'),
  32 + ),
  33 + ),
  34 + ),
  35 + ),
  36 + );
  37 +
  38 + void _openInteraction() => Get.toNamed(Routes.INTERACT);
  39 +}
  40 +
  41 +class FriendInteractionFloatingEntry extends StatelessWidget {
  42 + const FriendInteractionFloatingEntry({super.key, this.size = 72});
  43 +
  44 + final double size;
  45 +
  46 + @override
  47 + Widget build(BuildContext context) => GestureDetector(
  48 + behavior: HitTestBehavior.opaque,
  49 + onTap: () => Get.toNamed(Routes.INTERACT),
  50 + child: Image.asset(
  51 + 'assets/images/interact/interaction_float.png',
  52 + width: size,
  53 + height: size,
  54 + fit: BoxFit.contain,
  55 + ),
  56 + );
  57 +}