Commit c0004b571c3d2b6e6b9e2d0eda11613f7bc5881c

Authored by 权海
1 parent f5857469

feat(ui):新增分享文本和文件(图片等)的原生接口

@@ -340,6 +340,8 @@ interface PlatformHostApi { @@ -340,6 +340,8 @@ interface PlatformHostApi {
340 fun getFullUserAgent(): String 340 fun getFullUserAgent(): String
341 /** 是否是测试环境 */ 341 /** 是否是测试环境 */
342 fun isDebugEnvoriment(): Boolean 342 fun isDebugEnvoriment(): Boolean
  343 + fun shareText(text: String, callback: (Result<Boolean>) -> Unit)
  344 + fun shareFileData(databytes: ByteArray, callback: (Result<Boolean>) -> Unit)
343 /** 345 /**
344 * 更新用户信息, 有登录态后调用 346 * 更新用户信息, 有登录态后调用
345 * jsonString: UserPreferences的序列化string 347 * jsonString: UserPreferences的序列化string
@@ -421,6 +423,46 @@ interface PlatformHostApi { @@ -421,6 +423,46 @@ interface PlatformHostApi {
421 } 423 }
422 } 424 }
423 run { 425 run {
  426 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.shareText$separatedMessageChannelSuffix", codec)
  427 + if (api != null) {
  428 + channel.setMessageHandler { message, reply ->
  429 + val args = message as List<Any?>
  430 + val textArg = args[0] as String
  431 + api.shareText(textArg) { result: Result<Boolean> ->
  432 + val error = result.exceptionOrNull()
  433 + if (error != null) {
  434 + reply.reply(PlatformApiPigeonUtils.wrapError(error))
  435 + } else {
  436 + val data = result.getOrNull()
  437 + reply.reply(PlatformApiPigeonUtils.wrapResult(data))
  438 + }
  439 + }
  440 + }
  441 + } else {
  442 + channel.setMessageHandler(null)
  443 + }
  444 + }
  445 + run {
  446 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.shareFileData$separatedMessageChannelSuffix", codec)
  447 + if (api != null) {
  448 + channel.setMessageHandler { message, reply ->
  449 + val args = message as List<Any?>
  450 + val databytesArg = args[0] as ByteArray
  451 + api.shareFileData(databytesArg) { result: Result<Boolean> ->
  452 + val error = result.exceptionOrNull()
  453 + if (error != null) {
  454 + reply.reply(PlatformApiPigeonUtils.wrapError(error))
  455 + } else {
  456 + val data = result.getOrNull()
  457 + reply.reply(PlatformApiPigeonUtils.wrapResult(data))
  458 + }
  459 + }
  460 + }
  461 + } else {
  462 + channel.setMessageHandler(null)
  463 + }
  464 + }
  465 + run {
424 val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.updateLoginInfo$separatedMessageChannelSuffix", codec) 466 val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.updateLoginInfo$separatedMessageChannelSuffix", codec)
425 if (api != null) { 467 if (api != null) {
426 channel.setMessageHandler { message, reply -> 468 channel.setMessageHandler { message, reply ->
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 archiveVersion = 1; 3 archiveVersion = 1;
4 classes = { 4 classes = {
5 }; 5 };
6 - objectVersion = 77; 6 + objectVersion = 54;
7 objects = { 7 objects = {
8 8
9 /* Begin PBXBuildFile section */ 9 /* Begin PBXBuildFile section */
@@ -370,6 +370,8 @@ protocol PlatformHostApi { @@ -370,6 +370,8 @@ protocol PlatformHostApi {
370 func getFullUserAgent() throws -> String 370 func getFullUserAgent() throws -> String
371 /// 是否是测试环境 371 /// 是否是测试环境
372 func isDebugEnvoriment() throws -> Bool 372 func isDebugEnvoriment() throws -> Bool
  373 + func shareText(text: String, completion: @escaping (Result<Bool, Error>) -> Void)
  374 + func shareFileData(databytes: FlutterStandardTypedData, completion: @escaping (Result<Bool, Error>) -> Void)
373 /// 更新用户信息, 有登录态后调用 375 /// 更新用户信息, 有登录态后调用
374 /// jsonString: UserPreferences的序列化string 376 /// jsonString: UserPreferences的序列化string
375 /// baseUrl: 请求地址, https://api.doublefeel.cn 377 /// baseUrl: 请求地址, https://api.doublefeel.cn
@@ -439,6 +441,40 @@ class PlatformHostApiSetup { @@ -439,6 +441,40 @@ class PlatformHostApiSetup {
439 } else { 441 } else {
440 isDebugEnvorimentChannel.setMessageHandler(nil) 442 isDebugEnvorimentChannel.setMessageHandler(nil)
441 } 443 }
  444 + let shareTextChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.shareText\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  445 + if let api = api {
  446 + shareTextChannel.setMessageHandler { message, reply in
  447 + let args = message as! [Any?]
  448 + let textArg = args[0] as! String
  449 + api.shareText(text: textArg) { result in
  450 + switch result {
  451 + case .success(let res):
  452 + reply(wrapResult(res))
  453 + case .failure(let error):
  454 + reply(wrapError(error))
  455 + }
  456 + }
  457 + }
  458 + } else {
  459 + shareTextChannel.setMessageHandler(nil)
  460 + }
  461 + let shareFileDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.shareFileData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  462 + if let api = api {
  463 + shareFileDataChannel.setMessageHandler { message, reply in
  464 + let args = message as! [Any?]
  465 + let databytesArg = args[0] as! FlutterStandardTypedData
  466 + api.shareFileData(databytes: databytesArg) { result in
  467 + switch result {
  468 + case .success(let res):
  469 + reply(wrapResult(res))
  470 + case .failure(let error):
  471 + reply(wrapError(error))
  472 + }
  473 + }
  474 + }
  475 + } else {
  476 + shareFileDataChannel.setMessageHandler(nil)
  477 + }
442 /// 更新用户信息, 有登录态后调用 478 /// 更新用户信息, 有登录态后调用
443 /// jsonString: UserPreferences的序列化string 479 /// jsonString: UserPreferences的序列化string
444 /// baseUrl: 请求地址, https://api.doublefeel.cn 480 /// baseUrl: 请求地址, https://api.doublefeel.cn
@@ -98,6 +98,26 @@ final class PlatformHostApiImpl: PlatformHostApi { @@ -98,6 +98,26 @@ final class PlatformHostApiImpl: PlatformHostApi {
98 UIApplication.shared.open(url) 98 UIApplication.shared.open(url)
99 } 99 }
100 } 100 }
  101 +
  102 + func shareText(text: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
  103 + presentShareSheet(items: [text], completion: completion)
  104 + }
  105 +
  106 + func shareFileData(databytes: FlutterStandardTypedData, completion: @escaping (Result<Bool, any Error>) -> Void) {
  107 + let fileURL = FileManager.default.temporaryDirectory
  108 + .appendingPathComponent("doublefeel-share-\(UUID().uuidString)")
  109 + .appendingPathExtension("data")
  110 +
  111 + do {
  112 + try databytes.data.write(to: fileURL, options: .atomic)
  113 + } catch {
  114 + print("[PlatformHostApiImpl.shareFileData] return error: \(error)")
  115 + completion(.failure(error))
  116 + return
  117 + }
  118 +
  119 + presentShareSheet(items: [fileURL], cleanupURL: fileURL, completion: completion)
  120 + }
101 121
102 func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, any Error>) -> Void) { 122 func uploadFile(filePath: String, resourceType: HResourceType, completion: @escaping (Result<String?, any Error>) -> Void) {
103 let obsResourceType: HuaweiOBSResourceType 123 let obsResourceType: HuaweiOBSResourceType
@@ -274,3 +294,73 @@ final class PlatformHostApiImpl: PlatformHostApi { @@ -274,3 +294,73 @@ final class PlatformHostApiImpl: PlatformHostApi {
274 return userAgent 294 return userAgent
275 } 295 }
276 } 296 }
  297 +
  298 +//MARK: - 分享
  299 +extension PlatformHostApiImpl{
  300 + private func presentShareSheet(
  301 + items: [Any],
  302 + cleanupURL: URL? = nil,
  303 + completion: @escaping (Result<Bool, any Error>) -> Void
  304 + ) {
  305 + DispatchQueue.main.async {
  306 + guard let viewController = Self.topViewController() else {
  307 + if let cleanupURL {
  308 + try? FileManager.default.removeItem(at: cleanupURL)
  309 + }
  310 + let error = NSError(
  311 + domain: "PlatformHostApiImpl.Share",
  312 + code: 1,
  313 + userInfo: [NSLocalizedDescriptionKey: "Unable to find a view controller for sharing."]
  314 + )
  315 + print("[PlatformHostApiImpl.share] return error: \(error)")
  316 + completion(.failure(error))
  317 + return
  318 + }
  319 +
  320 + let activityController = UIActivityViewController(
  321 + activityItems: items,
  322 + applicationActivities: nil
  323 + )
  324 + activityController.popoverPresentationController?.sourceView = viewController.view
  325 + activityController.popoverPresentationController?.sourceRect = CGRect(
  326 + x: viewController.view.bounds.midX,
  327 + y: viewController.view.bounds.midY,
  328 + width: 1,
  329 + height: 1
  330 + )
  331 + activityController.completionWithItemsHandler = { _, completed, _, error in
  332 + if let cleanupURL {
  333 + try? FileManager.default.removeItem(at: cleanupURL)
  334 + }
  335 + if let error {
  336 + print("[PlatformHostApiImpl.share] return error: \(error)")
  337 + completion(.failure(error))
  338 + } else {
  339 + print("[PlatformHostApiImpl.share] return: \(completed)")
  340 + completion(.success(completed))
  341 + }
  342 + }
  343 + viewController.present(activityController, animated: true)
  344 + }
  345 + }
  346 +
  347 + private static func topViewController(
  348 + from rootViewController: UIViewController? = UIApplication.shared.connectedScenes
  349 + .compactMap { $0 as? UIWindowScene }
  350 + .first(where: { $0.activationState == .foregroundActive })?
  351 + .windows
  352 + .first(where: { $0.isKeyWindow })?
  353 + .rootViewController
  354 + ) -> UIViewController? {
  355 + if let presented = rootViewController?.presentedViewController {
  356 + return topViewController(from: presented)
  357 + }
  358 + if let navigationController = rootViewController as? UINavigationController {
  359 + return topViewController(from: navigationController.visibleViewController)
  360 + }
  361 + if let tabBarController = rootViewController as? UITabBarController {
  362 + return topViewController(from: tabBarController.selectedViewController)
  363 + }
  364 + return rootViewController
  365 + }
  366 +}
@@ -9,6 +9,7 @@ import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart'; @@ -9,6 +9,7 @@ import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart';
9 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 9 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
10 import 'package:doublefeel_flutter/core/result/app_result.dart'; 10 import 'package:doublefeel_flutter/core/result/app_result.dart';
11 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 11 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
  12 +import 'package:doublefeel_flutter/core/util/app_toast.dart';
12 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 13 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
13 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; 14 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
14 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 15 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
@@ -64,11 +65,16 @@ class BindPartnerController extends GetxController { @@ -64,11 +65,16 @@ class BindPartnerController extends GetxController {
64 65
65 bool get canSubmit => partnerIdInput.value.isNotEmpty; 66 bool get canSubmit => partnerIdInput.value.isNotEmpty;
66 67
67 - void onShareMyCode() {  
68 - Share.share("""嘿 ❤️ 我发现了一个叫 DoubleFeel 的 App! 68 + void onShareMyCode() async {
  69 + try {
  70 + final success =
  71 + await PlatformHostApi().shareText("""嘿 ❤️ 我发现了一个叫 DoubleFeel 的 App!
69 它可以帮我们互相关心 — 记录压力和睡眠、查看 HRV 与身体状态、及时了解彼此的健康状况。 72 它可以帮我们互相关心 — 记录压力和睡眠、查看 HRV 与身体状态、及时了解彼此的健康状况。
70 快来一起使用吧 👉 https://doublefeel.cn/ 73 快来一起使用吧 👉 https://doublefeel.cn/
71 我的好友id:${myInviteCode.value}"""); 74 我的好友id:${myInviteCode.value}""");
  75 + } catch (e) {
  76 + AppToast.show(e.toString());
  77 + }
72 } 78 }
73 79
74 Future<void> onSubmitPartnerId() async { 80 Future<void> onSubmitPartnerId() async {
@@ -422,6 +422,62 @@ class PlatformHostApi { @@ -422,6 +422,62 @@ class PlatformHostApi {
422 } 422 }
423 } 423 }
424 424
  425 + Future<bool> shareText(String text) async {
  426 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.shareText$pigeonVar_messageChannelSuffix';
  427 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  428 + pigeonVar_channelName,
  429 + pigeonChannelCodec,
  430 + binaryMessenger: pigeonVar_binaryMessenger,
  431 + );
  432 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[text]);
  433 + final List<Object?>? pigeonVar_replyList =
  434 + await pigeonVar_sendFuture as List<Object?>?;
  435 + if (pigeonVar_replyList == null) {
  436 + throw _createConnectionError(pigeonVar_channelName);
  437 + } else if (pigeonVar_replyList.length > 1) {
  438 + throw PlatformException(
  439 + code: pigeonVar_replyList[0]! as String,
  440 + message: pigeonVar_replyList[1] as String?,
  441 + details: pigeonVar_replyList[2],
  442 + );
  443 + } else if (pigeonVar_replyList[0] == null) {
  444 + throw PlatformException(
  445 + code: 'null-error',
  446 + message: 'Host platform returned null value for non-null return value.',
  447 + );
  448 + } else {
  449 + return (pigeonVar_replyList[0] as bool?)!;
  450 + }
  451 + }
  452 +
  453 + Future<bool> shareFileData(Uint8List databytes) async {
  454 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.shareFileData$pigeonVar_messageChannelSuffix';
  455 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  456 + pigeonVar_channelName,
  457 + pigeonChannelCodec,
  458 + binaryMessenger: pigeonVar_binaryMessenger,
  459 + );
  460 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[databytes]);
  461 + final List<Object?>? pigeonVar_replyList =
  462 + await pigeonVar_sendFuture as List<Object?>?;
  463 + if (pigeonVar_replyList == null) {
  464 + throw _createConnectionError(pigeonVar_channelName);
  465 + } else if (pigeonVar_replyList.length > 1) {
  466 + throw PlatformException(
  467 + code: pigeonVar_replyList[0]! as String,
  468 + message: pigeonVar_replyList[1] as String?,
  469 + details: pigeonVar_replyList[2],
  470 + );
  471 + } else if (pigeonVar_replyList[0] == null) {
  472 + throw PlatformException(
  473 + code: 'null-error',
  474 + message: 'Host platform returned null value for non-null return value.',
  475 + );
  476 + } else {
  477 + return (pigeonVar_replyList[0] as bool?)!;
  478 + }
  479 + }
  480 +
425 /// 更新用户信息, 有登录态后调用 481 /// 更新用户信息, 有登录态后调用
426 /// jsonString: UserPreferences的序列化string 482 /// jsonString: UserPreferences的序列化string
427 /// baseUrl: 请求地址, https://api.doublefeel.cn 483 /// baseUrl: 请求地址, https://api.doublefeel.cn
@@ -119,6 +119,11 @@ abstract class PlatformHostApi { @@ -119,6 +119,11 @@ abstract class PlatformHostApi {
119 /// 是否是测试环境 119 /// 是否是测试环境
120 bool isDebugEnvoriment(); 120 bool isDebugEnvoriment();
121 121
  122 + @async
  123 + bool shareText(String text);
  124 + @async
  125 + bool shareFileData(Uint8List databytes);
  126 +
122 /// 更新用户信息, 有登录态后调用 127 /// 更新用户信息, 有登录态后调用
123 /// jsonString: UserPreferences的序列化string 128 /// jsonString: UserPreferences的序列化string
124 /// baseUrl: 请求地址, https://api.doublefeel.cn 129 /// baseUrl: 请求地址, https://api.doublefeel.cn