Showing
8 changed files
with
146 additions
and
8 deletions
| @@ -104,8 +104,11 @@ class TodayController extends GetMaterialController { | @@ -104,8 +104,11 @@ class TodayController extends GetMaterialController { | ||
| 104 | // ── HRV 表盘引导 Banner ─────────────────── | 104 | // ── HRV 表盘引导 Banner ─────────────────── |
| 105 | final showHrvAdBanner = false.obs; | 105 | final showHrvAdBanner = false.obs; |
| 106 | final showPartnerAdBanner = false.obs; | 106 | final showPartnerAdBanner = false.obs; |
| 107 | + final showNotificationAuthorizationBanner = false.obs; | ||
| 107 | 108 | ||
| 108 | void dismissHrvAdBanner() => showHrvAdBanner.value = false; | 109 | void dismissHrvAdBanner() => showHrvAdBanner.value = false; |
| 110 | + void dismissNotificationAuthorizationBanner() => | ||
| 111 | + showNotificationAuthorizationBanner.value = false; | ||
| 109 | 112 | ||
| 110 | Future<void> dismissPartnerAdBanner() async { | 113 | Future<void> dismissPartnerAdBanner() async { |
| 111 | final userId = | 114 | final userId = |
| @@ -160,6 +163,7 @@ class TodayController extends GetMaterialController { | @@ -160,6 +163,7 @@ class TodayController extends GetMaterialController { | ||
| 160 | checkAddFriendVisible(); | 163 | checkAddFriendVisible(); |
| 161 | checkHrvAdBannerVisible(); | 164 | checkHrvAdBannerVisible(); |
| 162 | checkHealthDataAuthCardVisible(); | 165 | checkHealthDataAuthCardVisible(); |
| 166 | + checkNotificationAuthorizationBannerVisible(); | ||
| 163 | checkNotificationCardVisible(); | 167 | checkNotificationCardVisible(); |
| 164 | if (!Get.find<UserStateService>().isVip) { | 168 | if (!Get.find<UserStateService>().isVip) { |
| 165 | getProductList(); | 169 | getProductList(); |
| @@ -196,6 +200,24 @@ class TodayController extends GetMaterialController { | @@ -196,6 +200,24 @@ class TodayController extends GetMaterialController { | ||
| 196 | } | 200 | } |
| 197 | } | 201 | } |
| 198 | 202 | ||
| 203 | + Future<void> checkNotificationAuthorizationBannerVisible() async { | ||
| 204 | + if (isFriend) { | ||
| 205 | + showNotificationAuthorizationBanner.value = false; | ||
| 206 | + } else { | ||
| 207 | + try { | ||
| 208 | + Permission.notification.status.then((status) { | ||
| 209 | + if (status.isGranted) { | ||
| 210 | + showNotificationAuthorizationBanner.value = false; | ||
| 211 | + } else { | ||
| 212 | + showNotificationAuthorizationBanner.value = true; | ||
| 213 | + } | ||
| 214 | + }); | ||
| 215 | + } on Exception catch (e) { | ||
| 216 | + AppLogger.e(e); | ||
| 217 | + } | ||
| 218 | + } | ||
| 219 | + } | ||
| 220 | + | ||
| 199 | void checkAddFriendVisible() { | 221 | void checkAddFriendVisible() { |
| 200 | if (isFriend) { | 222 | if (isFriend) { |
| 201 | showPartnerAdBanner.value = false; | 223 | showPartnerAdBanner.value = false; |
| @@ -504,6 +526,7 @@ class TodayController extends GetMaterialController { | @@ -504,6 +526,7 @@ class TodayController extends GetMaterialController { | ||
| 504 | hrvAnnotations.clear(); | 526 | hrvAnnotations.clear(); |
| 505 | 527 | ||
| 506 | v2HealthData.value = null; | 528 | v2HealthData.value = null; |
| 529 | + v2ActivityTarget.value = null; | ||
| 507 | // v2StressScore.value = null; | 530 | // v2StressScore.value = null; |
| 508 | } | 531 | } |
| 509 | 532 | ||
| @@ -697,12 +720,13 @@ class TodayController extends GetMaterialController { | @@ -697,12 +720,13 @@ class TodayController extends GetMaterialController { | ||
| 697 | super.didChangeAppLifecycleState(state); | 720 | super.didChangeAppLifecycleState(state); |
| 698 | 721 | ||
| 699 | if (state == AppLifecycleState.resumed) { | 722 | if (state == AppLifecycleState.resumed) { |
| 700 | - // var today = DateUtils.dateOnly(DateTime.now()); | ||
| 701 | - // lastSelectableDay.value = today; | ||
| 702 | - // changeDate(today); | 723 | + var today = DateUtils.dateOnly(DateTime.now()); |
| 724 | + lastSelectableDay.value = today; | ||
| 725 | + changeDate(today); | ||
| 703 | checkAddFriendVisible(); | 726 | checkAddFriendVisible(); |
| 704 | checkHrvAdBannerVisible(); | 727 | checkHrvAdBannerVisible(); |
| 705 | checkHealthDataAuthCardVisible(); | 728 | checkHealthDataAuthCardVisible(); |
| 729 | + checkNotificationAuthorizationBannerVisible(); | ||
| 706 | _performHealthDataUpload(); | 730 | _performHealthDataUpload(); |
| 707 | // _startCoreCaculate(); | 731 | // _startCoreCaculate(); |
| 708 | // unawaited(loadDataForDate(selectedDate.value)); | 732 | // unawaited(loadDataForDate(selectedDate.value)); |
| @@ -720,7 +744,33 @@ class TodayController extends GetMaterialController { | @@ -720,7 +744,33 @@ class TodayController extends GetMaterialController { | ||
| 720 | checkAddFriendVisible(); | 744 | checkAddFriendVisible(); |
| 721 | checkHrvAdBannerVisible(); | 745 | checkHrvAdBannerVisible(); |
| 722 | checkHealthDataAuthCardVisible(isPullToRefresh: isPullToRefresh); | 746 | checkHealthDataAuthCardVisible(isPullToRefresh: isPullToRefresh); |
| 747 | + checkNotificationAuthorizationBannerVisible(); | ||
| 723 | _performHealthDataUpload(); | 748 | _performHealthDataUpload(); |
| 724 | // await loadDataForDate(selectedDate.value); | 749 | // await loadDataForDate(selectedDate.value); |
| 725 | } | 750 | } |
| 751 | + | ||
| 752 | + Future<void> requestNotificationAuthorization() async { | ||
| 753 | + try { | ||
| 754 | + final status = await Permission.notification.status; | ||
| 755 | + | ||
| 756 | + if (status.isGranted) { | ||
| 757 | + // 已授权,无需再请求 | ||
| 758 | + return; | ||
| 759 | + } | ||
| 760 | + | ||
| 761 | + if (status.isPermanentlyDenied) { | ||
| 762 | + // 用户永久拒绝(Android),引导去设置 | ||
| 763 | + await openAppSettings(); | ||
| 764 | + return; | ||
| 765 | + } | ||
| 766 | + | ||
| 767 | + // 未决定 → 弹出系统授权弹窗 | ||
| 768 | + final result = await Permission.notification.request(); | ||
| 769 | + AppLogger.d(result.isGranted | ||
| 770 | + ? 'Notification authorization granted' | ||
| 771 | + : 'Notification authorization skipped'); | ||
| 772 | + } catch (error) { | ||
| 773 | + AppLogger.e('Notification authorization failed: $error'); | ||
| 774 | + } | ||
| 775 | + } | ||
| 726 | } | 776 | } |
| @@ -55,6 +55,54 @@ class TodayHrvAdBanner extends StatelessWidget { | @@ -55,6 +55,54 @@ class TodayHrvAdBanner extends StatelessWidget { | ||
| 55 | } | 55 | } |
| 56 | } | 56 | } |
| 57 | 57 | ||
| 58 | +class TodayNotificationAuthorizationBanner extends StatelessWidget { | ||
| 59 | + const TodayNotificationAuthorizationBanner({ | ||
| 60 | + super.key, | ||
| 61 | + required this.controller, | ||
| 62 | + this.showMargin = true, | ||
| 63 | + }); | ||
| 64 | + | ||
| 65 | + final TodayController controller; | ||
| 66 | + final bool showMargin; | ||
| 67 | + | ||
| 68 | + @override | ||
| 69 | + Widget build(BuildContext context) { | ||
| 70 | + return Obx(() { | ||
| 71 | + if (!controller.showHrvAdBanner.value) return const SizedBox.shrink(); | ||
| 72 | + | ||
| 73 | + return GestureDetector( | ||
| 74 | + onTap: () async { | ||
| 75 | + ta.track('click_doublefeel_today_status', | ||
| 76 | + properties: {'ita': '点击开启通知权限'}); | ||
| 77 | + await controller.requestNotificationAuthorization(); | ||
| 78 | + controller.checkNotificationAuthorizationBannerVisible(); | ||
| 79 | + }, | ||
| 80 | + child: _TodayGuideBanner( | ||
| 81 | + title: context.l10n.turnOnNotifications, | ||
| 82 | + subtitle: | ||
| 83 | + context.l10n.stayUpToDateOnChangesInYourOwnAndYourFriendsHealth, | ||
| 84 | + right: Container( | ||
| 85 | + padding: const EdgeInsets.all(4), | ||
| 86 | + decoration: const BoxDecoration( | ||
| 87 | + color: Colors.white, | ||
| 88 | + shape: BoxShape.circle, | ||
| 89 | + ), | ||
| 90 | + child: Image.asset( | ||
| 91 | + 'assets/images/common/ic_arrow_forward.png', | ||
| 92 | + width: 20, | ||
| 93 | + height: 20, | ||
| 94 | + color: context.colors.primary, | ||
| 95 | + ), | ||
| 96 | + ), | ||
| 97 | + leftImagePath: 'assets/images/today/ic_banner_notify_permission.png', | ||
| 98 | + onClose: controller.dismissHrvAdBanner, | ||
| 99 | + showMargin: showMargin, | ||
| 100 | + ), | ||
| 101 | + ); | ||
| 102 | + }); | ||
| 103 | + } | ||
| 104 | +} | ||
| 105 | + | ||
| 58 | class TodayPartnerAdBanner extends StatelessWidget { | 106 | class TodayPartnerAdBanner extends StatelessWidget { |
| 59 | const TodayPartnerAdBanner({ | 107 | const TodayPartnerAdBanner({ |
| 60 | super.key, | 108 | super.key, |
| @@ -213,6 +261,8 @@ class TodayAdCarousel extends StatelessWidget { | @@ -213,6 +261,8 @@ class TodayAdCarousel extends StatelessWidget { | ||
| 213 | Widget build(BuildContext context) { | 261 | Widget build(BuildContext context) { |
| 214 | return Obx(() { | 262 | return Obx(() { |
| 215 | final showHrv = controller.showHrvAdBanner.value; | 263 | final showHrv = controller.showHrvAdBanner.value; |
| 264 | + final showNotificationAuthorization = | ||
| 265 | + controller.showNotificationAuthorizationBanner.value; | ||
| 216 | final showPartner = controller.showPartnerAdBanner.value && | 266 | final showPartner = controller.showPartnerAdBanner.value && |
| 217 | !controller.userStateService.isBound; | 267 | !controller.userStateService.isBound; |
| 218 | 268 | ||
| @@ -225,6 +275,14 @@ class TodayAdCarousel extends StatelessWidget { | @@ -225,6 +275,14 @@ class TodayAdCarousel extends StatelessWidget { | ||
| 225 | ), | 275 | ), |
| 226 | ); | 276 | ); |
| 227 | } | 277 | } |
| 278 | + if (showNotificationAuthorization) { | ||
| 279 | + activeBanners.add( | ||
| 280 | + TodayNotificationAuthorizationBanner( | ||
| 281 | + controller: controller, | ||
| 282 | + showMargin: false, | ||
| 283 | + ), | ||
| 284 | + ); | ||
| 285 | + } | ||
| 228 | if (showPartner) { | 286 | if (showPartner) { |
| 229 | activeBanners.add( | 287 | activeBanners.add( |
| 230 | TodayPartnerAdBanner( | 288 | TodayPartnerAdBanner( |
| @@ -201,7 +201,7 @@ class V2StressScore { | @@ -201,7 +201,7 @@ class V2StressScore { | ||
| 201 | 201 | ||
| 202 | factory V2StressScore.fromJson(Map<String, dynamic> json) { | 202 | factory V2StressScore.fromJson(Map<String, dynamic> json) { |
| 203 | return V2StressScore( | 203 | return V2StressScore( |
| 204 | - state: _parseInt(json['state']), | 204 | + state: _parseInt(json['state']) ?? 0, |
| 205 | hrvScore: _parseInt(json['hrv_score']), | 205 | hrvScore: _parseInt(json['hrv_score']), |
| 206 | hrScore: _parseInt(json['hr_score']), | 206 | hrScore: _parseInt(json['hr_score']), |
| 207 | latestHr: _parseInt(json['latest_hr']), | 207 | latestHr: _parseInt(json['latest_hr']), |
| @@ -710,5 +710,7 @@ | @@ -710,5 +710,7 @@ | ||
| 710 | "latestHrvTipAttentionAboveBaseline": "Your HRV is on the low side. Consider relaxing, keeping regular rest, and paying attention to nutrition and recovery.", | 710 | "latestHrvTipAttentionAboveBaseline": "Your HRV is on the low side. Consider relaxing, keeping regular rest, and paying attention to nutrition and recovery.", |
| 711 | "latestHrvTipAttentionBelowBaseline": "Your HRV is clearly below your usual level. Recent stress may be elevated, so try to rest and adjust your state.", | 711 | "latestHrvTipAttentionBelowBaseline": "Your HRV is clearly below your usual level. Recent stress may be elevated, so try to rest and adjust your state.", |
| 712 | "latestHrvTipOverloadAboveBaseline": "Your HRV is at a relatively low level. Your body may be under higher stress. If this is after exercise, a lower HRV can be normal. Rest and recover in time.", | 712 | "latestHrvTipOverloadAboveBaseline": "Your HRV is at a relatively low level. Your body may be under higher stress. If this is after exercise, a lower HRV can be normal. Rest and recover in time.", |
| 713 | - "latestHrvTipOverloadBelowBaseline": "Your HRV is clearly below your usual level. Your body may be under high stress. If this is after exercise, a lower HRV can be normal. Reduce exertion, rest in time, and support sleep recovery." | ||
| 714 | -} | 713 | + "latestHrvTipOverloadBelowBaseline": "Your HRV is clearly below your usual level. Your body may be under high stress. If this is after exercise, a lower HRV can be normal. Reduce exertion, rest in time, and support sleep recovery.", |
| 714 | + "turnOnNotifications": "Turn on Notifications", | ||
| 715 | + "stayUpToDateOnChangesInYourOwnAndYourFriendsHealth": "Stay up to date on changes in your own and your friends' health" | ||
| 716 | +} |
| @@ -1103,5 +1103,7 @@ | @@ -1103,5 +1103,7 @@ | ||
| 1103 | "latestHrvTipAttentionAboveBaseline": "你的 HRV 偏低,建议适当放松、规律休息,并注意饮食与恢复。", | 1103 | "latestHrvTipAttentionAboveBaseline": "你的 HRV 偏低,建议适当放松、规律休息,并注意饮食与恢复。", |
| 1104 | "latestHrvTipAttentionBelowBaseline": "你的 HRV 明显低于日常水平,近期可能压力偏高,建议尽量休息与调整状态。", | 1104 | "latestHrvTipAttentionBelowBaseline": "你的 HRV 明显低于日常水平,近期可能压力偏高,建议尽量休息与调整状态。", |
| 1105 | "latestHrvTipOverloadAboveBaseline": "你的 HRV 处于较低水平,身体可能正在承受较高压力(刚运动完 HRV 降低则属于正常情况),建议及时休息与恢复。", | 1105 | "latestHrvTipOverloadAboveBaseline": "你的 HRV 处于较低水平,身体可能正在承受较高压力(刚运动完 HRV 降低则属于正常情况),建议及时休息与恢复。", |
| 1106 | - "latestHrvTipOverloadBelowBaseline": "你的 HRV 明显低于平时水平,身体可能处于高压力状态(刚运动完 HRV 降低则属于正常情况),建议减少消耗、及时休息,并保证睡眠恢复。" | ||
| 1107 | -} | 1106 | + "latestHrvTipOverloadBelowBaseline": "你的 HRV 明显低于平时水平,身体可能处于高压力状态(刚运动完 HRV 降低则属于正常情况),建议减少消耗、及时休息,并保证睡眠恢复。", |
| 1107 | + "turnOnNotifications": "开启通知", | ||
| 1108 | + "stayUpToDateOnChangesInYourOwnAndYourFriendsHealth": "及时了解自己和好友的健康波动" | ||
| 1109 | +} |
| @@ -4082,6 +4082,18 @@ abstract class AppLocalizations { | @@ -4082,6 +4082,18 @@ abstract class AppLocalizations { | ||
| 4082 | /// In zh, this message translates to: | 4082 | /// In zh, this message translates to: |
| 4083 | /// **'你的 HRV 明显低于平时水平,身体可能处于高压力状态(刚运动完 HRV 降低则属于正常情况),建议减少消耗、及时休息,并保证睡眠恢复。'** | 4083 | /// **'你的 HRV 明显低于平时水平,身体可能处于高压力状态(刚运动完 HRV 降低则属于正常情况),建议减少消耗、及时休息,并保证睡眠恢复。'** |
| 4084 | String get latestHrvTipOverloadBelowBaseline; | 4084 | String get latestHrvTipOverloadBelowBaseline; |
| 4085 | + | ||
| 4086 | + /// No description provided for @turnOnNotifications. | ||
| 4087 | + /// | ||
| 4088 | + /// In zh, this message translates to: | ||
| 4089 | + /// **'开启通知'** | ||
| 4090 | + String get turnOnNotifications; | ||
| 4091 | + | ||
| 4092 | + /// No description provided for @stayUpToDateOnChangesInYourOwnAndYourFriendsHealth. | ||
| 4093 | + /// | ||
| 4094 | + /// In zh, this message translates to: | ||
| 4095 | + /// **'及时了解自己和好友的健康波动'** | ||
| 4096 | + String get stayUpToDateOnChangesInYourOwnAndYourFriendsHealth; | ||
| 4085 | } | 4097 | } |
| 4086 | 4098 | ||
| 4087 | class _AppLocalizationsDelegate | 4099 | class _AppLocalizationsDelegate |
| @@ -2288,4 +2288,11 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -2288,4 +2288,11 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 2288 | @override | 2288 | @override |
| 2289 | String get latestHrvTipOverloadBelowBaseline => | 2289 | String get latestHrvTipOverloadBelowBaseline => |
| 2290 | 'Your HRV is clearly below your usual level. Your body may be under high stress. If this is after exercise, a lower HRV can be normal. Reduce exertion, rest in time, and support sleep recovery.'; | 2290 | 'Your HRV is clearly below your usual level. Your body may be under high stress. If this is after exercise, a lower HRV can be normal. Reduce exertion, rest in time, and support sleep recovery.'; |
| 2291 | + | ||
| 2292 | + @override | ||
| 2293 | + String get turnOnNotifications => 'Turn on Notifications'; | ||
| 2294 | + | ||
| 2295 | + @override | ||
| 2296 | + String get stayUpToDateOnChangesInYourOwnAndYourFriendsHealth => | ||
| 2297 | + 'Stay up to date on changes in your own and your friends\' health'; | ||
| 2291 | } | 2298 | } |
| @@ -2184,4 +2184,11 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -2184,4 +2184,11 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 2184 | @override | 2184 | @override |
| 2185 | String get latestHrvTipOverloadBelowBaseline => | 2185 | String get latestHrvTipOverloadBelowBaseline => |
| 2186 | '你的 HRV 明显低于平时水平,身体可能处于高压力状态(刚运动完 HRV 降低则属于正常情况),建议减少消耗、及时休息,并保证睡眠恢复。'; | 2186 | '你的 HRV 明显低于平时水平,身体可能处于高压力状态(刚运动完 HRV 降低则属于正常情况),建议减少消耗、及时休息,并保证睡眠恢复。'; |
| 2187 | + | ||
| 2188 | + @override | ||
| 2189 | + String get turnOnNotifications => '开启通知'; | ||
| 2190 | + | ||
| 2191 | + @override | ||
| 2192 | + String get stayUpToDateOnChangesInYourOwnAndYourFriendsHealth => | ||
| 2193 | + '及时了解自己和好友的健康波动'; | ||
| 2187 | } | 2194 | } |
-
Please register or login to post a comment