diff --git a/ylgamehall/Source/SDK/WeChat/WeChatManager.swift b/ylgamehall/Source/SDK/WeChat/WeChatManager.swift index a58a028..f1ae88c 100644 --- a/ylgamehall/Source/SDK/WeChat/WeChatManager.swift +++ b/ylgamehall/Source/SDK/WeChat/WeChatManager.swift @@ -91,27 +91,45 @@ public final class WeChatManager: NSObject { case timeline // 朋友圈 } - public struct ShareLink: Sendable { - public let url: String - public let title: String - public let desc: String - public let thumbnailURL: String? + /// 链接分享载荷。`mediaTagName` 对齐 msext WXApiRequestHandler `TagName:` + /// 参数(写入 WXMediaMessage.mediaTagName,微信内部分类用)。 + /// 含 UIImage 故无法 Sendable —— 实际使用都在 MainActor 上发起,无跨 actor 风险。 + public struct ShareLink { + public let url: String + public let title: String + public let desc: String + public let thumb: UIImage? + public let mediaTagName: String? - public init(url: String, title: String, desc: String, thumbnailURL: String?) { - self.url = url; self.title = title; self.desc = desc; self.thumbnailURL = thumbnailURL + public init( + url: String, title: String, desc: String, + thumb: UIImage?, mediaTagName: String? = nil + ) { + self.url = url; self.title = title; self.desc = desc + self.thumb = thumb; self.mediaTagName = mediaTagName } } - /// 图片分享载荷(type=2 截图 / type=3 远端图)。 - /// imageData 是大图原始字节(JPEG 等),thumb 是缩略图源 — SDK - /// `setThumbImage:` 内部把 thumb 压到 32KB。 + /// 图片分享载荷(type=2 截图)。 + /// - `imageData`:大图原始字节(JPEG) + /// - `thumb`:缩略图源 — SDK `setThumbImage:` 内部压到 32KB + /// - `messageExt`:H5 的 description;写入 `WXMediaMessage.description` + /// 与 `.messageExt`,对齐 msext `WXApiRequestHandler sendImageData:MessageExt:` + /// - `mediaTagName`:对齐 msext `TagName:` 参数 public struct ShareImage { - public let imageData: Data - public let thumb: UIImage + public let imageData: Data + public let thumb: UIImage + public let messageExt: String + public let mediaTagName: String? - public init(imageData: Data, thumb: UIImage) { + public init( + imageData: Data, thumb: UIImage, + messageExt: String = "", mediaTagName: String? = nil + ) { self.imageData = imageData self.thumb = thumb + self.messageExt = messageExt + self.mediaTagName = mediaTagName } } @@ -127,6 +145,12 @@ public final class WeChatManager: NSObject { message.title = link.title message.description = link.desc message.mediaObject = media + if let mediaTagName = link.mediaTagName { + message.mediaTagName = mediaTagName + } + if let thumb = link.thumb { + message.setThumbImage(thumb) + } let req = SendMessageToWXReq() req.bText = false @@ -143,8 +167,11 @@ public final class WeChatManager: NSObject { } } - /// 图片分享(FIFO 串行,复用 sharePending)。msext gameController.m:637-678 - /// type=2 截图 / type=其它 远端图 共用同一条 WXImageObject + setThumbImage 路径。 + /// 图片分享(FIFO 串行,复用 sharePending)。对齐 msext WechatShareManager + /// `sendImageData:TagName:MessageExt:Action:ThumbImage:InScene:` 的字段映射: + /// TagName → mediaTagName + /// MessageExt → description(接收方预览可见)+ messageExt(SDK 内部回包) + /// ThumbImage → setThumbImage public func shareImage(_ img: ShareImage, scene: ShareScene) async throws { try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in sharePending.append(cont) @@ -153,9 +180,15 @@ public final class WeChatManager: NSObject { media.imageData = img.imageData let message = WXMediaMessage() - // 图片分享不需要 title / description(msext 字面:传 nil;这里传空串等价) + message.description = img.messageExt message.mediaObject = media message.setThumbImage(img.thumb) + if !img.messageExt.isEmpty { + message.messageExt = img.messageExt + } + if let mediaTagName = img.mediaTagName { + message.mediaTagName = mediaTagName + } let req = SendMessageToWXReq() req.bText = false diff --git a/ylgamehall/Source/Share/QQShare.swift b/ylgamehall/Source/Share/QQShare.swift index 89c88f4..4d54920 100644 --- a/ylgamehall/Source/Share/QQShare.swift +++ b/ylgamehall/Source/Share/QQShare.swift @@ -4,11 +4,11 @@ // // QQ 分享 SharePlatform 实现,对齐 daoqi msext QQShareManager // `shareWithContent:completion:`(按 type 分两个分支): -// - type == "2" → 截图分享:复制截图到 UIPasteboard + 拉起 mqqapi:// +// - type == "2" → 截图分享:复制截图到 UIPasteboard + 弹引导框("立即打开 QQ") // - else → 链接分享:mqqapi://share/to_fri?req_type=1&url=... // -// **不依赖 QQ SDK**(CLAUDE.md ADR-006):原项目走 SDK 弹原生会话列表, -// 新外壳走 URL Scheme + pasteboard 兜底实现等价 UX。 +// **不依赖 QQ SDK**(CLAUDE.md ADR-006):原项目走 SDK 弹原生会话列表带 +// 图片预览;无 SDK 路径下退化为"剪贴板 + 引导框",与 Douyin 同款 UX 模式。 // // encodeString 与 msext line 1262-1268 等价:unreserved 字符集(RFC 3986 // alphanumeric + "-._~"),其它全部 percent encode。 @@ -49,27 +49,59 @@ public final class QQShare: SharePlatform { /// 对齐 msext QQShareManager.m:1876-1906 isScreenshotShare 分支: /// 原项目用 QQ SDK shareToQQFriend QQShareTypeImage 直接传 UIImage 给 SDK; - /// 无 SDK 路径下退化为:截图 → pasteboard → 拉起 QQ(用户在聊天里粘贴)。 + /// 无 SDK 路径下退化为:截图 → UIPasteboard.image → 弹引导框("立即打开 QQ")。 + /// UX 模式与 DouyinShare 截图分享对齐(都是非 SDK 平台的剪贴板兜底)。 private func shareScreenshot() { do { let shot = try ImageProvider.captureScreenshot() UIPasteboard.general.image = shot + Self.log.debug("QQShare 截图已复制到剪贴板") } catch { Self.log.error("QQShare 截图失败 \(error.localizedDescription, privacy: .public)") - } - - let candidates = [ - "mqqapi://share/to_fri?file_type=news&version=1&cflag=0", - "mqq://im/chat?chat_type=select", - "mqqapi://", - "mqq://" - ] - for s in candidates { - guard let url = URL(string: s), - UIApplication.shared.canOpenURL(url) else { continue } - UIApplication.shared.open(url) return } + + let message = """ + ✅ 图片已复制到剪贴板 + + 🎯 分享到好友或群聊步骤: + 1️⃣ 打开 QQ,选择好友或群聊进入聊天 + 2️⃣ 在输入框长按粘贴图片 + 3️⃣ 点击发送即可分享 + """ + showGuidanceAlert(title: "QQ 分享准备完成", message: message) + } + + /// 弹"立即打开 QQ / 稍后分享"二选一 Alert,与 DouyinShare 同款 UX。 + private func showGuidanceAlert(title: String, message: String) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "立即打开 QQ", style: .default) { _ in + if let url = URL(string: "mqqapi://"), + UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } else if let url = URL(string: "mqq://"), + UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + }) + alert.addAction(UIAlertAction(title: "稍后分享", style: .cancel)) + + guard let topVC = Self.topViewController() else { return } + topVC.present(alert, animated: true) + } + + private static func topViewController() -> UIViewController? { + let scene = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .first(where: { $0.activationState == .foregroundActive }) + ?? UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first + guard var top = scene?.windows.first(where: \.isKeyWindow)?.rootViewController + ?? scene?.windows.first?.rootViewController else { return nil } + while let presented = top.presentedViewController { top = presented } + if let nav = top as? UINavigationController, let visible = nav.visibleViewController { + return visible + } + return top } // MARK: - 链接分享 diff --git a/ylgamehall/Source/Share/WechatShare.swift b/ylgamehall/Source/Share/WechatShare.swift index 0e42c18..f1969c6 100644 --- a/ylgamehall/Source/Share/WechatShare.swift +++ b/ylgamehall/Source/Share/WechatShare.swift @@ -42,7 +42,12 @@ public final class WechatShare: SharePlatform { let shot = try ImageProvider.captureScreenshot() guard let imageData = shot.jpegData(compressionQuality: 0.6) else { return } try await WeChatManager.shared.shareImage( - .init(imageData: imageData, thumb: thumb), + .init( + imageData: imageData, + thumb: thumb, + messageExt: content.desc, + mediaTagName: "\(Self.appTagPrefix)游戏截图分享" + ), scene: wxScene ) } else { @@ -52,7 +57,8 @@ public final class WechatShare: SharePlatform { url: content.webpageUrl, title: content.title, desc: content.desc, - thumbnailURL: nil + thumb: thumb, + mediaTagName: "\(Self.appTagPrefix)游戏分享" ), scene: wxScene ) @@ -63,6 +69,10 @@ public final class WechatShare: SharePlatform { } } + /// msext WechatShareManager 字面常量 "进贤聚友棋牌"(与 daoqi + /// SGDefineInfo.h gamehallname 同源),用于 WXMediaMessage.mediaTagName 拼接。 + private static let appTagPrefix = "进贤聚友棋牌" + /// app icon 缩略图。读 Info.plist `CFBundleIcons` → `CFBundlePrimaryIcon` /// → `CFBundleIconFiles` 的最后一个(最高分辨率),与原 msext /// WechatShareManager `getAppIconImage` 等价。失败兜底返回 1x1 透明图。