diff --git a/Podfile b/Podfile index 66c32d5..5ccdea0 100644 --- a/Podfile +++ b/Podfile @@ -5,4 +5,4 @@ platform :ios, '9.0' target 'msext' do # 添加七牛云SDK pod 'Qiniu', '~> 8.0' -end \ No newline at end of file +end diff --git a/msext/Class/Utils/QQShareManager.h b/msext/Class/Utils/QQShareManager.h index cf70707..5585f39 100644 --- a/msext/Class/Utils/QQShareManager.h +++ b/msext/Class/Utils/QQShareManager.h @@ -174,6 +174,14 @@ typedef NS_ENUM(NSInteger, QQShareType) { + (void)shareWithSystemShareContent:(id)shareContent completion:(void (^ _Nullable)(BOOL success))completion; +/** +* 分享到QQ(直接拉起QQ,不使用系统面板) +* @param shareContent 完整的分享内容对象 +* @param completion 分享完成回调 +*/ ++ (void)shareWithContent:(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 index 86a46a1..7082b25 100644 --- a/msext/Class/Utils/QQShareManager.m +++ b/msext/Class/Utils/QQShareManager.m @@ -8,9 +8,101 @@ #import "QQShareManager.h" #import "FuncPublic.h" // 用于截图功能 +#import // 引入 LinkPresentation 用于自定义分享预览 + +// 尝试检查是否存在 TencentOpenAPI SDK +#if __has_include() + #import + #import + #define HAS_QQ_SDK 1 +#else + #define HAS_QQ_SDK 0 +#endif + +// ----------------------------------------------------------------------------- +// QQShareActivityItemSource +// This class is used to customize the preview title and icon in the Share Sheet. +// ----------------------------------------------------------------------------- +@interface QQShareActivityItemSource : NSObject + +@property (nonatomic, strong) id content; // URL, String, or Image +@property (nonatomic, strong) NSString *title; // Preview Title +@property (nonatomic, strong) UIImage *image; // Preview Image (Icon) +@property (nonatomic, strong) NSString *desc; // Preview Description (Optional) + +- (instancetype)initWithContent:(id)content title:(NSString *)title image:(UIImage *)image description:(NSString *)desc; + +@end + +@implementation QQShareActivityItemSource + +- (instancetype)initWithContent:(id)content title:(NSString *)title image:(UIImage *)image description:(NSString *)desc { + self = [super init]; + if (self) { + _content = content; + _title = title; + _image = image; + _desc = desc; + } + return self; +} + +#pragma mark - UIActivityItemSource Methods + +- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController { + // Placeholder matches the content type + return self.content; +} + +- (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType { + // Return the actual content to share + return self.content; +} + +// Ensure the preview metadata is provided (iOS 13+) +- (LPLinkMetadata *)activityViewControllerLinkMetadata:(UIActivityViewController *)activityViewController API_AVAILABLE(ios(13.0)) { + LPLinkMetadata *metadata = [[LPLinkMetadata alloc] init]; + + // Set Preview Title (User requested App Name or specific title) + metadata.title = self.title ? self.title : @"分享内容"; + + // Set Preview Icon / Image + // 用户反馈 "图片分享正常,但其他情况(链接分享)图标有白边"。 + // 根本原因: LPLinkMetadata 对于 content-type 为 URL 的 preview, + // 如果提供的 image 是 iconProvider,iOS 默认会将其渲染为 "Icon" 样式(带白边、圆角遮罩等)。 + // 如果提供的 image 是 imageProvider,iOS 会将其渲染为 "Image/Thumbnail" 样式(通常是大图)。 + + // 如果我们希望它像 "图片分享" 那样全屏、无白边地展示,我们需要把它伪装成 imageProvider, + // 并且可能调整 NSItemProvider 类型。 + + // 经调研,system share sheet 的左侧预览区逻辑如下: + // 1. 如果有 imageProvider,尝试显示大图 (Thumbnail)。如果图是正方形,系统默认还是有 padding。 + // 2. 如果只有 iconProvider,由于它仅仅是一个图标,系统会将其放置在一个方形容器中,必定有白边。 + + // 最终尝试: + // 将其作为 imageProvider 提供,但这要求图片本身具备足够分辨率,且系统对于 URL Share 的 Layout 默认就是 Sidebar 模式。 + // 我们这里强制使用 imageProvider 试试看,这是目前看来最可能消除“仅Icon白边”的办法。 + + if (self.image) { + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.image]; + metadata.imageProvider = itemProvider; // 改回 imageProvider,这是大图预览的关键 + metadata.iconProvider = itemProvider; // 同时设置 + } + + // Set Original URL (if content is URL) + if ([self.content isKindOfClass:[NSURL class]]) { + metadata.originalURL = (NSURL *)self.content; + metadata.URL = (NSURL *)self.content; + } + + return metadata; +} + +@end // QQ URL Schemes #define kQQScheme @"mqqapi://" + #define kQQShareScheme @"mqqapi://share/" #define kQQFriendScheme @"mqqapi://share/to_fri?" #define kQQZoneScheme @"mqqapi://share/to_qzone?" @@ -101,117 +193,350 @@ static void(^QQShareCompletion)(BOOL) = nil; // 保存回调 QQShareCompletion = completion; - // 构建URL参数 - 使用QQ官方标准格式解决900101错误 + // 构建URL parameters based on OpenShare implementation logic to fix 900101 + // 900101 is often "Invalid AppID" or "Signature Mismatch". + // OpenShare uses Base64 for almost all text fields. + NSMutableString *urlString = [NSMutableString stringWithString:kQQFriendScheme]; - - // QQ官方要求的标准参数顺序和格式 + + // 1. Basic Parameters [urlString appendString:@"version=1"]; - [urlString appendString:@"&cflag=0"]; - [urlString appendString:@"&src_type=app"]; + [urlString appendString:@"&src_type=app"]; // Required + // [urlString appendString:@"&cflag=0"]; // OpenShare doesn't always send this, but let's keep it off for now or 0 + + // 2. App Identification + // thirdAppDisplayName MUST be Base64 encoded for mqqapi usually + NSString *base64Name = [self base64Encode:kQQAppName]; + NSString *encodedName = [self encodeString:base64Name]; + [urlString appendFormat:@"&thirdAppDisplayName=%@", encodedName]; + [urlString appendFormat:@"&app_name=%@", encodedName]; - // 尝试使用QQ最新推荐的参数格式 - [urlString appendFormat:@"&thirdAppDisplayName=%@", [self encodeString:kQQAppName]]; [urlString appendFormat:@"&app_id=%@", kQQAppID]; - [urlString appendFormat:@"&sdkv=2.9.0"]; - [urlString appendFormat:@"&sdkp=i"]; + [urlString appendFormat:@"&share_id=%@", kQQAppID]; - // 回调相关参数 + // 3. Callback + // mqqapi usually validates that callback_name matches "QQ" + Hex(AppID) + // 102793577 (Decimal) -> 06208169 (Hex) + // So callback_name should be QQ06208169 + NSString *hexCallbackName = @"QQ06208169"; [urlString appendString:@"&callback_type=scheme"]; - [urlString appendFormat:@"&callback_name=%@", [self encodeString:kQQCallbackScheme]]; + [urlString appendFormat:@"&callback_name=%@", hexCallbackName]; - // 获取Bundle ID用于验证 - NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier]; + // 4. Content Parameters - // 添加详细调试日志 - 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]]; + // ------------------------------------------------------------------------- + // 策略选择: + // 1. useSystemShare: 使用 iOS 系统分享面板 (UIActivityViewController) + // 2. useQQSDK: 使用腾讯官方 SDK (TencentOpenAPI) - 需确保已导入SDK + // 3. Fallback: 使用 URL Scheme (mqqapi://) - 无需SDK,直接调起 (Legacy) + // ------------------------------------------------------------------------- + BOOL useSystemShare = NO; // 默认为NO + BOOL useQQSDK = NO; // 默认为YES, 优先使用SDK + + // 决策逻辑: 是否运行系统分享 + BOOL runSystemShare = NO; + BOOL sdkIsAvailableAndEnabled = NO; + +#if HAS_QQ_SDK + if (useQQSDK) sdkIsAvailableAndEnabled = YES; +#endif + + if (sdkIsAvailableAndEnabled) { + // 如果 SDK 可用且开启,优先走 SDK 逻辑 (后面处理),跳过 SystemShare + runSystemShare = NO; + } else { + // 如果 SDK 不可用或未开启 + if (useSystemShare) { + // 用户显式开启系统分享 -> 运行 + runSystemShare = YES; + } else { + // 若都未开启,检查特殊情况: + // 图片分享无法通过 Scheme 直接调起会话,降级使用 SystemShare + if (type == QQShareTypeImage) { + runSystemShare = YES; } - if (description && description.length > 0) { - [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + } + } + + if (runSystemShare) { + NSMutableArray *activityItems = [NSMutableArray array]; + + // 准备预览图标 (App Icon) + UIImage *appIcon = [self getAppIcon]; + + // 准备预览标题 (App Name 优先,或者 Title) + NSString *previewTitle = kQQAppName; // 默认显示App名称 + if (title && title.length > 0) { + previewTitle = title; // 如果有特定标题,也可以显示标题,或者拼接 "App Name - Title" + } + + // 使用 QQShareActivityItemSource 封装内容以自定义预览 + if (@available(iOS 13.0, *)) { + switch (type) { + case QQShareTypeText: + if (description) { + // 用户需求:通过系统分享文本时希望是纯文本形式,而不是外部链接/卡片 + // 直接传递 NSString 对象,避免使用 UIActivityItemSource 或 LPLinkMetadata 封装 + [activityItems addObject:description]; + } + break; + + case QQShareTypeImage: + if (image) { + // 图片分享本身预览就是图片,通常无需干预。 + // 用户反馈使用 ActivityItemSource 封装后会导致图标显示有白边等问题。 + // 因此,对于 Image 分享,我们恢复直接传递 image 对象的原生方式,让系统自己处理最佳预览。 + [activityItems addObject:image]; + } + break; + + case QQShareTypeNews: + case QQShareTypeAudio: + case QQShareTypeVideo: + if (url) { + NSURL *shareURL = [NSURL URLWithString:url]; + if (shareURL) { + // 关键:将 URL 封装,并强制指定 appIcon 为 iconProvider + QQShareActivityItemSource *item = [[QQShareActivityItemSource alloc] initWithContent:shareURL title:previewTitle image:appIcon description:description]; + [activityItems addObject:item]; + } + } + // 注意:对于链接分享,通常只放一个 NSURL 对象即可。 + // 放入 Title/Image 可能会导致变成混合分享,某些 App 处理不好。 + // 通过 ItemSource,我们只分享 URL,但显示的元数据是自定义的。 + break; } - 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]]; + } else { + // iOS 13 以下降级处理 (不支持自定义预览) + switch (type) { + case QQShareTypeText: + if (title) [activityItems addObject:title]; + if (description) [activityItems addObject:description]; + break; + + case QQShareTypeImage: + if (image) [activityItems addObject:image]; + break; + + case QQShareTypeNews: + case QQShareTypeAudio: + case QQShareTypeVideo: + if (url) { + NSURL *shareURL = [NSURL URLWithString:url]; + if (shareURL) [activityItems addObject:shareURL]; + } + if (title) [activityItems addObject:title]; + // iOS 12及以下,直接传内容,系统自己处理 + break; + } + } + + if (activityItems.count > 0) { + + NSLog(@"🔍 [QQShareManager] 策略: 使用系统 UIActivityViewController 分享"); + dispatch_async(dispatch_get_main_queue(), ^{ + UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil]; + activityVC.completionWithItemsHandler = ^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) { + NSLog(@"[QQShareManager] 系统分享回调: completed=%d", completed); + if (completion) completion(completed); + }; + + UIViewController *topVC = [self topViewController]; + if (topVC) { + if ([activityVC respondsToSelector:@selector(popoverPresentationController)]) { + activityVC.popoverPresentationController.sourceView = topVC.view; + activityVC.popoverPresentationController.sourceRect = CGRectMake(topVC.view.bounds.size.width/2.0, topVC.view.bounds.size.height/2.0, 1.0, 1.0); + activityVC.popoverPresentationController.permittedArrowDirections = 0; + } + [topVC presentViewController:activityVC animated:YES completion:nil]; + } else { + if (completion) completion(NO); } - if (description && description.length > 0) { - [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + }); + return; // 结束执行,不再走下面的 URL Scheme 逻辑 + } + } + +#if HAS_QQ_SDK + // ------------------------------------------------------------------------- + // 策略: 腾讯官方 SDK 分享 + // ------------------------------------------------------------------------- + if (useQQSDK) { + NSLog(@"🔍 [QQShareManager] 策略: 使用腾讯官方 SDK 分享"); + + // 确保在主线程调用 (SDK要求) + dispatch_async(dispatch_get_main_queue(), ^{ + + QQApiObject *msgObj = nil; + + switch (type) { + case QQShareTypeText: { + QQApiTextObject *textObj = [QQApiTextObject objectWithText:description ? description : @""]; + textObj.title = title; + msgObj = textObj; + break; + } + case QQShareTypeImage: { + if (!image) { + if (completion) completion(NO); + return; + } + NSData *imgData = UIImageJPEGRepresentation(image, 0.8); + // Image Object + QQApiImageObject *imgObj = [QQApiImageObject objectWithData:imgData + previewImageData:imgData + title:title + description:description]; + msgObj = imgObj; + break; + } + case QQShareTypeNews: + case QQShareTypeAudio: + case QQShareTypeVideo: { + if (!url) { + if (completion) completion(NO); + return; + } + + NSData *previewData = nil; + if (thumbImage) { + previewData = UIImageJPEGRepresentation(thumbImage, 0.5); + } + + NSURL *targetUrl = [NSURL URLWithString:url]; + + if (type == QQShareTypeAudio) { + QQApiAudioObject *audioObj = [QQApiAudioObject objectWithURL:targetUrl + title:title + description:description + previewImageData:previewData]; + msgObj = audioObj; + } else if (type == QQShareTypeVideo) { + // Video usually needs flashURL, here acts as News Link basically + QQApiNewsObject *newsObj = [QQApiNewsObject objectWithURL:targetUrl + title:title + description:description + previewImageData:previewData]; + msgObj = newsObj; + } else { + // News + QQApiNewsObject *newsObj = [QQApiNewsObject objectWithURL:targetUrl + title:title + description:description + previewImageData:previewData]; + msgObj = newsObj; + } + break; + } + } + + if (msgObj) { + SendMessageToQQReq *req = [SendMessageToQQReq reqWithContent:msgObj]; + // 调起QQ + QQApiSendResultCode sent = [QQApiInterface sendReq:req]; + NSLog(@"[QQShareManager] SDK发送请求结果: %d", sent); + + // 处理同步结果 + // 注意: 最终成功与否通常依赖 AppDelegate 的 onResp 回调 + // EQQAPISENDSUCESS = 0 + if (sent == 0) { + NSLog(@"✅ SDK请求发送成功"); + // 这里不立即调用 completion(YES),等待回调 + } else { + NSLog(@"❌ SDK请求发送失败"); + if (completion) completion(NO); } } else { - if (completion) { - completion(NO); - } - return; + NSLog(@"❌ [QQShareManager] 构建 SDK 对象失败"); + if (completion) completion(NO); } + }); + + return; // 拦截后续 Scheme 逻辑 + } +#endif + + // Falls through to Legacy Scheme Logic if useSystemShare is NO or activityItems is empty + switch (type) { + case QQShareTypeText: + [urlString appendString:@"&file_type=text"]; + [urlString appendString:@"&req_type=0"]; // Text + if (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]]; + if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]]; break; + case QQShareTypeImage: { + if (!image) { + NSLog(@"❌ [QQShareManager] 图片对象为空"); + if (completion) completion(NO); + return; + } + + // 恢复原有的 URL Scheme 图片分享逻辑 (Dual Strategy) + // 策略: Base64 (小图) vs FilePath (大图/持久化) + BOOL useBase64Scheme = YES; + + if (useBase64Scheme) { + NSData *imgData = UIImageJPEGRepresentation(image, 0.5); + if (imgData.length > 1 * 1024 * 1024) imgData = UIImageJPEGRepresentation(image, 0.3); + NSString *base64Str = [imgData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; + [urlString appendString:@"&file_type=image"]; + [urlString appendFormat:@"&image_base64=%@", [self encodeString:base64Str]]; + [urlString appendString:@"&cflag=0"]; + } else { + NSString *imagePath = [self saveImageToDocumentsDirectory:image]; + [urlString appendString:@"&file_type=image"]; + [urlString appendString:@"&req_type=5"]; + [urlString appendString:@"&cflag=0"]; + + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + [pasteboard setData:UIImageJPEGRepresentation(image, 0.6) forPasteboardType:@"com.tencent.mqq.api.apiLargeData"]; + + if (title) [urlString appendFormat:@"&title=%@", [self encodeString:title]]; + if (description) [urlString appendFormat:@"&description=%@", [self encodeString:description]]; + + NSString *fullPath = [@"file://" stringByAppendingString:imagePath]; + NSString *encodedPath = [self encodeString:fullPath]; + [urlString appendFormat:@"&file_path=%@", encodedPath]; + [urlString appendFormat:@"&image_url=%@", encodedPath]; + [urlString appendFormat:@"&object_location=%@", encodedPath]; + } + break; + } + + case QQShareTypeNews: + // "News" (Link) share + [urlString appendString:@"&file_type=news"]; [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 (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]]; + if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]]; + if (url) [urlString appendFormat:@"&url=%@", [self encodeString:[self base64Encode:url]]]; + if (thumbImage) { - NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; - [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]]; + NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; + NSString *fullPath = [@"file://" stringByAppendingString:thumbPath]; + // Preview Image URL often requires BASE64 of the path string for OpenShare compatibility + [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[self base64Encode:fullPath]]]; } 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]]]; + [urlString appendString:@"&file_type=audio"]; + [urlString appendString:@"&req_type=2"]; + if (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]]; + if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]]; + if (url) [urlString appendFormat:@"&url=%@", [self encodeString:[self base64Encode:url]]]; + if (thumbImage) { + NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; + NSString *fullPath = [@"file://" stringByAppendingString:thumbPath]; + [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[self base64Encode:fullPath]]]; } 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]]]; - } + // Video might be similar break; } @@ -929,9 +1254,18 @@ static void(^QQShareCompletion)(BOOL) = nil; return YES; } ++ (NSString *)base64Encode:(NSString *)string { + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + return [data base64EncodedStringWithOptions:0]; +} + + (NSString *)encodeString:(NSString *)string { if (!string) return @""; - return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + // We must encode everything that is strictly reserved or unsafe in a query parameter value + // Especially + / = which are key for Base64 + NSMutableCharacterSet *allowed = [[NSCharacterSet alphanumericCharacterSet] mutableCopy]; + [allowed addCharactersInString:@"-._~"]; // Unreserved characters per RFC 3986 + return [string stringByAddingPercentEncodingWithAllowedCharacters:allowed]; } + (NSString *)saveImageToTempDirectory:(UIImage *)image { @@ -943,6 +1277,21 @@ static void(^QQShareCompletion)(BOOL) = nil; return filePath; } ++ (NSString *)saveImageToDocumentsDirectory:(UIImage *)image { + // Compress image to ensure it's not too large (similar to WeChat's 0.6 behavior) + NSData *imageData = UIImageJPEGRepresentation(image, 0.6); + NSString *fileName = [NSString stringWithFormat:@"qq_share_persistent_%@.jpg", [self generateUUID]]; + + // Use Documents directory for persistence + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths firstObject]; + NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName]; + + [imageData writeToFile:filePath atomically:YES]; + NSLog(@"🔍 [QQShareManager] Saved image to Documents: %@", filePath); + return filePath; +} + + (NSString *)generateUUID { CFUUIDRef uuid = CFUUIDCreate(NULL); CFStringRef uuidString = CFUUIDCreateString(NULL, uuid); @@ -1029,7 +1378,24 @@ static void(^QQShareCompletion)(BOOL) = nil; // 尝试获取应用桌面图标 UIImage *appIcon = nil; - // 方法1: 从Info.plist获取图标信息 + // 方法1.1: 优先尝试直接从 Asset Catalog 中获取 "AppIcon-1" + // 很多时候 Xcode 并不会把 AppIcon 打包成松散文件,而是编译进 Assets.car + // 我们发现 Info.plist 并未包含 CFBundleIcons (在某些旧项目配置中),但 Xcode 设置了 ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-1" + + appIcon = [UIImage imageNamed:@"AppIcon-1"]; + if (appIcon) { + NSLog(@"🔍 ✅ 获取应用图标成功: AppIcon-1"); + return appIcon; + } + + appIcon = [UIImage imageNamed:@"AppIcon"]; + if (appIcon) { + NSLog(@"🔍 ✅ 获取应用图标成功: AppIcon"); + return appIcon; + } + + // 方法1.2: 从Info.plist获取图标信息 + NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary]; // iOS 13+ 支持的新格式 @@ -1471,4 +1837,86 @@ static void(^QQShareCompletion)(BOOL) = nil; }]; } ++ (void)shareWithContent:(id)shareContent + completion:(void (^ _Nullable)(BOOL success))completion { + NSLog(@"🔍 [QQShareManager] 开始QQ直接分享(自动拉起会话选择)"); + + // 检查QQ是否已安装 + if (![self isQQInstalled]) { + NSLog(@"❌ [QQShareManager] QQ未安装"); + if (completion) { + completion(NO); + } + [self showAppNotInstalledAlert:@"QQ"]; + return; + } + + // 提取参数 + NSString *title = nil; + NSString *description = nil; + NSString *webpageUrl = nil; + NSString *type = nil; + + @try { + if ([shareContent respondsToSelector:@selector(valueForKey:)]) { + title = [shareContent valueForKey:@"title"]; + description = [shareContent valueForKey:@"desc"]; + webpageUrl = [shareContent valueForKey:@"webpageUrl"]; + type = [shareContent valueForKey:@"type"]; + } + } @catch (NSException *exception) { + NSLog(@"❌ [QQShareManager] 参数提取异常: %@", exception); + if (completion) completion(NO); + return; + } + + // 判断分享类型 + BOOL isScreenshotShare = [type intValue] == 2; + + if (isScreenshotShare) { + // 截图分享 (Image) + UIImage *screenImage = [FuncPublic getImageWithFullScreenshot]; + + // 如果截屏失败或者为空,尝试检查是否有 image 字段(可能是Base64或UIImage对象) + if (!screenImage) { + id imageObj = nil; + if ([shareContent respondsToSelector:@selector(valueForKey:)]) { + @try { imageObj = [shareContent valueForKey:@"image"]; } @catch (NSException *e) {} + } + if (imageObj && [imageObj isKindOfClass:[UIImage class]]) { + screenImage = imageObj; + } else if (imageObj && [imageObj isKindOfClass:[NSString class]]) { + NSData *data = [[NSData alloc] initWithBase64EncodedString:imageObj options:0]; + if (data) screenImage = [UIImage imageWithData:data]; + } + } + + if (!screenImage) { + NSLog(@"❌ [QQShareManager] 截图失败且未通过参数传入图片"); + if (completion) completion(NO); + return; + } + + [self shareToQQFriend:QQShareTypeImage + title:title + description:description + thumbImage:nil + url:nil + image:screenImage + completion:completion]; + } else { + // 网页/链接分享 (News) + // 默认使用应用图标作为缩略图 + UIImage *thumbImage = [self getAppIcon]; + + [self shareToQQFriend:QQShareTypeNews + title:title + description:description + thumbImage:thumbImage + url:webpageUrl + image:nil + completion:completion]; + } +} + @end \ No newline at end of file diff --git a/msext/Class/Utils/SharePanel.m b/msext/Class/Utils/SharePanel.m index c8e7caa..e18ab30 100644 --- a/msext/Class/Utils/SharePanel.m +++ b/msext/Class/Utils/SharePanel.m @@ -317,8 +317,9 @@ static SharePanel *sharedPanel = nil; SharePanelCompletionBlock completionBlock = self.completionBlock; // 使用QQShareManager处理QQ分享 - [QQShareManager shareWithSystemShareContent:content - completion:^(BOOL success) { + // 更改为shareWithContent以直接拉起QQ会话列表,而不是系统分享面板 + [QQShareManager shareWithContent:content + completion:^(BOOL success) { NSLog(@"QQ分享结果: %@", success ? @"✅ 成功" : @"❌ 失败"); if (completionBlock) { completionBlock(ShareTypeQQ, success); diff --git a/msext/Info.plist b/msext/Info.plist index 5402597..6cdb7cf 100755 --- a/msext/Info.plist +++ b/msext/Info.plist @@ -62,6 +62,16 @@ ylgame + + CFBundleTypeRole + Editor + CFBundleURLName + tencent + CFBundleURLSchemes + + tencent102793577 + + CFBundleTypeRole Editor