Showing
12 changed files
with
276 additions
and
51 deletions
| @@ -38,8 +38,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | @@ -38,8 +38,7 @@ class BindPartnerView extends GetView<BindPartnerController> { | ||
| 38 | children: [ | 38 | children: [ |
| 39 | _buildGradientBackground(), | 39 | _buildGradientBackground(), |
| 40 | Container( | 40 | Container( |
| 41 | - padding: | ||
| 42 | - EdgeInsets.only(top: MediaQuery.paddingOf(context).top + 4), | 41 | + padding: EdgeInsets.only(top: 4), |
| 43 | child: ListView( | 42 | child: ListView( |
| 44 | physics: const ClampingScrollPhysics(), | 43 | physics: const ClampingScrollPhysics(), |
| 45 | children: [ | 44 | children: [ |
| 1 | +import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | ||
| 3 | +import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/core/result/app_result.dart'; | ||
| 1 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 5 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 2 | import 'package:doublefeel_flutter/data/local/local_storage.dart'; | 6 | import 'package:doublefeel_flutter/data/local/local_storage.dart'; |
| 3 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | 7 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; |
| 4 | import 'package:flutter/material.dart'; | 8 | import 'package:flutter/material.dart'; |
| 5 | import 'package:flutter/services.dart'; | 9 | import 'package:flutter/services.dart'; |
| 10 | +import 'package:flutter/scheduler.dart'; | ||
| 11 | + | ||
| 6 | import 'package:get/get.dart'; | 12 | import 'package:get/get.dart'; |
| 7 | 13 | ||
| 8 | import '../../../routes/app_pages.dart'; | 14 | import '../../../routes/app_pages.dart'; |
| 9 | import '../../purchase/purchase_route_args.dart'; | 15 | import '../../purchase/purchase_route_args.dart'; |
| 10 | import '../../app_review_prompt/logic/app_review_prompt_logic.dart'; | 16 | import '../../app_review_prompt/logic/app_review_prompt_logic.dart'; |
| 11 | import '../../report_common/models/report_period.dart'; | 17 | import '../../report_common/models/report_period.dart'; |
| 18 | +import 'today_controller.dart'; | ||
| 12 | import 'trend/trend_controller.dart'; | 19 | import 'trend/trend_controller.dart'; |
| 13 | 20 | ||
| 14 | class HomeController extends GetxController { | 21 | class HomeController extends GetxController { |
| @@ -20,6 +27,7 @@ class HomeController extends GetxController { | @@ -20,6 +27,7 @@ class HomeController extends GetxController { | ||
| 20 | ); | 27 | ); |
| 21 | final LocalStorage _storage = Get.find<LocalStorage>(); | 28 | final LocalStorage _storage = Get.find<LocalStorage>(); |
| 22 | final UserStateService userStateService = Get.find<UserStateService>(); | 29 | final UserStateService userStateService = Get.find<UserStateService>(); |
| 30 | + final PlatformHostApi _platformHostApi = PlatformHostApi(); | ||
| 23 | 31 | ||
| 24 | /// 当前选中的底部 tab 索引 | 32 | /// 当前选中的底部 tab 索引 |
| 25 | final selectedIndex = 0.obs; | 33 | final selectedIndex = 0.obs; |
| @@ -29,6 +37,16 @@ class HomeController extends GetxController { | @@ -29,6 +37,16 @@ class HomeController extends GetxController { | ||
| 29 | void onInit() { | 37 | void onInit() { |
| 30 | super.onInit(); | 38 | super.onInit(); |
| 31 | _mainChannel.setMethodCallHandler(_handleNativeMethodCall); | 39 | _mainChannel.setMethodCallHandler(_handleNativeMethodCall); |
| 40 | + SchedulerBinding.instance.addPostFrameCallback((_) async { | ||
| 41 | + try { | ||
| 42 | + final route = await _platformHostApi.requestUnhandedUrl(); | ||
| 43 | + // final route ='doublefeel://flutter/home?tab=today&scroll_to_latest_hrv=true'; | ||
| 44 | + // final route = 'doublefeel://flutter/friend-home?user_id=131'; | ||
| 45 | + // final route = 'doublefeel://flutter/home?tab=trend&type=sleep'; | ||
| 46 | + // final route ='doublefeel://flutter/friend-trend?type=sleep&user_id=131'; | ||
| 47 | + _checkFlutterRoute(route); | ||
| 48 | + } catch (_) {} | ||
| 49 | + }); | ||
| 32 | } | 50 | } |
| 33 | 51 | ||
| 34 | @override | 52 | @override |
| @@ -37,15 +55,6 @@ class HomeController extends GetxController { | @@ -37,15 +55,6 @@ class HomeController extends GetxController { | ||
| 37 | super.onClose(); | 55 | super.onClose(); |
| 38 | } | 56 | } |
| 39 | 57 | ||
| 40 | - @override | ||
| 41 | - void onReady() async { | ||
| 42 | - final unhandedUrl = await PlatformHostApi().requestUnhandedUrl(); | ||
| 43 | - debugPrint( | ||
| 44 | - '[HomeController] requestUnhandedUrl: ' | ||
| 45 | - 'result: ${unhandedUrl}', | ||
| 46 | - ); | ||
| 47 | - } | ||
| 48 | - | ||
| 49 | Future<Object?> _handleNativeMethodCall(MethodCall call) async { | 58 | Future<Object?> _handleNativeMethodCall(MethodCall call) async { |
| 50 | switch (call.method) { | 59 | switch (call.method) { |
| 51 | case 'handleFlutterUrl': | 60 | case 'handleFlutterUrl': |
| @@ -66,7 +75,7 @@ class HomeController extends GetxController { | @@ -66,7 +75,7 @@ class HomeController extends GetxController { | ||
| 66 | _ => '', | 75 | _ => '', |
| 67 | }; | 76 | }; |
| 68 | lastFlutterUrl.value = url; | 77 | lastFlutterUrl.value = url; |
| 69 | - debugPrint('[HomeController.handleFlutterUrl] url: $url'); | 78 | + _checkFlutterRoute(url); |
| 70 | return url.isNotEmpty; | 79 | return url.isNotEmpty; |
| 71 | } | 80 | } |
| 72 | 81 | ||
| @@ -107,4 +116,126 @@ class HomeController extends GetxController { | @@ -107,4 +116,126 @@ class HomeController extends GetxController { | ||
| 107 | 116 | ||
| 108 | int _dateKey(DateTime date) => | 117 | int _dateKey(DateTime date) => |
| 109 | date.year * 10000 + date.month * 100 + date.day; | 118 | date.year * 10000 + date.month * 100 + date.day; |
| 119 | + | ||
| 120 | + _checkFlutterRoute(String? route) { | ||
| 121 | + try { | ||
| 122 | + if (route == null || route.isEmpty) return; | ||
| 123 | + | ||
| 124 | + final uri = Uri.tryParse(route); | ||
| 125 | + if (uri == null) return; | ||
| 126 | + | ||
| 127 | + final path = uri.path; // "/home", "/account-settings" 等 | ||
| 128 | + final params = uri.queryParameters; // {"tab": "today"} 等 | ||
| 129 | + | ||
| 130 | + _dispatchRoute(path, params); | ||
| 131 | + } catch (_) {} | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + /// 根据 path + query 参数分派导航动作 | ||
| 135 | + void _dispatchRoute(String path, Map<String, String> params) { | ||
| 136 | + switch (path) { | ||
| 137 | + case AppRoutes.home: | ||
| 138 | + _switchHomeTab(params['tab'], params); | ||
| 139 | + break; | ||
| 140 | + default: | ||
| 141 | + // 其他已注册路由 → 跳转页面,query 参数作为 arguments 传入 | ||
| 142 | + final isKnown = Get.routeTree.routes.any((r) => r.name == path); | ||
| 143 | + if (!isKnown) return; | ||
| 144 | + if (path == Routes.FRIEND_HOME || path == Routes.FRIEND_TREND) { | ||
| 145 | + final paramUserId = params['user_id']; | ||
| 146 | + if (paramUserId != null) { | ||
| 147 | + final friendUserId = int.tryParse(paramUserId); | ||
| 148 | + if (friendUserId != null) { | ||
| 149 | + final _friendApi = Get.find<FriendApi>(); | ||
| 150 | + _friendApi.getFriendInfo(friendUserId).then((result) { | ||
| 151 | + switch (result) { | ||
| 152 | + case AppSuccess(data: final info): | ||
| 153 | + { | ||
| 154 | + if (path == Routes.FRIEND_TREND) { | ||
| 155 | + TrendType? trendType = | ||
| 156 | + getTrendType(params['type'] ?? ''); | ||
| 157 | + if (trendType != null) { | ||
| 158 | + Get.toNamed(path, | ||
| 159 | + arguments: FriendTrendArguments( | ||
| 160 | + friendItem: info, | ||
| 161 | + initialTypeIndex: trendType.tabIndex, | ||
| 162 | + )); | ||
| 163 | + } | ||
| 164 | + } else { | ||
| 165 | + Get.toNamed(path, arguments: info); | ||
| 166 | + } | ||
| 167 | + } | ||
| 168 | + break; | ||
| 169 | + case AppFailure(): | ||
| 170 | + break; | ||
| 171 | + } | ||
| 172 | + }); | ||
| 173 | + } | ||
| 174 | + } | ||
| 175 | + } else { | ||
| 176 | + Get.toNamed(path, arguments: params.isNotEmpty ? params : null); | ||
| 177 | + } | ||
| 178 | + } | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + /// 根据 tab 名称切换首页底部 tab | ||
| 182 | + void _switchHomeTab(String? tab, Map<String, String> params) { | ||
| 183 | + switch (tab) { | ||
| 184 | + case 'today': | ||
| 185 | + changeTab(0); | ||
| 186 | + final scrollToLatestHrv = | ||
| 187 | + params['scroll_to_latest_hrv']?.toLowerCase() == 'true'; | ||
| 188 | + if (scrollToLatestHrv) { | ||
| 189 | + SchedulerBinding.instance.addPostFrameCallback((_) { | ||
| 190 | + final todayCtrl = Get.find<TodayController>(); | ||
| 191 | + if (todayCtrl.scrollController.hasClients) { | ||
| 192 | + todayCtrl.scrollController.animateTo( | ||
| 193 | + 850, | ||
| 194 | + duration: const Duration(milliseconds: 400), | ||
| 195 | + curve: Curves.easeInOut, | ||
| 196 | + ); | ||
| 197 | + } | ||
| 198 | + }); | ||
| 199 | + } | ||
| 200 | + break; | ||
| 201 | + case 'trend': | ||
| 202 | + changeTab(trendTabIndex); | ||
| 203 | + final type = params['type']; | ||
| 204 | + if (type != null) { | ||
| 205 | + SchedulerBinding.instance.addPostFrameCallback((_) { | ||
| 206 | + TrendType? trendType = getTrendType(type); | ||
| 207 | + if (trendType != null) { | ||
| 208 | + openTrend( | ||
| 209 | + trendType, | ||
| 210 | + ); | ||
| 211 | + } | ||
| 212 | + }); | ||
| 213 | + } | ||
| 214 | + break; | ||
| 215 | + case 'friends': | ||
| 216 | + changeTab(2); | ||
| 217 | + break; | ||
| 218 | + case 'my': | ||
| 219 | + changeTab(3); | ||
| 220 | + break; | ||
| 221 | + default: | ||
| 222 | + break; | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + TrendType? getTrendType(String type) { | ||
| 227 | + switch (type) { | ||
| 228 | + case 'sleep': | ||
| 229 | + return TrendType.sleep; | ||
| 230 | + | ||
| 231 | + case 'activity': | ||
| 232 | + return TrendType.activity; | ||
| 233 | + | ||
| 234 | + case 'hrv': | ||
| 235 | + return TrendType.hrv; | ||
| 236 | + default: | ||
| 237 | + break; | ||
| 238 | + } | ||
| 239 | + return null; | ||
| 240 | + } | ||
| 110 | } | 241 | } |
| @@ -83,6 +83,7 @@ class TodayController extends GetMaterialController { | @@ -83,6 +83,7 @@ class TodayController extends GetMaterialController { | ||
| 83 | final selectedDate = DateTime.now().obs; | 83 | final selectedDate = DateTime.now().obs; |
| 84 | final focusedDay = DateTime.now().obs; | 84 | final focusedDay = DateTime.now().obs; |
| 85 | final scrollOffset = 0.0.obs; | 85 | final scrollOffset = 0.0.obs; |
| 86 | + final scrollController = ScrollController(); | ||
| 86 | 87 | ||
| 87 | final isLoadingToday = false.obs; | 88 | final isLoadingToday = false.obs; |
| 88 | final showHealthDataAuthCardStatus = 1.obs; | 89 | final showHealthDataAuthCardStatus = 1.obs; |
| @@ -158,10 +159,14 @@ class TodayController extends GetMaterialController { | @@ -158,10 +159,14 @@ class TodayController extends GetMaterialController { | ||
| 158 | showHrvAdBanner.value = false; | 159 | showHrvAdBanner.value = false; |
| 159 | } else { | 160 | } else { |
| 160 | try { | 161 | try { |
| 161 | - bool hasInstalledWatchSurface = | ||
| 162 | - await WearEngineHostApi().hasInstalledWatchSurface(); | ||
| 163 | - showHrvAdBanner.value = !hasInstalledWatchSurface; | ||
| 164 | - } on Exception catch (e) {} | 162 | + WearEngineHostApi() |
| 163 | + .hasInstalledWatchSurface() | ||
| 164 | + .then((hasInstalledWatchSurface) { | ||
| 165 | + showHrvAdBanner.value = !hasInstalledWatchSurface; | ||
| 166 | + }); | ||
| 167 | + } on Exception catch (e) { | ||
| 168 | + AppLogger.e(e); | ||
| 169 | + } | ||
| 165 | } | 170 | } |
| 166 | } | 171 | } |
| 167 | 172 | ||
| @@ -228,7 +233,7 @@ class TodayController extends GetMaterialController { | @@ -228,7 +233,7 @@ class TodayController extends GetMaterialController { | ||
| 228 | AppLogger.d("requestHealthClientAuthorization : $success"); | 233 | AppLogger.d("requestHealthClientAuthorization : $success"); |
| 229 | if (success) { | 234 | if (success) { |
| 230 | _performDataUpload(); | 235 | _performDataUpload(); |
| 231 | - AppToast.show('刷新完成'); | 236 | + // AppToast.show('刷新完成'); |
| 232 | } else { | 237 | } else { |
| 233 | // Get.to(NoHealthDataPage( | 238 | // Get.to(NoHealthDataPage( |
| 234 | // onRefresh: _performDataUpload, | 239 | // onRefresh: _performDataUpload, |
| @@ -597,5 +602,8 @@ class TodayController extends GetMaterialController { | @@ -597,5 +602,8 @@ class TodayController extends GetMaterialController { | ||
| 597 | @override | 602 | @override |
| 598 | void didChangeAppLifecycleState(AppLifecycleState state) { | 603 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 599 | super.didChangeAppLifecycleState(state); | 604 | super.didChangeAppLifecycleState(state); |
| 605 | + checkAddFriendVisible(); | ||
| 606 | + checkHrvAdBannerVisible(); | ||
| 607 | + checkHealthDataAuthCardVisible(); | ||
| 600 | } | 608 | } |
| 601 | } | 609 | } |
| @@ -116,6 +116,7 @@ class TodayTabBody extends StatelessWidget { | @@ -116,6 +116,7 @@ class TodayTabBody extends StatelessWidget { | ||
| 116 | double pinnedHeaderHeight, | 116 | double pinnedHeaderHeight, |
| 117 | ) { | 117 | ) { |
| 118 | return CustomScrollView( | 118 | return CustomScrollView( |
| 119 | + controller: controller.scrollController, | ||
| 119 | physics: const ClampingScrollPhysics(), | 120 | physics: const ClampingScrollPhysics(), |
| 120 | slivers: [ | 121 | slivers: [ |
| 121 | SliverToBoxAdapter( | 122 | SliverToBoxAdapter( |
| @@ -147,15 +147,26 @@ class _BottomSheetFriendRow extends StatelessWidget { | @@ -147,15 +147,26 @@ class _BottomSheetFriendRow extends StatelessWidget { | ||
| 147 | const SizedBox(width: 8), | 147 | const SizedBox(width: 8), |
| 148 | // Name | 148 | // Name |
| 149 | Expanded( | 149 | Expanded( |
| 150 | - child: Text( | ||
| 151 | - _friendDisplayName(friend), | ||
| 152 | - style: const TextStyle( | ||
| 153 | - color: Color(0xFF0F0F11), | ||
| 154 | - fontSize: 14, | ||
| 155 | - fontWeight: FontWeight.w500, | ||
| 156 | - ), | ||
| 157 | - overflow: TextOverflow.ellipsis, | ||
| 158 | - ), | 150 | + child: Text.rich( |
| 151 | + overflow: TextOverflow.ellipsis, | ||
| 152 | + TextSpan(children: [ | ||
| 153 | + TextSpan( | ||
| 154 | + text: friend.remarkName?.trim(), | ||
| 155 | + style: TextStyle( | ||
| 156 | + color: context.colors.textPrimary, | ||
| 157 | + fontSize: 14, | ||
| 158 | + fontWeight: FontWeight.w500, | ||
| 159 | + ), | ||
| 160 | + ), | ||
| 161 | + TextSpan( | ||
| 162 | + text: '(${friend.remarkName?.trim()})', | ||
| 163 | + style: TextStyle( | ||
| 164 | + color: context.colors.textSecondary, | ||
| 165 | + fontSize: 14, | ||
| 166 | + fontWeight: FontWeight.w500, | ||
| 167 | + ), | ||
| 168 | + ), | ||
| 169 | + ])), | ||
| 159 | ), | 170 | ), |
| 160 | // Checkbox | 171 | // Checkbox |
| 161 | Container( | 172 | Container( |
| @@ -182,14 +193,4 @@ class _BottomSheetFriendRow extends StatelessWidget { | @@ -182,14 +193,4 @@ class _BottomSheetFriendRow extends StatelessWidget { | ||
| 182 | ), | 193 | ), |
| 183 | ); | 194 | ); |
| 184 | } | 195 | } |
| 185 | - | ||
| 186 | - String _friendDisplayName(FriendItem friend) { | ||
| 187 | - final remark = friend.remarkName?.trim(); | ||
| 188 | - if (remark != null && remark.isNotEmpty) return remark; | ||
| 189 | - | ||
| 190 | - final nickname = friend.friendNickname?.trim(); | ||
| 191 | - if (nickname != null && nickname.isNotEmpty) return nickname; | ||
| 192 | - | ||
| 193 | - return l10n.friendsUnknownFriend; | ||
| 194 | - } | ||
| 195 | } | 196 | } |
| @@ -9,6 +9,8 @@ class SplashPage extends GetView<SplashController> { | @@ -9,6 +9,8 @@ class SplashPage extends GetView<SplashController> { | ||
| 9 | @override | 9 | @override |
| 10 | Widget build(BuildContext context) { | 10 | Widget build(BuildContext context) { |
| 11 | return const Scaffold( | 11 | return const Scaffold( |
| 12 | + extendBodyBehindAppBar: true, | ||
| 13 | + extendBody: true, | ||
| 12 | body: Center( | 14 | body: Center( |
| 13 | child: CircularProgressIndicator(), | 15 | child: CircularProgressIndicator(), |
| 14 | ), | 16 | ), |
| 1 | +import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart'; | ||
| 1 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 2 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 2 | import 'package:get/get.dart'; | 3 | import 'package:get/get.dart'; |
| 3 | 4 | ||
| @@ -6,8 +7,11 @@ import '../controllers/user_onboarding_controller.dart'; | @@ -6,8 +7,11 @@ import '../controllers/user_onboarding_controller.dart'; | ||
| 6 | class UserOnboardingBinding extends Bindings { | 7 | class UserOnboardingBinding extends Bindings { |
| 7 | @override | 8 | @override |
| 8 | void dependencies() { | 9 | void dependencies() { |
| 9 | - Get.lazyPut<UserOnboardingController>( | ||
| 10 | - () => UserOnboardingController(Get.find<UserApi>()), | 10 | + Get.put( |
| 11 | + UserOnboardingController( | ||
| 12 | + Get.find<UserApi>(), | ||
| 13 | + Get.find<AppleHealthUploadTool>(), | ||
| 14 | + ), | ||
| 11 | ); | 15 | ); |
| 12 | } | 16 | } |
| 13 | } | 17 | } |
| 1 | +import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | ||
| 1 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 3 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 2 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 4 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 3 | import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; | 5 | import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; |
| 4 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 6 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 7 | +import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; | ||
| 5 | import 'package:flutter/material.dart'; | 8 | import 'package:flutter/material.dart'; |
| 6 | import 'package:get/get.dart'; | 9 | import 'package:get/get.dart'; |
| 7 | import 'package:permission_handler/permission_handler.dart'; | 10 | import 'package:permission_handler/permission_handler.dart'; |
| @@ -17,8 +20,10 @@ class UserOnboardingController extends GetxController { | @@ -17,8 +20,10 @@ class UserOnboardingController extends GetxController { | ||
| 17 | final selections = <int, Set<int>>{}.obs; | 20 | final selections = <int, Set<int>>{}.obs; |
| 18 | final selectionVersion = 0.obs; | 21 | final selectionVersion = 0.obs; |
| 19 | 22 | ||
| 20 | - UserOnboardingController(this._userApi); | 23 | + UserOnboardingController(this._userApi, this._appleHealthUploadTool); |
| 21 | final UserApi _userApi; | 24 | final UserApi _userApi; |
| 25 | + final AppleHealthUploadTool _appleHealthUploadTool; | ||
| 26 | + final HealthKitHostApi _hostApi = HealthKitHostApi(); | ||
| 22 | 27 | ||
| 23 | bool hasSelection(int pageIndex) => | 28 | bool hasSelection(int pageIndex) => |
| 24 | selections[pageIndex]?.isNotEmpty ?? false; | 29 | selections[pageIndex]?.isNotEmpty ?? false; |
| @@ -204,31 +209,84 @@ class UserOnboardingController extends GetxController { | @@ -204,31 +209,84 @@ class UserOnboardingController extends GetxController { | ||
| 204 | if (userStateService.isBound) { | 209 | if (userStateService.isBound) { |
| 205 | Get.offAllNamed(AppRoutes.home); | 210 | Get.offAllNamed(AppRoutes.home); |
| 206 | } else { | 211 | } else { |
| 207 | - Get.offAllNamed(AppRoutes.bindPartner); | 212 | + Get.offAllNamed(AppRoutes.bindPartner, arguments: {'from': 'onboarding'}); |
| 213 | + } | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + /// 上传 Apple Health 新数据。 | ||
| 217 | + Future<void> _performDataUpload() async { | ||
| 218 | + try { | ||
| 219 | + final result = | ||
| 220 | + await _appleHealthUploadTool.uploadAllNewDataFromHealthKit(); | ||
| 221 | + // AppLogger.i( | ||
| 222 | + // 'Apple Health upload finished: ' | ||
| 223 | + // 'common=${result.commonUploadSuccess}(${result.commonCount}), ' | ||
| 224 | + // 'sleep=${result.sleepUploadSuccess}(${result.sleepCount}), ' | ||
| 225 | + // 'activityTarget=${result.activityTargetUploadSuccess}' | ||
| 226 | + // '(${result.activityTargetCount}), ' | ||
| 227 | + // 'success=${result.isSuccess}', | ||
| 228 | + // ); | ||
| 229 | + } catch (error, stackTrace) { | ||
| 230 | + AppLogger.e('Apple Health upload failed', error, stackTrace); | ||
| 208 | } | 231 | } |
| 209 | } | 232 | } |
| 210 | 233 | ||
| 211 | Future<void> requestHealthAuthorization() async { | 234 | Future<void> requestHealthAuthorization() async { |
| 212 | if (isAndroid) { | 235 | if (isAndroid) { |
| 213 | return; | 236 | return; |
| 214 | - } | ||
| 215 | - try { | ||
| 216 | - await Get.find<HealthKitUploadService>().requestClientAuthorization(); | ||
| 217 | - } catch (error) { | ||
| 218 | - debugPrint('Health authorization skipped: $error'); | 237 | + } else { |
| 238 | + if (GetPlatform.isIOS) { | ||
| 239 | + try { | ||
| 240 | + final result = await _hostApi.checkHealthAppAuthorization(); | ||
| 241 | + AppLogger.d("checkHealthAppAuthorization : $result"); | ||
| 242 | + if (result.status == 0) { | ||
| 243 | + bool success = await _hostApi.requestHealthClientAuthorization(); | ||
| 244 | + AppLogger.d("requestHealthClientAuthorization : $success"); | ||
| 245 | + if (success) { | ||
| 246 | + _performDataUpload(); | ||
| 247 | + } else { | ||
| 248 | + // Get.to(NoHealthDataPage( | ||
| 249 | + // onRefresh: _performDataUpload, | ||
| 250 | + // )); | ||
| 251 | + } | ||
| 252 | + return; | ||
| 253 | + } | ||
| 254 | + if (result.status == 1) { | ||
| 255 | + _performDataUpload(); | ||
| 256 | + } | ||
| 257 | + } catch (e) {} | ||
| 258 | + } | ||
| 219 | } | 259 | } |
| 220 | } | 260 | } |
| 221 | 261 | ||
| 222 | Future<void> requestNotificationAuthorization() async { | 262 | Future<void> requestNotificationAuthorization() async { |
| 223 | try { | 263 | try { |
| 224 | - final result = await Permission.notification.request(); | ||
| 225 | - if (result.isGranted) { | ||
| 226 | - debugPrint('Notification authorization granted'); | ||
| 227 | - } else { | ||
| 228 | - debugPrint('Notification authorization skipped'); | 264 | + final status = await Permission.notification.status; |
| 265 | + | ||
| 266 | + if (status.isGranted) { | ||
| 267 | + // 已授权,无需再请求 | ||
| 268 | + return; | ||
| 269 | + } | ||
| 270 | + | ||
| 271 | + if (status.isPermanentlyDenied) { | ||
| 272 | + // 用户永久拒绝(Android),引导去设置 | ||
| 273 | + await openAppSettings(); | ||
| 274 | + return; | ||
| 229 | } | 275 | } |
| 276 | + | ||
| 277 | + if (status.isDenied && GetPlatform.isIOS) { | ||
| 278 | + // iOS 上被拒绝后无法再弹窗,引导去设置 | ||
| 279 | + await openAppSettings(); | ||
| 280 | + return; | ||
| 281 | + } | ||
| 282 | + | ||
| 283 | + // 未决定 → 弹出系统授权弹窗 | ||
| 284 | + final result = await Permission.notification.request(); | ||
| 285 | + AppLogger.d(result.isGranted | ||
| 286 | + ? 'Notification authorization granted' | ||
| 287 | + : 'Notification authorization skipped'); | ||
| 230 | } catch (error) { | 288 | } catch (error) { |
| 231 | - debugPrint('Notification authorization skipped: $error'); | 289 | + AppLogger.e('Notification authorization failed: $error'); |
| 232 | } | 290 | } |
| 233 | } | 291 | } |
| 234 | } | 292 | } |
| @@ -83,6 +83,7 @@ abstract final class AppPages { | @@ -83,6 +83,7 @@ abstract final class AppPages { | ||
| 83 | GetPage( | 83 | GetPage( |
| 84 | name: AppRoutes.login, | 84 | name: AppRoutes.login, |
| 85 | page: () => const LoginView(), | 85 | page: () => const LoginView(), |
| 86 | + transition: Transition.fade, | ||
| 86 | binding: LoginBinding(), | 87 | binding: LoginBinding(), |
| 87 | ), | 88 | ), |
| 88 | GetPage( | 89 | GetPage( |
| @@ -98,6 +99,7 @@ abstract final class AppPages { | @@ -98,6 +99,7 @@ abstract final class AppPages { | ||
| 98 | GetPage( | 99 | GetPage( |
| 99 | name: AppRoutes.home, | 100 | name: AppRoutes.home, |
| 100 | page: () => const HomePage(), | 101 | page: () => const HomePage(), |
| 102 | + transition: Transition.fade, | ||
| 101 | binding: HomeBinding(), | 103 | binding: HomeBinding(), |
| 102 | ), | 104 | ), |
| 103 | GetPage( | 105 | GetPage( |
| @@ -22,6 +22,23 @@ class FriendApi { | @@ -22,6 +22,23 @@ class FriendApi { | ||
| 22 | ); | 22 | ); |
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | + Future<AppResult<FriendItem>> getFriendInfo( | ||
| 26 | + int userId, | ||
| 27 | + ) { | ||
| 28 | + return safeCall( | ||
| 29 | + call: () async { | ||
| 30 | + final response = await _dioClient.dio.get( | ||
| 31 | + ApiPaths.friendInfo, | ||
| 32 | + queryParameters: { | ||
| 33 | + 'friend_user_id': userId, | ||
| 34 | + }, | ||
| 35 | + ); | ||
| 36 | + return FriendItem.fromJson(response.data as Map<String, dynamic>); | ||
| 37 | + }, | ||
| 38 | + errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, | ||
| 39 | + ); | ||
| 40 | + } | ||
| 41 | + | ||
| 25 | Future<AppResult<FriendItem>> addFriendHandleError( | 42 | Future<AppResult<FriendItem>> addFriendHandleError( |
| 26 | String uniqueCode, HttpErrorHandlingPolicy policy) { | 43 | String uniqueCode, HttpErrorHandlingPolicy policy) { |
| 27 | return safeCall( | 44 | return safeCall( |
| @@ -71,4 +71,5 @@ abstract final class ApiPaths { | @@ -71,4 +71,5 @@ abstract final class ApiPaths { | ||
| 71 | 71 | ||
| 72 | // friends | 72 | // friends |
| 73 | static const friends = '/client/doublefeel/health/v2/friends/'; | 73 | static const friends = '/client/doublefeel/health/v2/friends/'; |
| 74 | + static const friendInfo = '/client/doublefeel/health/v2/friend_info/'; | ||
| 74 | } | 75 | } |
-
Please register or login to post a comment