Showing
12 changed files
with
266 additions
and
50 deletions
| @@ -202,7 +202,7 @@ class _ProfileHeader extends StatelessWidget { | @@ -202,7 +202,7 @@ class _ProfileHeader extends StatelessWidget { | ||
| 202 | ), | 202 | ), |
| 203 | const SizedBox(height: 1), | 203 | const SizedBox(height: 1), |
| 204 | Text( | 204 | Text( |
| 205 | - 'ID:${user?.id ?? 0}', | 205 | + 'ID:${user?.uniqueCode ?? ''}', |
| 206 | maxLines: 1, | 206 | maxLines: 1, |
| 207 | overflow: TextOverflow.ellipsis, | 207 | overflow: TextOverflow.ellipsis, |
| 208 | style: TextStyle( | 208 | style: TextStyle( |
| @@ -239,12 +239,7 @@ class TodayTabBody extends StatelessWidget { | @@ -239,12 +239,7 @@ class TodayTabBody extends StatelessWidget { | ||
| 239 | controller, | 239 | controller, |
| 240 | )); | 240 | )); |
| 241 | }), | 241 | }), |
| 242 | - Obx(() => controller.showHrvAdBanner.value | ||
| 243 | - ? TodayHrvAdBanner(controller: controller) | ||
| 244 | - : const SizedBox.shrink()), | ||
| 245 | - Obx(() => controller.showPartnerAdBanner.value | ||
| 246 | - ? TodayPartnerAdBanner(controller: controller) | ||
| 247 | - : const SizedBox.shrink()), | 242 | + TodayAdCarousel(controller: controller), |
| 248 | TodayHrvNumberCard(controller: controller), | 243 | TodayHrvNumberCard(controller: controller), |
| 249 | const SizedBox(height: 12), | 244 | const SizedBox(height: 12), |
| 250 | TodayHrvChartCard(controller: controller), | 245 | TodayHrvChartCard(controller: controller), |
| @@ -286,7 +281,8 @@ class TodayTabBody extends StatelessWidget { | @@ -286,7 +281,8 @@ class TodayTabBody extends StatelessWidget { | ||
| 286 | return GestureDetector( | 281 | return GestureDetector( |
| 287 | onTap: showHrvMeasurementBottomSheet, | 282 | onTap: showHrvMeasurementBottomSheet, |
| 288 | child: Container( | 283 | child: Container( |
| 289 | - padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), | 284 | + margin: EdgeInsets.only(top: 6), |
| 285 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), | ||
| 290 | decoration: BoxDecoration( | 286 | decoration: BoxDecoration( |
| 291 | color: Colors.white, | 287 | color: Colors.white, |
| 292 | borderRadius: BorderRadius.circular(16), | 288 | borderRadius: BorderRadius.circular(16), |
| @@ -99,7 +99,7 @@ class DfTabBar extends StatelessWidget { | @@ -99,7 +99,7 @@ class DfTabBar extends StatelessWidget { | ||
| 99 | height: 24, | 99 | height: 24, |
| 100 | gaplessPlayback: true, | 100 | gaplessPlayback: true, |
| 101 | ), | 101 | ), |
| 102 | - const SizedBox(height: 2), | 102 | + const SizedBox(height: 0.5), |
| 103 | Text( | 103 | Text( |
| 104 | tabLabels[i], | 104 | tabLabels[i], |
| 105 | style: TextStyle( | 105 | style: TextStyle( |
| 1 | +import 'dart:async'; | ||
| 1 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 2 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 2 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 3 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 3 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 4 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| @@ -8,9 +9,14 @@ import 'package:get/get.dart'; | @@ -8,9 +9,14 @@ import 'package:get/get.dart'; | ||
| 8 | import '../../controllers/today_controller.dart'; | 9 | import '../../controllers/today_controller.dart'; |
| 9 | 10 | ||
| 10 | class TodayHrvAdBanner extends StatelessWidget { | 11 | class TodayHrvAdBanner extends StatelessWidget { |
| 11 | - const TodayHrvAdBanner({super.key, required this.controller}); | 12 | + const TodayHrvAdBanner({ |
| 13 | + super.key, | ||
| 14 | + required this.controller, | ||
| 15 | + this.showMargin = true, | ||
| 16 | + }); | ||
| 12 | 17 | ||
| 13 | final TodayController controller; | 18 | final TodayController controller; |
| 19 | + final bool showMargin; | ||
| 14 | 20 | ||
| 15 | @override | 21 | @override |
| 16 | Widget build(BuildContext context) { | 22 | Widget build(BuildContext context) { |
| @@ -29,7 +35,7 @@ class TodayHrvAdBanner extends StatelessWidget { | @@ -29,7 +35,7 @@ class TodayHrvAdBanner extends StatelessWidget { | ||
| 29 | subtitle: context.l10n.stayOnTopOfYourHealthFluctuations, | 35 | subtitle: context.l10n.stayOnTopOfYourHealthFluctuations, |
| 30 | right: Container( | 36 | right: Container( |
| 31 | padding: const EdgeInsets.all(4), | 37 | padding: const EdgeInsets.all(4), |
| 32 | - decoration: BoxDecoration( | 38 | + decoration: const BoxDecoration( |
| 33 | color: Colors.white, | 39 | color: Colors.white, |
| 34 | shape: BoxShape.circle, | 40 | shape: BoxShape.circle, |
| 35 | ), | 41 | ), |
| @@ -42,6 +48,7 @@ class TodayHrvAdBanner extends StatelessWidget { | @@ -42,6 +48,7 @@ class TodayHrvAdBanner extends StatelessWidget { | ||
| 42 | ), | 48 | ), |
| 43 | leftImagePath: 'assets/images/today/ic_banner_watch_hrv_theme.png', | 49 | leftImagePath: 'assets/images/today/ic_banner_watch_hrv_theme.png', |
| 44 | onClose: controller.dismissHrvAdBanner, | 50 | onClose: controller.dismissHrvAdBanner, |
| 51 | + showMargin: showMargin, | ||
| 45 | ), | 52 | ), |
| 46 | ); | 53 | ); |
| 47 | }); | 54 | }); |
| @@ -49,9 +56,14 @@ class TodayHrvAdBanner extends StatelessWidget { | @@ -49,9 +56,14 @@ class TodayHrvAdBanner extends StatelessWidget { | ||
| 49 | } | 56 | } |
| 50 | 57 | ||
| 51 | class TodayPartnerAdBanner extends StatelessWidget { | 58 | class TodayPartnerAdBanner extends StatelessWidget { |
| 52 | - const TodayPartnerAdBanner({super.key, required this.controller}); | 59 | + const TodayPartnerAdBanner({ |
| 60 | + super.key, | ||
| 61 | + required this.controller, | ||
| 62 | + this.showMargin = true, | ||
| 63 | + }); | ||
| 53 | 64 | ||
| 54 | final TodayController controller; | 65 | final TodayController controller; |
| 66 | + final bool showMargin; | ||
| 55 | 67 | ||
| 56 | @override | 68 | @override |
| 57 | Widget build(BuildContext context) { | 69 | Widget build(BuildContext context) { |
| @@ -92,6 +104,7 @@ class TodayPartnerAdBanner extends StatelessWidget { | @@ -92,6 +104,7 @@ class TodayPartnerAdBanner extends StatelessWidget { | ||
| 92 | ), | 104 | ), |
| 93 | leftImagePath: 'assets/images/today/ic_banner_add_contact.png', | 105 | leftImagePath: 'assets/images/today/ic_banner_add_contact.png', |
| 94 | onClose: controller.dismissPartnerAdBanner, | 106 | onClose: controller.dismissPartnerAdBanner, |
| 107 | + showMargin: showMargin, | ||
| 95 | ), | 108 | ), |
| 96 | ); | 109 | ); |
| 97 | }); | 110 | }); |
| @@ -105,6 +118,7 @@ class _TodayGuideBanner extends StatelessWidget { | @@ -105,6 +118,7 @@ class _TodayGuideBanner extends StatelessWidget { | ||
| 105 | required this.onClose, | 118 | required this.onClose, |
| 106 | this.right, | 119 | this.right, |
| 107 | required this.leftImagePath, | 120 | required this.leftImagePath, |
| 121 | + this.showMargin = true, | ||
| 108 | }); | 122 | }); |
| 109 | 123 | ||
| 110 | final String title; | 124 | final String title; |
| @@ -112,6 +126,7 @@ class _TodayGuideBanner extends StatelessWidget { | @@ -112,6 +126,7 @@ class _TodayGuideBanner extends StatelessWidget { | ||
| 112 | final VoidCallback onClose; | 126 | final VoidCallback onClose; |
| 113 | final Widget? right; | 127 | final Widget? right; |
| 114 | final String leftImagePath; | 128 | final String leftImagePath; |
| 129 | + final bool showMargin; | ||
| 115 | 130 | ||
| 116 | @override | 131 | @override |
| 117 | Widget build(BuildContext context) { | 132 | Widget build(BuildContext context) { |
| @@ -119,7 +134,7 @@ class _TodayGuideBanner extends StatelessWidget { | @@ -119,7 +134,7 @@ class _TodayGuideBanner extends StatelessWidget { | ||
| 119 | 134 | ||
| 120 | return Container( | 135 | return Container( |
| 121 | height: 65, | 136 | height: 65, |
| 122 | - margin: const EdgeInsets.only(bottom: 12), | 137 | + margin: showMargin ? const EdgeInsets.only(bottom: 12) : EdgeInsets.zero, |
| 123 | decoration: BoxDecoration( | 138 | decoration: BoxDecoration( |
| 124 | color: colors.brandBackgroundLight, | 139 | color: colors.brandBackgroundLight, |
| 125 | borderRadius: BorderRadius.circular(16), | 140 | borderRadius: BorderRadius.circular(16), |
| @@ -188,3 +203,137 @@ class _TodayGuideBanner extends StatelessWidget { | @@ -188,3 +203,137 @@ class _TodayGuideBanner extends StatelessWidget { | ||
| 188 | ); | 203 | ); |
| 189 | } | 204 | } |
| 190 | } | 205 | } |
| 206 | + | ||
| 207 | +class TodayAdCarousel extends StatelessWidget { | ||
| 208 | + const TodayAdCarousel({super.key, required this.controller}); | ||
| 209 | + | ||
| 210 | + final TodayController controller; | ||
| 211 | + | ||
| 212 | + @override | ||
| 213 | + Widget build(BuildContext context) { | ||
| 214 | + return Obx(() { | ||
| 215 | + final showHrv = controller.showHrvAdBanner.value; | ||
| 216 | + final showPartner = controller.showPartnerAdBanner.value && | ||
| 217 | + !controller.userStateService.isBound; | ||
| 218 | + | ||
| 219 | + final activeBanners = <Widget>[]; | ||
| 220 | + if (showHrv) { | ||
| 221 | + activeBanners.add( | ||
| 222 | + TodayHrvAdBanner( | ||
| 223 | + controller: controller, | ||
| 224 | + showMargin: false, | ||
| 225 | + ), | ||
| 226 | + ); | ||
| 227 | + } | ||
| 228 | + if (showPartner) { | ||
| 229 | + activeBanners.add( | ||
| 230 | + TodayPartnerAdBanner( | ||
| 231 | + controller: controller, | ||
| 232 | + showMargin: false, | ||
| 233 | + ), | ||
| 234 | + ); | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + if (activeBanners.isEmpty) { | ||
| 238 | + return const SizedBox.shrink(); | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + if (activeBanners.length == 1) { | ||
| 242 | + return Padding( | ||
| 243 | + padding: const EdgeInsets.only(bottom: 12), | ||
| 244 | + child: activeBanners.first, | ||
| 245 | + ); | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + return Container( | ||
| 249 | + height: 65, | ||
| 250 | + margin: const EdgeInsets.only(bottom: 12), | ||
| 251 | + child: _BannerSlider(items: activeBanners), | ||
| 252 | + ); | ||
| 253 | + }); | ||
| 254 | + } | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +class _BannerSlider extends StatefulWidget { | ||
| 258 | + const _BannerSlider({required this.items}); | ||
| 259 | + final List<Widget> items; | ||
| 260 | + | ||
| 261 | + @override | ||
| 262 | + State<_BannerSlider> createState() => _BannerSliderState(); | ||
| 263 | +} | ||
| 264 | + | ||
| 265 | +class _BannerSliderState extends State<_BannerSlider> { | ||
| 266 | + late PageController _pageController; | ||
| 267 | + Timer? _autoScrollTimer; | ||
| 268 | + bool _userScrolling = false; | ||
| 269 | + | ||
| 270 | + int get _realCount => widget.items.length; | ||
| 271 | + int get _virtualCount => _realCount * 10000; | ||
| 272 | + int get _initialPage => _virtualCount ~/ 2; | ||
| 273 | + | ||
| 274 | + @override | ||
| 275 | + void initState() { | ||
| 276 | + super.initState(); | ||
| 277 | + _pageController = PageController(initialPage: _initialPage); | ||
| 278 | + WidgetsBinding.instance.addPostFrameCallback((_) { | ||
| 279 | + if (mounted) { | ||
| 280 | + _startAutoScroll(); | ||
| 281 | + } | ||
| 282 | + }); | ||
| 283 | + } | ||
| 284 | + | ||
| 285 | + void _startAutoScroll() { | ||
| 286 | + _autoScrollTimer?.cancel(); | ||
| 287 | + _autoScrollTimer = Timer.periodic(const Duration(seconds: 4), (_) { | ||
| 288 | + if (!mounted || _userScrolling) return; | ||
| 289 | + if (_pageController.hasClients) { | ||
| 290 | + final next = (_pageController.page?.round() ?? _initialPage) + 1; | ||
| 291 | + _pageController.animateToPage( | ||
| 292 | + next, | ||
| 293 | + duration: const Duration(milliseconds: 500), | ||
| 294 | + curve: Curves.easeInOut, | ||
| 295 | + ); | ||
| 296 | + } | ||
| 297 | + }); | ||
| 298 | + } | ||
| 299 | + | ||
| 300 | + void _stopAutoScroll() { | ||
| 301 | + _autoScrollTimer?.cancel(); | ||
| 302 | + _autoScrollTimer = null; | ||
| 303 | + } | ||
| 304 | + | ||
| 305 | + @override | ||
| 306 | + void didUpdateWidget(covariant _BannerSlider oldWidget) { | ||
| 307 | + super.didUpdateWidget(oldWidget); | ||
| 308 | + if (widget.items.length != oldWidget.items.length) { | ||
| 309 | + _stopAutoScroll(); | ||
| 310 | + _pageController.dispose(); | ||
| 311 | + _pageController = PageController(initialPage: _initialPage); | ||
| 312 | + _startAutoScroll(); | ||
| 313 | + } | ||
| 314 | + } | ||
| 315 | + | ||
| 316 | + @override | ||
| 317 | + void dispose() { | ||
| 318 | + _stopAutoScroll(); | ||
| 319 | + _pageController.dispose(); | ||
| 320 | + super.dispose(); | ||
| 321 | + } | ||
| 322 | + | ||
| 323 | + @override | ||
| 324 | + Widget build(BuildContext context) { | ||
| 325 | + return Listener( | ||
| 326 | + onPointerDown: (_) => _userScrolling = true, | ||
| 327 | + onPointerUp: (_) => _userScrolling = false, | ||
| 328 | + onPointerCancel: (_) => _userScrolling = false, | ||
| 329 | + child: PageView.builder( | ||
| 330 | + controller: _pageController, | ||
| 331 | + itemCount: _virtualCount, | ||
| 332 | + itemBuilder: (context, index) { | ||
| 333 | + final realIndex = index % _realCount; | ||
| 334 | + return widget.items[realIndex]; | ||
| 335 | + }, | ||
| 336 | + ), | ||
| 337 | + ); | ||
| 338 | + } | ||
| 339 | +} |
| @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 7 | import 'package:fl_chart/fl_chart.dart'; | 7 | import 'package:fl_chart/fl_chart.dart'; |
| 8 | import 'package:flutter/material.dart'; | 8 | import 'package:flutter/material.dart'; |
| 9 | import 'package:get/get.dart'; | 9 | import 'package:get/get.dart'; |
| 10 | +import 'package:lottie/lottie.dart'; | ||
| 10 | 11 | ||
| 11 | import '../../controllers/today_controller.dart'; | 12 | import '../../controllers/today_controller.dart'; |
| 12 | import 'hrv_principle_explanation_bottom_sheet.dart'; | 13 | import 'hrv_principle_explanation_bottom_sheet.dart'; |
| @@ -47,7 +48,7 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -47,7 +48,7 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 47 | }; | 48 | }; |
| 48 | 49 | ||
| 49 | return Container( | 50 | return Container( |
| 50 | - padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), | 51 | + padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), |
| 51 | decoration: BoxDecoration( | 52 | decoration: BoxDecoration( |
| 52 | color: Colors.white, | 53 | color: Colors.white, |
| 53 | borderRadius: BorderRadius.circular(16), | 54 | borderRadius: BorderRadius.circular(16), |
| @@ -59,16 +60,26 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -59,16 +60,26 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 59 | const SizedBox(height: 14), | 60 | const SizedBox(height: 14), |
| 60 | SizedBox( | 61 | SizedBox( |
| 61 | height: 174, | 62 | height: 174, |
| 62 | - child: hrvSpots.isEmpty | ||
| 63 | - ? _EmptyChart(text: context.l10n.noDataAvailableForToday) | ||
| 64 | - : _HrvLineChart( | ||
| 65 | - spots: hrvSpots, | ||
| 66 | - stateMap: hrvStateMap, | ||
| 67 | - maxSeconds: _maxChartSeconds(), | ||
| 68 | - getColorByState: _getColorByState, | ||
| 69 | - getTooltipChildren: _getTooltipChildren, | ||
| 70 | - timeLabel: _timeLabel, | ||
| 71 | - ), | 63 | + child: controller.isLoadingToday.value |
| 64 | + ? Center( | ||
| 65 | + child: Lottie.asset( | ||
| 66 | + 'assets/lottie/loading.json', | ||
| 67 | + width: 130, | ||
| 68 | + height: 90, | ||
| 69 | + repeat: true, | ||
| 70 | + animate: true, | ||
| 71 | + ), | ||
| 72 | + ) | ||
| 73 | + : hrvSpots.isEmpty | ||
| 74 | + ? _EmptyChart(text: context.l10n.noDataAvailableForToday) | ||
| 75 | + : _HrvLineChart( | ||
| 76 | + spots: hrvSpots, | ||
| 77 | + stateMap: hrvStateMap, | ||
| 78 | + maxSeconds: _maxChartSeconds(), | ||
| 79 | + getColorByState: _getColorByState, | ||
| 80 | + getTooltipChildren: _getTooltipChildren, | ||
| 81 | + timeLabel: _timeLabel, | ||
| 82 | + ), | ||
| 72 | ), | 83 | ), |
| 73 | Padding( | 84 | Padding( |
| 74 | padding: const EdgeInsets.only(bottom: 14, top: 16), | 85 | padding: const EdgeInsets.only(bottom: 14, top: 16), |
| @@ -116,13 +127,23 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -116,13 +127,23 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 116 | ), | 127 | ), |
| 117 | SizedBox( | 128 | SizedBox( |
| 118 | height: 190, | 129 | height: 190, |
| 119 | - child: _buildStressChart( | ||
| 120 | - context, | ||
| 121 | - sleepList, | ||
| 122 | - stressPoints, | ||
| 123 | - selectedDate, | ||
| 124 | - vipInfo?.isVip == true, | ||
| 125 | - ), | 130 | + child: controller.isLoadingToday.value |
| 131 | + ? Center( | ||
| 132 | + child: Lottie.asset( | ||
| 133 | + 'assets/lottie/loading.json', | ||
| 134 | + width: 130, | ||
| 135 | + height: 90, | ||
| 136 | + repeat: true, | ||
| 137 | + animate: true, | ||
| 138 | + ), | ||
| 139 | + ) | ||
| 140 | + : _buildStressChart( | ||
| 141 | + context, | ||
| 142 | + sleepList, | ||
| 143 | + stressPoints, | ||
| 144 | + selectedDate, | ||
| 145 | + vipInfo?.isVip == true, | ||
| 146 | + ), | ||
| 126 | ), | 147 | ), |
| 127 | ], | 148 | ], |
| 128 | ), | 149 | ), |
| @@ -139,14 +160,14 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -139,14 +160,14 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 139 | Builder(builder: (context) { | 160 | Builder(builder: (context) { |
| 140 | final s = (sameDay | 161 | final s = (sameDay |
| 141 | ? context.l10n.todaySHrvTrend | 162 | ? context.l10n.todaySHrvTrend |
| 142 | - : context.l10n.averageHrvForTheDay); | 163 | + : context.l10n.hrvTrendForThatDay); |
| 143 | return Text( | 164 | return Text( |
| 144 | (controller.isFriend | 165 | (controller.isFriend |
| 145 | ? context.l10n.reportOtherPossessiveTitle(s) | 166 | ? context.l10n.reportOtherPossessiveTitle(s) |
| 146 | : s), | 167 | : s), |
| 147 | style: TextStyle( | 168 | style: TextStyle( |
| 148 | color: context.colors.textPrimary, | 169 | color: context.colors.textPrimary, |
| 149 | - fontSize: 16, | 170 | + fontSize: 18, |
| 150 | fontWeight: FontWeight.w600, | 171 | fontWeight: FontWeight.w600, |
| 151 | ), | 172 | ), |
| 152 | ); | 173 | ); |
| @@ -363,7 +384,7 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -363,7 +384,7 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 363 | List<TextSpan>? _getTooltip(BarChartRodData rod, int x, int? state) { | 384 | List<TextSpan>? _getTooltip(BarChartRodData rod, int x, int? state) { |
| 364 | return [ | 385 | return [ |
| 365 | TextSpan( | 386 | TextSpan( |
| 366 | - text: '○ ${_getStatusByState(state)}', | 387 | + text: '○ ${_getStatusByState(state)} ${_formatNumber(rod.toY)}%', |
| 367 | style: TextStyle( | 388 | style: TextStyle( |
| 368 | color: _getColorByState(state), | 389 | color: _getColorByState(state), |
| 369 | fontWeight: FontWeight.bold, | 390 | fontWeight: FontWeight.bold, |
| @@ -377,8 +398,7 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -377,8 +398,7 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 377 | ), | 398 | ), |
| 378 | ), | 399 | ), |
| 379 | TextSpan( | 400 | TextSpan( |
| 380 | - text: | ||
| 381 | - '♥ ${l10n.pressure} ${_formatNumber(rod.toY)} · ${_formatHour(x.toDouble())}', | 401 | + text: '♥ ${l10n.pressure} ${_formatHour(x.toDouble())}', |
| 382 | style: const TextStyle( | 402 | style: const TextStyle( |
| 383 | color: Color(0xFF78787D), | 403 | color: Color(0xFF78787D), |
| 384 | fontWeight: FontWeight.bold, | 404 | fontWeight: FontWeight.bold, |
| @@ -510,13 +530,18 @@ class _HrvLineChartState extends State<_HrvLineChart> { | @@ -510,13 +530,18 @@ class _HrvLineChartState extends State<_HrvLineChart> { | ||
| 510 | showTitles: true, | 530 | showTitles: true, |
| 511 | reservedSize: 20, | 531 | reservedSize: 20, |
| 512 | interval: 6 * 3600, | 532 | interval: 6 * 3600, |
| 513 | - getTitlesWidget: (val, _) { | 533 | + getTitlesWidget: (val, meta) { |
| 514 | final label = widget.timeLabel(val); | 534 | final label = widget.timeLabel(val); |
| 515 | if (label == null) return const SizedBox.shrink(); | 535 | if (label == null) return const SizedBox.shrink(); |
| 516 | - return Text( | ||
| 517 | - label, | ||
| 518 | - style: TextStyle( | ||
| 519 | - fontSize: 10, color: context.colors.textTertiary), | 536 | + return SideTitleWidget( |
| 537 | + meta: meta, | ||
| 538 | + fitInside: SideTitleFitInsideData.fromTitleMeta(meta, | ||
| 539 | + distanceFromEdge: 0), | ||
| 540 | + child: Text( | ||
| 541 | + label, | ||
| 542 | + style: TextStyle( | ||
| 543 | + fontSize: 10, color: context.colors.textTertiary), | ||
| 544 | + ), | ||
| 520 | ); | 545 | ); |
| 521 | }, | 546 | }, |
| 522 | ), | 547 | ), |
| @@ -679,12 +704,11 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -679,12 +704,11 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 679 | const rightAxisWidth = 22.0; | 704 | const rightAxisWidth = 22.0; |
| 680 | const slotSec = 6 * 60; | 705 | const slotSec = 6 * 60; |
| 681 | final slotCount = (widget.maxSeconds / slotSec).ceil().clamp(1, 9999); | 706 | final slotCount = (widget.maxSeconds / slotSec).ceil().clamp(1, 9999); |
| 682 | - const double groupsSpace = 0.5; // 设置间隔为 1 像素 | 707 | + const double groupsSpace = 0; // 设置间隔为 1 像素 |
| 683 | final barWidth = (constraints.maxWidth - | 708 | final barWidth = (constraints.maxWidth - |
| 684 | rightAxisWidth - | 709 | rightAxisWidth - |
| 685 | (groupsSpace * (slotCount - 1))) / | 710 | (groupsSpace * (slotCount - 1))) / |
| 686 | slotCount; | 711 | slotCount; |
| 687 | - | ||
| 688 | // 根据 V2SleepTimeRange 计算多段睡眠区间的 Positioned widgets | 712 | // 根据 V2SleepTimeRange 计算多段睡眠区间的 Positioned widgets |
| 689 | final sleepWidgets = <Widget>[]; | 713 | final sleepWidgets = <Widget>[]; |
| 690 | 714 | ||
| @@ -866,7 +890,7 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -866,7 +890,7 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 866 | sideTitles: SideTitles( | 890 | sideTitles: SideTitles( |
| 867 | showTitles: true, | 891 | showTitles: true, |
| 868 | reservedSize: 22, | 892 | reservedSize: 22, |
| 869 | - interval: 60, | 893 | + interval: 6 * 3600, |
| 870 | getTitlesWidget: (val, meta) { | 894 | getTitlesWidget: (val, meta) { |
| 871 | final label = widget.timeLabel(val); | 895 | final label = widget.timeLabel(val); |
| 872 | if (label == null) { | 896 | if (label == null) { |
| @@ -874,6 +898,9 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -874,6 +898,9 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 874 | } | 898 | } |
| 875 | return SideTitleWidget( | 899 | return SideTitleWidget( |
| 876 | meta: meta, | 900 | meta: meta, |
| 901 | + fitInside: SideTitleFitInsideData.fromTitleMeta( | ||
| 902 | + meta, | ||
| 903 | + distanceFromEdge: 0), | ||
| 877 | child: Text( | 904 | child: Text( |
| 878 | label, | 905 | label, |
| 879 | style: TextStyle( | 906 | style: TextStyle( |
| @@ -932,7 +959,9 @@ class _StressBarChartState extends State<_StressBarChart> { | @@ -932,7 +959,9 @@ class _StressBarChartState extends State<_StressBarChart> { | ||
| 932 | bottom: 5, | 959 | bottom: 5, |
| 933 | ), | 960 | ), |
| 934 | getTooltipItem: (group, groupIndex, rod, rodIndex) { | 961 | getTooltipItem: (group, groupIndex, rod, rodIndex) { |
| 935 | - final state = slotStateMap[group.x]?.state; | 962 | + final entry = slotStateMap[group.x]; |
| 963 | + if (entry == null) return null; | ||
| 964 | + final state = entry.state; | ||
| 936 | return BarTooltipItem( | 965 | return BarTooltipItem( |
| 937 | '', | 966 | '', |
| 938 | const TextStyle( | 967 | const TextStyle( |
| @@ -48,8 +48,22 @@ class LoginController extends GetxController { | @@ -48,8 +48,22 @@ class LoginController extends GetxController { | ||
| 48 | 48 | ||
| 49 | AppleSignInModel? appleSignInModel; | 49 | AppleSignInModel? appleSignInModel; |
| 50 | 50 | ||
| 51 | - void toggleTermsChecked() { | ||
| 52 | - termsChecked.value = !termsChecked.value; | 51 | + Future<void> toggleTermsChecked() async { |
| 52 | + final newValue = !termsChecked.value; | ||
| 53 | + termsChecked.value = newValue; | ||
| 54 | + try { | ||
| 55 | + final local = Get.find<LocalStorage>(); | ||
| 56 | + await local.setTermsAgreed(newValue); | ||
| 57 | + if (newValue && !_userStateService.sdksInitialized) { | ||
| 58 | + await _userStateService.initializeSdks(); | ||
| 59 | + } | ||
| 60 | + if (needTrackWelcomepage) { | ||
| 61 | + ta.track('enter_doublefeel_welcome_page'); | ||
| 62 | + needTrackWelcomepage = false; | ||
| 63 | + } | ||
| 64 | + } catch (e) { | ||
| 65 | + AppLogger.e(e.toString()); | ||
| 66 | + } | ||
| 53 | } | 67 | } |
| 54 | 68 | ||
| 55 | void onPhoneLoginPressed() { | 69 | void onPhoneLoginPressed() { |
| @@ -104,6 +118,10 @@ class LoginController extends GetxController { | @@ -104,6 +118,10 @@ class LoginController extends GetxController { | ||
| 104 | @override | 118 | @override |
| 105 | void onInit() { | 119 | void onInit() { |
| 106 | super.onInit(); | 120 | super.onInit(); |
| 121 | + LocalStorage.open().then((local) { | ||
| 122 | + termsChecked.value = local.termsAgreed; | ||
| 123 | + }); | ||
| 124 | + | ||
| 107 | phoneController.addListener(() { | 125 | phoneController.addListener(() { |
| 108 | phoneInput.value = phoneController.text; | 126 | phoneInput.value = phoneController.text; |
| 109 | }); | 127 | }); |
| @@ -124,9 +142,15 @@ class LoginController extends GetxController { | @@ -124,9 +142,15 @@ class LoginController extends GetxController { | ||
| 124 | @override | 142 | @override |
| 125 | void onReady() { | 143 | void onReady() { |
| 126 | super.onReady(); | 144 | super.onReady(); |
| 127 | - ta.track('enter_doublefeel_welcome_page'); | 145 | + if (_userStateService.sdksInitialized) { |
| 146 | + ta.track('enter_doublefeel_welcome_page'); | ||
| 147 | + } else { | ||
| 148 | + needTrackWelcomepage = true; | ||
| 149 | + } | ||
| 128 | } | 150 | } |
| 129 | 151 | ||
| 152 | + bool needTrackWelcomepage = false; | ||
| 153 | + | ||
| 130 | String get cleanPhone => phoneInput.value.replaceAll(' ', ''); | 154 | String get cleanPhone => phoneInput.value.replaceAll(' ', ''); |
| 131 | 155 | ||
| 132 | bool get canRequestCode => | 156 | bool get canRequestCode => |
| @@ -21,10 +21,14 @@ class UserStateService { | @@ -21,10 +21,14 @@ class UserStateService { | ||
| 21 | final PushService _pushService; | 21 | final PushService _pushService; |
| 22 | final ThinkingDataService _thinkingDataService; | 22 | final ThinkingDataService _thinkingDataService; |
| 23 | 23 | ||
| 24 | + bool sdksInitialized = false; | ||
| 25 | + | ||
| 24 | /// Called after the user accepts terms (cold start, not logged in yet). | 26 | /// Called after the user accepts terms (cold start, not logged in yet). |
| 25 | Future<void> initializeSdks() async { | 27 | Future<void> initializeSdks() async { |
| 28 | + if (sdksInitialized) return; | ||
| 26 | await _thinkingDataService.init(); | 29 | await _thinkingDataService.init(); |
| 27 | await _imService.init(); | 30 | await _imService.init(); |
| 31 | + sdksInitialized = true; | ||
| 28 | } | 32 | } |
| 29 | 33 | ||
| 30 | Future<void> onLogin() async { | 34 | Future<void> onLogin() async { |
| @@ -561,5 +561,6 @@ | @@ -561,5 +561,6 @@ | ||
| 561 | }, | 561 | }, |
| 562 | "freeRedemptionOffer": "Free Redemption Offer", | 562 | "freeRedemptionOffer": "Free Redemption Offer", |
| 563 | "cellPhoneNumber": "Cell phone number", | 563 | "cellPhoneNumber": "Cell phone number", |
| 564 | - "todayOnWeeklyCalendar": "today" | 564 | + "todayOnWeeklyCalendar": "today", |
| 565 | + "hrvTrendForThatDay": "HRV Trend for That Day" | ||
| 565 | } | 566 | } |
| @@ -849,5 +849,6 @@ | @@ -849,5 +849,6 @@ | ||
| 849 | }, | 849 | }, |
| 850 | "freeRedemptionOffer": "免费兑换优惠", | 850 | "freeRedemptionOffer": "免费兑换优惠", |
| 851 | "cellPhoneNumber": "手机号", | 851 | "cellPhoneNumber": "手机号", |
| 852 | - "todayOnWeeklyCalendar": "今" | 852 | + "todayOnWeeklyCalendar": "今", |
| 853 | + "hrvTrendForThatDay": "该日HRV趋势" | ||
| 853 | } | 854 | } |
| @@ -3326,6 +3326,12 @@ abstract class AppLocalizations { | @@ -3326,6 +3326,12 @@ abstract class AppLocalizations { | ||
| 3326 | /// In zh, this message translates to: | 3326 | /// In zh, this message translates to: |
| 3327 | /// **'今'** | 3327 | /// **'今'** |
| 3328 | String get todayOnWeeklyCalendar; | 3328 | String get todayOnWeeklyCalendar; |
| 3329 | + | ||
| 3330 | + /// No description provided for @hrvTrendForThatDay. | ||
| 3331 | + /// | ||
| 3332 | + /// In zh, this message translates to: | ||
| 3333 | + /// **'该日HRV趋势'** | ||
| 3334 | + String get hrvTrendForThatDay; | ||
| 3329 | } | 3335 | } |
| 3330 | 3336 | ||
| 3331 | class _AppLocalizationsDelegate | 3337 | class _AppLocalizationsDelegate |
| @@ -1853,4 +1853,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1853,4 +1853,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1853 | 1853 | ||
| 1854 | @override | 1854 | @override |
| 1855 | String get todayOnWeeklyCalendar => 'today'; | 1855 | String get todayOnWeeklyCalendar => 'today'; |
| 1856 | + | ||
| 1857 | + @override | ||
| 1858 | + String get hrvTrendForThatDay => 'HRV Trend for That Day'; | ||
| 1856 | } | 1859 | } |
| @@ -1754,4 +1754,7 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1754,4 +1754,7 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1754 | 1754 | ||
| 1755 | @override | 1755 | @override |
| 1756 | String get todayOnWeeklyCalendar => '今'; | 1756 | String get todayOnWeeklyCalendar => '今'; |
| 1757 | + | ||
| 1758 | + @override | ||
| 1759 | + String get hrvTrendForThatDay => '该日HRV趋势'; | ||
| 1757 | } | 1760 | } |
-
Please register or login to post a comment