Commit dd1f741d8f15a217bf787501580a713dbf1a1a63

Authored by 权海
1 parent fe7e25a8

feat(ui):首页互动入口

import 'package:doublefeel_flutter/app/modules/home/widgets/df_tab_bar.dart';
import 'package:doublefeel_flutter/app/modules/interact/widgets/friend_interaction_entry.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
... ... @@ -32,52 +33,51 @@ class FriendsTab extends GetView<FriendsController> {
Expanded(
child: ColoredBox(
color: const Color(0xFFF5F2FF),
child: Obx(
() {
final friends = controller.friends;
final hasFriends = friends.isNotEmpty;
final isFull = controller.isFull;
final healthData = controller.selfHealthData.value;
final currentUser =
userPreferences.preferences.value.meUserInfo;
final isSelfInitialLoading =
controller.isLoading.value && healthData == null;
final isFriendsInitialLoading =
controller.isLoading.value && !hasFriends;
final selfHealthCard = _SelfHealthCard(
name: currentUser?.nickname?.trim().isNotEmpty == true
? currentUser!.nickname!.trim()
: '-',
avatarUrl: currentUser?.avatar,
updatedAt: healthData?.updatedAt ??
context.l10n.friendsWaitingForData,
sleepQualityScore: healthData?.sleepQualityScore,
steps: healthData?.steps,
activeCalories: healthData?.activeCalories,
stressState:
healthData?.stressState ?? FriendStressState.wait,
stressValueText: healthData?.stressValueText,
isLoading: isSelfInitialLoading,
);
child: Obx(() {
final friends = controller.friends;
final hasFriends = friends.isNotEmpty;
final isFull = controller.isFull;
final healthData = controller.selfHealthData.value;
final currentUser =
userPreferences.preferences.value.meUserInfo;
final isSelfInitialLoading =
controller.isLoading.value && healthData == null;
final isFriendsInitialLoading =
controller.isLoading.value && !hasFriends;
final selfHealthCard = _SelfHealthCard(
name: currentUser?.nickname?.trim().isNotEmpty == true
? currentUser!.nickname!.trim()
: '-',
avatarUrl: currentUser?.avatar,
updatedAt:
healthData?.updatedAt ??
context.l10n.friendsWaitingForData,
sleepQualityScore: healthData?.sleepQualityScore,
steps: healthData?.steps,
activeCalories: healthData?.activeCalories,
stressState:
healthData?.stressState ?? FriendStressState.wait,
stressValueText: healthData?.stressValueText,
isLoading: isSelfInitialLoading,
);
if (isFriendsInitialLoading) {
return _buildLoadingFriendsView(context, selfHealthCard);
}
if (!hasFriends) {
return _buildEmptyFriendsView(
context,
selfHealthCard,
isFull: isFull,
);
}
return _buildFriendsListView(
if (isFriendsInitialLoading) {
return _buildLoadingFriendsView(context, selfHealthCard);
}
if (!hasFriends) {
return _buildEmptyFriendsView(
context,
selfHealthCard,
friends: friends,
isFull: isFull,
);
},
),
}
return _buildFriendsListView(
context,
selfHealthCard,
friends: friends,
isFull: isFull,
);
}),
),
),
],
... ... @@ -86,24 +86,13 @@ class FriendsTab extends GetView<FriendsController> {
);
}
Widget _buildLoadingFriendsView(
BuildContext context,
Widget selfHealthCard,
) {
Widget _buildLoadingFriendsView(BuildContext context, Widget selfHealthCard) {
return RefreshIndicator(
onRefresh: controller.refreshData,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.fromLTRB(
0,
0,
0,
_emptyListBottomPadding(context),
),
children: [
selfHealthCard,
const _FriendsLoadingIndicator(),
],
padding: EdgeInsets.fromLTRB(0, 0, 0, _emptyListBottomPadding(context)),
children: [selfHealthCard, const _FriendsLoadingIndicator()],
),
);
}
... ... @@ -117,12 +106,7 @@ class FriendsTab extends GetView<FriendsController> {
onRefresh: controller.refreshData,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.fromLTRB(
0,
0,
0,
_emptyListBottomPadding(context),
),
padding: EdgeInsets.fromLTRB(0, 0, 0, _emptyListBottomPadding(context)),
children: [
selfHealthCard,
_EmptyFriendsView(
... ... @@ -225,6 +209,11 @@ class FriendsTab extends GetView<FriendsController> {
],
),
),
Positioned(
right: 16,
bottom: _floatingAddButtonBottom(context) + 52,
child: const FriendInteractionFloatingEntry(),
),
],
);
}
... ... @@ -238,8 +227,9 @@ class FriendsTab extends GetView<FriendsController> {
}
double _tabBarAvoidanceBottom(BuildContext context) {
final bottomInset =
Theme.of(context).platform == TargetPlatform.iOS ? 34.0 : 0.0;
final bottomInset = Theme.of(context).platform == TargetPlatform.iOS
? 34.0
: 0.0;
return bottomInset + DfTabBar.height + DfTabBar.bottomMargin;
}
... ... @@ -265,10 +255,7 @@ class FriendsTab extends GetView<FriendsController> {
}
void _openFriendHome(FriendHealthData friend) {
Get.toNamed(
Routes.FRIEND_HOME,
arguments: friend.friendItem,
);
Get.toNamed(Routes.FRIEND_HOME, arguments: friend.friendItem);
}
void _handleFriendAction(FriendCardAction action, FriendHealthData friend) {
... ... @@ -386,11 +373,7 @@ class _FriendsHeader extends StatelessWidget {
onPressed: () {
_navigatorToPrivacySettings(context);
},
icon: Image.asset(
R.assetsImagesSettings,
width: 24,
height: 24,
),
icon: Image.asset(R.assetsImagesSettings, width: 24, height: 24),
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(width: 40, height: 40),
),
... ... @@ -406,10 +389,7 @@ _navigatorToPrivacySettings(BuildContext context) {
}
class _EmptyFriendsView extends StatelessWidget {
const _EmptyFriendsView({
required this.enabled,
this.onTap,
});
const _EmptyFriendsView({required this.enabled, this.onTap});
final bool enabled;
final VoidCallback? onTap;
... ... @@ -441,10 +421,7 @@ class _EmptyFriendsView extends StatelessWidget {
),
),
const SizedBox(height: 12),
_AddFriendButton(
enabled: enabled,
onTap: onTap,
),
_AddFriendButton(enabled: enabled, onTap: onTap),
],
),
);
... ... @@ -471,52 +448,53 @@ class _AddFriendButton extends StatelessWidget {
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 240,
height: 48,
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
width: 240,
height: 48,
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
),
),
Opacity(
opacity: enabled ? 1 : 0.4,
child: Container(
width: 240,
height: 48,
decoration: BoxDecoration(
color: Colors.white,
color: const Color(0xFF845EEE),
borderRadius: BorderRadius.circular(24),
),
),
Opacity(
opacity: enabled ? 1 : 0.4,
child: Container(
width: 240,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF845EEE),
borderRadius: BorderRadius.circular(24),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/common/ic_add.png',
width: 18,
height: 18,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/common/ic_add.png',
width: 18,
height: 18,
color: Colors.white,
),
const SizedBox(width: 6),
Text(
_buttonText(context),
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.4,
),
const SizedBox(width: 6),
Text(
_buttonText(context),
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.4,
),
),
],
),
),
],
),
)
],
)),
),
),
],
),
),
),
);
}
... ...
... ... @@ -54,6 +54,7 @@ class AppHomeController extends GetxController {
}
bool get hasWatchedFriend => watchedFriend.value != null;
bool get hasAnyFriend => _friendsController.friends.isNotEmpty;
bool get isShowingSelf => selectedTargetId.value == selfTodayTag;
String get selfName {
final info = _userPreferencesStorage.preferences.value.meUserInfo;
... ...
... ... @@ -42,6 +42,7 @@ class AppHomeTab extends GetView<AppHomeController> {
stressCard: showPeopleSwitcher
? AppHomeStressCard(controller: todayController)
: null,
showInteractionEntry: controller.hasAnyFriend,
);
}
... ...
import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measurement_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/modules/interact/widgets/friend_interaction_entry.dart';
import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart';
import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_bottom_slogan.dart';
import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart';
... ... @@ -40,6 +41,7 @@ class TodayTabBody extends StatelessWidget {
this.useHomeHeader = false,
this.headerFooter,
this.stressCard,
this.showInteractionEntry = false,
});
final TodayController controller;
... ... @@ -55,6 +57,8 @@ class TodayTabBody extends StatelessWidget {
/// The card starts below the week calendar so [headerFooter] can overlap it.
final Widget? stressCard;
final bool showInteractionEntry;
final _weekCalendarHeight = 58.0;
final _topContentHeight = /*_topBarHeight + _weekCalendarHeight*/ 106.0;
static const _peopleSwitcherAspectRatio = 252 / 1029;
... ... @@ -156,6 +160,12 @@ class TodayTabBody extends StatelessWidget {
),
),
),
if (controller.isFriend || showInteractionEntry)
const Positioned(
right: 16,
bottom: 92,
child: FriendInteractionFloatingEntry(),
),
],
),
);
... ...
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/app/modules/interact/widgets/friend_interaction_entry.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -21,7 +22,9 @@ class TodayHrvNumberCard extends StatelessWidget {
),
child: Obx(() {
var sameDay = DateUtils.isSameDay(
controller.selectedDate.value, controller.lastSelectableDay.value);
controller.selectedDate.value,
controller.lastSelectableDay.value,
);
var otherHrv = (sameDay
? context.l10n.todaySAverageHrv
... ... @@ -39,6 +42,7 @@ class TodayHrvNumberCard extends StatelessWidget {
: otherHrv,
value: hrvAvg > 0 ? hrvAvg.toString() : '-',
unit: 'ms',
interactionEnabled: controller.isFriend && hrvAvg > 0,
),
),
... ... @@ -46,12 +50,18 @@ class TodayHrvNumberCard extends StatelessWidget {
child: _NumberItem(
label: controller.isFriend
? context.l10n.reportOtherPossessiveTitle(
context.l10n.restingHeartRate)
context.l10n.restingHeartRate,
)
: context.l10n.restingHeartRate,
value: controller.v2HealthData.value?.lastRestingHrValue
value:
controller.v2HealthData.value?.lastRestingHrValue
?.toString() ??
'-',
unit: 'bpm',
interactionEnabled:
controller.isFriend &&
(controller.v2HealthData.value?.lastRestingHrValue ?? 0) >
0,
),
),
],
... ... @@ -65,11 +75,13 @@ class _NumberItem extends StatelessWidget {
final String label;
final String value;
final String unit;
final bool? interactionEnabled;
const _NumberItem({
required this.label,
required this.value,
required this.unit,
this.interactionEnabled,
});
@override
... ... @@ -79,13 +91,23 @@ class _NumberItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: context.colors.textTertiary,
fontWeight: FontWeight.w400,
),
Row(
children: [
Expanded(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: context.colors.textTertiary,
fontWeight: FontWeight.w400,
),
),
),
if (interactionEnabled != null)
FriendInteractionButton(enabled: interactionEnabled!),
],
),
const SizedBox(height: 6),
Row(
... ...
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/app/modules/interact/widgets/friend_interaction_entry.dart';
import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/arc_progress_widget.dart';
import 'package:doublefeel_flutter/app/widget/circular_gradient_progress/combine.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
... ... @@ -28,29 +29,31 @@ class TodaySleepCard extends StatelessWidget {
Color sleepStateColor = sleepDuration == 0
? context.colors.textPrimary
: healthData?.sleepStateColor(context) ??
context.colors.textPrimary;
String sleepStateStr =
sleepDuration == 0 ? '-' : healthData?.sleepStateStr() ?? '-';
context.colors.textPrimary;
String sleepStateStr = sleepDuration == 0
? '-'
: healthData?.sleepStateStr() ?? '-';
String sleepAverageHeartRate =
(healthData?.hrAvg == null || healthData?.hrAvg == 0)
? '-'
: '${healthData?.hrAvg}';
? '-'
: '${healthData?.hrAvg}';
double sleepDurationProgress =
(activityTarget?.sleepTargetDuration ?? 28800) == 0 ||
sleepDuration == 0
? -1
: ((healthData?.sleepDuration ?? 0) /
(activityTarget?.sleepTargetDuration ?? 28800) *
360);
double sleepScoreProgress =
sleepDuration == 0 ? -1 : (healthData?.sleepScore ?? 0.0) * 3.6;
sleepDuration == 0
? -1
: ((healthData?.sleepDuration ?? 0) /
(activityTarget?.sleepTargetDuration ?? 28800) *
360);
double sleepScoreProgress = sleepDuration == 0
? -1
: (healthData?.sleepScore ?? 0.0) * 3.6;
double? sleepDurationRatio =
(activityTarget?.sleepTargetDuration ?? 28800) == 0 ||
sleepDuration == 0
? null
: ((healthData?.sleepDuration ?? 0) /
(activityTarget?.sleepTargetDuration ?? 28800));
sleepDuration == 0
? null
: ((healthData?.sleepDuration ?? 0) /
(activityTarget?.sleepTargetDuration ?? 28800));
double? sleepScoreRatio = sleepDuration == 0
? null
: ((healthData?.sleepScore ?? 0.0) / 100.0);
... ... @@ -62,50 +65,51 @@ class TodaySleepCard extends StatelessWidget {
actionText: l10n.viewSleepReport,
metrics: [
_MetricBlock(
label: l10n.duration,
labelColor: context.colors.primary,
value: (sleepDuration > 0)
? Text.rich(
TextSpan(
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
children: [
if (sleepHours > 0) ...[
TextSpan(text: '$sleepHours'),
TextSpan(
text: l10n.sleepDailyUnitHour,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
)
],
if (sleepMinutes > 0) ...[
TextSpan(text: '$sleepMinutes'),
TextSpan(
text: l10n.sleepDailyUnitMinute,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
]
],
),
)
: Text(
'-',
label: l10n.duration,
labelColor: context.colors.primary,
value: (sleepDuration > 0)
? Text.rich(
TextSpan(
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
)),
children: [
if (sleepHours > 0) ...[
TextSpan(text: '$sleepHours'),
TextSpan(
text: l10n.sleepDailyUnitHour,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
if (sleepMinutes > 0) ...[
TextSpan(text: '$sleepMinutes'),
TextSpan(
text: l10n.sleepDailyUnitMinute,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
],
),
)
: Text(
'-',
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
_MetricBlock(
label: l10n.quality,
labelColor: const Color(0xFF7B9BFB),
... ... @@ -192,8 +196,10 @@ class TodaySleepCard extends StatelessWidget {
],
centerCircleSizeRatio: 0.3,
gapRatio: 0.1,
interpolatedColorRatio: 1),
interpolatedColorRatio: 1,
),
),
interactionEnabled: controller.isFriend && sleepDuration > 0,
);
}),
);
... ... @@ -252,9 +258,7 @@ class TodayActivityCard extends StatelessWidget {
fontSize: 16,
fontWeight: FontWeight.w600,
),
children: [
const TextSpan(text: '-'),
],
children: [const TextSpan(text: '-')],
),
);
}
... ... @@ -290,20 +294,18 @@ class TodayActivityCard extends StatelessWidget {
fontSize: 16,
fontWeight: FontWeight.w600,
),
children: [
const TextSpan(text: '-'),
],
children: [const TextSpan(text: '-')],
),
);
}
double activityProgress =
(activity > 0 && (activityTarget?.move ?? 0) > 0)
? (activity / (activityTarget?.move ?? 0)) * 360
: -1;
? (activity / (activityTarget?.move ?? 0)) * 360
: -1;
double exerciseProgress =
(exercise > 0 && (activityTarget?.exercise ?? 0) > 0)
? (exercise / (activityTarget?.exercise ?? 0)) * 360
: -1;
? (exercise / (activityTarget?.exercise ?? 0)) * 360
: -1;
double standProgress = (stand > 0 && (activityTarget?.stand ?? 0) > 0)
? (stand / (activityTarget?.stand ?? 0)) * 360
: -1;
... ... @@ -312,8 +314,8 @@ class TodayActivityCard extends StatelessWidget {
: null;
double? exerciseRatio =
(exercise > 0 && (activityTarget?.exercise ?? 0) > 0)
? (exercise / (activityTarget?.exercise ?? 0))
: null;
? (exercise / (activityTarget?.exercise ?? 0))
: null;
double? standRatio = (stand > 0 && (activityTarget?.stand ?? 0) > 0)
? (stand / (activityTarget?.stand ?? 0))
: null;
... ... @@ -405,7 +407,7 @@ class TodayActivityCard extends StatelessWidget {
sweepAngles: [
activityProgress,
exerciseProgress,
standProgress
standProgress,
],
backgroundColors: [
context.colors.chartPink.withOpacity(0.4),
... ... @@ -413,20 +415,17 @@ class TodayActivityCard extends StatelessWidget {
context.colors.chartBlue.withOpacity(0.4),
],
gradientColors: [
[
context.colors.chartPink,
],
[
context.colors.chartGreen,
],
[
context.colors.chartBlue,
],
[context.colors.chartPink],
[context.colors.chartGreen],
[context.colors.chartBlue],
],
),
],
),
),
interactionEnabled:
controller.isFriend &&
(activity > 0 || exercise > 0 || stand > 0),
);
}),
);
... ... @@ -441,6 +440,7 @@ class _TodaySummaryCard extends StatelessWidget {
required this.actionText,
required this.metrics,
required this.rightWidget,
this.interactionEnabled,
});
final String iconAsset;
... ... @@ -450,6 +450,7 @@ class _TodaySummaryCard extends StatelessWidget {
final List<Widget> metrics;
final Widget rightWidget;
final bool? interactionEnabled;
@override
Widget build(BuildContext context) {
... ... @@ -466,12 +467,7 @@ class _TodaySummaryCard extends StatelessWidget {
children: [
Row(
children: [
Image.asset(
iconAsset,
width: 16,
height: 16,
color: iconColor,
),
Image.asset(iconAsset, width: 16, height: 16, color: iconColor),
const SizedBox(width: 4),
Text(
title,
... ... @@ -481,6 +477,10 @@ class _TodaySummaryCard extends StatelessWidget {
fontWeight: FontWeight.w600,
),
),
if (interactionEnabled != null) ...[
const SizedBox(width: 8),
FriendInteractionButton(enabled: interactionEnabled!),
],
const Spacer(),
Text(
actionText,
... ... @@ -510,7 +510,7 @@ class _TodaySummaryCard extends StatelessWidget {
],
),
),
rightWidget
rightWidget,
],
),
],
... ...
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class FriendInteractionButton extends StatelessWidget {
const FriendInteractionButton({
super.key,
required this.enabled,
this.size = 24,
});
final bool enabled;
final double size;
@override
Widget build(BuildContext context) => Opacity(
opacity: enabled ? 1 : 0.4,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: enabled ? _openInteraction : null,
child: SizedBox(
width: size,
height: size,
child: DecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFF845EEE),
shape: BoxShape.circle,
),
child: Padding(
padding: EdgeInsets.all(size / 8),
child: Image.asset('assets/images/interact/icon_stick.png'),
),
),
),
),
);
void _openInteraction() => Get.toNamed(Routes.INTERACT);
}
class FriendInteractionFloatingEntry extends StatelessWidget {
const FriendInteractionFloatingEntry({super.key, this.size = 72});
final double size;
@override
Widget build(BuildContext context) => GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Get.toNamed(Routes.INTERACT),
child: Image.asset(
'assets/images/interact/interaction_float.png',
width: size,
height: size,
fit: BoxFit.contain,
),
);
}
... ...