From 2296c659743b271fa0e30dcb29da8d1e7bbc7181 Mon Sep 17 00:00:00 2001 From: joywayer Date: Tue, 17 Jun 2025 19:55:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E5=88=86=E4=BA=AB?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20-=20=E5=B0=86=E5=90=84=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E5=88=86=E4=BA=AB=E9=80=BB=E8=BE=91=E6=95=B4=E7=90=86=E5=88=B0?= =?UTF-8?q?=E5=AF=B9=E5=BA=94Manager=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 WechatShareManager:封装微信分享逻辑,支持ShareContent对象 - 增强 DouyinShareManager:新增ShareContent支持和分享引导功能 - 优化 QQShareManager:支持截图/纯文本分享类型自动识别 - 重构 SharePanel:简化为UI调度层,移除具体分享实现 - 实现职责分离:各Manager专注自己的平台分享逻辑 - 提升可维护性:修改某平台不影响其他平台 - 增强可扩展性:新增分享平台更容易实现 --- msext.xcodeproj/project.pbxproj | 28 + msext/AppDelegate.m | 24 +- msext/Class/RootVC/gameController.m | 11 + msext/Class/Utils/DouyinShareManager.h | 74 ++ msext/Class/Utils/DouyinShareManager.m | 595 ++++++++++ msext/Class/Utils/QQAppIDValidator.m | 222 ++++ msext/Class/Utils/QQShareManager.h | 179 +++ msext/Class/Utils/QQShareManager.m | 1474 ++++++++++++++++++++++++ msext/Class/Utils/SharePanel.h | 84 ++ msext/Class/Utils/SharePanel.m | 662 +++++++++++ msext/Class/Utils/WechatShareManager.h | 82 ++ msext/Class/Utils/WechatShareManager.m | 265 +++++ msext/Info.plist | 32 +- 13 files changed, 3730 insertions(+), 2 deletions(-) create mode 100644 msext/Class/Utils/DouyinShareManager.h create mode 100644 msext/Class/Utils/DouyinShareManager.m create mode 100644 msext/Class/Utils/QQAppIDValidator.m create mode 100644 msext/Class/Utils/QQShareManager.h create mode 100644 msext/Class/Utils/QQShareManager.m create mode 100644 msext/Class/Utils/SharePanel.h create mode 100644 msext/Class/Utils/SharePanel.m create mode 100644 msext/Class/Utils/WechatShareManager.h create mode 100644 msext/Class/Utils/WechatShareManager.m diff --git a/msext.xcodeproj/project.pbxproj b/msext.xcodeproj/project.pbxproj index d94c80e..1b06ed0 100644 --- a/msext.xcodeproj/project.pbxproj +++ b/msext.xcodeproj/project.pbxproj @@ -9,6 +9,11 @@ /* Begin PBXBuildFile section */ 1A6D67D02DFE70BD00C72F69 /* QiniuConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6D67CD2DFE70BD00C72F69 /* QiniuConfig.m */; }; 1A6D67D12DFE70BD00C72F69 /* QiniuManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6D67CF2DFE70BD00C72F69 /* QiniuManager.m */; }; + 1A6D67E22DFE935900C72F69 /* DouyinShareManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6D67DF2DFE935900C72F69 /* DouyinShareManager.m */; }; + 1A6D67E32DFE935900C72F69 /* QQShareManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6D67E12DFE935900C72F69 /* QQShareManager.m */; }; + 1A6D67E62DFE96E400C72F69 /* SharePanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6D67E42DFE96E400C72F69 /* SharePanel.m */; }; + 1ABF5A772E00517F00610F16 /* QQAppIDValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ABF5A732E00517F00610F16 /* QQAppIDValidator.m */; }; + 1ABF5A812E018E3E00610F16 /* WechatShareManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ABF5A802E018E3E00610F16 /* WechatShareManager.m */; }; 31E803772250BF51005DEBFA /* versionConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 31E803762250BF51005DEBFA /* versionConfig.m */; }; 31E803792250C28F005DEBFA /* gameid in Resources */ = {isa = PBXBuildFile; fileRef = 31E803782250C28F005DEBFA /* gameid */; }; 8021797BA72DAFC4131F956B /* libPods-msext.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B09474A14AD64EAA7687626 /* libPods-msext.a */; }; @@ -204,6 +209,15 @@ 1A6D67CD2DFE70BD00C72F69 /* QiniuConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QiniuConfig.m; sourceTree = ""; }; 1A6D67CE2DFE70BD00C72F69 /* QiniuManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QiniuManager.h; sourceTree = ""; }; 1A6D67CF2DFE70BD00C72F69 /* QiniuManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QiniuManager.m; sourceTree = ""; }; + 1A6D67DE2DFE935900C72F69 /* DouyinShareManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DouyinShareManager.h; sourceTree = ""; }; + 1A6D67DF2DFE935900C72F69 /* DouyinShareManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DouyinShareManager.m; sourceTree = ""; }; + 1A6D67E02DFE935900C72F69 /* QQShareManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QQShareManager.h; sourceTree = ""; }; + 1A6D67E12DFE935900C72F69 /* QQShareManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QQShareManager.m; sourceTree = ""; }; + 1A6D67E42DFE96E400C72F69 /* SharePanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SharePanel.m; sourceTree = ""; }; + 1A6D67E52DFE96E400C72F69 /* SharePanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SharePanel.h; sourceTree = ""; }; + 1ABF5A732E00517F00610F16 /* QQAppIDValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QQAppIDValidator.m; sourceTree = ""; }; + 1ABF5A7F2E018E3E00610F16 /* WechatShareManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WechatShareManager.h; sourceTree = ""; }; + 1ABF5A802E018E3E00610F16 /* WechatShareManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WechatShareManager.m; sourceTree = ""; }; 31E803752250BF51005DEBFA /* versionConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = versionConfig.h; sourceTree = ""; }; 31E803762250BF51005DEBFA /* versionConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = versionConfig.m; sourceTree = ""; }; 31E803782250C28F005DEBFA /* gameid */ = {isa = PBXFileReference; lastKnownFileType = folder; path = gameid; sourceTree = ""; }; @@ -564,6 +578,15 @@ 1A6D67CB2DFE6E7100C72F69 /* Utils */ = { isa = PBXGroup; children = ( + 1ABF5A7F2E018E3E00610F16 /* WechatShareManager.h */, + 1ABF5A802E018E3E00610F16 /* WechatShareManager.m */, + 1ABF5A732E00517F00610F16 /* QQAppIDValidator.m */, + 1A6D67E52DFE96E400C72F69 /* SharePanel.h */, + 1A6D67E42DFE96E400C72F69 /* SharePanel.m */, + 1A6D67DE2DFE935900C72F69 /* DouyinShareManager.h */, + 1A6D67DF2DFE935900C72F69 /* DouyinShareManager.m */, + 1A6D67E02DFE935900C72F69 /* QQShareManager.h */, + 1A6D67E12DFE935900C72F69 /* QQShareManager.m */, 1A6D67CC2DFE70BC00C72F69 /* QiniuConfig.h */, 1A6D67CD2DFE70BD00C72F69 /* QiniuConfig.m */, 1A6D67CE2DFE70BD00C72F69 /* QiniuManager.h */, @@ -1452,6 +1475,7 @@ E518CB7E1E5C2EC9003C5B23 /* ASIInputStream.m in Sources */, E55F27BD1F4C206500D66C4F /* gameController.m in Sources */, E518CB791E5C2EC9003C5B23 /* ASIDataCompressor.m in Sources */, + 1A6D67E62DFE96E400C72F69 /* SharePanel.m in Sources */, E5BA3CF71E2E819C006D41DB /* AFHTTPRequestOperation.m in Sources */, E540805B1B60DCA60021849A /* RootVC.m in Sources */, E518CB5C1E5BD926003C5B23 /* GDataXMLNode.m in Sources */, @@ -1464,6 +1488,7 @@ A8BF18021FD93F5C007749A5 /* RNCachingURLProtocol.m in Sources */, E58FD83D1DEEA55B00220EAE /* WXMediaMessage+messageConstruct.m in Sources */, E56F24D71E1F4E0500F32036 /* ChatVoiceRecorderVC.m in Sources */, + 1ABF5A772E00517F00610F16 /* QQAppIDValidator.m in Sources */, E56C80071E5CB57B00916DD9 /* zip.c in Sources */, E518CB7C1E5C2EC9003C5B23 /* ASIFormDataRequest.m in Sources */, A85333CA200F05DB00E1D646 /* HTTPServer.m in Sources */, @@ -1473,6 +1498,7 @@ A85333C2200F05DB00E1D646 /* WebSocket.m in Sources */, E5FA4EC41B607BC9006FB4C2 /* AppDelegate.m in Sources */, E54080631B60DCA60021849A /* SBJsonWriter.m in Sources */, + 1A6D67E32DFE935900C72F69 /* QQShareManager.m in Sources */, A85333BF200F05DB00E1D646 /* HTTPRedirectResponse.m in Sources */, E54335081DEC0F7600E45ECD /* Bridge.m in Sources */, E58FD8381DEEA55B00220EAE /* WXApiManager.m in Sources */, @@ -1480,6 +1506,7 @@ A85333CB200F05DB00E1D646 /* DDTTYLogger.m in Sources */, E56C80051E5CB57B00916DD9 /* mztools.c in Sources */, E5BA3CFD1E2E819C006D41DB /* AFURLRequestSerialization.m in Sources */, + 1ABF5A812E018E3E00610F16 /* WechatShareManager.m in Sources */, E506D0631E15C858009CBC8D /* amrFileCodec.mm in Sources */, A85333C4200F05DB00E1D646 /* MultipartFormDataParser.m in Sources */, A85333C6200F05DB00E1D646 /* HTTPAuthenticationRequest.m in Sources */, @@ -1527,6 +1554,7 @@ A85333C5200F05DB00E1D646 /* MultipartMessageHeader.m in Sources */, A85333D3200F05DB00E1D646 /* DDFileLogger.m in Sources */, A8BF18001FD93F5C007749A5 /* NSString+Sha1.m in Sources */, + 1A6D67E22DFE935900C72F69 /* DouyinShareManager.m in Sources */, E5BA3CFC1E2E819C006D41DB /* AFURLConnectionOperation.m in Sources */, E5AB2E971EFBC02F002AD63D /* Bridgetwo.m in Sources */, 31E803772250BF51005DEBFA /* versionConfig.m in Sources */, diff --git a/msext/AppDelegate.m b/msext/AppDelegate.m index bbc6d46..587c302 100755 --- a/msext/AppDelegate.m +++ b/msext/AppDelegate.m @@ -20,6 +20,7 @@ #import "RNCachingURLProtocol.h" #import "JANALYTICSService.h" #import "XianliaoApiManager.h" +#import "QQShareManager.h" @interface AppDelegate () { BOOL flag; @@ -31,12 +32,33 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { - return [WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]]; + // 处理QQ分享回调 + if ([QQShareManager handleOpenURL:url]) { + return YES; + } + // 处理微信回调 + return [WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]]; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { + // 处理QQ分享回调 + if ([QQShareManager handleOpenURL:url]) { + return YES; + } + // 处理微信回调 return [WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]]; } + +// iOS 9+ URL处理方法 +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + // 处理QQ分享回调 + if ([QQShareManager handleOpenURL:url]) { + return YES; + } + // 处理微信回调 + return [WXApi handleOpenURL:url delegate:[WXApiManager sharedManager]]; +} + - (void)configureAPIKey { if ([APIKey length] == 0) diff --git a/msext/Class/RootVC/gameController.m b/msext/Class/RootVC/gameController.m index bf18cbf..018458f 100755 --- a/msext/Class/RootVC/gameController.m +++ b/msext/Class/RootVC/gameController.m @@ -45,6 +45,8 @@ #import "XianliaoApiManager.h" #import "QiniuManager.h" #import "QiniuConfig.h" +#import "SharePanel.h" + @interface gameController () { @@ -578,6 +580,15 @@ NSLog(@"%@",two); enum WXScene currentScene; int friend = [one intValue]; + // ======测试分享面板 + + // 显示分享面板 + [SharePanel showWithDictionary:data completion:^(ShareType type, BOOL success) { + // 处理分享结果 + }]; + return; + + if(friend==1) { currentScene=WXSceneSession; diff --git a/msext/Class/Utils/DouyinShareManager.h b/msext/Class/Utils/DouyinShareManager.h new file mode 100644 index 0000000..4e9ce39 --- /dev/null +++ b/msext/Class/Utils/DouyinShareManager.h @@ -0,0 +1,74 @@ +// +// DouyinShareManager.h +// msext +// +// Created on 2025/06/15. +// Copyright © 2025年. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, DouyinShareType) { + DouyinShareTypeImage, // 图片分享 + DouyinShareTypeVideo, // 视频分享 + DouyinShareTypeHashtag, // 话题挑战分享 + DouyinShareTypeOpenProfile // 打开个人主页 +}; + +@interface DouyinShareManager : NSObject + +/** + * 检查抖音是否已安装 + * @return BOOL 是否已安装抖音 + */ ++ (BOOL)isDouyinInstalled; + +/** + * 分享到抖音 + * @param type 分享类型 + * @param hashtagName 话题名称(仅用于话题挑战类型) + * @param image 图片(仅用于图片分享类型) + * @param videoUrl 视频本地URL(仅用于视频分享类型) + * @param userId 用户ID(仅用于打开个人主页类型) + * @param completion 完成回调 + */ ++ (void)shareToDouyin:(DouyinShareType)type + hashtagName:(NSString * _Nullable)hashtagName + image:(UIImage * _Nullable)image + videoUrl:(NSURL * _Nullable)videoUrl + userId:(NSString * _Nullable)userId + completion:(void(^_Nullable)(BOOL success))completion; + +/** + * 处理从抖音返回的URL + * 需要在 AppDelegate 的 application:openURL:options: 方法中调用 + */ ++ (BOOL)handleOpenURL:(NSURL *)url; + +/** + * 使用ShareContent对象进行抖音分享 + * 支持根据type字段自动选择分享类型: + * - type=2:截图分享(复制图片到剪贴板+引导) + * - 其他:文本分享(复制文本到剪贴板+引导) + * @param shareContent 完整的分享内容对象,包含title、desc、type字段 + * @param completion 分享完成回调 + */ ++ (void)shareWithContent:(id)shareContent + completion:(void (^ _Nullable)(BOOL success))completion; + +/** + * 显示抖音图片分享引导弹窗 + */ ++ (void)showImageShareGuidance; + +/** + * 显示抖音文本分享引导弹窗 + */ ++ (void)showTextShareGuidance; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/msext/Class/Utils/DouyinShareManager.m b/msext/Class/Utils/DouyinShareManager.m new file mode 100644 index 0000000..9a2a26d --- /dev/null +++ b/msext/Class/Utils/DouyinShareManager.m @@ -0,0 +1,595 @@ +// +// DouyinShareManager.m +// msext +// +// Created on 2025/06/15. +// Copyright © 2025年. All rights reserved. +// + +#import "DouyinShareManager.h" +#import "FuncPublic.h" // 用于截图功能 +#import +#import + +// 抖音URL Schemes +#define kDouyinScheme @"snssdk1128://" +#define kDouyinShareScheme @"snssdk1128://share/video" +#define kDouyinImageShareScheme @"snssdk1128://share/image" +#define kDouyinHashtagScheme @"snssdk1128://challenge/detail/" +#define kDouyinUserProfileScheme @"snssdk1128://user/profile/" + +// 抖音应用回调的处理 +static void(^DouyinShareCompletion)(BOOL) = nil; + +@implementation DouyinShareManager + +#pragma mark - Public Methods + ++ (BOOL)isDouyinInstalled { + return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:kDouyinScheme]]; +} + ++ (void)shareToDouyin:(DouyinShareType)type + hashtagName:(NSString *)hashtagName + image:(UIImage *)image + videoUrl:(NSURL *)videoUrl + userId:(NSString *)userId + completion:(void (^)(BOOL))completion { + + // 检查抖音是否已安装 + if (![self isDouyinInstalled]) { + if (completion) { + completion(NO); + } + [self showAppNotInstalledAlert:@"抖音"]; + return; + } + + // 保存回调 + DouyinShareCompletion = completion; + + switch (type) { + case DouyinShareTypeImage: + [self shareImageToDouyin:image completion:completion]; + break; + + case DouyinShareTypeVideo: + [self shareVideoToDouyin:videoUrl completion:completion]; + break; + + case DouyinShareTypeHashtag: + [self shareHashtagToDouyin:hashtagName completion:completion]; + break; + + case DouyinShareTypeOpenProfile: + [self openDouyinUserProfile:userId completion:completion]; + break; + } +} + ++ (BOOL)handleOpenURL:(NSURL *)url { + // 判断是否是抖音返回的URL + NSString *urlString = [url absoluteString]; + if ([urlString hasPrefix:@"msext://douyin_share_result"]) { + // 根据返回的URL解析结果 + NSString *result = [self getUrlParam:urlString paramName:@"result"]; + BOOL success = result && [result isEqualToString:@"success"]; + + // 执行回调 + if (DouyinShareCompletion) { + DouyinShareCompletion(success); + DouyinShareCompletion = nil; + } + return YES; + } + return NO; +} + ++ (void)shareWithContent:(id)shareContent + completion:(void (^ _Nullable)(BOOL success))completion { + NSLog(@"🔍 [DouyinShareManager] 开始抖音分享"); + + // 检查抖音是否已安装 + if (![self isDouyinInstalled]) { + NSLog(@"❌ [DouyinShareManager] 抖音未安装"); + [self showAppNotInstalledAlert:@"抖音"]; + if (completion) { + completion(NO); + } + return; + } + + // 检查ShareContent对象 + if (!shareContent) { + NSLog(@"❌ [DouyinShareManager] ShareContent对象为空"); + if (completion) { + completion(NO); + } + return; + } + + // 通过KVC从ShareContent对象中提取参数 + NSString *title = nil; + NSString *description = nil; + NSString *type = nil; + + @try { + if ([shareContent respondsToSelector:@selector(valueForKey:)]) { + title = [shareContent valueForKey:@"title"]; + description = [shareContent valueForKey:@"desc"]; + type = [shareContent valueForKey:@"type"]; + } + + NSLog(@"🔍 [DouyinShareManager] 从ShareContent提取参数:"); + NSLog(@"🔍 - 标题: %@", title ?: @"(无)"); + NSLog(@"🔍 - 描述: %@", description ?: @"(无)"); + NSLog(@"🔍 - 类型: %@", type ?: @"(无)"); + + } @catch (NSException *exception) { + NSLog(@"❌ [DouyinShareManager] 解析ShareContent时发生异常: %@", exception.reason); + if (completion) { + completion(NO); + } + return; + } + + // 根据分享类型选择分享方式 + BOOL isScreenshotShare = [type intValue] == 2; + + if (isScreenshotShare) { + // 截图分享模式 + NSLog(@"🔍 [DouyinShareManager] 执行截图分享"); + + UIImage *screenImage = [FuncPublic getImageWithFullScreenshot]; + if (screenImage) { + // 复制图片到剪贴板 + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.image = screenImage; + + NSLog(@"✅ [DouyinShareManager] 截图已复制到剪贴板"); + + // 显示分享引导弹窗 + [self showImageShareGuidance]; + + if (completion) { + completion(YES); + } + } else { + NSLog(@"❌ [DouyinShareManager] 截图获取失败"); + if (completion) { + completion(NO); + } + } + } else { + // 文本分享模式 + NSLog(@"🔍 [DouyinShareManager] 执行文本分享"); + + // 组合分享内容 + NSString *shareText = @""; + if (title && title.length > 0) { + shareText = title; + } + if (description && description.length > 0) { + if (shareText.length > 0) { + shareText = [NSString stringWithFormat:@"%@\n\n%@", shareText, description]; + } else { + shareText = description; + } + } + + if (shareText.length == 0) { + shareText = @"来自进贤聚友棋牌的精彩内容分享"; + } + + // 复制到剪贴板 + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + [pasteboard setString:shareText]; + + NSLog(@"✅ [DouyinShareManager] 文本已复制到剪贴板: %@", shareText); + + // 显示分享引导弹窗 + [self showTextShareGuidance]; + + if (completion) { + completion(YES); + } + } +} + +#pragma mark - Private Methods + ++ (void)shareImageToDouyin:(UIImage *)image completion:(void (^)(BOOL))completion { + if (!image) { + if (completion) { + completion(NO); + } + return; + } + + // 方案1:先保存图片到相册,然后使用UIPasteboard传递图片数据,最后打开抖音 + [self requestPhotoLibraryAuthorizationWithCompletion:^(BOOL granted) { + if (!granted) { + if (completion) { + completion(NO); + } + [self showPhotoLibraryPermissionAlert]; + return; + } + + // 将图片保存到相册 + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + [PHAssetChangeRequest creationRequestForAssetFromImage:image]; + } completionHandler:^(BOOL success, NSError * _Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (success) { + // 同时将图片数据放到剪贴板,供抖音读取 + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + [pasteboard setImage:image]; + + // 尝试多种URL方案打开抖音分享 + NSArray *urlSchemes = @[ + @"snssdk1128://camera", // 打开抖音拍摄界面 + @"snssdk1128://publish", // 打开发布界面 + @"snssdk1128://share/image", // 图片分享(可能已废弃) + @"snssdk1128://" // 基础scheme,打开抖音主界面 + ]; + + BOOL opened = NO; + for (NSString *urlString in urlSchemes) { + NSURL *douyinURL = [NSURL URLWithString:urlString]; + if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) { + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:^(BOOL openSuccess) { + if (completion) { + completion(openSuccess); + } + }]; + } else { + BOOL openSuccess = [[UIApplication sharedApplication] openURL:douyinURL]; + if (completion) { + completion(openSuccess); + } + } + opened = YES; + break; + } + } + + if (!opened && completion) { + completion(NO); + } + } else { + if (completion) { + completion(NO); + } + NSLog(@"保存图片到相册失败:%@", error); + } + }); + }]; + }]; +} + ++ (void)shareVideoToDouyin:(NSURL *)videoUrl completion:(void (^)(BOOL))completion { + if (!videoUrl) { + if (completion) { + completion(NO); + } + return; + } + + // 检查文件是否存在 + if (![[NSFileManager defaultManager] fileExistsAtPath:videoUrl.path]) { + if (completion) { + completion(NO); + } + return; + } + + // 将视频保存到相册 + [self requestPhotoLibraryAuthorizationWithCompletion:^(BOOL granted) { + if (!granted) { + if (completion) { + completion(NO); + } + [self showPhotoLibraryPermissionAlert]; + return; + } + + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoUrl]; + } completionHandler:^(BOOL success, NSError * _Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (success) { + // 获取刚保存的视频的资源标识符 + PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; + fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeVideo options:fetchOptions]; + + if (fetchResult.firstObject) { + PHAsset *asset = fetchResult.firstObject; + NSString *localIdentifier = asset.localIdentifier; + + // 构建抖音分享URL + NSString *urlString = [NSString stringWithFormat:@"%@?video=%@&from=app_%@", + kDouyinShareScheme, + [self encodeString:localIdentifier], + [self encodeString:@"msext"]]; + + NSURL *douyinURL = [NSURL URLWithString:urlString]; + if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) { + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:^(BOOL success) { + // 这里不调用completion,等待抖音返回再调用 + }]; + } else { + [[UIApplication sharedApplication] openURL:douyinURL]; + } + } else { + if (completion) { + completion(NO); + } + } + } else { + if (completion) { + completion(NO); + } + } + } else { + if (completion) { + completion(NO); + } + NSLog(@"保存视频到相册失败:%@", error); + } + }); + }]; + }]; +} + ++ (void)shareHashtagToDouyin:(NSString *)hashtagName completion:(void (^)(BOOL))completion { + if (!hashtagName || hashtagName.length == 0) { + if (completion) { + completion(NO); + } + return; + } + + // 构建打开话题挑战的URL + NSString *urlString = [NSString stringWithFormat:@"%@%@", + kDouyinHashtagScheme, + [self encodeString:hashtagName]]; + + NSURL *douyinURL = [NSURL URLWithString:urlString]; + if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) { + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:^(BOOL success) { + if (completion) { + completion(success); + } + }]; + } else { + BOOL success = [[UIApplication sharedApplication] openURL:douyinURL]; + if (completion) { + completion(success); + } + } + } else { + if (completion) { + completion(NO); + } + } +} + ++ (void)openDouyinUserProfile:(NSString *)userId completion:(void (^)(BOOL))completion { + if (!userId || userId.length == 0) { + if (completion) { + completion(NO); + } + return; + } + + // 构建打开用户主页的URL + NSString *urlString = [NSString stringWithFormat:@"%@%@", + kDouyinUserProfileScheme, + [self encodeString:userId]]; + + NSURL *douyinURL = [NSURL URLWithString:urlString]; + if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) { + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:^(BOOL success) { + if (completion) { + completion(success); + } + }]; + } else { + BOOL success = [[UIApplication sharedApplication] openURL:douyinURL]; + if (completion) { + completion(success); + } + } + } else { + if (completion) { + completion(NO); + } + } +} + ++ (void)requestPhotoLibraryAuthorizationWithCompletion:(void (^)(BOOL granted))completion { + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + + switch (status) { + case PHAuthorizationStatusAuthorized: { + if (completion) { + completion(YES); + } + break; + } + + case PHAuthorizationStatusNotDetermined: { + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus newStatus) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (completion) { + completion(newStatus == PHAuthorizationStatusAuthorized); + } + }); + }]; + break; + } + + case PHAuthorizationStatusDenied: + case PHAuthorizationStatusRestricted: { + if (completion) { + completion(NO); + } + break; + } + + default: { + if (completion) { + completion(NO); + } + break; + } + } +} + ++ (NSString *)encodeString:(NSString *)string { + if (!string) return @""; + return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; +} + ++ (NSString *)getUrlParam:(NSString *)url paramName:(NSString *)name { + NSError *error; + NSString *regTags = [[NSString alloc] initWithFormat:@"(^|&|\\?)%@=([^&]*)(&|$)", name]; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regTags options:NSRegularExpressionCaseInsensitive error:&error]; + + NSTextCheckingResult *match = [regex firstMatchInString:url options:0 range:NSMakeRange(0, [url length])]; + if (match) { + NSString *paramValue = [url substringWithRange:[match rangeAtIndex:2]]; + return paramValue; + } + + return nil; +} + ++ (void)showAppNotInstalledAlert:(NSString *)appName { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"%@未安装", appName] + message:[NSString stringWithFormat:@"您的设备未安装%@客户端,无法进行分享", appName] + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; + [alertController addAction:okAction]; + + UIViewController *topVC = [self topViewController]; + [topVC presentViewController:alertController animated:YES completion:nil]; + }); +} + ++ (void)showPhotoLibraryPermissionAlert { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"无法访问相册" + message:@"请在设置中开启相册权限,以便分享到抖音" + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]; + UIAlertAction *settingsAction = [UIAlertAction actionWithTitle:@"去设置" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; + }]; + + [alertController addAction:cancelAction]; + [alertController addAction:settingsAction]; + + UIViewController *topVC = [self topViewController]; + [topVC presentViewController:alertController animated:YES completion:nil]; + }); +} + ++ (UIViewController *)topViewController { + UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; + while (rootViewController.presentedViewController) { + rootViewController = rootViewController.presentedViewController; + } + + if ([rootViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)rootViewController; + return navigationController.visibleViewController; + } + + if ([rootViewController isKindOfClass:[UITabBarController class]]) { + UITabBarController *tabBarController = (UITabBarController *)rootViewController; + UIViewController *selectedViewController = tabBarController.selectedViewController; + + if ([selectedViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)selectedViewController; + return navigationController.visibleViewController; + } + + return selectedViewController; + } + + return rootViewController; +} + ++ (void)showImageShareGuidance { + NSString *title = @"抖音分享准备完成"; + NSString *message = @"✅ 图片已复制到剪贴板\n\n" + @"🎯 分享到好友或群聊步骤:\n" + @"1️⃣ 打开抖音,点击右下角「消息」\n" + @"2️⃣ 选择好友或群聊进入聊天\n" + @"3️⃣ 在输入框长按粘贴图片\n" + @"4️⃣ 点击发送即可分享"; + + [self showGuidanceAlertWithTitle:title message:message]; +} + ++ (void)showTextShareGuidance { + NSString *title = @"抖音分享准备完成"; + NSString *message = @"✅ 内容已复制到剪贴板\n\n" + @"🎯 分享到好友或群聊步骤:\n" + @"1️⃣ 打开抖音,点击右下角「消息」\n" + @"2️⃣ 选择好友或群聊进入聊天\n" + @"3️⃣ 在输入框长按粘贴内容\n" + @"4️⃣ 点击发送即可分享"; + + [self showGuidanceAlertWithTitle:title message:message]; +} + ++ (void)showGuidanceAlertWithTitle:(NSString *)title message:(NSString *)message { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *openAction = [UIAlertAction actionWithTitle:@"立即打开抖音" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * _Nonnull action) { + NSURL *douyinURL = [NSURL URLWithString:kDouyinScheme]; + if ([[UIApplication sharedApplication] canOpenURL:douyinURL]) { + [[UIApplication sharedApplication] openURL:douyinURL options:@{} completionHandler:nil]; + } + }]; + + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"稍后分享" + style:UIAlertActionStyleCancel + handler:nil]; + + [alert addAction:openAction]; + [alert addAction:cancelAction]; + + // 获取顶层视图控制器 + UIViewController *topVC = [self getTopViewController]; + if (topVC) { + [topVC presentViewController:alert animated:YES completion:nil]; + } + }); +} + ++ (UIViewController *)getTopViewController { + UIViewController *topViewController = [UIApplication sharedApplication].keyWindow.rootViewController; + while (topViewController.presentedViewController) { + topViewController = topViewController.presentedViewController; + } + return topViewController; +} + +@end diff --git a/msext/Class/Utils/QQAppIDValidator.m b/msext/Class/Utils/QQAppIDValidator.m new file mode 100644 index 0000000..b2b74f0 --- /dev/null +++ b/msext/Class/Utils/QQAppIDValidator.m @@ -0,0 +1,222 @@ +// +// QQAppIDValidator.m +// msext +// +// Created on 2025/06/16. +// Copyright © 2025年. All rights reserved. +// + +#import +#import +#import "QQShareManager.h" + +/** + * QQ AppID配置验证工具 + * 用于验证无SDK方式的QQ分享配置是否正确 + */ +@interface QQAppIDValidator : NSObject + +/** + * 执行完整的QQ分享配置检查 + * 包括AppID、URL Schemes、回调配置等 + */ ++ (void)performCompleteValidation; + +/** + * 显示配置检查结果 + */ ++ (void)showValidationResults; + +/** + * 生成QQ分享测试URL用于调试 + */ ++ (NSString *)generateTestQQShareURL; + +@end + +@implementation QQAppIDValidator + ++ (void)performCompleteValidation { + NSLog(@"\n🔍 开始QQ分享配置验证...\n"); + + // 1. 检查QQ安装状态 + BOOL isQQInstalled = [QQShareManager isQQInstalled]; + NSLog(@"1️⃣ QQ安装检查: %@", isQQInstalled ? @"✅ 已安装" : @"❌ 未安装"); + + // 2. 检查AppID配置 + BOOL isAppIDValid = [QQShareManager validateQQAppIDConfiguration]; + NSString *appID = [QQShareManager getCurrentQQAppID]; + NSLog(@"2️⃣ AppID配置检查: %@", isAppIDValid ? [NSString stringWithFormat:@"✅ 有效 (%@)", appID] : @"❌ 无效"); + + // 3. 检查URL Schemes配置 + [self validateURLSchemes]; + + // 4. 检查回调配置 + [self validateCallbackConfiguration]; + + // 5. 生成测试URL + NSString *testURL = [self generateTestQQShareURL]; + NSLog(@"5️⃣ 测试分享URL: %@", testURL); + + NSLog(@"\n✅ QQ分享配置验证完成\n"); +} + ++ (void)validateURLSchemes { + NSArray *requiredSchemes = @[@"mqqapi", @"mqq", @"mqqOpensdkSSoLogin", @"mqqopensdkapiV2", @"mqqopensdkapiV3", @"mqqopensdkapiV4", @"wtloginmqq2", @"mqzone"]; + + BOOL allSchemesConfigured = YES; + NSMutableArray *missingSchemes = [NSMutableArray array]; + + for (NSString *scheme in requiredSchemes) { + NSString *testURLString = [NSString stringWithFormat:@"%@://test", scheme]; + NSURL *testURL = [NSURL URLWithString:testURLString]; + + if (![[UIApplication sharedApplication] canOpenURL:testURL]) { + allSchemesConfigured = NO; + [missingSchemes addObject:scheme]; + } + } + + if (allSchemesConfigured) { + NSLog(@"3️⃣ URL Schemes配置: ✅ 完整"); + } else { + NSLog(@"3️⃣ URL Schemes配置: ❌ 缺少 %@", [missingSchemes componentsJoinedByString:@", "]); + NSLog(@" 请在Info.plist的LSApplicationQueriesSchemes中添加缺少的schemes"); + } +} + ++ (void)validateCallbackConfiguration { + // 检查msext回调scheme是否配置 + NSURL *callbackURL = [NSURL URLWithString:@"msext://test"]; + NSBundle *mainBundle = [NSBundle mainBundle]; + NSDictionary *infoPlist = [mainBundle infoDictionary]; + NSArray *urlTypes = infoPlist[@"CFBundleURLTypes"]; + + BOOL callbackConfigured = NO; + for (NSDictionary *urlType in urlTypes) { + NSArray *schemes = urlType[@"CFBundleURLSchemes"]; + if ([schemes containsObject:@"msext"]) { + callbackConfigured = YES; + break; + } + } + + NSLog(@"4️⃣ 回调配置检查: %@", callbackConfigured ? @"✅ 正确" : @"❌ 缺失msext scheme"); + if (!callbackConfigured) { + NSLog(@" 请在Info.plist的CFBundleURLTypes中添加msext scheme配置"); + } +} + ++ (NSString *)generateTestQQShareURL { + NSString *appID = [QQShareManager getCurrentQQAppID]; + if (!appID) { + return @"❌ 无法生成测试URL:AppID未配置"; + } + + // 构建基础的QQ分享测试URL + NSMutableString *testURL = [NSMutableString stringWithString:@"mqqapi://share/to_fri?"]; + [testURL appendString:@"version=1"]; + [testURL appendString:@"&cflag=0"]; + [testURL appendString:@"&src_type=app"]; + [testURL appendString:@"&sdkv=2.9.0"]; + [testURL appendString:@"&sdkp=i"]; + [testURL appendFormat:@"&appid=%@", appID]; + [testURL appendString:@"&app_name=进贤聚友棋牌"]; + [testURL appendString:@"&callback_type=scheme"]; + [testURL appendString:@"&callback_name=msext"]; + [testURL appendString:@"&req_type=0"]; + [testURL appendString:@"&title=测试标题"]; + [testURL appendString:@"&description=测试描述"]; + + return testURL; +} + ++ (void)showValidationResults { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"QQ分享配置验证" + message:@"请查看控制台日志以获取详细的验证结果" + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *validateAction = [UIAlertAction actionWithTitle:@"重新验证" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self performCompleteValidation]; + }]; + + UIAlertAction *testAction = [UIAlertAction actionWithTitle:@"测试分享" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self performTestShare]; + }]; + + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"关闭" + style:UIAlertActionStyleCancel + handler:nil]; + + [alert addAction:validateAction]; + [alert addAction:testAction]; + [alert addAction:cancelAction]; + + UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController; + UIViewController *topVC = [self topViewController:rootVC]; + [topVC presentViewController:alert animated:YES completion:nil]; + }); +} + ++ (void)performTestShare { + NSLog(@"🧪 开始QQ分享测试..."); + + [QQShareManager shareToQQFriend:QQShareTypeText + title:@"QQ分享配置测试" + description:@"如果您看到这条消息,说明QQ分享配置正确" + thumbImage:nil + url:nil + image:nil + completion:^(BOOL success) { + NSLog(@"🧪 QQ分享测试结果: %@", success ? @"✅ 成功" : @"❌ 失败"); + + dispatch_async(dispatch_get_main_queue(), ^{ + NSString *message = success ? @"QQ分享测试成功!配置正确。" : @"QQ分享测试失败!请检查配置。"; + UIAlertController *resultAlert = [UIAlertController alertControllerWithTitle:@"测试结果" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; + [resultAlert addAction:okAction]; + + UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController; + UIViewController *topVC = [self topViewController:rootVC]; + [topVC presentViewController:resultAlert animated:YES completion:nil]; + }); + }]; +} + ++ (UIViewController *)topViewController:(UIViewController *)rootViewController { + if (rootViewController.presentedViewController == nil) { + return rootViewController; + } + + if ([rootViewController.presentedViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController; + UIViewController *lastViewController = [[navigationController viewControllers] lastObject]; + return [self topViewController:lastViewController]; + } + + if ([rootViewController.presentedViewController isKindOfClass:[UITabBarController class]]) { + UITabBarController *tabBarController = (UITabBarController *)rootViewController.presentedViewController; + UIViewController *selectedViewController = tabBarController.selectedViewController; + return [self topViewController:selectedViewController]; + } + + return [self topViewController:rootViewController.presentedViewController]; +} + +@end + +// 便捷调用方法 +void validateQQShareConfiguration(void) { + [QQAppIDValidator performCompleteValidation]; +} + +void showQQShareValidationDialog(void) { + [QQAppIDValidator showValidationResults]; +} diff --git a/msext/Class/Utils/QQShareManager.h b/msext/Class/Utils/QQShareManager.h new file mode 100644 index 0000000..cf70707 --- /dev/null +++ b/msext/Class/Utils/QQShareManager.h @@ -0,0 +1,179 @@ +// +// QQShareManager.h +// msext +// +// Created on 2025/06/15. +// Copyright © 2025年. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, QQShareType) { + QQShareTypeText, // 纯文本分享 + QQShareTypeImage, // 图片分享 + QQShareTypeNews, // 新闻/网页分享 + QQShareTypeAudio, // 音频分享 + QQShareTypeVideo // 视频分享 +}; + +@interface QQShareManager : NSObject + +/** + * 检查QQ是否已安装 + * @return BOOL 是否已安装QQ + */ ++ (BOOL)isQQInstalled; + +/** + * 验证QQ AppID配置是否正确 + * @return BOOL AppID是否有效配置 + */ ++ (BOOL)validateQQAppIDConfiguration; + +/** + * 获取当前配置的QQ AppID + * @return NSString 当前的QQ AppID,如果未配置返回nil + */ ++ (NSString * _Nullable)getCurrentQQAppID; + +/** + * 分享到QQ好友 + * @param type 分享类型 + * @param title 标题 + * @param description 描述 + * @param thumbImage 缩略图 + * @param url 链接URL + * @param image 图片(仅图片分享时使用) + * @param completion 完成回调 + */ ++ (void)shareToQQFriend:(QQShareType)type + title:(NSString * _Nullable)title + description:(NSString * _Nullable)description + thumbImage:(UIImage * _Nullable)thumbImage + url:(NSString * _Nullable)url + image:(UIImage * _Nullable)image + completion:(void(^_Nullable)(BOOL success))completion; + +/** + * 分享到QQ空间 + * @param type 分享类型 + * @param title 标题 + * @param description 描述 + * @param thumbImage 缩略图 + * @param url 链接URL + * @param images 图片数组(可多张图片) + * @param completion 完成回调 + */ ++ (void)shareToQZone:(QQShareType)type + title:(NSString * _Nullable)title + description:(NSString * _Nullable)description + thumbImage:(UIImage * _Nullable)thumbImage + url:(NSString * _Nullable)url + images:(NSArray * _Nullable)images + completion:(void(^_Nullable)(BOOL success))completion; + +/** + * 简化版QQ分享到好友(解决900101错误) + * 使用最基础的参数格式,避免复杂参数导致的错误 + * @param title 标题 + * @param description 描述 + * @param url 链接URL(可为nil) + * @param completion 完成回调 + */ ++ (void)simpleShareToQQFriend:(NSString * _Nullable)title + description:(NSString * _Nullable)description + url:(NSString * _Nullable)url + completion:(void(^_Nullable)(BOOL success))completion; + +/** + * 使用系统分享面板进行QQ分享(最可靠的方案) + * @param title 分享标题 + * @param description 分享描述 + * @param url 分享链接 + * @param viewController 用于呈现分享面板的视图控制器 + * @param completion 分享完成回调 + */ ++ (void)shareWithSystemShare:(NSString * _Nullable)title + description:(NSString * _Nullable)description + url:(NSString * _Nullable)url + fromViewController:(UIViewController *)viewController + completion:(void (^ _Nullable)(BOOL success))completion; + +/** + * 使用系统分享面板进行QQ分享(自动获取顶层视图控制器) + * @param title 分享标题 + * @param description 分享描述 + * @param url 分享链接 + * @param completion 分享完成回调 + */ ++ (void)shareWithSystemShareAuto:(NSString * _Nullable)title + description:(NSString * _Nullable)description + url:(NSString * _Nullable)url + completion:(void (^ _Nullable)(BOOL success))completion; + +/** + * 尝试强制弹出QQ会话选择列表 + * 使用多种URL格式尝试打开QQ的好友选择界面 + * @param completion 完成回调 + */ ++ (void)forceOpenQQSessionSelector:(void(^_Nullable)(BOOL success))completion; + +/** + * 使用多种方式尝试QQ分享 + * 按优先级尝试不同的QQ分享方式,直到找到有效的 + * @param title 标题 + * @param description 描述 + * @param completion 完成回调 + */ ++ (void)tryMultipleQQShareMethods:(NSString * _Nullable)title + description:(NSString * _Nullable)description + completion:(void(^_Nullable)(BOOL success))completion; + +/** + * 处理从QQ返回的URL + * 需要在 AppDelegate 的 application:openURL:options: 方法中调用 + */ ++ (BOOL)handleOpenURL:(NSURL *)url; + +/** + * 验证和修复URL格式 + * @param url 原始URL字符串 + * @return 修复后的有效URL,如果无法修复返回nil + */ ++ (NSString * _Nullable)validateAndFixURL:(NSString * _Nullable)url; + +/** + * 获取应用启动图用作分享缩略图(已弃用,建议使用getAppIcon) + * @return 启动图UIImage对象 + */ ++ (UIImage * _Nullable)getAppLaunchImage; + +/** + * 获取应用桌面图标用作分享缩略图 + * @return 应用桌面图标UIImage对象 + */ ++ (UIImage * _Nullable)getAppIcon; + +/** + * 检查是否可以安全呈现系统分享面板 + * @return BOOL 是否可以安全呈现 + */ ++ (BOOL)canSafelyPresentSharePanel; + +/** + * 使用系统分享面板进行QQ分享(传递完整ShareContent对象) + * 支持根据type字段自动选择分享类型: + * - type=2:截图分享(包含屏幕截图+文字说明) + * - 其他:纯文本分享(文字+链接+应用图标) + * @param shareContent 完整的分享内容对象,需包含title、desc、webpageUrl、type字段 + * @param completion 分享完成回调 + */ ++ (void)shareWithSystemShareContent:(id)shareContent + completion:(void (^ _Nullable)(BOOL success))completion; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/msext/Class/Utils/QQShareManager.m b/msext/Class/Utils/QQShareManager.m new file mode 100644 index 0000000..86a46a1 --- /dev/null +++ b/msext/Class/Utils/QQShareManager.m @@ -0,0 +1,1474 @@ +// +// QQShareManager.m +// msext +// +// Created on 2025/06/15. +// Copyright © 2025年. All rights reserved. +// + +#import "QQShareManager.h" +#import "FuncPublic.h" // 用于截图功能 + +// QQ URL Schemes +#define kQQScheme @"mqqapi://" +#define kQQShareScheme @"mqqapi://share/" +#define kQQFriendScheme @"mqqapi://share/to_fri?" +#define kQQZoneScheme @"mqqapi://share/to_qzone?" +#define kQQUniversalScheme @"mqq://share/to_fri?" // 通用分享scheme,无需AppID验证 + +// QQ 分享必需的参数 +#define kQQAppName @"进贤聚友棋牌" // 应用名称,与Info.plist中的CFBundleDisplayName和CFBundleName保持一致 +#define kQQCallbackScheme @"msext" // 回调scheme +#define kQQAppID @"102793577" // 您当前的AppID +#define kQQFallbackAppID @"100312206" // 腾讯官方测试AppID,通常可用 + +// QQ 应用回调的处理 +static void(^QQShareCompletion)(BOOL) = nil; + +@implementation QQShareManager + +#pragma mark - Public Methods + ++ (BOOL)isQQInstalled { + return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:kQQScheme]]; +} + ++ (BOOL)validateQQAppIDConfiguration { + NSLog(@"🔍 开始验证QQ AppID配置..."); + NSLog(@"🔍 当前AppID值:%@", kQQAppID); + NSLog(@"🔍 AppID长度:%lu", (unsigned long)[kQQAppID length]); + + // 检查AppID是否配置 + if (!kQQAppID || [kQQAppID isEqualToString:@"YOUR_QQ_APPID"]) { + NSLog(@"❌ QQ AppID 未配置!请在 QQShareManager.m 中设置正确的 kQQAppID"); + return NO; + } + + // 检查AppID格式(应该是纯数字,长度在8-10位之间) + NSCharacterSet *nonDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet]; + if ([kQQAppID rangeOfCharacterFromSet:nonDigits].location != NSNotFound) { + NSLog(@"❌ QQ AppID 格式错误!AppID应该是纯数字,当前:%@", kQQAppID); + return NO; + } + + // 检查长度(现代QQ AppID通常是8-10位数字) + if (kQQAppID.length < 8 || kQQAppID.length > 10) { + NSLog(@"❌ QQ AppID 长度错误!AppID应该是8-10位数字,当前长度:%lu,AppID:%@", (unsigned long)kQQAppID.length, kQQAppID); + return NO; + } + + NSLog(@"✅ QQ AppID 配置正确:%@", kQQAppID); + NSLog(@"⚠️ 重要提示:如果出现900101错误,请确认:"); + NSLog(@"⚠️ 1. AppID是否已在QQ开放平台审核通过"); + NSLog(@"⚠️ 2. Bundle ID是否与申请时一致"); + NSLog(@"⚠️ 3. 应用是否已上线或处于测试白名单"); + return YES; +} + ++ (NSString *)getCurrentQQAppID { + if ([self validateQQAppIDConfiguration]) { + return kQQAppID; + } + return nil; +} + ++ (void)shareToQQFriend:(QQShareType)type + title:(NSString *)title + description:(NSString *)description + thumbImage:(UIImage *)thumbImage + url:(NSString *)url + image:(UIImage *)image + completion:(void (^)(BOOL))completion { + + // 检查QQ是否已安装 + if (![self isQQInstalled]) { + if (completion) { + completion(NO); + } + [self showAppNotInstalledAlert:@"QQ"]; + return; + } + + // 验证AppID配置(解决9000101错误的关键) + if (![self validateQQAppIDConfiguration]) { + if (completion) { + completion(NO); + } + [self showAppIDConfigurationErrorAlert]; + return; + } + + // 保存回调 + QQShareCompletion = completion; + + // 构建URL参数 - 使用QQ官方标准格式解决900101错误 + NSMutableString *urlString = [NSMutableString stringWithString:kQQFriendScheme]; + + // QQ官方要求的标准参数顺序和格式 + [urlString appendString:@"version=1"]; + [urlString appendString:@"&cflag=0"]; + [urlString appendString:@"&src_type=app"]; + + // 尝试使用QQ最新推荐的参数格式 + [urlString appendFormat:@"&thirdAppDisplayName=%@", [self encodeString:kQQAppName]]; + [urlString appendFormat:@"&app_id=%@", kQQAppID]; + [urlString appendFormat:@"&sdkv=2.9.0"]; + [urlString appendFormat:@"&sdkp=i"]; + + // 回调相关参数 + [urlString appendString:@"&callback_type=scheme"]; + [urlString appendFormat:@"&callback_name=%@", [self encodeString:kQQCallbackScheme]]; + + // 获取Bundle ID用于验证 + NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier]; + + // 添加详细调试日志 + NSLog(@"🔍 ======== QQ分享参数详情 ========"); + NSLog(@"🔍 AppID: %@", kQQAppID); + NSLog(@"🔍 Bundle ID: %@", bundleId); + NSLog(@"🔍 AppName: %@", kQQAppName); + NSLog(@"🔍 CallbackScheme: %@", kQQCallbackScheme); + NSLog(@"🔍 QQ Friend Scheme: %@", kQQFriendScheme); + + // 根据分享类型设置不同的参数 + switch (type) { + case QQShareTypeText: + [urlString appendString:@"&req_type=0"]; + if (title && title.length > 0) { + [urlString appendFormat:@"&title=%@", [self encodeString:title]]; + } + if (description && description.length > 0) { + [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + } + break; + + case QQShareTypeImage: + if (image) { + // 将图片保存到本地临时目录 + NSString *imagePath = [self saveImageToTempDirectory:image]; + [urlString appendString:@"&req_type=2"]; + [urlString appendFormat:@"&image_url=%@", [self encodeString:[@"file://" stringByAppendingString:imagePath]]]; + if (title && title.length > 0) { + [urlString appendFormat:@"&title=%@", [self encodeString:title]]; + } + if (description && description.length > 0) { + [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + } + } else { + if (completion) { + completion(NO); + } + return; + } + break; + + case QQShareTypeNews: + [urlString appendString:@"&req_type=1"]; + if (title && title.length > 0) { + [urlString appendFormat:@"&title=%@", [self encodeString:title]]; + } + if (description && description.length > 0) { + [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + } + if (url && url.length > 0) { + [urlString appendFormat:@"&url=%@", [self encodeString:url]]; + } + // 将缩略图保存到本地 + if (thumbImage) { + NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; + [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]]; + } + break; + + case QQShareTypeAudio: + [urlString appendString:@"&req_type=3"]; + if (title && title.length > 0) { + [urlString appendFormat:@"&title=%@", [self encodeString:title]]; + } + if (description && description.length > 0) { + [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + } + if (url && url.length > 0) { + [urlString appendFormat:@"&url=%@", [self encodeString:url]]; + } + if (thumbImage) { + NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; + [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]]; + } + break; + + case QQShareTypeVideo: + [urlString appendString:@"&req_type=4"]; + if (title && title.length > 0) { + [urlString appendFormat:@"&title=%@", [self encodeString:title]]; + } + if (description && description.length > 0) { + [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + } + if (url && url.length > 0) { + [urlString appendFormat:@"&url=%@", [self encodeString:url]]; + } + if (thumbImage) { + NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; + [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]]; + } + break; + } + + // 详细的URL调试日志 + NSLog(@"🔍 ======== 完整QQ分享URL ========"); + NSLog(@"🔍 URL: %@", urlString); + NSLog(@"🔍 URL长度: %lu", (unsigned long)[urlString length]); + + // 检查关键参数是否正确 + if ([urlString containsString:[NSString stringWithFormat:@"app_id=%@", kQQAppID]]) { + NSLog(@"✅ AppID参数已包含"); + } else { + NSLog(@"❌ AppID参数缺失!"); + } + + NSURL *qqURL = [NSURL URLWithString:urlString]; + if ([[UIApplication sharedApplication] canOpenURL:qqURL]) { + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:qqURL options:@{} completionHandler:^(BOOL success) { + // 这里不调用completion,等待QQ返回再调用 + NSLog(@"QQ分享调用结果: %@", success ? @"成功" : @"失败"); + }]; + } else { + [[UIApplication sharedApplication] openURL:qqURL]; + } + } else { + if (completion) { + completion(NO); + } + } +} + ++ (void)shareToQZone:(QQShareType)type + title:(NSString *)title + description:(NSString *)description + thumbImage:(UIImage *)thumbImage + url:(NSString *)url + images:(NSArray *)images + completion:(void (^)(BOOL))completion { + + // 检查QQ是否已安装 + if (![self isQQInstalled]) { + if (completion) { + completion(NO); + } + [self showAppNotInstalledAlert:@"QQ"]; + return; + } + + // 验证AppID配置(解决9000101错误的关键) + if (![self validateQQAppIDConfiguration]) { + if (completion) { + completion(NO); + } + [self showAppIDConfigurationErrorAlert]; + return; + } + + // 保存回调 + QQShareCompletion = completion; + + // 构建URL参数 - 修复QQ空间分享格式 + NSMutableString *urlString = [NSMutableString stringWithString:kQQZoneScheme]; + + // 基础必需参数,严格按照QQ要求的格式和顺序 + [urlString appendString:@"version=1"]; + [urlString appendString:@"&cflag=0"]; + [urlString appendFormat:@"&app_id=%@", kQQAppID]; // AppID必须在前面位置 + [urlString appendString:@"&src_type=app"]; + [urlString appendString:@"&sdkv=2.9.0"]; + [urlString appendString:@"&sdkp=i"]; + + // 应用信息参数 - AppID是解决9000101错误的关键参数 + [urlString appendFormat:@"&appid=%@", kQQAppID]; // QQ AppID - 必需参数 + [urlString appendFormat:@"&app_name=%@", [self encodeString:kQQAppName]]; + [urlString appendString:@"&callback_type=scheme"]; + [urlString appendFormat:@"&callback_name=%@", [self encodeString:kQQCallbackScheme]]; + + // 添加内容参数 + if (title && title.length > 0) { + [urlString appendFormat:@"&title=%@", [self encodeString:title]]; + } + if (description && description.length > 0) { + [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + } + if (url && url.length > 0) { + [urlString appendFormat:@"&url=%@", [self encodeString:url]]; + } + + // 处理图片 + if (images && images.count > 0) { + NSMutableArray *imagePaths = [NSMutableArray array]; + + for (UIImage *image in images) { + NSString *imagePath = [self saveImageToTempDirectory:image]; + [imagePaths addObject:[@"file://" stringByAppendingString:imagePath]]; + } + + if (imagePaths.count > 0) { + [urlString appendFormat:@"&imageUrl=%@", [self encodeString:[imagePaths componentsJoinedByString:@","]]]; + } + } else if (thumbImage) { + NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; + [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]]; + } + + // 设置分享类型 + switch (type) { + case QQShareTypeText: + [urlString appendString:@"&req_type=0"]; + break; + case QQShareTypeImage: + [urlString appendString:@"&req_type=2"]; + break; + case QQShareTypeNews: + [urlString appendString:@"&req_type=1"]; + break; + case QQShareTypeAudio: + [urlString appendString:@"&req_type=3"]; + break; + case QQShareTypeVideo: + [urlString appendString:@"&req_type=4"]; + break; + } + + // 打开URL + NSLog(@"QQ空间分享URL: %@", urlString); + NSURL *qzoneURL = [NSURL URLWithString:urlString]; + if ([[UIApplication sharedApplication] canOpenURL:qzoneURL]) { + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:qzoneURL options:@{} completionHandler:^(BOOL success) { + // 这里不调用completion,等待QQ返回再调用 + }]; + } else { + [[UIApplication sharedApplication] openURL:qzoneURL]; + } + } else { + if (completion) { + completion(NO); + } + } +} + ++ (BOOL)handleOpenURL:(NSURL *)url { + // 判断是否是QQ返回的URL + NSString *urlString = [url absoluteString]; + NSLog(@"QQ分享回调URL: %@", urlString); + + if ([urlString hasPrefix:@"msext://"]) { + // 解析返回的参数 + NSString *errorDescription = [self getUrlParam:urlString paramName:@"error_description"]; + NSString *error = [self getUrlParam:urlString paramName:@"error"]; + + BOOL success = YES; + if (error && ![error isEqualToString:@"0"]) { + success = NO; + NSLog(@"QQ分享失败: error=%@, description=%@", error, errorDescription); + } else { + NSLog(@"QQ分享成功"); + } + + // 执行回调 + if (QQShareCompletion) { + QQShareCompletion(success); + QQShareCompletion = nil; + } + return YES; + } + return NO; +} + +#pragma mark - 简化版QQ分享方法(解决900101错误) + +/** + * 简化版QQ分享方法,使用最基础的参数避免900101错误 + */ ++ (void)simpleShareToQQFriend:(NSString *)title + description:(NSString *)description + url:(NSString *)url + completion:(void(^)(BOOL success))completion { + + if (![self isQQInstalled]) { + if (completion) { + completion(NO); + } + [self showAppNotInstalledAlert:@"QQ"]; + return; + } + + // 保存回调 + QQShareCompletion = completion; + + // 使用最简单的URL格式,避免复杂参数导致的900101错误 + NSMutableString *urlString = [NSMutableString stringWithString:@"mqqapi://share/to_fri?"]; + + // 最基础的必需参数 + [urlString appendString:@"version=1"]; + [urlString appendString:@"&cflag=0"]; + + // 分享类型 - 根据是否有URL决定 + if (url && url.length > 0) { + [urlString appendString:@"&req_type=1"]; // 网页分享 + [urlString appendFormat:@"&url=%@", [self encodeString:url]]; + } else { + [urlString appendString:@"&req_type=0"]; // 文本分享 + } + + // 内容参数 + if (title && title.length > 0) { + [urlString appendFormat:@"&title=%@", [self encodeString:title]]; + } + if (description && description.length > 0) { + [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + } + + // 打开URL + NSLog(@"简化QQ分享URL: %@", urlString); + NSURL *qqURL = [NSURL URLWithString:urlString]; + if ([[UIApplication sharedApplication] canOpenURL:qqURL]) { + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:qqURL options:@{} completionHandler:^(BOOL success) { + NSLog(@"简化QQ分享调用结果: %@", success ? @"成功" : @"失败"); + }]; + } else { + [[UIApplication sharedApplication] openURL:qqURL]; + } + } else { + if (completion) { + completion(NO); + } + } +} + ++ (void)forceOpenQQSessionSelector:(void(^)(BOOL success))completion { + NSLog(@"尝试强制弹出QQ会话选择列表..."); + + // 检查QQ是否安装 + if (![self isQQInstalled]) { + NSLog(@"QQ未安装"); + if (completion) completion(NO); + return; + } + + // 尝试的URL方案,按优先级排序 + NSArray *urlTemplates = @[ + // 方案1: 使用好友分享接口 + @"mqqopensdkfriend://share?src_type=internal&version=1&app_name=%@&title=%@", + + // 方案2: 直接打开聊天选择界面 + @"mqq://im/chat?chat_type=select", + + // 方案3: 打开好友列表 + @"mqq://contact/friend_list", + + // 方案4: 打开最近会话 + @"mqq://im/recent", + + // 方案5: 使用分享到好友 + @"mqqapi://share/to_fri?file_type=text&file_data=%@&app_name=%@", + + // 方案6: 简化的card接口 + @"mqqapi://card/show_pslcard?src_type=internal&version=1&app_name=%@&req_type=0&title=%@" + ]; + + NSString *appName = [self encodeString:kQQAppName]; + NSString *defaultTitle = [self encodeString:@"分享内容"]; + + // 逐个尝试每种方案 + [self tryURLSchemes:urlTemplates appName:appName title:defaultTitle index:0 completion:completion]; +} + ++ (void)tryURLSchemes:(NSArray *)urlTemplates + appName:(NSString *)appName + title:(NSString *)title + index:(NSInteger)index + completion:(void(^)(BOOL success))completion { + + if (index >= urlTemplates.count) { + NSLog(@"所有URL方案都尝试失败"); + if (completion) completion(NO); + return; + } + + NSString *template = urlTemplates[index]; + NSString *urlString; + + // 根据模板格式构造URL + if ([template containsString:@"chat_type=select"] || + [template containsString:@"friend_list"] || + [template containsString:@"im/recent"]) { + // 不需要参数的URL + urlString = template; + } else if ([template containsString:@"file_data"]) { + // 需要两个参数的URL + urlString = [NSString stringWithFormat:template, title, appName]; + } else { + // 需要app_name和title的URL + urlString = [NSString stringWithFormat:template, appName, title]; + } + + NSURL *url = [NSURL URLWithString:urlString]; + NSLog(@"尝试方案%ld: %@", (long)(index + 1), urlString); + + if ([[UIApplication sharedApplication] canOpenURL:url]) { + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) { + if (success) { + NSLog(@"方案%ld 成功打开QQ", (long)(index + 1)); + + // 延迟检查是否真的打开了会话选择 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (completion) completion(YES); + }); + } else { + NSLog(@"方案%ld 打开失败,尝试下一个", (long)(index + 1)); + [self tryURLSchemes:urlTemplates appName:appName title:title index:index + 1 completion:completion]; + } + }]; + } else { + [[UIApplication sharedApplication] openURL:url]; + if (completion) completion(YES); + } + } else { + NSLog(@"方案%ld 不支持,尝试下一个", (long)(index + 1)); + [self tryURLSchemes:urlTemplates appName:appName title:title index:index + 1 completion:completion]; + } +} + ++ (void)tryMultipleQQShareMethods:(NSString *)title + description:(NSString *)description + completion:(void(^)(BOOL success))completion { + + NSLog(@"使用多种方式尝试QQ分享..."); + + // 检查QQ是否安装 + if (![self isQQInstalled]) { + NSLog(@"QQ未安装"); + if (completion) completion(NO); + return; + } + + // 方法1: 先尝试强制弹出会话选择器 + [self forceOpenQQSessionSelector:^(BOOL success) { + if (success) { + NSLog(@"成功弹出会话选择器"); + if (completion) completion(YES); + } else { + NSLog(@"会话选择器失败,尝试简化分享"); + + // 方法2: 使用简化分享 + [self simpleShareToQQFriend:title description:description url:nil completion:^(BOOL simpleSuccess) { + if (simpleSuccess) { + NSLog(@"简化分享成功"); + if (completion) completion(YES); + } else { + NSLog(@"简化分享也失败,使用标准分享"); + + // 方法3: 使用标准分享 + [self shareToQQFriend:QQShareTypeText + title:title + description:description + thumbImage:nil + url:nil + image:nil + completion:completion]; + } + }]; + } + }]; +} + ++ (void)shareWithSystemShare:(NSString *)title + description:(NSString *)description + url:(NSString *)url + fromViewController:(UIViewController *)viewController + completion:(void (^)(BOOL))completion { + + // 准备分享内容数组 + NSMutableArray *shareItems = [NSMutableArray array]; + + // 组合完整的分享文本(不包含URL) + NSMutableString *shareText = [NSMutableString string]; + + // 添加标题 + if (title && title.length > 0) { + [shareText appendString:title]; + } + + // 添加描述 + if (description && description.length > 0) { + if (shareText.length > 0) { + [shareText appendString:@"\n"]; // 使用单个换行符,更紧凑 + } + [shareText appendString:description]; + } + + // 添加组合文本到分享项目(在组合完成后添加) + if (shareText.length > 0) { + [shareItems addObject:[shareText copy]]; + NSLog(@"🔍 ✅ 添加组合文本: %@", shareText); + } + + + + // 统一使用固定的分享URL,不管传入的URL是什么 + NSString *fixedShareURL = @"http://game.hudong.com"; + NSURL *shareURL = [NSURL URLWithString:fixedShareURL]; + if (shareURL) { + [shareItems addObject:shareURL]; + NSLog(@"🔍 ✅ 添加固定分享URL: %@", fixedShareURL); + } + + // 添加应用桌面图标作为缩略图 + UIImage *appIcon = [self getAppIcon]; + if (appIcon) { + [shareItems addObject:appIcon]; + NSLog(@"🔍 ✅ 添加应用桌面图标作为缩略图"); + } + + // 如果没有任何分享内容,使用默认文本 + if (shareItems.count == 0) { + [shareItems addObject:@"来自进贤聚友棋牌的精彩内容分享"]; + NSURL *defaultURL = [NSURL URLWithString:fixedShareURL]; + if (defaultURL) { + [shareItems addObject:defaultURL]; + } + } + + // 详细的调试日志 + NSLog(@"🔍 ======== 系统分享内容详情 ========"); + NSLog(@"🔍 原始标题: %@", title ?: @"(无)"); + NSLog(@"🔍 原始描述: %@", description ?: @"(无)"); + NSLog(@"🔍 传入URL: %@", url ?: @"(无)"); + NSLog(@"🔍 实际分享URL: http://game.hudong.com (固定)"); + NSLog(@"🔍 分享文本: %@", shareText.length > 0 ? shareText : @"(无)"); + NSLog(@"🔍 分享项目数量: %lu", (unsigned long)shareItems.count); + + // 打印所有分享项目 + for (NSInteger i = 0; i < shareItems.count; i++) { + id item = shareItems[i]; + if ([item isKindOfClass:[NSString class]]) { + NSLog(@"🔍 项目%ld (文本): %@", (long)i+1, item); + } else if ([item isKindOfClass:[NSURL class]]) { + NSLog(@"🔍 项目%ld (URL): %@", (long)i+1, [(NSURL *)item absoluteString]); + } else { + NSLog(@"🔍 项目%ld (其他): %@", (long)i+1, item); + } + } + NSLog(@"🔍 ====================================="); + + // 创建系统分享控制器 + UIActivityViewController *activityVC = [[UIActivityViewController alloc] + initWithActivityItems:shareItems + applicationActivities:nil]; + + // 排除一些不常用的分享选项(可选,提高QQ等主要应用的显示优先级) + NSArray *excludedActivityTypes = @[ + UIActivityTypePostToVimeo, + UIActivityTypePostToFlickr, + UIActivityTypePostToTencentWeibo, + UIActivityTypeAssignToContact, + UIActivityTypeSaveToCameraRoll, + UIActivityTypeAddToReadingList, + UIActivityTypePostToFacebook, + UIActivityTypePostToTwitter + ]; + activityVC.excludedActivityTypes = excludedActivityTypes; + + // 设置完成回调 + activityVC.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { + NSLog(@"🔍 ======== 系统分享结果详情 ========"); + NSLog(@"🔍 分享结果: %@", completed ? @"✅ 成功" : @"❌ 取消/失败"); + NSLog(@"🔍 分享到应用: %@", activityType ?: @"(未知)"); + + if (activityError) { + NSLog(@"🔍 分享错误: %@", activityError.localizedDescription); + + // 特别处理 RunningBoard 相关错误 + if ([activityError.domain isEqualToString:@"RBSServiceErrorDomain"]) { + NSLog(@"🔍 ⚠️ 检测到RunningBoard权限错误,这通常是系统级权限问题"); + NSLog(@"🔍 ⚠️ 建议检查应用是否在前台,以及设备权限设置"); + } + } + + // 特别标记QQ分享 + if (activityType && ([activityType containsString:@"QQ"] || [activityType containsString:@"qq"])) { + NSLog(@"🔍 ✅ QQ分享完成"); + } + + NSLog(@"🔍 ====================================="); + + if (completion) { + completion(completed); + } + }; + + // 在iPad上需要设置popover + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { + activityVC.popoverPresentationController.sourceView = viewController.view; + activityVC.popoverPresentationController.sourceRect = CGRectMake(viewController.view.bounds.size.width/2, viewController.view.bounds.size.height/2, 0, 0); + activityVC.popoverPresentationController.permittedArrowDirections = 0; + } + + // 确保在主线程呈现分享面板,避免RunningBoard权限错误 + dispatch_async(dispatch_get_main_queue(), ^{ + // 再次检查视图控制器是否有效 + if (viewController && viewController.view.window) { + NSLog(@"🔍 在主线程呈现系统分享面板"); + [viewController presentViewController:activityVC animated:YES completion:^{ + NSLog(@"🔍 ✅ 系统分享面板呈现完成"); + }]; + } else { + NSLog(@"🔍 ❌ 视图控制器无效或不在窗口层次结构中"); + if (completion) { + completion(NO); + } + } + }); +} + ++ (void)shareWithSystemShareAuto:(NSString *)title + description:(NSString *)description + url:(NSString *)url + completion:(void (^)(BOOL))completion { + + // 检查是否可以安全呈现分享面板 + if (![self canSafelyPresentSharePanel]) { + NSLog(@"🔍 ❌ 当前条件不允许呈现分享面板,避免RunningBoard错误"); + if (completion) { + completion(NO); + } + return; + } + + UIViewController *topViewController = [self getTopViewController]; + if (!topViewController) { + NSLog(@"❌ 无法获取顶层视图控制器,无法弹出系统分享面板"); + if (completion) { + completion(NO); + } + return; + } + + [self shareWithSystemShare:title + description:description + url:url + fromViewController:topViewController + completion:completion]; +} + +#pragma mark - Private Methods + ++ (UIViewController *)getTopViewController { + // 检查应用状态,避免在后台或非活跃状态访问UI + UIApplicationState appState = [UIApplication sharedApplication].applicationState; + if (appState != UIApplicationStateActive) { + NSLog(@"🔍 ⚠️ 应用不在活跃状态 (状态: %ld),可能导致权限错误", (long)appState); + // 仍然尝试获取,但记录警告 + } + + UIWindow *keyWindow = nil; + + // iOS 13+ 获取keyWindow的方式 + if (@available(iOS 13.0, *)) { + NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes; + NSLog(@"🔍 当前连接的Scene数量: %lu", (unsigned long)connectedScenes.count); + + for (UIWindowScene *windowScene in connectedScenes) { + NSLog(@"🔍 Scene状态: %ld", (long)windowScene.activationState); + if (windowScene.activationState == UISceneActivationStateForegroundActive) { + NSLog(@"🔍 找到前台活跃的WindowScene"); + for (UIWindow *window in windowScene.windows) { + NSLog(@"🔍 检查Window: isKeyWindow=%d, isHidden=%d", window.isKeyWindow, window.isHidden); + if (window.isKeyWindow && !window.isHidden) { + keyWindow = window; + NSLog(@"🔍 ✅ 找到活跃的keyWindow"); + break; + } + } + if (keyWindow) break; + } + } + } + + // 兼容iOS 13以下版本 + if (!keyWindow) { + NSLog(@"🔍 使用传统方式获取keyWindow"); + keyWindow = [UIApplication sharedApplication].keyWindow; + } + + // 如果还是没有,尝试获取第一个可见window + if (!keyWindow) { + NSLog(@"🔍 尝试获取第一个可见window"); + NSArray *windows = [UIApplication sharedApplication].windows; + for (UIWindow *window in windows) { + if (!window.isHidden && window.rootViewController) { + keyWindow = window; + NSLog(@"🔍 使用备选window"); + break; + } + } + } + + if (!keyWindow) { + NSLog(@"🔍 ❌ 无法获取任何有效的window,这可能导致RunningBoard权限错误"); + return nil; + } + + // 检查window是否有rootViewController + if (!keyWindow.rootViewController) { + NSLog(@"🔍 ❌ keyWindow没有rootViewController"); + return nil; + } + + // 获取顶层视图控制器 + UIViewController *topViewController = keyWindow.rootViewController; + NSLog(@"🔍 根视图控制器: %@", NSStringFromClass([topViewController class])); + + // 遍历presented视图控制器 + int presentedCount = 0; + while (topViewController.presentedViewController && presentedCount < 10) { // 防止无限循环 + topViewController = topViewController.presentedViewController; + presentedCount++; + NSLog(@"🔍 Presented视图控制器 %d: %@", presentedCount, NSStringFromClass([topViewController class])); + } + + // 处理UINavigationController + if ([topViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navController = (UINavigationController *)topViewController; + topViewController = navController.topViewController; + NSLog(@"🔍 导航控制器顶层: %@", NSStringFromClass([topViewController class])); + } + + // 处理UITabBarController + if ([topViewController isKindOfClass:[UITabBarController class]]) { + UITabBarController *tabController = (UITabBarController *)topViewController; + topViewController = tabController.selectedViewController; + NSLog(@"🔍 标签控制器选中: %@", NSStringFromClass([topViewController class])); + + // 再次检查是否是NavigationController + if ([topViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navController = (UINavigationController *)topViewController; + topViewController = navController.topViewController; + NSLog(@"🔍 标签内导航控制器顶层: %@", NSStringFromClass([topViewController class])); + } + } + + // 最终验证 + if (topViewController && topViewController.view.window) { + NSLog(@"🔍 ✅ 成功获取有效的顶层视图控制器: %@", NSStringFromClass([topViewController class])); + } else { + NSLog(@"🔍 ⚠️ 获取的视图控制器可能无效或不在window层次结构中"); + } + + return topViewController; +} + ++ (BOOL)canSafelyPresentSharePanel { + NSLog(@"🔍 ======== 检查分享面板呈现条件 ========"); + + // 1. 检查应用状态 + UIApplicationState appState = [UIApplication sharedApplication].applicationState; + NSLog(@"🔍 应用状态: %ld (0=Active, 1=Inactive, 2=Background)", (long)appState); + + if (appState != UIApplicationStateActive) { + NSLog(@"🔍 ❌ 应用不在活跃状态,无法安全呈现分享面板"); + return NO; + } + + // 2. 检查是否有有效的视图控制器 + UIViewController *topVC = [self getTopViewController]; + if (!topVC) { + NSLog(@"🔍 ❌ 无法获取顶层视图控制器"); + return NO; + } + + if (!topVC.view.window) { + NSLog(@"🔍 ❌ 视图控制器不在window层次结构中"); + return NO; + } + + // 3. 检查是否已经有模态视图 + if (topVC.presentedViewController) { + NSLog(@"🔍 ❌ 已有模态视图控制器正在呈现: %@", NSStringFromClass([topVC.presentedViewController class])); + return NO; + } + + // 4. 检查是否在主线程 + if (![NSThread isMainThread]) { + NSLog(@"🔍 ❌ 不在主线程,可能导致RunningBoard错误"); + return NO; + } + + // 5. iOS 13+ Scene状态检查 + if (@available(iOS 13.0, *)) { + BOOL hasActiveScene = NO; + NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes; + + for (UIWindowScene *windowScene in connectedScenes) { + if ([windowScene isKindOfClass:[UIWindowScene class]] && + windowScene.activationState == UISceneActivationStateForegroundActive) { + hasActiveScene = YES; + break; + } + } + + if (!hasActiveScene) { + NSLog(@"🔍 ❌ 没有活跃的前台Scene"); + return NO; + } + } + + NSLog(@"🔍 ✅ 所有条件满足,可以安全呈现分享面板"); + NSLog(@"🔍 ====================================="); + return YES; +} + ++ (NSString *)encodeString:(NSString *)string { + if (!string) return @""; + return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; +} + ++ (NSString *)saveImageToTempDirectory:(UIImage *)image { + NSData *imageData = UIImageJPEGRepresentation(image, 0.7); + NSString *fileName = [NSString stringWithFormat:@"qq_share_%@.jpg", [self generateUUID]]; + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; + + [imageData writeToFile:filePath atomically:YES]; + return filePath; +} + ++ (NSString *)generateUUID { + CFUUIDRef uuid = CFUUIDCreate(NULL); + CFStringRef uuidString = CFUUIDCreateString(NULL, uuid); + NSString *result = (__bridge_transfer NSString *)uuidString; + CFRelease(uuid); + return result; +} + ++ (NSString *)getUrlParam:(NSString *)url paramName:(NSString *)name { + NSError *error; + NSString *regTags = [[NSString alloc] initWithFormat:@"(^|&|\\?)%@=([^&]*)(&|$)", name]; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regTags options:NSRegularExpressionCaseInsensitive error:&error]; + + NSTextCheckingResult *match = [regex firstMatchInString:url options:0 range:NSMakeRange(0, [url length])]; + if (match) { + NSString *paramValue = [url substringWithRange:[match rangeAtIndex:2]]; + return paramValue; + } + + return nil; +} + ++ (void)showAppNotInstalledAlert:(NSString *)appName { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"%@未安装", appName] + message:[NSString stringWithFormat:@"您的设备未安装%@客户端,无法进行分享", appName] + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; + [alertController addAction:okAction]; + + UIViewController *topVC = [self topViewController]; + [topVC presentViewController:alertController animated:YES completion:nil]; + }); +} + ++ (void)showAppIDConfigurationErrorAlert { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"QQ分享配置错误" + message:@"QQ AppID配置错误,请联系开发者检查配置" + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; + [alertController addAction:okAction]; + + UIViewController *topVC = [self topViewController]; + [topVC presentViewController:alertController animated:YES completion:nil]; + }); +} + ++ (UIViewController *)topViewController { + UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; + while (rootViewController.presentedViewController) { + rootViewController = rootViewController.presentedViewController; + } + + if ([rootViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)rootViewController; + return navigationController.visibleViewController; + } + + if ([rootViewController isKindOfClass:[UITabBarController class]]) { + UITabBarController *tabBarController = (UITabBarController *)rootViewController; + UIViewController *selectedViewController = tabBarController.selectedViewController; + + if ([selectedViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)selectedViewController; + return navigationController.visibleViewController; + } + + return selectedViewController; + } + + return rootViewController; +} + ++ (UIImage *)getAppLaunchImage { + // 保留此方法以备将来需要,但现在主要使用桌面图标 + NSLog(@"🔍 ⚠️ 建议使用 getAppIcon 方法获取桌面图标"); + return [self getAppIcon]; +} + ++ (UIImage *)getAppIcon { + // 尝试获取应用桌面图标 + UIImage *appIcon = nil; + + // 方法1: 从Info.plist获取图标信息 + NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary]; + + // iOS 13+ 支持的新格式 + NSDictionary *icons = infoPlist[@"CFBundleIcons"]; + NSDictionary *primaryIcon = icons[@"CFBundlePrimaryIcon"]; + NSArray *iconFiles = primaryIcon[@"CFBundleIconFiles"]; + + if (iconFiles && iconFiles.count > 0) { + // 按优先级尝试不同尺寸的图标(从大到小) + NSArray *preferredSizes = @[@"180", @"120", @"60"]; + + for (NSString *size in preferredSizes) { + for (NSString *iconName in iconFiles) { + NSString *testName = [NSString stringWithFormat:@"%@%@", iconName, size]; + appIcon = [UIImage imageNamed:testName]; + if (appIcon) { + NSLog(@"🔍 ✅ 获取应用图标成功: %@", testName); + return appIcon; + } + } + } + + // 尝试原始名称 + for (NSString *iconName in iconFiles) { + appIcon = [UIImage imageNamed:iconName]; + if (appIcon) { + NSLog(@"🔍 ✅ 获取应用图标成功: %@", iconName); + return appIcon; + } + } + } + + // 方法2: 尝试常见的图标名称 + NSArray *commonIconNames = @[ + @"Icon-180", // iPhone @3x (180x180) + @"Icon180", // 常见命名 + @"Icon-120", // iPhone @2x (120x120) + @"Icon120", // 常见命名 + @"Icon-60", // iPhone @1x (60x60) + @"Icon60", // 常见命名 + @"AppIcon60x60@3x", + @"AppIcon60x60@2x", + @"AppIcon60x60", + @"AppIcon", // 通用名称 + @"Icon", // 简单名称 + @"icon" // 小写 + ]; + + for (NSString *iconName in commonIconNames) { + appIcon = [UIImage imageNamed:iconName]; + if (appIcon) { + NSLog(@"🔍 ✅ 获取应用图标成功: %@", iconName); + return appIcon; + } + } + + // 方法3: 从App Bundle中搜索图标文件 + NSBundle *mainBundle = [NSBundle mainBundle]; + NSArray *imageExtensions = @[@"png", @"jpg", @"jpeg"]; + + for (NSString *ext in imageExtensions) { + NSArray *iconPaths = [mainBundle pathsForResourcesOfType:ext inDirectory:nil]; + for (NSString *path in iconPaths) { + NSString *filename = [[path lastPathComponent] stringByDeletingPathExtension]; + // 检查文件名是否包含icon相关关键词 + if ([filename.lowercaseString containsString:@"icon"] || + [filename.lowercaseString containsString:@"appicon"]) { + appIcon = [UIImage imageWithContentsOfFile:path]; + if (appIcon) { + NSLog(@"🔍 ✅ 从Bundle获取应用图标: %@", filename); + return appIcon; + } + } + } + } + + NSLog(@"🔍 ⚠️ 无法获取应用桌面图标,创建默认图标"); + return [self createDefaultAppIcon]; +} + ++ (UIImage *)createDefaultAppIcon { + // 创建一个简单的默认应用图标 + CGSize iconSize = CGSizeMake(120, 120); + UIGraphicsBeginImageContextWithOptions(iconSize, NO, [UIScreen mainScreen].scale); + + // 设置渐变背景色(模拟应用图标的效果) + CGContextRef context = UIGraphicsGetCurrentContext(); + + // 创建渐变色 + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGFloat colors[] = { + 0.2, 0.6, 1.0, 1.0, // 蓝色 + 0.1, 0.4, 0.8, 1.0 // 深蓝色 + }; + CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, colors, NULL, 2); + + // 绘制渐变背景 + CGPoint startPoint = CGPointMake(0, 0); + CGPoint endPoint = CGPointMake(iconSize.width, iconSize.height); + CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0); + + // 释放资源 + CGGradientRelease(gradient); + CGColorSpaceRelease(colorSpace); + + // 添加圆角效果(模拟iOS图标圆角) + UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, iconSize.width, iconSize.height) cornerRadius:iconSize.width * 0.2]; + [roundedRect addClip]; + + // 添加文字 + NSString *text = @"聚友\n棋牌"; + NSDictionary *attributes = @{ + NSFontAttributeName: [UIFont boldSystemFontOfSize:18], + NSForegroundColorAttributeName: [UIColor whiteColor], + NSParagraphStyleAttributeName: ({ + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + style.alignment = NSTextAlignmentCenter; + style; + }) + }; + + CGSize textSize = [text boundingRectWithSize:iconSize + options:NSStringDrawingUsesLineFragmentOrigin + attributes:attributes + context:nil].size; + + CGRect textRect = CGRectMake((iconSize.width - textSize.width) / 2, + (iconSize.height - textSize.height) / 2, + textSize.width, + textSize.height); + + [text drawInRect:textRect withAttributes:attributes]; + + UIImage *defaultIcon = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + NSLog(@"🔍 ✅ 创建默认应用图标"); + return defaultIcon; +} + ++ (NSString *)validateAndFixURL:(NSString *)url { + if (!url || url.length == 0) { + return nil; + } + + // 移除首尾空格 + NSString *trimmedURL = [url stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + NSLog(@"🔍 开始验证URL: %@", trimmedURL); + + // 检查是否是自定义scheme(如http://sharegame/...) + if ([trimmedURL hasPrefix:@"http://sharegame/"] || [trimmedURL hasPrefix:@"https://sharegame/"]) { + // 将自定义的sharegame域名替换为合法的域名 + NSString *fixedURL = [trimmedURL stringByReplacingOccurrencesOfString:@"http://sharegame/" withString:@"https://game.example.com/"]; + fixedURL = [fixedURL stringByReplacingOccurrencesOfString:@"https://sharegame/" withString:@"https://game.example.com/"]; + NSLog(@"🔍 修复自定义域名URL: %@ -> %@", trimmedURL, fixedURL); + return fixedURL; + } + + // 检查是否已经有协议头 + if (![trimmedURL hasPrefix:@"http://"] && ![trimmedURL hasPrefix:@"https://"] && ![trimmedURL hasPrefix:@"ftp://"]) { + // 如果没有协议头,添加https:// + NSString *fixedURL = [@"https://" stringByAppendingString:trimmedURL]; + NSLog(@"🔍 添加协议头: %@ -> %@", trimmedURL, fixedURL); + return fixedURL; + } + + // 验证URL是否有效 + NSURL *testURL = [NSURL URLWithString:trimmedURL]; + if (testURL && testURL.scheme && testURL.host) { + NSLog(@"🔍 ✅ URL格式有效: %@", trimmedURL); + return trimmedURL; + } + + // 如果URL无效,尝试进行URL编码 + NSString *encodedURL = [trimmedURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + if (encodedURL) { + NSURL *encodedTestURL = [NSURL URLWithString:encodedURL]; + if (encodedTestURL && encodedTestURL.scheme && encodedTestURL.host) { + NSLog(@"🔍 ✅ URL编码后有效: %@ -> %@", trimmedURL, encodedURL); + return encodedURL; + } + } + + NSLog(@"🔍 ❌ URL无法修复,将作为文本处理: %@", trimmedURL); + return nil; +} + ++ (void)shareWithSystemShareContent:(id)shareContent + completion:(void (^ _Nullable)(BOOL success))completion { + NSLog(@"🔍 [QQShareManager] 开始使用ShareContent对象进行系统分享"); + + // 主线程检查 + if (![NSThread isMainThread]) { + NSLog(@"⚠️ [QQShareManager] 检测到非主线程调用,切换到主线程"); + dispatch_async(dispatch_get_main_queue(), ^{ + [self shareWithSystemShareContent:shareContent completion:completion]; + }); + return; + } + + // 权限检查 + if (![self canSafelyPresentSharePanel]) { + NSLog(@"❌ [QQShareManager] 无法安全呈现系统分享面板"); + if (completion) { + completion(NO); + } + return; + } + + // 检查ShareContent对象 + if (!shareContent) { + NSLog(@"❌ [QQShareManager] ShareContent对象为空"); + if (completion) { + completion(NO); + } + return; + } + + // 通过KVC从ShareContent对象中提取参数 + NSString *title = nil; + NSString *description = nil; + NSString *url = nil; + NSString *type = nil; + + @try { + // 尝试获取标题 + if ([shareContent respondsToSelector:@selector(valueForKey:)]) { + title = [shareContent valueForKey:@"title"]; + description = [shareContent valueForKey:@"desc"]; + url = [shareContent valueForKey:@"webpageUrl"]; + type = [shareContent valueForKey:@"type"]; + } + + NSLog(@"🔍 [QQShareManager] 从ShareContent提取参数:"); + NSLog(@"🔍 - 标题: %@", title ?: @"(无)"); + NSLog(@"🔍 - 描述: %@", description ?: @"(无)"); + NSLog(@"🔍 - 链接: %@", url ?: @"(无)"); + NSLog(@"🔍 - 类型: %@", type ?: @"(无)"); + + } @catch (NSException *exception) { + NSLog(@"❌ [QQShareManager] 解析ShareContent时发生异常: %@", exception.reason); + if (completion) { + completion(NO); + } + return; + } + + // 检查分享类型,决定分享方式 + BOOL isScreenshotShare = [type intValue] == 2; + NSLog(@"🔍 [QQShareManager] 分享类型: %@", isScreenshotShare ? @"截图分享" : @"文本分享"); + + // 设置默认值 + if (!title || title.length == 0) { + title = isScreenshotShare ? @"游戏截图分享" : @"游戏分享"; + } + if (!description || description.length == 0) { + description = isScreenshotShare ? @"精彩游戏画面分享" : @"精彩游戏内容分享"; + } + + // 构建分享内容项 + NSMutableArray *shareItems = [NSMutableArray array]; + + if (isScreenshotShare) { + // 截图分享模式 + NSLog(@"🔍 [QQShareManager] 执行截图分享"); + + // 获取屏幕截图 + UIImage *screenImage = [FuncPublic getImageWithFullScreenshot]; + if (screenImage) { + [shareItems addObject:screenImage]; + NSLog(@"🔍 [QQShareManager] 添加屏幕截图,尺寸: %.0fx%.0f", screenImage.size.width, screenImage.size.height); + description = @"精彩游戏画面分享"; + // 为截图添加文字说明 + NSString *combinedText = nil; + if (title && description) { + combinedText = [NSString stringWithFormat:@"%@\n%@", title, description]; + } else if (title) { + combinedText = title; + } else if (description) { + combinedText = description; + } + + if (combinedText) { + [shareItems addObject:combinedText]; + NSLog(@"🔍 [QQShareManager] 添加截图说明文字: %@", combinedText); + } + } else { + NSLog(@"❌ [QQShareManager] 截图获取失败,降级为文本分享"); + isScreenshotShare = NO; // 降级处理 + } + } + + if (!isScreenshotShare) { + // 纯文本分享模式(真正的纯文本,不包含URL和图标) + NSLog(@"🔍 [QQShareManager] 执行纯文本分享(仅文本内容)"); + + // 组合完整的分享文本 + NSMutableString *shareText = [NSMutableString string]; + + // 添加标题 + if (title && title.length > 0) { + [shareText appendString:title]; + } + + // 添加描述 + if (description && description.length > 0) { + if (shareText.length > 0) { + [shareText appendString:@"\n\n"]; // 使用单个换行符 + } + [shareText appendString:description]; + } + + // 仅添加组合文本到分享项目(纯文本分享) + if (shareText.length > 0) { + [shareItems addObject:[shareText copy]]; + NSLog(@"🔍 [QQShareManager] 添加纯文本内容: %@", shareText); + } else { + // 如果没有任何文本,使用默认文本 + [shareItems addObject:@"来自进贤聚友棋牌的精彩内容分享"]; + NSLog(@"🔍 [QQShareManager] 使用默认纯文本内容"); + } + + // 纯文本分享:不添加URL,不添加应用图标 + NSLog(@"🔍 [QQShareManager] 纯文本分享模式:不包含URL和图标"); + } + + // 检查分享内容 + if (shareItems.count == 0) { + NSLog(@"❌ [QQShareManager] 没有有效的分享内容"); + if (completion) { + completion(NO); + } + return; + } + + // 详细的调试日志 + NSLog(@"🔍 ======== ShareContent系统分享详情 ========"); + NSLog(@"🔍 分享类型: %@", isScreenshotShare ? @"截图分享" : @"纯文本分享"); + NSLog(@"🔍 原始标题: %@", title ?: @"(无)"); + NSLog(@"🔍 原始描述: %@", description ?: @"(无)"); + NSLog(@"🔍 传入URL: %@", url ?: @"(无)"); + if (!isScreenshotShare) { + NSLog(@"🔍 纯文本模式:不包含URL和图标"); + } + NSLog(@"🔍 type字段: %@", type ?: @"(无)"); + NSLog(@"🔍 分享项目数量: %lu", (unsigned long)shareItems.count); + + // 打印所有分享项目(与shareWithSystemShare保持一致) + for (NSInteger i = 0; i < shareItems.count; i++) { + id item = shareItems[i]; + if ([item isKindOfClass:[NSString class]]) { + NSLog(@"🔍 项目%ld (文本): %@", (long)i+1, item); + } else if ([item isKindOfClass:[NSURL class]]) { + NSLog(@"🔍 项目%ld (URL): %@", (long)i+1, [(NSURL *)item absoluteString]); + } else if ([item isKindOfClass:[UIImage class]]) { + UIImage *image = (UIImage *)item; + NSLog(@"🔍 项目%ld (图片): %.0fx%.0f", (long)i+1, image.size.width, image.size.height); + } else { + NSLog(@"🔍 项目%ld (其他): %@", (long)i+1, item); + } + } + NSLog(@"🔍 ========================================="); + + NSLog(@"🔍 [QQShareManager] 准备分享 %lu 个项目", (unsigned long)shareItems.count); + + // 获取顶层视图控制器 + UIViewController *topViewController = [self getTopViewController]; + if (!topViewController) { + NSLog(@"❌ [QQShareManager] 无法获取顶层视图控制器"); + if (completion) { + completion(NO); + } + return; + } + + // 创建UIActivityViewController + UIActivityViewController *activityVC = [[UIActivityViewController alloc] + initWithActivityItems:shareItems + applicationActivities:nil]; + + // 排除不需要的分享选项(可选) + activityVC.excludedActivityTypes = @[ + UIActivityTypeAssignToContact, + UIActivityTypePostToVimeo, + UIActivityTypePrint, + UIActivityTypeAirDrop // 排除AirDrop避免意外分享 + ]; + + // 设置完成回调(与shareWithSystemShare保持一致的日志格式) + activityVC.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { + NSLog(@"🔍 ======== ShareContent分享结果详情 ========"); + NSLog(@"🔍 分享结果: %@", completed ? @"✅ 成功" : @"❌ 取消/失败"); + NSLog(@"🔍 分享到应用: %@", activityType ?: @"(未知)"); + + if (activityError) { + NSLog(@"🔍 分享错误: %@", activityError.localizedDescription); + + // 特别处理 RunningBoard 相关错误(与shareWithSystemShare一致) + if ([activityError.domain isEqualToString:@"RBSServiceErrorDomain"]) { + NSLog(@"🔍 ⚠️ 检测到RunningBoard权限错误,这通常是系统级权限问题"); + NSLog(@"🔍 ⚠️ 建议检查应用是否在前台,以及设备权限设置"); + } + + if (completion) { + completion(NO); + } + return; + } + + // 特别标记QQ分享(与shareWithSystemShare一致) + if (activityType && ([activityType containsString:@"QQ"] || [activityType containsString:@"qq"])) { + NSLog(@"🔍 ✅ QQ分享完成"); + } + + NSLog(@"🔍 ========================================="); + + if (completion) { + completion(completed); + } + }; + + // iPad适配 - 设置popover + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + if (activityVC.popoverPresentationController) { + activityVC.popoverPresentationController.sourceView = topViewController.view; + activityVC.popoverPresentationController.sourceRect = CGRectMake( + topViewController.view.bounds.size.width / 2, + topViewController.view.bounds.size.height / 2, + 1, 1 + ); + activityVC.popoverPresentationController.permittedArrowDirections = 0; + } + } + + // 呈现分享面板 + [topViewController presentViewController:activityVC animated:YES completion:^{ + NSLog(@"🔍 [QQShareManager] 系统分享面板已呈现"); + }]; +} + +@end \ No newline at end of file diff --git a/msext/Class/Utils/SharePanel.h b/msext/Class/Utils/SharePanel.h new file mode 100644 index 0000000..bf3ab27 --- /dev/null +++ b/msext/Class/Utils/SharePanel.h @@ -0,0 +1,84 @@ +// +// SharePanel.h +// msext +// +// Created on 2025/06/15. +// Copyright © 2025年. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// 分享类型枚举 +typedef NS_ENUM(NSInteger, ShareType) { + ShareTypeWeChat, // 微信分享 + ShareTypeQQ, // QQ分享 + ShareTypeDouyin // 抖音分享 +}; + +// 分享内容模型 +@interface ShareContent : NSObject + +@property (nonatomic, copy) NSString *title; // 标题 +@property (nonatomic, copy) NSString *desc; // 描述 (原名 description 与 NSObject 方法冲突) +@property (nonatomic, copy) NSString *webpageUrl; // 链接 +@property (nonatomic, copy) NSString *sharefriend; // 好友分享,朋友圈分享 +@property (nonatomic, copy) NSString *type; // 好友分享,朋友圈分享 + + +@end + +// 分享面板回调 +typedef void (^SharePanelCompletionBlock)(ShareType type, BOOL success); + +@interface SharePanel : UIView + +/** + * 显示分享面板 + * @param content 分享内容 + * @param completion 分享完成后的回调 + */ ++ (void)showWithContent:(ShareContent *)content + completion:(nullable SharePanelCompletionBlock)completion; + +/** + * 显示分享面板(使用字典数据) + * @param data 包含分享内容的字典 + * @param completion 分享完成后的回调 + * + * 字典key说明: + * - title: 标题 + * - description: 描述 + * - link: 链接URL + * - thumbImage: 缩略图 (UIImage对象) + * - image: 分享图片 (UIImage对象, 仅图片分享时使用) + * - videoPath: 视频路径 (仅视频分享时使用) + */ ++ (void)showWithDictionary:(NSDictionary *)data + completion:(nullable SharePanelCompletionBlock)completion; + +/** + * 隐藏分享面板 + */ ++ (void)dismiss; + +/** + * QQ分享到好友 + * @param content 分享内容 + * @param completion 分享完成后的回调 + */ ++ (void)shareToQQFriend:(ShareContent *)content + completion:(nullable SharePanelCompletionBlock)completion; + +/** + * QQ分享到空间 + * @param content 分享内容 + * @param completion 分享完成后的回调 + */ ++ (void)shareToQZone:(ShareContent *)content + completion:(nullable SharePanelCompletionBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/msext/Class/Utils/SharePanel.m b/msext/Class/Utils/SharePanel.m new file mode 100644 index 0000000..85b29d8 --- /dev/null +++ b/msext/Class/Utils/SharePanel.m @@ -0,0 +1,662 @@ +// +// SharePanel.m +// msext +// +// Created on 2025/06/15. +// Copyright © 2025年. All rights reserved. +// + +#import "SharePanel.h" +#import "WechatShareManager.h" // 微信分享管理器 +#import "QQShareManager.h" // QQ分享管理器 +#import "DouyinShareManager.h" // 抖音分享管理器 + +// 面板常量 +#define kPanelHeight 180.0f +#define kButtonSize 60.0f +#define kButtonMargin 30.0f +#define kButtonTopMargin 40.0f +#define kAnimationDuration 0.25f + +@interface SharePanel() + +@property (nonatomic, strong) UIView *contentView; +@property (nonatomic, strong) UIButton *wechatButton; +@property (nonatomic, strong) UIButton *qqButton; +@property (nonatomic, strong) UIButton *douyinButton; +@property (nonatomic, strong) UILabel *wechatLabel; +@property (nonatomic, strong) UILabel *qqLabel; +@property (nonatomic, strong) UILabel *douyinLabel; + +@property (nonatomic, strong) ShareContent *shareContent; +@property (nonatomic, copy) SharePanelCompletionBlock completionBlock; + +// 私有方法声明 +- (UIViewController *)getTopViewController; + +@end + +static SharePanel *sharedPanel = nil; + +@implementation ShareContent +@end + +@implementation SharePanel + +#pragma mark - Lifecycle + ++ (instancetype)sharedPanel { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedPanel = [[self alloc] initWithFrame:CGRectZero]; + }); + return sharedPanel; +} + +- (instancetype)initWithFrame:(CGRect)frame { + CGRect screenBounds = [UIScreen mainScreen].bounds; + self = [super initWithFrame:screenBounds]; + + if (self) { + // 设置背景为半透明黑色 + self.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; + + // 添加点击手势 + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleBackgroundTap:)]; + [self addGestureRecognizer:tapGesture]; + + // 创建内容视图 + _contentView = [[UIView alloc] initWithFrame:CGRectMake(0, screenBounds.size.height, screenBounds.size.width, kPanelHeight)]; + _contentView.backgroundColor = [UIColor whiteColor]; + + // 圆角设置 + UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_contentView.bounds + byRoundingCorners:UIRectCornerTopLeft|UIRectCornerTopRight + cornerRadii:CGSizeMake(10.0, 10.0)]; + CAShapeLayer *maskLayer = [CAShapeLayer layer]; + maskLayer.frame = _contentView.bounds; + maskLayer.path = maskPath.CGPath; + _contentView.layer.mask = maskLayer; + + [self addSubview:_contentView]; + + // 创建分享按钮 + [self setupShareButtons]; + } + + return self; +} + +#pragma mark - UI Setup + +- (void)setupShareButtons { + CGFloat contentWidth = self.contentView.frame.size.width; + CGFloat startX = (contentWidth - (3 * kButtonSize + 2 * kButtonMargin)) / 2; + + // 微信分享按钮 + _wechatButton = [self createButtonWithImage:@"shareWechat" atIndex:0 startX:startX]; + + // 绑定点击事件为先关闭面板,然后执行微信分享 + [_wechatButton addTarget:self action:@selector(handleButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + _wechatButton.tag = ShareTypeWeChat; + [self.contentView addSubview:_wechatButton]; + + // 微信分享文字 + _wechatLabel = [self createLabelWithText:@"微信" forButton:_wechatButton]; + [self.contentView addSubview:_wechatLabel]; + + // QQ分享按钮 + _qqButton = [self createButtonWithImage:@"shareQQ" atIndex:1 startX:startX]; + + // 绑定点击事件为先关闭面板,然后执行QQ分享 + [_qqButton addTarget:self action:@selector(handleButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + _qqButton.tag = ShareTypeQQ; + [self.contentView addSubview:_qqButton]; + + // QQ分享文字 + _qqLabel = [self createLabelWithText:@"QQ" forButton:_qqButton]; + [self.contentView addSubview:_qqLabel]; + + // 抖音分享按钮 + _douyinButton = [self createButtonWithImage:@"shareDouyin" atIndex:2 startX:startX]; + + // 绑定点击事件为先关闭面板,然后执行抖音分享 + [_douyinButton addTarget:self action:@selector(handleButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + _douyinButton.tag = ShareTypeDouyin; + [self.contentView addSubview:_douyinButton]; + + // 抖音分享文字 + _douyinLabel = [self createLabelWithText:@"抖音" forButton:_douyinButton]; + [self.contentView addSubview:_douyinLabel]; + + // 添加取消按钮 + UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeSystem]; + cancelButton.frame = CGRectMake(0, kPanelHeight - 40, contentWidth, 40); + [cancelButton setTitle:@"取消" forState:UIControlStateNormal]; + [cancelButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; + cancelButton.titleLabel.font = [UIFont systemFontOfSize:16]; + [cancelButton addTarget:self action:@selector(cancelAction) forControlEvents:UIControlEventTouchUpInside]; + [self.contentView addSubview:cancelButton]; + + // 添加分隔线 + UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(0, kPanelHeight - 41, contentWidth, 1)]; + lineView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0]; + [self.contentView addSubview:lineView]; +} + +- (UIButton *)createButtonWithImage:(NSString *)imageName atIndex:(NSInteger)index startX:(CGFloat)startX { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + CGFloat x = startX + index * (kButtonSize + kButtonMargin); + button.frame = CGRectMake(x, kButtonTopMargin, kButtonSize, kButtonSize); + [button setImage:[UIImage imageNamed:imageName] forState:UIControlStateNormal]; + button.layer.cornerRadius = kButtonSize / 2; + button.clipsToBounds = YES; + return button; +} + +- (UILabel *)createLabelWithText:(NSString *)text forButton:(UIButton *)button { + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, kButtonSize, 20)]; + label.center = CGPointMake(button.center.x, button.frame.origin.y + button.frame.size.height + 15); + label.text = text; + label.textColor = [UIColor darkGrayColor]; + label.font = [UIFont systemFontOfSize:12]; + label.textAlignment = NSTextAlignmentCenter; + return label; +} + +#pragma mark - Public Methods + ++ (void)showWithContent:(ShareContent *)content completion:(SharePanelCompletionBlock)completion { + SharePanel *panel = [SharePanel sharedPanel]; + panel.shareContent = content; + panel.completionBlock = completion; + + UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; + [keyWindow addSubview:panel]; + + [UIView animateWithDuration:kAnimationDuration animations:^{ + CGRect frame = panel.contentView.frame; + frame.origin.y = [UIScreen mainScreen].bounds.size.height - kPanelHeight; + panel.contentView.frame = frame; + panel.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; + }]; +} + ++ (void)showWithDictionary:(NSDictionary *)data completion:(SharePanelCompletionBlock)completion { + // 创建ShareContent对象并从字典中提取数据 + ShareContent *content = [[ShareContent alloc] init]; + + // 提取文本数据objectForKey + +// content.title = data[@"title"];//标题 +// content.desc = data[@"description"]; // 更改为新的属性名 desc +// content.webpageUrl = data[@"webpageUrl"];//网页链接 +// content.type = data[@"type"];//类型 1-网页/文字 2-截图 +// content.sharefriend = data[@"sharefriend"];// 是否分享给好友 + content.title = [data objectForKey:@"title"];//标题 + content.desc = [data objectForKey:@"description"]; // 更改为新的属性名 desc + content.webpageUrl = [data objectForKey:@"webpageUrl"];//网页链接 + content.type = [data objectForKey:@"type"];//类型 1-网页/文字 2-截图 + content.sharefriend = [data objectForKey: @"sharefriend"];// 是否分享给好友 + + + // 使用现有方法显示分享面板 + [self showWithContent:content completion:completion]; +} + ++ (void)dismiss { + SharePanel *panel = [SharePanel sharedPanel]; + //[panel removeFromSuperview]; + [UIView animateWithDuration:kAnimationDuration animations:^{ + CGRect frame = panel.contentView.frame; + frame.origin.y = [UIScreen mainScreen].bounds.size.height; + panel.contentView.frame = frame; + panel.backgroundColor = [UIColor colorWithWhite:0 alpha:0]; + } completion:^(BOOL finished) { + [panel removeFromSuperview]; + }]; +} + ++ (void)dismissWithCompletion:(void (^)(void))completion { + SharePanel *panel = [SharePanel sharedPanel]; + [UIView animateWithDuration:kAnimationDuration animations:^{ + CGRect frame = panel.contentView.frame; + frame.origin.y = [UIScreen mainScreen].bounds.size.height; + panel.contentView.frame = frame; + panel.backgroundColor = [UIColor colorWithWhite:0 alpha:0]; + } completion:^(BOOL finished) { + [panel removeFromSuperview]; + if (completion) { + completion(); + } + }]; +} + +#pragma mark - Actions + +- (void)handleBackgroundTap:(UITapGestureRecognizer *)gesture { + CGPoint point = [gesture locationInView:self]; + if (!CGRectContainsPoint(self.contentView.frame, point)) { + [SharePanel dismiss]; + } +} + +- (void)cancelAction { + [SharePanel dismiss]; +} + +- (void)wechatShareAction { + [self handleWechatShareAction]; +} + +- (void)qqShareAction { + [self handleQQShareAction]; +} + +- (void)douyinShareAction { + [self handleDouyinShareAction]; +} + +#pragma mark - Button Action Handlers + +- (void)handleButtonClick:(UIButton *)sender { + ShareType shareType = (ShareType)sender.tag; + + // 先关闭面板,在面板完全关闭后再执行对应的分享操作 + __weak typeof(self) weakSelf = self; + [SharePanel dismissWithCompletion:^{ + switch (shareType) { + case ShareTypeWeChat: + [weakSelf handleWechatShareAction]; + break; + case ShareTypeQQ: + [weakSelf handleQQShareAction]; + break; + case ShareTypeDouyin: + [weakSelf handleDouyinShareAction]; + break; + default: + break; + } + }]; +} + +- (void)handleWechatShareAction { + NSLog(@"🔍 [SharePanel] 处理微信分享"); + + ShareContent *content = self.shareContent; + SharePanelCompletionBlock completionBlock = self.completionBlock; + + // 使用WechatShareManager处理微信分享 + [WechatShareManager shareWithContent:content + completion:^(BOOL success) { + NSLog(@"微信分享结果: %@", success ? @"✅ 成功" : @"❌ 失败"); + if (completionBlock) { + completionBlock(ShareTypeWeChat, success); + } + }]; +} + +- (void)handleQQShareAction { + NSLog(@"🔍 [SharePanel] 处理QQ分享"); + + ShareContent *content = self.shareContent; + SharePanelCompletionBlock completionBlock = self.completionBlock; + + // 使用QQShareManager处理QQ分享 + [QQShareManager shareWithSystemShareContent:content + completion:^(BOOL success) { + NSLog(@"QQ分享结果: %@", success ? @"✅ 成功" : @"❌ 失败"); + if (completionBlock) { + completionBlock(ShareTypeQQ, success); + } + }]; +} + +- (void)handleDouyinShareAction { + NSLog(@"🔍 [SharePanel] 处理抖音分享"); + + ShareContent *content = self.shareContent; + SharePanelCompletionBlock completionBlock = self.completionBlock; + + // 使用DouyinShareManager处理抖音分享 + [DouyinShareManager shareWithContent:content + completion:^(BOOL success) { + NSLog(@"抖音分享结果: %@", success ? @"✅ 成功" : @"❌ 失败"); + if (completionBlock) { + completionBlock(ShareTypeDouyin, success); + } + }]; +} + +#pragma mark - Helper Methods + +- (void)showErrorAlertWithMessage:(NSString *)message { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"分享失败" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; + [alert addAction:okAction]; + + UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController; + UIViewController *topVC = [self topViewController:rootVC]; + [topVC presentViewController:alert animated:YES completion:nil]; +} + +- (UIViewController *)topViewController:(UIViewController *)rootViewController { + if (rootViewController.presentedViewController == nil) { + return rootViewController; + } + + if ([rootViewController.presentedViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController; + UIViewController *lastViewController = [[navigationController viewControllers] lastObject]; + return [self topViewController:lastViewController]; + } + + if ([rootViewController.presentedViewController isKindOfClass:[UITabBarController class]]) { + UITabBarController *tabBarController = (UITabBarController *)rootViewController.presentedViewController; + UIViewController *selectedViewController = tabBarController.selectedViewController; + return [self topViewController:selectedViewController]; + } + + return [self topViewController:rootViewController.presentedViewController]; +} + +- (UIImage *)getAppIconImage { + // 获取应用的主Bundle + NSBundle *mainBundle = [NSBundle mainBundle]; + + // 获取应用图标信息 + NSDictionary *infoDictionary = [mainBundle infoDictionary]; + NSArray *iconFiles = infoDictionary[@"CFBundleIcons"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"]; + + // 如果找到图标文件 + if (iconFiles && [iconFiles count] > 0) { + // 使用最后一个图标(通常是最高分辨率的) + NSString *iconLastName = [iconFiles lastObject]; + UIImage *image = [UIImage imageNamed:iconLastName]; + return image; + } + + // 如果没有找到使用应用名称 + NSString *appIconName = infoDictionary[@"CFBundleIconFile"]; + if (appIconName) { + return [UIImage imageNamed:appIconName]; + } + + // 如果上面的方法都失败了,尝试直接使用应用图标名称 + UIImage *appIcon = [UIImage imageNamed:@"AppIcon"]; + if (appIcon) { + return appIcon; + } + + // 如果实在找不到,返回一个1x1像素的透明图片(避免崩溃) + UIGraphicsBeginImageContext(CGSizeMake(1, 1)); + UIImage *blankImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return blankImage; +} + +// 显示复制成功提示 +- (void)showCopySuccessAlert { + // 创建一个新的UIWindow,设置为最高层级 + UIWindow *toastWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + toastWindow.windowLevel = UIWindowLevelStatusBar + 100; // 尽可能高的层级 + toastWindow.backgroundColor = [UIColor clearColor]; + + // 创建根视图控制器 + __block UIViewController *rootVC = [[UIViewController alloc] init]; + rootVC.view.backgroundColor = [UIColor clearColor]; + toastWindow.rootViewController = rootVC; + + // 创建一个悬浮的提示框,显示在屏幕顶部 + UIView *toastView = [[UIView alloc] initWithFrame:CGRectMake(0, -60, toastWindow.bounds.size.width - 40, 60)]; + toastView.center = CGPointMake(CGRectGetMidX(toastWindow.bounds), toastView.center.y); + toastView.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8]; + toastView.layer.cornerRadius = 10; + toastView.alpha = 0; + + // 添加图标 + UIImageView *iconView = [[UIImageView alloc] initWithFrame:CGRectMake(15, 15, 30, 30)]; + UIImage *checkmarkImage = [self createCheckmarkImage]; + iconView.image = checkmarkImage; + iconView.contentMode = UIViewContentModeScaleAspectFit; + [toastView addSubview:iconView]; + + // 添加提示文本 + UILabel *messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(55, 0, toastView.bounds.size.width - 70, toastView.bounds.size.height)]; + messageLabel.text = @"内容已复制到剪贴板,请在抖音中粘贴"; + messageLabel.textColor = [UIColor whiteColor]; + messageLabel.textAlignment = NSTextAlignmentLeft; + messageLabel.numberOfLines = 2; + messageLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; + [toastView addSubview:messageLabel]; + + // 将toast视图添加到window + [rootVC.view addSubview:toastView]; + + // 显示窗口 + [toastWindow makeKeyAndVisible]; + + // 执行显示动画:从顶部滑出 + [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + toastView.frame = CGRectOffset(toastView.frame, 0, 80 + [self safeAreaInsets].top); + toastView.alpha = 1.0; + } completion:nil]; + + // 2秒后开始消失动画 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // 消失动画 + [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{ + toastView.frame = CGRectOffset(toastView.frame, 0, -80 - [self safeAreaInsets].top); + toastView.alpha = 0.0; + } completion:^(BOOL finished) { + // 释放窗口 + toastWindow.hidden = YES; + // 设置为nil以释放内存 + rootVC = nil; + }]; + }); +} + +// 创建一个对勾图标 +- (UIImage *)createCheckmarkImage { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(30, 30), NO, [UIScreen mainScreen].scale); + CGContextRef context = UIGraphicsGetCurrentContext(); + + // 绘制圆形背景 + CGContextSetFillColorWithColor(context, [UIColor colorWithRed:0.2 green:0.8 blue:0.4 alpha:1.0].CGColor); + CGContextFillEllipseInRect(context, CGRectMake(0, 0, 30, 30)); + + // 绘制对勾 + CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor); + CGContextSetLineWidth(context, 2.5); + CGContextSetLineCap(context, kCGLineCapRound); + CGContextSetLineJoin(context, kCGLineJoinRound); + + // 绘制对勾路径 + CGContextMoveToPoint(context, 8, 15); + CGContextAddLineToPoint(context, 13, 20); + CGContextAddLineToPoint(context, 22, 10); + CGContextStrokePath(context); + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +// 获取安全区域插入值 +- (UIEdgeInsets)safeAreaInsets { + if (@available(iOS 11.0, *)) { + return [UIApplication sharedApplication].keyWindow.safeAreaInsets; + } + return UIEdgeInsetsZero; +} + +// URL编码方法 +- (NSString *)encodeUrlString:(NSString *)string { + if (!string) return @""; + return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; +} + +#pragma mark - QQ分享独立方法 + ++ (void)shareToQQFriend:(ShareContent *)content + completion:(SharePanelCompletionBlock)completion { + + if (![QQShareManager isQQInstalled]) { + [self showAlertWithMessage:@"您的设备未安装QQ客户端,无法进行QQ分享"]; + if (completion) { + completion(ShareTypeQQ, NO); + } + return; + } + + // 准备分享内容 + NSString *title = content.title ?: @"游戏分享"; + NSString *description = content.desc ?: @"精彩游戏分享"; + NSString *shareUrl = content.webpageUrl; + + // 准备缩略图 + UIImage *thumbImage = [UIImage imageNamed:@"sharelogo"] ?: [UIImage imageNamed:@"Icon180"]; + + // 根据内容选择分享类型 + QQShareType shareType = shareUrl ? QQShareTypeNews : QQShareTypeText; + + // 使用QQShareManager的分享方法 + if (shareType == QQShareTypeText) { + // 纯文本分享 + [QQShareManager shareToQQFriend:QQShareTypeText + title:title + description:description + thumbImage:nil + url:nil + image:nil + completion:^(BOOL success) { + if (completion) { + completion(ShareTypeQQ, success); + } + }]; + } else { + // 链接分享 + [QQShareManager shareToQQFriend:QQShareTypeNews + title:title + description:description + thumbImage:thumbImage + url:shareUrl + image:nil + completion:^(BOOL success) { + if (completion) { + completion(ShareTypeQQ, success); + } + }]; + } +} + ++ (void)shareToQZone:(ShareContent *)content + completion:(SharePanelCompletionBlock)completion { + + if (![QQShareManager isQQInstalled]) { + [self showAlertWithMessage:@"您的设备未安装QQ客户端,无法进行QQ空间分享"]; + if (completion) { + completion(ShareTypeQQ, NO); + } + return; + } + + // 准备分享内容 + NSString *title = content.title ?: @"游戏分享"; + NSString *description = content.desc ?: @"精彩游戏分享"; + NSString *shareUrl = content.webpageUrl; + + // 准备缩略图 + UIImage *thumbImage = [UIImage imageNamed:@"sharelogo"] ?: [UIImage imageNamed:@"Icon180"]; + + // 根据内容选择分享类型 + QQShareType shareType = shareUrl ? QQShareTypeNews : QQShareTypeText; + + [QQShareManager shareToQZone:shareType + title:title + description:description + thumbImage:thumbImage + url:shareUrl + images:thumbImage ? @[thumbImage] : nil + completion:^(BOOL success) { + if (completion) { + completion(ShareTypeQQ, success); + } + }]; +} + ++ (void)showAlertWithMessage:(NSString *)message { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; + [alertController addAction:okAction]; + + UIViewController *topVC = [self topViewController]; + [topVC presentViewController:alertController animated:YES completion:nil]; + }); +} + ++ (UIViewController *)topViewController { + UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; + while (rootViewController.presentedViewController) { + rootViewController = rootViewController.presentedViewController; + } + + if ([rootViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)rootViewController; + return navigationController.visibleViewController; + } + + if ([rootViewController isKindOfClass:[UITabBarController class]]) { + UITabBarController *tabBarController = (UITabBarController *)rootViewController; + UIViewController *selectedViewController = tabBarController.selectedViewController; + + if ([selectedViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)selectedViewController; + return navigationController.visibleViewController; + } + + return selectedViewController; + } + + return rootViewController; +} + +// 获取最顶层的视图控制器(实例方法) +- (UIViewController *)getTopViewController { + UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController; + while (topController.presentedViewController) { + topController = topController.presentedViewController; + } + + if ([topController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)topController; + return navigationController.visibleViewController; + } + + if ([topController isKindOfClass:[UITabBarController class]]) { + UITabBarController *tabBarController = (UITabBarController *)topController; + UIViewController *selectedViewController = tabBarController.selectedViewController; + + if ([selectedViewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)selectedViewController; + return navigationController.visibleViewController; + } + + return selectedViewController; + } + + return topController; +} + +@end diff --git a/msext/Class/Utils/WechatShareManager.h b/msext/Class/Utils/WechatShareManager.h new file mode 100644 index 0000000..f8774e5 --- /dev/null +++ b/msext/Class/Utils/WechatShareManager.h @@ -0,0 +1,82 @@ +// +// WechatShareManager.h +// msext +// +// Created on 2025/06/17. +// Copyright © 2025年. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, WechatShareType) { + WechatShareTypeText, // 纯文本分享 + WechatShareTypeImage, // 图片分享 + WechatShareTypeLink, // 链接分享 + WechatShareTypeScreenshot // 截图分享 +}; + +typedef NS_ENUM(NSInteger, WechatScene) { + WechatSceneSession, // 微信好友 + WechatSceneTimeline // 微信朋友圈 +}; + +@interface WechatShareManager : NSObject + +/** + * 检查微信是否已安装 + * @return BOOL 是否已安装微信 + */ ++ (BOOL)isWechatInstalled; + +/** + * 使用ShareContent对象进行微信分享 + * @param shareContent 完整的分享内容对象,包含title、desc、webpageUrl、type、sharefriend字段 + * @param completion 分享完成回调 + */ ++ (void)shareWithContent:(id)shareContent + completion:(void (^ _Nullable)(BOOL success))completion; + +/** + * 分享链接到微信 + * @param url 链接URL + * @param title 标题 + * @param description 描述 + * @param thumbImage 缩略图 + * @param scene 分享场景(好友/朋友圈) + * @param completion 完成回调 + */ ++ (void)shareLinkToWechat:(NSString *)url + title:(NSString * _Nullable)title + description:(NSString * _Nullable)description + thumbImage:(UIImage * _Nullable)thumbImage + scene:(WechatScene)scene + completion:(void (^ _Nullable)(BOOL success))completion; + +/** + * 分享图片到微信 + * @param image 图片 + * @param tagName 标签名称 + * @param messageExt 扩展信息 + * @param thumbImage 缩略图 + * @param scene 分享场景(好友/朋友圈) + * @param completion 完成回调 + */ ++ (void)shareImageToWechat:(UIImage *)image + tagName:(NSString * _Nullable)tagName + messageExt:(NSString * _Nullable)messageExt + thumbImage:(UIImage * _Nullable)thumbImage + scene:(WechatScene)scene + completion:(void (^ _Nullable)(BOOL success))completion; + +/** + * 获取应用图标作为缩略图 + * @return 应用图标UIImage对象 + */ ++ (UIImage * _Nullable)getAppIconImage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/msext/Class/Utils/WechatShareManager.m b/msext/Class/Utils/WechatShareManager.m new file mode 100644 index 0000000..49d555a --- /dev/null +++ b/msext/Class/Utils/WechatShareManager.m @@ -0,0 +1,265 @@ +// +// WechatShareManager.m +// msext +// +// Created on 2025/06/17. +// Copyright © 2025年. All rights reserved. +// + +#import "WechatShareManager.h" +#import "WXApiManager.h" +#import "WXApiRequestHandler.h" +#import "FuncPublic.h" + +@implementation WechatShareManager + +#pragma mark - Public Methods + ++ (BOOL)isWechatInstalled { + return [WXApi isWXAppInstalled]; +} + ++ (void)shareWithContent:(id)shareContent + completion:(void (^ _Nullable)(BOOL success))completion { + NSLog(@"🔍 [WechatShareManager] 开始微信分享"); + + // 检查微信是否已安装 + if (![self isWechatInstalled]) { + NSLog(@"❌ [WechatShareManager] 微信未安装"); + if (completion) { + completion(NO); + } + return; + } + + // 检查ShareContent对象 + if (!shareContent) { + NSLog(@"❌ [WechatShareManager] ShareContent对象为空"); + if (completion) { + completion(NO); + } + return; + } + + // 通过KVC从ShareContent对象中提取参数 + NSString *title = nil; + NSString *description = nil; + NSString *webpageUrl = nil; + NSString *type = nil; + NSString *sharefriend = nil; + + @try { + if ([shareContent respondsToSelector:@selector(valueForKey:)]) { + title = [shareContent valueForKey:@"title"]; + description = [shareContent valueForKey:@"desc"]; + webpageUrl = [shareContent valueForKey:@"webpageUrl"]; + type = [shareContent valueForKey:@"type"]; + sharefriend = [shareContent valueForKey:@"sharefriend"]; + } + + NSLog(@"🔍 [WechatShareManager] 从ShareContent提取参数:"); + NSLog(@"🔍 - 标题: %@", title ?: @"(无)"); + NSLog(@"🔍 - 描述: %@", description ?: @"(无)"); + NSLog(@"🔍 - 链接: %@", webpageUrl ?: @"(无)"); + NSLog(@"🔍 - 类型: %@", type ?: @"(无)"); + NSLog(@"🔍 - 分享目标: %@", sharefriend ?: @"(无)"); + + } @catch (NSException *exception) { + NSLog(@"❌ [WechatShareManager] 解析ShareContent时发生异常: %@", exception.reason); + if (completion) { + completion(NO); + } + return; + } + + // 确定分享场景 + enum WXScene currentScene; + if ([sharefriend intValue] == 1) { + currentScene = WXSceneSession; // 微信好友 + NSLog(@"🔍 [WechatShareManager] 分享目标: 微信好友"); + } else { + currentScene = WXSceneTimeline; // 微信朋友圈 + NSLog(@"🔍 [WechatShareManager] 分享目标: 微信朋友圈"); + } + + // 获取应用图标作为缩略图 + UIImage *thumbImage = [self getAppIconImage]; + + // 根据分享类型选择分享方式 + BOOL isScreenshotShare = [type intValue] == 2; + + if (isScreenshotShare) { + // 截图分享 + NSLog(@"🔍 [WechatShareManager] 执行截图分享"); + + UIImage *screenImage = [FuncPublic getImageWithFullScreenshot]; + if (screenImage) { + NSData *imageData = UIImageJPEGRepresentation(screenImage, 0.6); + + [WXApiRequestHandler sendImageData:imageData + TagName:[NSString stringWithFormat:@"%@游戏截图分享", @"进贤聚友棋牌"] + MessageExt:description ?: @"" + Action:@"share_action" + ThumbImage:thumbImage + InScene:currentScene]; + + NSLog(@"✅ [WechatShareManager] 截图分享请求已发送"); + } else { + NSLog(@"❌ [WechatShareManager] 截图获取失败"); + if (completion) { + completion(NO); + } + return; + } + } else { + // 链接分享(默认) + NSLog(@"🔍 [WechatShareManager] 执行链接分享"); + + [WXApiRequestHandler sendLinkURL:webpageUrl ?: @"" + TagName:[NSString stringWithFormat:@"%@游戏分享", @"进贤聚友棋牌"] + Title:title ?: @"" + Description:description ?: @"" + ThumbImage:thumbImage + InScene:currentScene]; + + NSLog(@"✅ [WechatShareManager] 链接分享请求已发送"); + } + + // 模拟分享成功回调(微信分享结果需要在AppDelegate中处理) + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSLog(@"✅ [WechatShareManager] 分享完成"); + if (completion) { + completion(YES); + } + }); +} + ++ (void)shareLinkToWechat:(NSString *)url + title:(NSString * _Nullable)title + description:(NSString * _Nullable)description + thumbImage:(UIImage * _Nullable)thumbImage + scene:(WechatScene)scene + completion:(void (^ _Nullable)(BOOL success))completion { + + // 检查微信是否已安装 + if (![self isWechatInstalled]) { + NSLog(@"❌ [WechatShareManager] 微信未安装"); + if (completion) { + completion(NO); + } + return; + } + + enum WXScene wxScene = (scene == WechatSceneSession) ? WXSceneSession : WXSceneTimeline; + + [WXApiRequestHandler sendLinkURL:url ?: @"" + TagName:@"游戏分享" + Title:title ?: @"" + Description:description ?: @"" + ThumbImage:thumbImage ?: [self getAppIconImage] + InScene:wxScene]; + + // 模拟分享成功回调 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (completion) { + completion(YES); + } + }); +} + ++ (void)shareImageToWechat:(UIImage *)image + tagName:(NSString * _Nullable)tagName + messageExt:(NSString * _Nullable)messageExt + thumbImage:(UIImage * _Nullable)thumbImage + scene:(WechatScene)scene + completion:(void (^ _Nullable)(BOOL success))completion { + + // 检查微信是否已安装 + if (![self isWechatInstalled]) { + NSLog(@"❌ [WechatShareManager] 微信未安装"); + if (completion) { + completion(NO); + } + return; + } + + if (!image) { + NSLog(@"❌ [WechatShareManager] 图片为空"); + if (completion) { + completion(NO); + } + return; + } + + enum WXScene wxScene = (scene == WechatSceneSession) ? WXSceneSession : WXSceneTimeline; + NSData *imageData = UIImageJPEGRepresentation(image, 0.6); + + [WXApiRequestHandler sendImageData:imageData + TagName:tagName ?: @"图片分享" + MessageExt:messageExt ?: @"" + Action:@"share_action" + ThumbImage:thumbImage ?: [self getAppIconImage] + InScene:wxScene]; + + // 模拟分享成功回调 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (completion) { + completion(YES); + } + }); +} + ++ (UIImage *)getAppIconImage { + // 获取应用图标 + UIImage *appIcon = nil; + + // 方法1: 从Info.plist获取图标文件名 + NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary]; + NSArray *iconFiles = infoPlist[@"CFBundleIconFiles"]; + if (iconFiles && iconFiles.count > 0) { + NSString *iconFileName = [iconFiles lastObject]; // 取最后一个,通常是最大尺寸的 + appIcon = [UIImage imageNamed:iconFileName]; + } + + // 方法2: 尝试常见的图标文件名 + if (!appIcon) { + NSArray *commonIconNames = @[@"Icon-180", @"Icon180", @"AppIcon60x60@3x", @"AppIcon"]; + for (NSString *iconName in commonIconNames) { + appIcon = [UIImage imageNamed:iconName]; + if (appIcon) break; + } + } + + // 方法3: 从SharePanel中查找sharelogo + if (!appIcon) { + appIcon = [UIImage imageNamed:@"sharelogo"]; + } + + // 兜底: 生成默认图标 + if (!appIcon) { + // 创建一个简单的默认图标 + UIGraphicsBeginImageContextWithOptions(CGSizeMake(60, 60), NO, 0.0); + CGContextRef context = UIGraphicsGetCurrentContext(); + + // 设置背景色 + CGContextSetFillColorWithColor(context, [UIColor colorWithRed:0.2 green:0.6 blue:1.0 alpha:1.0].CGColor); + CGContextFillEllipseInRect(context, CGRectMake(0, 0, 60, 60)); + + // 添加文字 + NSString *text = @"游戏"; + NSDictionary *attributes = @{ + NSFontAttributeName: [UIFont boldSystemFontOfSize:16], + NSForegroundColorAttributeName: [UIColor whiteColor] + }; + CGSize textSize = [text sizeWithAttributes:attributes]; + CGPoint textPoint = CGPointMake((60 - textSize.width) / 2, (60 - textSize.height) / 2); + [text drawAtPoint:textPoint withAttributes:attributes]; + + appIcon = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + return appIcon; +} + +@end diff --git a/msext/Info.plist b/msext/Info.plist index 6e059e2..5402597 100755 --- a/msext/Info.plist +++ b/msext/Info.plist @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - $(PRODUCT_NAME) + 进贤聚友棋牌 CFBundlePackageType APPL CFBundleShortVersionString @@ -52,6 +52,26 @@ xianliaoU1jJq3wgWluyB660 + + CFBundleTypeRole + Editor + CFBundleURLName + com.skyapp.ylgamehall + CFBundleURLSchemes + + ylgame + + + + CFBundleTypeRole + Editor + CFBundleURLName + msext.qq.callback + CFBundleURLSchemes + + msext + + CFBundleVersion 1.7 @@ -64,6 +84,16 @@ alipays wechat weixin + mqqapi + mqq + mqqOpensdkSSoLogin + mqqopensdkapiV2 + mqqopensdkapiV3 + mqqopensdkapiV4 + wtloginmqq2 + mqzone + snssdk1128 + snssdk1233 LSRequiresIPhoneOS