Showing
11 changed files
with
310 additions
and
164 deletions
| @@ -107,4 +107,9 @@ class PlatformHostApiImpl(private val application: android.app.Application) : Pl | @@ -107,4 +107,9 @@ class PlatformHostApiImpl(private val application: android.app.Application) : Pl | ||
| 107 | callback: (Result<String?>) -> Unit | 107 | callback: (Result<String?>) -> Unit |
| 108 | ) { | 108 | ) { |
| 109 | } | 109 | } |
| 110 | + | ||
| 111 | + override fun jumpAppSetting(): Boolean { | ||
| 112 | + return true | ||
| 113 | + } | ||
| 114 | + | ||
| 110 | } | 115 | } |
| @@ -117,13 +117,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -117,13 +117,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 117 | time.isBefore(date.add(const Duration(days: 1)))) | 117 | time.isBefore(date.add(const Duration(days: 1)))) |
| 118 | ActivityBurnHeartRatePoint(time: time, bpm: bpm.toDouble()), | 118 | ActivityBurnHeartRatePoint(time: time, bpm: bpm.toDouble()), |
| 119 | ]..sort((a, b) => a.time.compareTo(b.time)); | 119 | ]..sort((a, b) => a.time.compareTo(b.time)); |
| 120 | - final sleepTimes = <DateTime>[ | ||
| 121 | - for (final item in data.sleepTimeList ?? const <SleepTimeList>[]) | ||
| 122 | - if (_parseDateTime(item.fromTime, fallbackDate: date) case final from?) | ||
| 123 | - from, | ||
| 124 | - for (final item in data.sleepTimeList ?? const <SleepTimeList>[]) | ||
| 125 | - if (_parseDateTime(item.toTime, fallbackDate: date) case final to?) to, | ||
| 126 | - ]..sort(); | 120 | + final sleepRange = _dailySleepRange(date, data.sleepTimeList); |
| 127 | 121 | ||
| 128 | return ActivityBurnReport( | 122 | return ActivityBurnReport( |
| 129 | date: date, | 123 | date: date, |
| @@ -140,13 +134,47 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -140,13 +134,47 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 140 | heartRate: ActivityBurnHeartRateSummary( | 134 | heartRate: ActivityBurnHeartRateSummary( |
| 141 | startTime: date, | 135 | startTime: date, |
| 142 | endTime: points.isEmpty ? null : points.last.time, | 136 | endTime: points.isEmpty ? null : points.last.time, |
| 143 | - sleepStartTime: sleepTimes.isEmpty ? null : sleepTimes.first, | ||
| 144 | - sleepEndTime: sleepTimes.isEmpty ? null : sleepTimes.last, | 137 | + sleepStartTime: sleepRange?.start, |
| 138 | + sleepEndTime: sleepRange?.end, | ||
| 145 | points: points, | 139 | points: points, |
| 146 | ), | 140 | ), |
| 147 | ); | 141 | ); |
| 148 | } | 142 | } |
| 149 | 143 | ||
| 144 | + _SleepRange? _dailySleepRange( | ||
| 145 | + DateTime date, | ||
| 146 | + List<SleepTimeList>? sleepTimeList, | ||
| 147 | + ) { | ||
| 148 | + final dayStart = _normalizeDate(date); | ||
| 149 | + final dayEnd = dayStart.add(const Duration(days: 1)); | ||
| 150 | + DateTime? earliestStart; | ||
| 151 | + DateTime? latestEndInDay; | ||
| 152 | + | ||
| 153 | + for (final item in sleepTimeList ?? const <SleepTimeList>[]) { | ||
| 154 | + final from = _parseDateTime(item.fromTime, fallbackDate: dayStart); | ||
| 155 | + final to = _parseDateTime(item.toTime, fallbackDate: dayStart); | ||
| 156 | + if (from == null || to == null || !to.isAfter(from)) continue; | ||
| 157 | + if (!from.isBefore(dayEnd) || !to.isAfter(dayStart)) continue; | ||
| 158 | + | ||
| 159 | + final visibleStart = from.isBefore(dayStart) ? dayStart : from; | ||
| 160 | + if (earliestStart == null || visibleStart.isBefore(earliestStart)) { | ||
| 161 | + earliestStart = visibleStart; | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + final visibleEnd = to.isAfter(dayEnd) ? dayEnd : to; | ||
| 165 | + if (!visibleEnd.isBefore(dayStart) && !visibleEnd.isAfter(dayEnd)) { | ||
| 166 | + if (latestEndInDay == null || visibleEnd.isAfter(latestEndInDay)) { | ||
| 167 | + latestEndInDay = visibleEnd; | ||
| 168 | + } | ||
| 169 | + } | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + final start = earliestStart; | ||
| 173 | + final end = latestEndInDay; | ||
| 174 | + if (start == null || end == null || !end.isAfter(start)) return null; | ||
| 175 | + return _SleepRange(start, end); | ||
| 176 | + } | ||
| 177 | + | ||
| 150 | Value? _dailyOverallValues( | 178 | Value? _dailyOverallValues( |
| 151 | DateTime date, | 179 | DateTime date, |
| 152 | List<OverallList>? overallList, | 180 | List<OverallList>? overallList, |
| @@ -287,6 +315,13 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | @@ -287,6 +315,13 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource { | ||
| 287 | date.year * 10000 + date.month * 100 + date.day; | 315 | date.year * 10000 + date.month * 100 + date.day; |
| 288 | } | 316 | } |
| 289 | 317 | ||
| 318 | +class _SleepRange { | ||
| 319 | + const _SleepRange(this.start, this.end); | ||
| 320 | + | ||
| 321 | + final DateTime start; | ||
| 322 | + final DateTime end; | ||
| 323 | +} | ||
| 324 | + | ||
| 290 | class MockActivityBurnReportDataSource implements ActivityBurnReportDataSource { | 325 | class MockActivityBurnReportDataSource implements ActivityBurnReportDataSource { |
| 291 | const MockActivityBurnReportDataSource(); | 326 | const MockActivityBurnReportDataSource(); |
| 292 | 327 |
| @@ -49,109 +49,47 @@ class FriendsTab extends GetView<FriendsController> { | @@ -49,109 +49,47 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 49 | final stressValue = stressScore?.state == 0 | 49 | final stressValue = stressScore?.state == 0 |
| 50 | ? null | 50 | ? null |
| 51 | : stressScore?.comprehensiveScore?.toDouble(); | 51 | : stressScore?.comprehensiveScore?.toDouble(); |
| 52 | - | ||
| 53 | - return Stack( | ||
| 54 | - children: [ | ||
| 55 | - RefreshIndicator( | ||
| 56 | - onRefresh: controller.refreshData, | ||
| 57 | - child: ListView( | ||
| 58 | - physics: const AlwaysScrollableScrollPhysics(), | ||
| 59 | - padding: EdgeInsets.fromLTRB( | ||
| 60 | - 0, | ||
| 61 | - 0, | ||
| 62 | - 0, | ||
| 63 | - hasFriends ? 188.dp : 172, | 52 | + final selfHealthCard = _SelfHealthCard( |
| 53 | + name: currentUser?.nickname?.trim().isNotEmpty == true | ||
| 54 | + ? currentUser!.nickname!.trim() | ||
| 55 | + : '-', | ||
| 56 | + avatarUrl: currentUser?.avatar, | ||
| 57 | + updatedAt: updatedAt == null | ||
| 58 | + ? context.l10n.friendsWaitingForData | ||
| 59 | + : context.l10n.friendsUpdatedAt( | ||
| 60 | + DateFormat('HH:mm').format(updatedAt), | ||
| 64 | ), | 61 | ), |
| 65 | - children: [ | ||
| 66 | - _SelfHealthCard( | ||
| 67 | - name: | ||
| 68 | - currentUser?.nickname?.trim().isNotEmpty == | ||
| 69 | - true | ||
| 70 | - ? currentUser!.nickname!.trim() | ||
| 71 | - : '-', | ||
| 72 | - avatarUrl: currentUser?.avatar, | ||
| 73 | - updatedAt: updatedAt == null | ||
| 74 | - ? context.l10n.friendsWaitingForData | ||
| 75 | - : context.l10n.friendsUpdatedAt( | ||
| 76 | - DateFormat('HH:mm').format(updatedAt), | ||
| 77 | - ), | ||
| 78 | - sleepQualityScore: | ||
| 79 | - _normalizedScore(healthData?.sleepScore), | ||
| 80 | - steps: healthData?.steps == null | ||
| 81 | - ? null | ||
| 82 | - : context.l10n.friendsStepCount( | ||
| 83 | - healthData!.steps!, | ||
| 84 | - ), | ||
| 85 | - statusText: stressValue == null | ||
| 86 | - ? context.l10n.friendsWaitingForData | ||
| 87 | - : HrvStressLevel.fromRealtimeStress( | ||
| 88 | - stressValue, | ||
| 89 | - ).label, | ||
| 90 | - stressValue: stressValue, | ||
| 91 | - isLoading: isSelfInitialLoading, | ||
| 92 | - ), | ||
| 93 | - if (isFriendsInitialLoading) | ||
| 94 | - const _FriendsLoadingIndicator() | ||
| 95 | - else if (!hasFriends) | ||
| 96 | - const _EmptyFriendsView() | ||
| 97 | - else ...[ | ||
| 98 | - const SizedBox(height: 12), | ||
| 99 | - ...friends.map( | ||
| 100 | - (friend) => Padding( | ||
| 101 | - padding: const EdgeInsets.fromLTRB( | ||
| 102 | - 16, | ||
| 103 | - 0, | ||
| 104 | - 16, | ||
| 105 | - 12, | ||
| 106 | - ), | ||
| 107 | - child: FriendHealthCard( | ||
| 108 | - name: friend.name, | ||
| 109 | - remark: friend.remark, | ||
| 110 | - avatarUrl: friend.avatarUrl, | ||
| 111 | - updatedAt: friend.updatedAt, | ||
| 112 | - sleepQualityScore: | ||
| 113 | - friend.sleepQualityScore, | ||
| 114 | - steps: friend.steps, | ||
| 115 | - statusText: friend.statusText, | ||
| 116 | - stressValue: friend.stressValue, | ||
| 117 | - isOnWatchFace: friend.isOnWatchFace, | ||
| 118 | - onTap: () => _openFriendHome(friend), | ||
| 119 | - onMoreSelected: (action) { | ||
| 120 | - _handleFriendAction(action, friend); | ||
| 121 | - }, | ||
| 122 | - ), | ||
| 123 | - ), | ||
| 124 | - ), | ||
| 125 | - ], | ||
| 126 | - if (!hasFriends && !isFriendsInitialLoading) ...[ | ||
| 127 | - const SizedBox(height: 8), | ||
| 128 | - _AddFriendButton( | ||
| 129 | - enabled: !isFull, | ||
| 130 | - onTap: isFull ? null : _handleAddFriend, | ||
| 131 | - ), | ||
| 132 | - if (isFull) const _FullFriendsTip(), | ||
| 133 | - ], | ||
| 134 | - ], | ||
| 135 | - ), | ||
| 136 | - ), | ||
| 137 | - if (hasFriends) | ||
| 138 | - Positioned( | ||
| 139 | - left: 0, | ||
| 140 | - right: 0, | ||
| 141 | - bottom: _floatingAddButtonBottom(context), | ||
| 142 | - child: Column( | ||
| 143 | - children: [ | ||
| 144 | - _AddFriendButton( | ||
| 145 | - enabled: !isFull, | ||
| 146 | - friendCount: friends.length, | ||
| 147 | - maxFriends: FriendsController.maxFriends, | ||
| 148 | - onTap: isFull ? null : _handleAddFriend, | ||
| 149 | - ), | ||
| 150 | - if (isFull) const _FullFriendsTip(), | ||
| 151 | - ], | 62 | + sleepQualityScore: |
| 63 | + _normalizedScore(healthData?.sleepScore), | ||
| 64 | + steps: healthData?.steps == null | ||
| 65 | + ? null | ||
| 66 | + : context.l10n.friendsStepCount( | ||
| 67 | + healthData!.steps!, | ||
| 152 | ), | 68 | ), |
| 153 | - ), | ||
| 154 | - ], | 69 | + statusText: stressValue == null |
| 70 | + ? context.l10n.friendsWaitingForData | ||
| 71 | + : HrvStressLevel.fromRealtimeStress( | ||
| 72 | + stressValue, | ||
| 73 | + ).label, | ||
| 74 | + stressValue: stressValue, | ||
| 75 | + isLoading: isSelfInitialLoading, | ||
| 76 | + ); | ||
| 77 | + | ||
| 78 | + if (isFriendsInitialLoading) { | ||
| 79 | + return _buildLoadingFriendsView(context, selfHealthCard); | ||
| 80 | + } | ||
| 81 | + if (!hasFriends) { | ||
| 82 | + return _buildEmptyFriendsView( | ||
| 83 | + context, | ||
| 84 | + selfHealthCard, | ||
| 85 | + isFull: isFull, | ||
| 86 | + ); | ||
| 87 | + } | ||
| 88 | + return _buildFriendsListView( | ||
| 89 | + context, | ||
| 90 | + selfHealthCard, | ||
| 91 | + friends: friends, | ||
| 92 | + isFull: isFull, | ||
| 155 | ); | 93 | ); |
| 156 | }, | 94 | }, |
| 157 | ), | 95 | ), |
| @@ -163,13 +101,127 @@ class FriendsTab extends GetView<FriendsController> { | @@ -163,13 +101,127 @@ class FriendsTab extends GetView<FriendsController> { | ||
| 163 | ); | 101 | ); |
| 164 | } | 102 | } |
| 165 | 103 | ||
| 104 | + Widget _buildLoadingFriendsView( | ||
| 105 | + BuildContext context, | ||
| 106 | + Widget selfHealthCard, | ||
| 107 | + ) { | ||
| 108 | + return RefreshIndicator( | ||
| 109 | + onRefresh: controller.refreshData, | ||
| 110 | + child: ListView( | ||
| 111 | + physics: const AlwaysScrollableScrollPhysics(), | ||
| 112 | + padding: EdgeInsets.fromLTRB( | ||
| 113 | + 0, | ||
| 114 | + 0, | ||
| 115 | + 0, | ||
| 116 | + _emptyListBottomPadding(context), | ||
| 117 | + ), | ||
| 118 | + children: [ | ||
| 119 | + selfHealthCard, | ||
| 120 | + const _FriendsLoadingIndicator(), | ||
| 121 | + ], | ||
| 122 | + ), | ||
| 123 | + ); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + Widget _buildEmptyFriendsView( | ||
| 127 | + BuildContext context, | ||
| 128 | + Widget selfHealthCard, { | ||
| 129 | + required bool isFull, | ||
| 130 | + }) { | ||
| 131 | + return RefreshIndicator( | ||
| 132 | + onRefresh: controller.refreshData, | ||
| 133 | + child: ListView( | ||
| 134 | + physics: const AlwaysScrollableScrollPhysics(), | ||
| 135 | + padding: EdgeInsets.fromLTRB( | ||
| 136 | + 0, | ||
| 137 | + 0, | ||
| 138 | + 0, | ||
| 139 | + _emptyListBottomPadding(context), | ||
| 140 | + ), | ||
| 141 | + children: [ | ||
| 142 | + selfHealthCard, | ||
| 143 | + _EmptyFriendsView( | ||
| 144 | + enabled: !isFull, | ||
| 145 | + onTap: isFull ? null : _handleAddFriend, | ||
| 146 | + ), | ||
| 147 | + ], | ||
| 148 | + ), | ||
| 149 | + ); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + Widget _buildFriendsListView( | ||
| 153 | + BuildContext context, | ||
| 154 | + Widget selfHealthCard, { | ||
| 155 | + required List<FriendHealthData> friends, | ||
| 156 | + required bool isFull, | ||
| 157 | + }) { | ||
| 158 | + return Stack( | ||
| 159 | + children: [ | ||
| 160 | + RefreshIndicator( | ||
| 161 | + onRefresh: controller.refreshData, | ||
| 162 | + child: ListView( | ||
| 163 | + physics: const AlwaysScrollableScrollPhysics(), | ||
| 164 | + padding: EdgeInsets.fromLTRB(0, 0, 0, 188.dp), | ||
| 165 | + children: [ | ||
| 166 | + selfHealthCard, | ||
| 167 | + const SizedBox(height: 12), | ||
| 168 | + ...friends.map( | ||
| 169 | + (friend) => Padding( | ||
| 170 | + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), | ||
| 171 | + child: FriendHealthCard( | ||
| 172 | + name: friend.name, | ||
| 173 | + remark: friend.remark, | ||
| 174 | + avatarUrl: friend.avatarUrl, | ||
| 175 | + updatedAt: friend.updatedAt, | ||
| 176 | + sleepQualityScore: friend.sleepQualityScore, | ||
| 177 | + steps: friend.steps, | ||
| 178 | + statusText: friend.statusText, | ||
| 179 | + stressValue: friend.stressValue, | ||
| 180 | + isOnWatchFace: friend.isOnWatchFace, | ||
| 181 | + onTap: () => _openFriendHome(friend), | ||
| 182 | + onMoreSelected: (action) { | ||
| 183 | + _handleFriendAction(action, friend); | ||
| 184 | + }, | ||
| 185 | + ), | ||
| 186 | + ), | ||
| 187 | + ), | ||
| 188 | + ], | ||
| 189 | + ), | ||
| 190 | + ), | ||
| 191 | + Positioned( | ||
| 192 | + left: 0, | ||
| 193 | + right: 0, | ||
| 194 | + bottom: _floatingAddButtonBottom(context), | ||
| 195 | + child: Column( | ||
| 196 | + children: [ | ||
| 197 | + _AddFriendButton( | ||
| 198 | + enabled: !isFull, | ||
| 199 | + friendCount: friends.length, | ||
| 200 | + maxFriends: FriendsController.maxFriends, | ||
| 201 | + onTap: isFull ? null : _handleAddFriend, | ||
| 202 | + ), | ||
| 203 | + if (isFull) const _FullFriendsTip(), | ||
| 204 | + ], | ||
| 205 | + ), | ||
| 206 | + ), | ||
| 207 | + ], | ||
| 208 | + ); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + double _emptyListBottomPadding(BuildContext context) { | ||
| 212 | + return _tabBarAvoidanceBottom(context) + 24; | ||
| 213 | + } | ||
| 214 | + | ||
| 166 | double _floatingAddButtonBottom(BuildContext context) { | 215 | double _floatingAddButtonBottom(BuildContext context) { |
| 216 | + return _tabBarAvoidanceBottom(context) + 23.dp; | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + double _tabBarAvoidanceBottom(BuildContext context) { | ||
| 167 | const tabBarHeight = 56.0; | 220 | const tabBarHeight = 56.0; |
| 168 | const tabBarBottomPadding = 8.0; | 221 | const tabBarBottomPadding = 8.0; |
| 169 | return MediaQuery.viewPaddingOf(context).bottom + | 222 | return MediaQuery.viewPaddingOf(context).bottom + |
| 170 | tabBarHeight + | 223 | tabBarHeight + |
| 171 | - tabBarBottomPadding + | ||
| 172 | - 23.dp; | 224 | + tabBarBottomPadding; |
| 173 | } | 225 | } |
| 174 | 226 | ||
| 175 | Future<void> _handleAddFriend() async { | 227 | Future<void> _handleAddFriend() async { |
| @@ -324,7 +376,13 @@ _navigatorToPrivacySettings() { | @@ -324,7 +376,13 @@ _navigatorToPrivacySettings() { | ||
| 324 | } | 376 | } |
| 325 | 377 | ||
| 326 | class _EmptyFriendsView extends StatelessWidget { | 378 | class _EmptyFriendsView extends StatelessWidget { |
| 327 | - const _EmptyFriendsView(); | 379 | + const _EmptyFriendsView({ |
| 380 | + required this.enabled, | ||
| 381 | + this.onTap, | ||
| 382 | + }); | ||
| 383 | + | ||
| 384 | + final bool enabled; | ||
| 385 | + final VoidCallback? onTap; | ||
| 328 | 386 | ||
| 329 | @override | 387 | @override |
| 330 | Widget build(BuildContext context) { | 388 | Widget build(BuildContext context) { |
| @@ -351,6 +409,12 @@ class _EmptyFriendsView extends StatelessWidget { | @@ -351,6 +409,12 @@ class _EmptyFriendsView extends StatelessWidget { | ||
| 351 | fontWeight: FontWeight.w400, | 409 | fontWeight: FontWeight.w400, |
| 352 | ), | 410 | ), |
| 353 | ), | 411 | ), |
| 412 | + const SizedBox(height: 12), | ||
| 413 | + _AddFriendButton( | ||
| 414 | + enabled: enabled, | ||
| 415 | + onTap: onTap, | ||
| 416 | + ), | ||
| 417 | + if (!enabled) const _FullFriendsTip(), | ||
| 354 | ], | 418 | ], |
| 355 | ), | 419 | ), |
| 356 | ); | 420 | ); |
| @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; | @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; | ||
| 4 | import 'package:get/get.dart'; | 4 | import 'package:get/get.dart'; |
| 5 | 5 | ||
| 6 | import '../../../routes/app_pages.dart'; | 6 | import '../../../routes/app_pages.dart'; |
| 7 | +import '../../purchase/purchase_route_args.dart'; | ||
| 7 | import '../../app_review_prompt/logic/app_review_prompt_logic.dart'; | 8 | import '../../app_review_prompt/logic/app_review_prompt_logic.dart'; |
| 8 | import '../../report_common/models/report_period.dart'; | 9 | import '../../report_common/models/report_period.dart'; |
| 9 | import 'trend/trend_controller.dart'; | 10 | import 'trend/trend_controller.dart'; |
| @@ -39,7 +40,10 @@ class HomeController extends GetxController { | @@ -39,7 +40,10 @@ class HomeController extends GetxController { | ||
| 39 | date: today, | 40 | date: today, |
| 40 | userId: userId, | 41 | userId: userId, |
| 41 | ); | 42 | ); |
| 42 | - await Get.toNamed(Routes.PURCHASE); | 43 | + await Get.toNamed( |
| 44 | + Routes.PURCHASE, | ||
| 45 | + arguments: {PurchaseRouteArgs.showCloseButton: true}, | ||
| 46 | + ); | ||
| 43 | return true; | 47 | return true; |
| 44 | } | 48 | } |
| 45 | 49 |
| @@ -42,9 +42,10 @@ class HrvReportLogic extends ReportPeriodLogic { | @@ -42,9 +42,10 @@ class HrvReportLogic extends ReportPeriodLogic { | ||
| 42 | (nextPeriod == ReportPeriod.week || nextPeriod == ReportPeriod.month)) { | 42 | (nextPeriod == ReportPeriod.week || nextPeriod == ReportPeriod.month)) { |
| 43 | final now = DateTime.now(); | 43 | final now = DateTime.now(); |
| 44 | final selectedYear = selectedDate.value.year; | 44 | final selectedYear = selectedDate.value.year; |
| 45 | - selectedDate.value = selectedYear == now.year | 45 | + final date = selectedYear == now.year |
| 46 | ? DateTime(now.year, now.month, now.day) | 46 | ? DateTime(now.year, now.month, now.day) |
| 47 | : DateTime(selectedYear); | 47 | : DateTime(selectedYear); |
| 48 | + return selectQuery(nextPeriod, date, forceRefresh: forceRefresh); | ||
| 48 | } | 49 | } |
| 49 | return super.selectPeriod(nextPeriod, forceRefresh: forceRefresh); | 50 | return super.selectPeriod(nextPeriod, forceRefresh: forceRefresh); |
| 50 | } | 51 | } |
| @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 4 | import 'package:doublefeel_flutter/r.dart'; | 4 | import 'package:doublefeel_flutter/r.dart'; |
| 5 | import 'package:flutter/material.dart'; | 5 | import 'package:flutter/material.dart'; |
| 6 | import 'package:get/get.dart'; | 6 | import 'package:get/get.dart'; |
| 7 | +import 'package:lottie/lottie.dart'; | ||
| 7 | 8 | ||
| 8 | import '../controllers/premium_activated_controller.dart'; | 9 | import '../controllers/premium_activated_controller.dart'; |
| 9 | 10 | ||
| @@ -59,49 +60,54 @@ class PremiumActivatedView extends GetView<PremiumActivatedController> { | @@ -59,49 +60,54 @@ class PremiumActivatedView extends GetView<PremiumActivatedController> { | ||
| 59 | } | 60 | } |
| 60 | 61 | ||
| 61 | Widget _buildContentView(BuildContext context) { | 62 | Widget _buildContentView(BuildContext context) { |
| 62 | - return Column( | ||
| 63 | - mainAxisAlignment: MainAxisAlignment.start, | ||
| 64 | - crossAxisAlignment: CrossAxisAlignment.center, | 63 | + return Stack( |
| 65 | children: [ | 64 | children: [ |
| 66 | - SizedBox(height: 150.h), | ||
| 67 | - const _FloatingPremiumIcon(), | ||
| 68 | - SizedBox(height: 84.h), | ||
| 69 | - Text( | ||
| 70 | - context.l10n.premiumActivatedTitle, | ||
| 71 | - style: const TextStyle( | ||
| 72 | - fontSize: 20, | ||
| 73 | - color: Color(0xFF0F0F11), | ||
| 74 | - fontWeight: FontWeightExt.semibold), | ||
| 75 | - textAlign: TextAlign.center, | ||
| 76 | - ), | ||
| 77 | - SizedBox(height: 8), | ||
| 78 | - Padding( | ||
| 79 | - padding: const EdgeInsets.symmetric(horizontal: 24.0), | ||
| 80 | - child: Text(context.l10n.premiumActivatedDescription, | ||
| 81 | - style: const TextStyle(fontSize: 14, color: Color(0xFF0F0F11)), | ||
| 82 | - textAlign: TextAlign.center), | ||
| 83 | - ), | ||
| 84 | - Expanded(child: Container()), | ||
| 85 | - GestureDetector( | ||
| 86 | - onTap: () { | ||
| 87 | - controller.executeContinueLogic(context); | ||
| 88 | - }, | ||
| 89 | - child: Container( | ||
| 90 | - width: 280.dp, | ||
| 91 | - height: 48.dp, | ||
| 92 | - decoration: BoxDecoration( | ||
| 93 | - color: Color(0xFF845EEE), | ||
| 94 | - borderRadius: BorderRadius.circular(24.dp), | 65 | + Lottie.asset('assets/lottie/scatter_flowers.json'), |
| 66 | + Column( | ||
| 67 | + mainAxisAlignment: MainAxisAlignment.start, | ||
| 68 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
| 69 | + children: [ | ||
| 70 | + SizedBox(height: 150.h), | ||
| 71 | + const _FloatingPremiumIcon(), | ||
| 72 | + SizedBox(height: 84.h), | ||
| 73 | + Text( | ||
| 74 | + context.l10n.premiumActivatedTitle, | ||
| 75 | + style: const TextStyle( | ||
| 76 | + fontSize: 20, | ||
| 77 | + color: Color(0xFF0F0F11), | ||
| 78 | + fontWeight: FontWeightExt.semibold), | ||
| 79 | + textAlign: TextAlign.center, | ||
| 95 | ), | 80 | ), |
| 96 | - alignment: Alignment.center, | ||
| 97 | - child: Text(context.l10n.premiumActivatedContinue, | ||
| 98 | - style: const TextStyle( | ||
| 99 | - fontSize: 16, | ||
| 100 | - color: Colors.white, | ||
| 101 | - fontWeight: FontWeightExt.bold)), | ||
| 102 | - ), | 81 | + SizedBox(height: 8), |
| 82 | + Padding( | ||
| 83 | + padding: const EdgeInsets.symmetric(horizontal: 24.0), | ||
| 84 | + child: Text(context.l10n.premiumActivatedDescription, | ||
| 85 | + style: const TextStyle(fontSize: 14, color: Color(0xFF0F0F11)), | ||
| 86 | + textAlign: TextAlign.center), | ||
| 87 | + ), | ||
| 88 | + Expanded(child: Container()), | ||
| 89 | + GestureDetector( | ||
| 90 | + onTap: () { | ||
| 91 | + controller.executeContinueLogic(context); | ||
| 92 | + }, | ||
| 93 | + child: Container( | ||
| 94 | + width: 280.dp, | ||
| 95 | + height: 48.dp, | ||
| 96 | + decoration: BoxDecoration( | ||
| 97 | + color: Color(0xFF845EEE), | ||
| 98 | + borderRadius: BorderRadius.circular(24.dp), | ||
| 99 | + ), | ||
| 100 | + alignment: Alignment.center, | ||
| 101 | + child: Text(context.l10n.premiumActivatedContinue, | ||
| 102 | + style: const TextStyle( | ||
| 103 | + fontSize: 16, | ||
| 104 | + color: Colors.white, | ||
| 105 | + fontWeight: FontWeightExt.bold)), | ||
| 106 | + ), | ||
| 107 | + ), | ||
| 108 | + SizedBox(height: 36), | ||
| 109 | + ], | ||
| 103 | ), | 110 | ), |
| 104 | - SizedBox(height: 36), | ||
| 105 | ], | 111 | ], |
| 106 | ); | 112 | ); |
| 107 | } | 113 | } |
| @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; | @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; | ||
| 6 | import 'package:get/get.dart'; | 6 | import 'package:get/get.dart'; |
| 7 | 7 | ||
| 8 | import '../controllers/purchase_controller.dart'; | 8 | import '../controllers/purchase_controller.dart'; |
| 9 | +import '../purchase_route_args.dart'; | ||
| 9 | import '../widgets/purchase_bottom_bar.dart'; | 10 | import '../widgets/purchase_bottom_bar.dart'; |
| 10 | import '../widgets/purchase_colors.dart'; | 11 | import '../widgets/purchase_colors.dart'; |
| 11 | import '../widgets/purchase_plan_card.dart'; | 12 | import '../widgets/purchase_plan_card.dart'; |
| @@ -176,6 +177,9 @@ class _PurchaseNavigationBar extends StatelessWidget { | @@ -176,6 +177,9 @@ class _PurchaseNavigationBar extends StatelessWidget { | ||
| 176 | @override | 177 | @override |
| 177 | Widget build(BuildContext context) { | 178 | Widget build(BuildContext context) { |
| 178 | final topInset = MediaQuery.paddingOf(context).top; | 179 | final topInset = MediaQuery.paddingOf(context).top; |
| 180 | + final arguments = Get.arguments; | ||
| 181 | + final showCloseButton = arguments is Map && | ||
| 182 | + arguments[PurchaseRouteArgs.showCloseButton] == true; | ||
| 179 | return Obx( | 183 | return Obx( |
| 180 | () => AnimatedContainer( | 184 | () => AnimatedContainer( |
| 181 | duration: const Duration(milliseconds: 160), | 185 | duration: const Duration(milliseconds: 160), |
| @@ -188,7 +192,9 @@ class _PurchaseNavigationBar extends StatelessWidget { | @@ -188,7 +192,9 @@ class _PurchaseNavigationBar extends StatelessWidget { | ||
| 188 | IconButton( | 192 | IconButton( |
| 189 | onPressed: Get.back, | 193 | onPressed: Get.back, |
| 190 | icon: Image.asset( | 194 | icon: Image.asset( |
| 191 | - 'assets/images/common/ic_nav_back.webp', | 195 | + showCloseButton |
| 196 | + ? R.assetsImagesCloseIcon | ||
| 197 | + : R.assetsImagesNavBackIcon, | ||
| 192 | width: 24, | 198 | width: 24, |
| 193 | height: 24, | 199 | height: 24, |
| 194 | ), | 200 | ), |
| @@ -16,6 +16,7 @@ abstract class ReportPeriodLogic { | @@ -16,6 +16,7 @@ abstract class ReportPeriodLogic { | ||
| 16 | final selectedPeriod = ReportPeriod.day.obs; | 16 | final selectedPeriod = ReportPeriod.day.obs; |
| 17 | final selectedDate = _today().obs; | 17 | final selectedDate = _today().obs; |
| 18 | final isLoading = false.obs; | 18 | final isLoading = false.obs; |
| 19 | + final Map<ReportPeriod, DateTime> _datesByPeriod = {}; | ||
| 19 | 20 | ||
| 20 | List<ReportPeriod> get supportedPeriods => ReportPeriod.values; | 21 | List<ReportPeriod> get supportedPeriods => ReportPeriod.values; |
| 21 | 22 | ||
| @@ -48,8 +49,9 @@ abstract class ReportPeriodLogic { | @@ -48,8 +49,9 @@ abstract class ReportPeriodLogic { | ||
| 48 | 49 | ||
| 49 | void initializeQuery(ReportPeriod period, DateTime date) { | 50 | void initializeQuery(ReportPeriod period, DateTime date) { |
| 50 | final nextPeriod = normalizePeriod(period); | 51 | final nextPeriod = normalizePeriod(period); |
| 52 | + _seedPeriodDates(date); | ||
| 51 | selectedPeriod.value = nextPeriod; | 53 | selectedPeriod.value = nextPeriod; |
| 52 | - selectedDate.value = normalizeDate(nextPeriod, date); | 54 | + selectedDate.value = _datesByPeriod[nextPeriod]!; |
| 53 | } | 55 | } |
| 54 | 56 | ||
| 55 | Future<void> selectQuery( | 57 | Future<void> selectQuery( |
| @@ -65,8 +67,10 @@ abstract class ReportPeriodLogic { | @@ -65,8 +67,10 @@ abstract class ReportPeriodLogic { | ||
| 65 | selectedDate.value == nextDate) { | 67 | selectedDate.value == nextDate) { |
| 66 | return Future.value(); | 68 | return Future.value(); |
| 67 | } | 69 | } |
| 70 | + _datesByPeriod[selectedPeriod.value] = selectedDate.value; | ||
| 68 | selectedPeriod.value = nextPeriod; | 71 | selectedPeriod.value = nextPeriod; |
| 69 | selectedDate.value = nextDate; | 72 | selectedDate.value = nextDate; |
| 73 | + _datesByPeriod[nextPeriod] = nextDate; | ||
| 70 | return loadReport(); | 74 | return loadReport(); |
| 71 | } | 75 | } |
| 72 | 76 | ||
| @@ -126,7 +130,11 @@ abstract class ReportPeriodLogic { | @@ -126,7 +130,11 @@ abstract class ReportPeriodLogic { | ||
| 126 | if (!shouldRefresh && selectedPeriod.value == nextPeriod) { | 130 | if (!shouldRefresh && selectedPeriod.value == nextPeriod) { |
| 127 | return Future.value(); | 131 | return Future.value(); |
| 128 | } | 132 | } |
| 133 | + _datesByPeriod[selectedPeriod.value] = selectedDate.value; | ||
| 129 | selectedPeriod.value = nextPeriod; | 134 | selectedPeriod.value = nextPeriod; |
| 135 | + selectedDate.value = _datesByPeriod[nextPeriod] ?? | ||
| 136 | + normalizeDate(nextPeriod, selectedDate.value); | ||
| 137 | + _datesByPeriod[nextPeriod] = selectedDate.value; | ||
| 130 | return loadReport(); | 138 | return loadReport(); |
| 131 | } | 139 | } |
| 132 | 140 | ||
| @@ -138,6 +146,7 @@ abstract class ReportPeriodLogic { | @@ -138,6 +146,7 @@ abstract class ReportPeriodLogic { | ||
| 138 | final nextDate = ReportDateRangeConfig.clampDate(date); | 146 | final nextDate = ReportDateRangeConfig.clampDate(date); |
| 139 | if (!shouldRefresh && selectedDate.value == nextDate) return Future.value(); | 147 | if (!shouldRefresh && selectedDate.value == nextDate) return Future.value(); |
| 140 | selectedDate.value = nextDate; | 148 | selectedDate.value = nextDate; |
| 149 | + _datesByPeriod[selectedPeriod.value] = nextDate; | ||
| 141 | return loadReport(); | 150 | return loadReport(); |
| 142 | } | 151 | } |
| 143 | 152 | ||
| @@ -153,6 +162,7 @@ abstract class ReportPeriodLogic { | @@ -153,6 +162,7 @@ abstract class ReportPeriodLogic { | ||
| 153 | ); | 162 | ); |
| 154 | if (!shouldRefresh && weekStart == nextWeekStart) return Future.value(); | 163 | if (!shouldRefresh && weekStart == nextWeekStart) return Future.value(); |
| 155 | selectedDate.value = nextWeekStart; | 164 | selectedDate.value = nextWeekStart; |
| 165 | + _datesByPeriod[selectedPeriod.value] = nextWeekStart; | ||
| 156 | return loadReport(); | 166 | return loadReport(); |
| 157 | } | 167 | } |
| 158 | 168 | ||
| @@ -169,6 +179,7 @@ abstract class ReportPeriodLogic { | @@ -169,6 +179,7 @@ abstract class ReportPeriodLogic { | ||
| 169 | ); | 179 | ); |
| 170 | if (!shouldRefresh && monthStart == nextMonth) return Future.value(); | 180 | if (!shouldRefresh && monthStart == nextMonth) return Future.value(); |
| 171 | selectedDate.value = nextMonth; | 181 | selectedDate.value = nextMonth; |
| 182 | + _datesByPeriod[selectedPeriod.value] = nextMonth; | ||
| 172 | return loadReport(); | 183 | return loadReport(); |
| 173 | } | 184 | } |
| 174 | 185 | ||
| @@ -185,6 +196,7 @@ abstract class ReportPeriodLogic { | @@ -185,6 +196,7 @@ abstract class ReportPeriodLogic { | ||
| 185 | return Future.value(); | 196 | return Future.value(); |
| 186 | } | 197 | } |
| 187 | selectedDate.value = DateTime(nextYear); | 198 | selectedDate.value = DateTime(nextYear); |
| 199 | + _datesByPeriod[selectedPeriod.value] = selectedDate.value; | ||
| 188 | return loadReport(); | 200 | return loadReport(); |
| 189 | } | 201 | } |
| 190 | 202 | ||
| @@ -212,6 +224,7 @@ abstract class ReportPeriodLogic { | @@ -212,6 +224,7 @@ abstract class ReportPeriodLogic { | ||
| 212 | final candidate = _previousCandidate; | 224 | final candidate = _previousCandidate; |
| 213 | if (_isBeforeRange(candidate)) return Future.value(); | 225 | if (_isBeforeRange(candidate)) return Future.value(); |
| 214 | selectedDate.value = candidate; | 226 | selectedDate.value = candidate; |
| 227 | + _datesByPeriod[selectedPeriod.value] = candidate; | ||
| 215 | return loadReport(); | 228 | return loadReport(); |
| 216 | } | 229 | } |
| 217 | 230 | ||
| @@ -219,6 +232,7 @@ abstract class ReportPeriodLogic { | @@ -219,6 +232,7 @@ abstract class ReportPeriodLogic { | ||
| 219 | final candidate = _nextCandidate; | 232 | final candidate = _nextCandidate; |
| 220 | if (_isAfterRange(candidate)) return Future.value(); | 233 | if (_isAfterRange(candidate)) return Future.value(); |
| 221 | selectedDate.value = candidate; | 234 | selectedDate.value = candidate; |
| 235 | + _datesByPeriod[selectedPeriod.value] = candidate; | ||
| 222 | return loadReport(); | 236 | return loadReport(); |
| 223 | } | 237 | } |
| 224 | 238 | ||
| @@ -226,6 +240,12 @@ abstract class ReportPeriodLogic { | @@ -226,6 +240,12 @@ abstract class ReportPeriodLogic { | ||
| 226 | 240 | ||
| 227 | void dispose() {} | 241 | void dispose() {} |
| 228 | 242 | ||
| 243 | + void _seedPeriodDates(DateTime date) { | ||
| 244 | + for (final period in supportedPeriods) { | ||
| 245 | + _datesByPeriod[period] = normalizeDate(period, date); | ||
| 246 | + } | ||
| 247 | + } | ||
| 248 | + | ||
| 229 | DateTime get _previousCandidate => switch (selectedPeriod.value) { | 249 | DateTime get _previousCandidate => switch (selectedPeriod.value) { |
| 230 | ReportPeriod.year => DateTime(selectedDate.value.year - 1), | 250 | ReportPeriod.year => DateTime(selectedDate.value.year - 1), |
| 231 | ReportPeriod.month => | 251 | ReportPeriod.month => |
| @@ -1205,13 +1205,6 @@ class _ChartPlotFrame extends StatelessWidget { | @@ -1205,13 +1205,6 @@ class _ChartPlotFrame extends StatelessWidget { | ||
| 1205 | ), | 1205 | ), |
| 1206 | ), | 1206 | ), |
| 1207 | ), | 1207 | ), |
| 1208 | - Padding( | ||
| 1209 | - padding: const EdgeInsets.only( | ||
| 1210 | - left: horizontalInset, | ||
| 1211 | - right: rightAxisWidth, | ||
| 1212 | - ), | ||
| 1213 | - child: child, | ||
| 1214 | - ), | ||
| 1215 | Positioned.fill( | 1208 | Positioned.fill( |
| 1216 | bottom: bottomTitleHeight, | 1209 | bottom: bottomTitleHeight, |
| 1217 | child: CustomPaint( | 1210 | child: CustomPaint( |
| @@ -1222,6 +1215,13 @@ class _ChartPlotFrame extends StatelessWidget { | @@ -1222,6 +1215,13 @@ class _ChartPlotFrame extends StatelessWidget { | ||
| 1222 | ), | 1215 | ), |
| 1223 | ), | 1216 | ), |
| 1224 | ), | 1217 | ), |
| 1218 | + Padding( | ||
| 1219 | + padding: const EdgeInsets.only( | ||
| 1220 | + left: horizontalInset, | ||
| 1221 | + right: rightAxisWidth, | ||
| 1222 | + ), | ||
| 1223 | + child: child, | ||
| 1224 | + ), | ||
| 1225 | Positioned.fill( | 1225 | Positioned.fill( |
| 1226 | bottom: bottomTitleHeight, | 1226 | bottom: bottomTitleHeight, |
| 1227 | child: _RightAxisLabels( | 1227 | child: _RightAxisLabels( |
| 1 | class R { | 1 | class R { |
| 2 | static final String assetsImagesNavBackIcon = | 2 | static final String assetsImagesNavBackIcon = |
| 3 | 'assets/images/common/ic_nav_back.webp'; | 3 | 'assets/images/common/ic_nav_back.webp'; |
| 4 | + static final String assetsImagesCloseIcon = | ||
| 5 | + 'assets/images/common/ic_close.png'; | ||
| 4 | static final String assetsImagesHealthGoodArrow = | 6 | static final String assetsImagesHealthGoodArrow = |
| 5 | 'assets/images/common/ic_health_good_arrow.webp'; | 7 | 'assets/images/common/ic_health_good_arrow.webp'; |
| 6 | static final String assetsImagesHealthBadArrow = | 8 | static final String assetsImagesHealthBadArrow = |
-
Please register or login to post a comment