Commit 0c45ba2f740556d4e87681ac94d6cc6406f23ad8

Authored by 刘宏哲
1 parent f3ff05fc

feat(app): ohos cropper适配

1 import 'dart:async'; 1 import 'dart:async';
  2 +import 'dart:io';
2 3
3 import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; 4 import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
4 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 5 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
@@ -7,15 +8,16 @@ import 'package:doublefeel_flutter/core/logging/app_logger.dart'; @@ -7,15 +8,16 @@ import 'package:doublefeel_flutter/core/logging/app_logger.dart';
7 import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; 8 import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
8 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 9 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
9 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 10 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
  11 +import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
10 import 'package:doublefeel_flutter/core/result/app_result.dart'; 12 import 'package:doublefeel_flutter/core/result/app_result.dart';
11 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 13 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
12 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; 14 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
13 import 'package:doublefeel_flutter/data/models/user/user_models.dart'; 15 import 'package:doublefeel_flutter/data/models/user/user_models.dart';
14 -import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; 16 +import 'package:flutter/material.dart';
  17 +import 'package:flutter/services.dart';
15 import 'package:get/get.dart'; 18 import 'package:get/get.dart';
16 import 'package:image_cropper/image_cropper.dart'; 19 import 'package:image_cropper/image_cropper.dart';
17 import 'package:image_picker/image_picker.dart'; 20 import 'package:image_picker/image_picker.dart';
18 -import 'package:logger/logger.dart';  
19 21
20 class MyController extends GetxController { 22 class MyController extends GetxController {
21 MyController(this._userApi, this._vipApi, this._themeApi); 23 MyController(this._userApi, this._vipApi, this._themeApi);
@@ -64,22 +66,45 @@ class MyController extends GetxController { @@ -64,22 +66,45 @@ class MyController extends GetxController {
64 ); 66 );
65 if (picked == null) return; // 用户取消 67 if (picked == null) return; // 用户取消
66 AppLogger.e(picked.path); 68 AppLogger.e(picked.path);
67 - // 2. 用 image_cropper 裁剪为正方形  
68 - final CroppedFile? cropped = await ImageCropper().cropImage(  
69 - sourcePath: picked.path,  
70 - aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),  
71 - uiSettings: [  
72 - AndroidUiSettings(  
73 - toolbarTitle: '裁剪头像',  
74 - lockAspectRatio: true,  
75 - ),  
76 - IOSUiSettings(  
77 - title: '裁剪头像',  
78 - aspectRatioLockEnabled: true,  
79 - resetAspectRatioEnabled: false,  
80 - ),  
81 - ],  
82 - ); 69 + // OHOS 版 image_cropper 使用 Flutter 页面承载裁剪 UI,必须显式传入
  70 + // BuildContext(该插件目前通过 WebUiSettings 读取该参数)。
  71 + final isOhos = Platform.operatingSystem.toLowerCase() == 'ohos';
  72 + final cropperContext = Get.context ?? Get.overlayContext;
  73 + CroppedFile? cropped;
  74 + try {
  75 + cropped = await ImageCropper().cropImage(
  76 + sourcePath: picked.path,
  77 + aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
  78 + uiSettings: [
  79 + AndroidUiSettings(
  80 + toolbarTitle: '裁剪头像',
  81 + lockAspectRatio: true,
  82 + ),
  83 + IOSUiSettings(
  84 + title: '裁剪头像',
  85 + aspectRatioLockEnabled: true,
  86 + resetAspectRatioEnabled: false,
  87 + ),
  88 + if (isOhos && cropperContext != null)
  89 + WebUiSettings(context: cropperContext),
  90 + ],
  91 + );
  92 + } finally {
  93 + if (isOhos) {
  94 + // 等待 cropper 路由的退场动画结束,防止其 AppBar 覆盖恢复的样式。
  95 + await Future<void>.delayed(const Duration(milliseconds: 400));
  96 +
  97 + SystemChrome.setSystemUIOverlayStyle(
  98 + const SystemUiOverlayStyle(
  99 + statusBarColor: Colors.transparent,
  100 + statusBarIconBrightness: Brightness.dark,
  101 + statusBarBrightness: Brightness.light,
  102 + systemNavigationBarColor: Colors.transparent,
  103 + systemNavigationBarIconBrightness: Brightness.dark,
  104 + ),
  105 + );
  106 + }
  107 + }
83 if (cropped == null) return; // 用户取消裁剪 108 if (cropped == null) return; // 用户取消裁剪
84 109
85 final ossPath = await _platformHostApi.uploadFile( 110 final ossPath = await _platformHostApi.uploadFile(
@@ -44,4 +44,12 @@ enum HuaweiHealthDataType { @@ -44,4 +44,12 @@ enum HuaweiHealthDataType {
44 const HuaweiHealthDataType(this.dataType); 44 const HuaweiHealthDataType(this.dataType);
45 45
46 final int dataType; 46 final int dataType;
  47 +
  48 + /// Resolves the server/native `data_type` value to a supported type.
  49 + static HuaweiHealthDataType? fromDataType(int dataType) {
  50 + for (final type in values) {
  51 + if (type.dataType == dataType) return type;
  52 + }
  53 + return null;
  54 + }
47 } 55 }
@@ -2,6 +2,7 @@ import 'dart:async'; @@ -2,6 +2,7 @@ import 'dart:async';
2 2
3 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 3 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
4 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; 4 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
  5 +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/huawei_health_data_type.dart';
5 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; 6 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
6 7
7 import '../logging/app_logger.dart'; 8 import '../logging/app_logger.dart';
@@ -14,6 +15,8 @@ import 'web_socket_service.dart'; @@ -14,6 +15,8 @@ import 'web_socket_service.dart';
14 15
15 /// Login lifecycle and SDK initialization. 16 /// Login lifecycle and SDK initialization.
16 class UserStateService { 17 class UserStateService {
  18 + static const _healthDataUpdateXTag = 'health_data_update';
  19 +
17 UserStateService( 20 UserStateService(
18 this._environmentConfig, 21 this._environmentConfig,
19 this._userPrefs, 22 this._userPrefs,
@@ -33,6 +36,7 @@ class UserStateService { @@ -33,6 +36,7 @@ class UserStateService {
33 final ThinkingDataService _thinkingDataService; 36 final ThinkingDataService _thinkingDataService;
34 final HealthRawDataCoreService _healthRawDataCoreService; 37 final HealthRawDataCoreService _healthRawDataCoreService;
35 final WebSocketService _webSocketService; 38 final WebSocketService _webSocketService;
  39 + StreamSubscription<WebSocketEvent>? _healthDataUpdateSubscription;
36 40
37 bool sdksInitialized = false; 41 bool sdksInitialized = false;
38 42
@@ -53,6 +57,7 @@ class UserStateService { @@ -53,6 +57,7 @@ class UserStateService {
53 // after login, but must not delay the rest of the authenticated session. 57 // after login, but must not delay the rest of the authenticated session.
54 unawaited(_pushService.registerPushToken()); 58 unawaited(_pushService.registerPushToken());
55 await _imService.connect(); 59 await _imService.connect();
  60 + _listenForHealthDataUpdates();
56 _webSocketService.start(); 61 _webSocketService.start();
57 } 62 }
58 63
@@ -61,6 +66,8 @@ class UserStateService { @@ -61,6 +66,8 @@ class UserStateService {
61 _pushService.cancelPushTokenRegistration(); 66 _pushService.cancelPushTokenRegistration();
62 _imService.disconnect(); 67 _imService.disconnect();
63 _webSocketService.stop(); 68 _webSocketService.stop();
  69 + await _healthDataUpdateSubscription?.cancel();
  70 + _healthDataUpdateSubscription = null;
64 _healthRawDataCoreService.closeDatabase(); 71 _healthRawDataCoreService.closeDatabase();
65 if (callServerLogout) { 72 if (callServerLogout) {
66 await _userApi.logout(); 73 await _userApi.logout();
@@ -74,6 +81,45 @@ class UserStateService { @@ -74,6 +81,45 @@ class UserStateService {
74 AppLogger.i('UserStateService.onLogout completed'); 81 AppLogger.i('UserStateService.onLogout completed');
75 } 82 }
76 83
  84 + void _listenForHealthDataUpdates() {
  85 + _healthDataUpdateSubscription ??= _webSocketService
  86 + .eventsFor(_healthDataUpdateXTag)
  87 + .listen(_onHealthDataUpdate);
  88 + }
  89 +
  90 + void _onHealthDataUpdate(WebSocketEvent event) {
  91 + final dataType = _healthDataTypeFromBody(event.body);
  92 + if (dataType == null) {
  93 + AppLogger.w(
  94 + 'Ignored health_data_update with an invalid data_type: ${event.body}',
  95 + );
  96 + return;
  97 + }
  98 +
  99 + final processing = _healthRawDataCoreService.onHealthDataUpdated(
  100 + dataTypes: <int>[dataType.dataType],
  101 + );
  102 + unawaited(processing.catchError((Object error, StackTrace stackTrace) {
  103 + AppLogger.w('Failed to process health_data_update', error, stackTrace);
  104 + }));
  105 + }
  106 +
  107 + HuaweiHealthDataType? _healthDataTypeFromBody(Object? body) {
  108 + if (body is! Map) return null;
  109 + final value = body['data_type'];
  110 + final dataType = switch (value) {
  111 + final int number => number,
  112 + final double number
  113 + when number.isFinite && number == number.truncateToDouble() =>
  114 + number.toInt(),
  115 + final String text => int.tryParse(text),
  116 + _ => null,
  117 + };
  118 + return dataType == null
  119 + ? null
  120 + : HuaweiHealthDataType.fromDataType(dataType);
  121 + }
  122 +
77 void _trackLogin() { 123 void _trackLogin() {
78 final uid = _userPrefs.preferences.value.meUserInfo?.id ?? 0; 124 final uid = _userPrefs.preferences.value.meUserInfo?.id ?? 0;
79 if (uid > 0) { 125 if (uid > 0) {
@@ -451,7 +451,9 @@ export class PlatformHostApiImpl extends PlatformHostApi { @@ -451,7 +451,9 @@ export class PlatformHostApiImpl extends PlatformHostApi {
451 id: notificationId, 451 id: notificationId,
452 content: { 452 content: {
453 notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, 453 notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
454 - normal: { title, text: content, additionalText: link }, 454 + // The deep link is carried by the WantAgent. Do not render it in the
  455 + // notification body, where query parameters may be exposed to users.
  456 + normal: { title, text: content },
455 }, 457 },
456 wantAgent: notificationWantAgent, 458 wantAgent: notificationWantAgent,
457 }); 459 });