diff --git a/ylgamehall/Source/Bridge/Handlers/FriendsShareHandler.swift b/ylgamehall/Source/Bridge/Handlers/FriendsShareHandler.swift index 87a1d87..a036b57 100644 --- a/ylgamehall/Source/Bridge/Handlers/FriendsShareHandler.swift +++ b/ylgamehall/Source/Bridge/Handlers/FriendsShareHandler.swift @@ -5,16 +5,17 @@ // H5 → Native handler:friendsSharetypeUrlToptitleDescript — 分享 // 契约:docs/H5-Native-Contract.md §3.1 [2] + §3.2 [11]sharesuccess // -// 完整流程(与 msext gameController.m:575-597 等价): +// 流程(与 msext gameController.m:575-597 等价,调用即视为成功): // 1. 入参解析 ShareContent -// 2. ShareCenter.dispatch: -// - sharefriend == "1" → SharePanel 三选一(微信好友 / QQ / 抖音) -// - sharefriend == "2" → 直接 WechatShare 朋友圈 -// 3. 分享完成 → bridge.call("sharesuccess", { success: "2", type: }) +// 2. responseCallback("sharefriend") +// 3. 立即 bridge.call("sharesuccess", { success:"2", type: }) +// (**有意为之**:H5 端把"调用分享接口"等同"分享成功",无须等 SDK 回包; +// URL Scheme / 剪贴板路径本身就无返回。) +// 4. 异步 fire-and-forget 调用 ShareCenter.dispatch 拉起真实分享 UI // // 各平台 SDK 依赖: -// - 微信:Phase 4.E 接入项目方 SDK(当前 WechatShare 是 stub) -// - QQ + 抖音:不依赖 SDK,走 URL Scheme(Phase 4.C/4.D 真实落地,当前 stub) +// - 微信:通过 WeChatManager(已接入 1.8.2 SDK) +// - QQ + 抖音:不依赖 SDK,走 URL Scheme + 剪贴板(CLAUDE.md ADR-006) // import Foundation @@ -28,6 +29,7 @@ public enum FriendsShareHandler { // cb: "sharefriend"(msext 字面,沿用) // 反向 callback:sharesuccess({ success: "2", type: sharefriend 原值 }) bridge.register("friendsSharetypeUrlToptitleDescript") { [weak bridge] data, callback in + // 1. responseCallback —— msext 字面 "sharefriend" callback?(.string("sharefriend")) guard let obj = data?.asObject else { return } @@ -39,25 +41,20 @@ public enum FriendsShareHandler { title: obj["title"]?.asString ?? "", desc: obj["description"]?.asString ?? "" ) - let sharefriend = content.sharefriend await MainActor.run { - ShareCenter.dispatch(content) { result in - // success / cancelled / notInstalled / failed — msext 行为: - // 只要不是明确失败就发 sharesuccess(乐观策略,URL Scheme 无返回) - switch result { - case .success, .cancelled: - bridge?.call("sharesuccess", - data: .object([ - "success": .string("2"), - "type": .string(sharefriend) - ]), - callback: nil) - case .notInstalled, .failed: - // 不发 sharesuccess(H5 业务期 timeout 后自行 fallback) - break - } - } + // 2. 立即触发 sharesuccess —— 有意为之:调用即视为成功 + bridge?.call( + "sharesuccess", + data: .object([ + "success": .string("2"), + "type": .string(content.sharefriend) + ]), + callback: nil + ) + + // 3. fire-and-forget 拉起真实分享 UI + ShareCenter.dispatch(content) } } } diff --git a/ylgamehall/Source/Share/DouyinShare.swift b/ylgamehall/Source/Share/DouyinShare.swift index 2efbf2d..1fe0c4b 100644 --- a/ylgamehall/Source/Share/DouyinShare.swift +++ b/ylgamehall/Source/Share/DouyinShare.swift @@ -2,19 +2,15 @@ // DouyinShare.swift // ylgamehall // -// 抖音分享 SharePlatform 实现(Phase 4.D 完整)。 +// 抖音分享 SharePlatform 实现,1:1 对齐 daoqi msext DouyinShareManager +// `shareWithContent:completion:`(按 type 分两个分支): +// - type == "2" → 截图分享:截图 → UIPasteboard → 弹"立即打开抖音"引导框 +// - else → 文字分享:title + "\n\n" + desc → UIPasteboard → 弹引导框 // -// 移植 msext DouyinShareManager.m:201-269 shareImageToDouyin 流程: -// 1. 拿图片源(type=2 → ImageProvider.captureScreenshot;其它 → -// ImageProvider.downloadRemote);type=1 链接 → 抖音不原生支持纯链接, -// 退化为截屏分享(msext 行为:所有 H5 分享到抖音都走图片) -// 2. PhotoLibrarySaver.save 保存到相册(addOnly 权限) -// 3. UIPasteboard 兜底放图片数据,供抖音读取 -// 4. 按顺序尝试 snssdk1128://camera / publish / share/image / 基础 scheme, -// canOpenURL 命中即调起 -// 5. 调起即视为 success(URL Scheme 单向无返回) +// **不依赖抖音 SDK**(CLAUDE.md ADR-006):抖音的图片 / 文本分享原本就是 +// 剪贴板 + 引导用户在抖音内粘贴的非 SDK 路径,新外壳照搬。 // -// **不依赖抖音 SDK**。 +// fire-and-forget:sharesuccess 已在 FriendsShareHandler 立即触发。 // import UIKit @@ -27,68 +23,113 @@ public final class DouyinShare: SharePlatform { private static let log = Logger(subsystem: "ylgamehall", category: "DouyinShare") + private static let douyinScheme = "snssdk1128://" + public let name = "Douyin" public var isInstalled: Bool { - guard let url = URL(string: "snssdk1128://") else { return false } + guard let url = URL(string: Self.douyinScheme) else { return false } return UIApplication.shared.canOpenURL(url) } public init() {} - public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult { - guard isInstalled else { return .notInstalled(platform: name) } + public func share(_ content: ShareContent, scene: ShareScene) { + guard isInstalled else { return } - // 1. 拿图片源 - let image: UIImage + if content.type == "2" { + shareScreenshot() + } else { + shareText(content) + } + } + + // MARK: - 截图分享 + + /// 对齐 msext DouyinShareManager.m:139-162 isScreenshotShare 分支: + /// 截图 → pasteboard.image → 弹引导框(不自动拉起抖音,等用户点"立即打开")。 + private func shareScreenshot() { do { - switch content.type { - case "2": - image = try ImageProvider.captureScreenshot() - case "1": - // 抖音不支持纯链接分享,退化为当前屏截屏(msext 行为) - Self.log.debug("DouyinShare: type=1 链接,退化为截屏分享") - image = try ImageProvider.captureScreenshot() - default: - // type == 其它 → 远端图片 URL - image = try await ImageProvider.downloadRemote(content.webpageUrl) + let shot = try ImageProvider.captureScreenshot() + UIPasteboard.general.image = shot + Self.log.debug("DouyinShare 截图已复制到剪贴板") + } catch { + Self.log.error("DouyinShare 截图失败 \(error.localizedDescription, privacy: .public)") + return + } + + let message = """ + ✅ 图片已复制到剪贴板 + + 🎯 分享到好友或群聊步骤: + 1️⃣ 打开抖音,点击右下角「消息」 + 2️⃣ 选择好友或群聊进入聊天 + 3️⃣ 在输入框长按粘贴图片 + 4️⃣ 点击发送即可分享 + """ + showGuidanceAlert(title: "抖音分享准备完成", message: message) + } + + // MARK: - 文字分享 + + /// 对齐 msext DouyinShareManager.m:163-196 else 分支: + /// title + "\n\n" + desc 拼字符串 → pasteboard.string → 弹引导框。 + /// 双侧都空时用 msext 同款兜底文案"来自进贤聚友棋牌的精彩内容分享"。 + private func shareText(_ content: ShareContent) { + var shareText = "" + if !content.title.isEmpty { + shareText = content.title + } + if !content.desc.isEmpty { + shareText = shareText.isEmpty ? content.desc : "\(shareText)\n\n\(content.desc)" + } + if shareText.isEmpty { + shareText = "来自进贤聚友棋牌的精彩内容分享" + } + UIPasteboard.general.string = shareText + Self.log.debug("DouyinShare 文字已复制到剪贴板") + + let message = """ + ✅ 内容已复制到剪贴板 + + 🎯 分享到好友或群聊步骤: + 1️⃣ 打开抖音,点击右下角「消息」 + 2️⃣ 选择好友或群聊进入聊天 + 3️⃣ 在输入框长按粘贴内容 + 4️⃣ 点击发送即可分享 + """ + showGuidanceAlert(title: "抖音分享准备完成", message: message) + } + + // MARK: - 引导弹窗 + + /// 弹"立即打开抖音 / 稍后分享"二选一 Alert,对齐 msext + /// `showGuidanceAlertWithTitle:message:`。 + private func showGuidanceAlert(title: String, message: String) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "立即打开抖音", style: .default) { _ in + if let url = URL(string: Self.douyinScheme), + UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) } - } catch { - Self.log.error("DouyinShare: 获取图片失败 \(error.localizedDescription, privacy: .public)") - return .failed(reason: "获取图片失败: \(error.localizedDescription)") + }) + 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 } - - // 2. 保存到相册(含权限检查) - do { - try await PhotoLibrarySaver.save(image) - } catch PhotoLibrarySaverError.permissionDenied { - Self.log.warning("DouyinShare: 相册写入权限被拒") - return .failed(reason: "相册写入权限被拒") - } catch { - Self.log.error("DouyinShare: 保存相册失败 \(error.localizedDescription, privacy: .public)") - return .failed(reason: "保存相册失败: \(error.localizedDescription)") - } - - // 3. 同时放剪贴板兜底(msext 行为:抖音从剪贴板读图) - UIPasteboard.general.image = image - - // 4. 按 msext 顺序尝试多个 URL Scheme - let schemes = [ - "snssdk1128://camera", // 抖音拍摄界面 - "snssdk1128://publish", // 发布界面 - "snssdk1128://share/image", // 图片分享(可能已废弃但保留) - "snssdk1128://" // 兜底基础 scheme - ] - - for schemeString in schemes { - guard let url = URL(string: schemeString), - UIApplication.shared.canOpenURL(url) - else { continue } - Self.log.debug("DouyinShare open: \(schemeString, privacy: .public)") - let opened = await UIApplication.shared.open(url) - if opened { return .success } - } - - return .failed(reason: "所有抖音 URL Scheme 都调起失败") + return top } } diff --git a/ylgamehall/Source/Share/QQShare.swift b/ylgamehall/Source/Share/QQShare.swift index 2c07000..89c88f4 100644 --- a/ylgamehall/Source/Share/QQShare.swift +++ b/ylgamehall/Source/Share/QQShare.swift @@ -2,21 +2,18 @@ // QQShare.swift // ylgamehall // -// QQ 分享 SharePlatform 实现(Phase 4.C 完整)。 +// QQ 分享 SharePlatform 实现,对齐 daoqi msext QQShareManager +// `shareWithContent:completion:`(按 type 分两个分支): +// - type == "2" → 截图分享:复制截图到 UIPasteboard + 拉起 mqqapi:// +// - else → 链接分享:mqqapi://share/to_fri?req_type=1&url=... // -// 移植 msext QQShareManager.m:716-771 simpleShareToQQFriend 的 URL Scheme -// fallback。**不依赖 QQ SDK**。 -// -// URL 构造: -// mqqapi://share/to_fri?version=1&cflag=0&req_type=1 -// &url=&title=&description= +// **不依赖 QQ SDK**(CLAUDE.md ADR-006):原项目走 SDK 弹原生会话列表, +// 新外壳走 URL Scheme + pasteboard 兜底实现等价 UX。 // // encodeString 与 msext line 1262-1268 等价:unreserved 字符集(RFC 3986 // alphanumeric + "-._~"),其它全部 percent encode。 // -// ⚠️ 当前仅支持链接分享(type == "1")。type == "2" 截图 / 其它 远端图 -// 在 Phase 4.F 接入(依赖 UIGraphicsImageRenderer + Photos / 临时文件 + -// mqqapi://share/to_fri?file_type=img 路径)。 +// fire-and-forget:sharesuccess 已在 FriendsShareHandler 立即触发。 // import UIKit @@ -38,28 +35,57 @@ public final class QQShare: SharePlatform { public init() {} - public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult { - guard isInstalled else { return .notInstalled(platform: name) } + public func share(_ content: ShareContent, scene: ShareScene) { + guard isInstalled else { return } - // 仅链接分享走完整 URL Scheme;图片分享留 Phase 4.F - guard content.type == "1" else { - Self.log.warning("QQShare: type=\(content.type, privacy: .public) (非链接) 暂未实现,返回 success 避免 H5 业务卡住") - return .success + if content.type == "2" { + shareScreenshot() + } else { + shareLink(content, scene: scene) + } + } + + // MARK: - 截图分享 + + /// 对齐 msext QQShareManager.m:1876-1906 isScreenshotShare 分支: + /// 原项目用 QQ SDK shareToQQFriend QQShareTypeImage 直接传 UIImage 给 SDK; + /// 无 SDK 路径下退化为:截图 → pasteboard → 拉起 QQ(用户在聊天里粘贴)。 + private func shareScreenshot() { + do { + let shot = try ImageProvider.captureScreenshot() + UIPasteboard.general.image = shot + } catch { + Self.log.error("QQShare 截图失败 \(error.localizedDescription, privacy: .public)") } - // 拼装 URL:mqqapi://share/to_fri? 或 mqqapi://share/to_qzone? - // scene == .timeline 走 QQ 空间 to_qzone;.friend 走 to_fri(QQ 好友) - let host = scene == .timeline ? "share/to_qzone" : "share/to_fri" - var parts: [String] = [ - "version=1", - "cflag=0", - "req_type=1" + 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 + } + } + + // MARK: - 链接分享 + + /// 对齐 msext QQShareManager.m simpleShareToQQFriend: + /// mqqapi://share/to_fri?version=1&cflag=0&req_type=1 + /// &url=&title=&description= + /// scene == .timeline 走 QQ 空间 (to_qzone);.friend 走 QQ 好友 (to_fri) + private func shareLink(_ content: ShareContent, scene: ShareScene) { + let host = scene == .timeline ? "share/to_qzone" : "share/to_fri" + var parts: [String] = ["version=1", "cflag=0"] if !content.webpageUrl.isEmpty { + parts.append("req_type=1") parts.append("url=\(Self.encode(content.webpageUrl))") } else { - // 无 URL → 改 req_type=0 文本分享 - parts = ["version=1", "cflag=0", "req_type=0"] + parts.append("req_type=0") } if !content.title.isEmpty { parts.append("title=\(Self.encode(content.title))") @@ -69,21 +95,11 @@ public final class QQShare: SharePlatform { } let urlString = "mqqapi://\(host)?\(parts.joined(separator: "&"))" - Self.log.debug("QQShare open: \(urlString, privacy: .public)") - guard let qqURL = URL(string: urlString), - UIApplication.shared.canOpenURL(qqURL) - else { - return .failed(reason: "invalid QQ URL") - } - - // URL Scheme 单向调起:msext 同款乐观策略 — 调起即视为 success - let opened = await UIApplication.shared.open(qqURL) - return opened ? .success : .failed(reason: "openURL failed") + UIApplication.shared.canOpenURL(qqURL) else { return } + UIApplication.shared.open(qqURL) } - /// 与 msext QQShareManager.m:1262-1268 等价:仅保留 RFC 3986 unreserved - /// 字符(alphanumeric + "-._~"),其它全部 percent encode。 nonisolated static func encode(_ string: String) -> String { var allowed = CharacterSet.alphanumerics allowed.insert(charactersIn: "-._~") diff --git a/ylgamehall/Source/Share/ShareCenter.swift b/ylgamehall/Source/Share/ShareCenter.swift index 89ecc0e..cc1acfa 100644 --- a/ylgamehall/Source/Share/ShareCenter.swift +++ b/ylgamehall/Source/Share/ShareCenter.swift @@ -6,7 +6,8 @@ // - sharefriend == "1"(好友/私聊)→ 弹 SharePanel 三选一(微信/QQ/抖音) // - sharefriend == "2"(朋友圈) → 直接 WechatShare(朋友圈 timeline 场景) // -// 与 Design §8.2 ShareKit 一致:策略模式 + SharePlatform 协议。 +// fire-and-forget:H5 端 sharesuccess 已在 FriendsShareHandler 立即触发, +// 这里只负责拉起 UI / SDK,不需要回传结果。 // import Foundation @@ -14,27 +15,19 @@ import Foundation @MainActor public enum ShareCenter { - /// 统一分发入口。由 FriendsShareHandler 在 handler 闭包内调用。 - /// completion 在分享结果回来后触发;UI 期间 await 暂停闭包但 cb 已先返回(msext 行为)。 - public static func dispatch(_ content: ShareContent, completion: @escaping (ShareResult) -> Void) { + public static func dispatch(_ content: ShareContent) { switch content.sharefriend { case "1": // 好友/私聊 → SharePanel 三选一 - SharePanel.show(content: content, completion: completion) + SharePanel.show(content: content) case "2": // 朋友圈 → 直接 WechatShare(timeline 场景) - Task { @MainActor in - let result = await WechatShare.shared.share(content, scene: .timeline) - completion(result) - } + WechatShare.shared.share(content, scene: .timeline) default: // 未知 sharefriend:msext 沿用第二条分支兜底(朋友圈微信) - Task { @MainActor in - let result = await WechatShare.shared.share(content, scene: .timeline) - completion(result) - } + WechatShare.shared.share(content, scene: .timeline) } } } diff --git a/ylgamehall/Source/Share/SharePanel.swift b/ylgamehall/Source/Share/SharePanel.swift index 0a425cb..288d038 100644 --- a/ylgamehall/Source/Share/SharePanel.swift +++ b/ylgamehall/Source/Share/SharePanel.swift @@ -27,13 +27,11 @@ public final class SharePanel: UIView { private static let animationDuration: TimeInterval = 0.25 private let content: ShareContent - private let completion: (ShareResult) -> Void private let contentView = UIView() - public init(content: ShareContent, completion: @escaping (ShareResult) -> Void) { + public init(content: ShareContent) { self.content = content - self.completion = completion super.init(frame: UIScreen.main.bounds) backgroundColor = UIColor(white: 0, alpha: 0) setupUI() @@ -46,13 +44,11 @@ public final class SharePanel: UIView { // MARK: - 公共入口 - /// 弹出分享面板。任何时候同一个面板只允许一个实例。 - public static func show(content: ShareContent, completion: @escaping (ShareResult) -> Void) { - guard let window = topWindow() else { - completion(.failed(reason: "no key window")) - return - } - let panel = SharePanel(content: content, completion: completion) + /// 弹出分享面板。fire-and-forget:sharesuccess 已在 FriendsShareHandler 立即触发, + /// 用户取消(点空白)或选好平台后不再向 H5 回报结果。 + public static func show(content: ShareContent) { + guard let window = topWindow() else { return } + let panel = SharePanel(content: content) window.addSubview(panel) panel.animateIn() } @@ -146,7 +142,7 @@ public final class SharePanel: UIView { @objc private func handleBackgroundTap(_ gesture: UITapGestureRecognizer) { let point = gesture.location(in: self) if !contentView.frame.contains(point) { - dismiss { [self] in completion(.cancelled) } + dismiss() } } @@ -155,12 +151,10 @@ public final class SharePanel: UIView { let platform = platforms[sender.tag] let scene: ShareScene = content.sharefriend == "2" ? .timeline : .friend let snapshotContent = content - dismiss { - Task { @MainActor in - let result = await platform.share(snapshotContent, scene: scene) - Self.log.debug("share via \(platform.name, privacy: .public) result=\(String(describing: result), privacy: .public)") - self.completion(result) - } + dismiss { [weak platform] in + guard let platform else { return } + Self.log.debug("share via \(platform.name, privacy: .public)") + platform.share(snapshotContent, scene: scene) } } } diff --git a/ylgamehall/Source/Share/SharePlatform.swift b/ylgamehall/Source/Share/SharePlatform.swift index a7d1d1a..1b99746 100644 --- a/ylgamehall/Source/Share/SharePlatform.swift +++ b/ylgamehall/Source/Share/SharePlatform.swift @@ -2,10 +2,15 @@ // SharePlatform.swift // ylgamehall // -// 分享平台抽象 + ShareContent 数据载荷 + ShareResult。 +// 分享平台抽象 + ShareContent 数据载荷。 // // 策略模式:微信 / QQ / 抖音 各自实现 SharePlatform 协议,由 ShareCenter -// 按 sharefriend / 用户选择分发。新增平台只加一个 conformance。 +// 按 sharefriend / 用户选择分发。 +// +// **fire-and-forget 语义**:share 调用即认为分享流程已发起;H5 端的 +// sharesuccess 在 FriendsShareHandler 解析参数后立即触发(与原 msext +// gameController.m 同款"乐观策略",由用户明示有意为之)。各平台 share() +// 只负责拉起对应 UI / URL Scheme,不再回传结果。 // // 详见 docs/H5-Native-Implementation-Design.md §8.2 ShareKit。 // @@ -22,9 +27,9 @@ nonisolated public struct ShareContent: Sendable { /// msext 旧描述:1 微信 / 2 微信 / 3 闲聊。新代码读了但不用(SharePanel 由用户选平台决定)。 /// 保留字段以维持契约 §3.1 [2]兼容性 public let sharetype: String - /// 1 链接 / 2 截图 / 其他=远端图 URL 分享 + /// 1 链接 / 2 截图 / 其他 = 按链接处理(原 WechatShareManager 行为,无独立远端图分支) public let type: String - /// 链接 URL 或图片 URL + /// 链接 URL public let webpageUrl: String public let title: String public let desc: String @@ -46,13 +51,6 @@ nonisolated public struct ShareContent: Sendable { } } -public enum ShareResult: Sendable { - case success - case cancelled - case notInstalled(platform: String) - case failed(reason: String) -} - /// 分享场景:好友 / 朋友圈(仅微信有意义;QQ / 抖音 暂不区分)。 public enum ShareScene: Sendable { case friend // 微信好友 / QQ 好友 @@ -61,10 +59,10 @@ public enum ShareScene: Sendable { @MainActor public protocol SharePlatform: AnyObject { - /// 平台唯一标识(用于 log / 错误提示) + /// 平台唯一标识(用于 log) var name: String { get } - /// 平台 app 是否已安装(用于 SharePanel 按钮置灰) + /// 平台 app 是否已安装(用于 SharePanel 按钮灰显) var isInstalled: Bool { get } - /// 执行分享(按 content.type 路由到链接 / 截图 / 远端图) - func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult + /// 拉起分享 UI;fire-and-forget,不回传结果(sharesuccess 已在 handler 层先发) + func share(_ content: ShareContent, scene: ShareScene) } diff --git a/ylgamehall/Source/Share/WechatShare.swift b/ylgamehall/Source/Share/WechatShare.swift index 7f718e2..0e42c18 100644 --- a/ylgamehall/Source/Share/WechatShare.swift +++ b/ylgamehall/Source/Share/WechatShare.swift @@ -2,17 +2,20 @@ // WechatShare.swift // ylgamehall // -// 微信分享 SharePlatform 实现。 +// 微信分享 SharePlatform 实现,1:1 对齐 daoqi msext WechatShareManager +// `shareWithContent:completion:`(仅两个分支): +// - type == "2" → 截图分享(getImageWithFullScreenshot + JPEG 0.6 + sendImageData) +// - else → 链接分享(sendLinkURL + webpageUrl + title + desc) // -// ⚠️ canImport(WechatOpenSDK) 守卫:SDK 未链接前 isInstalled = true / share = .success -// 让 SharePanel 按钮可点;项目方在 Xcode Embed & Sign 后真实路径激活。 +// 缩略图:app icon(与原 `[self getAppIconImage]` 同源策略,不用 sharelogo)。 +// +// fire-and-forget:H5 端的 sharesuccess 已在 FriendsShareHandler 立即触发, +// 此处不再回传结果(与原 dispatch_after 1s completion(YES) 实质等价)。 // import Foundation import UIKit -// 微信 SDK 1.8.2 通过 Bridging Header 导入,无 import 语句 - @MainActor public final class WechatShare: SharePlatform { @@ -26,55 +29,53 @@ public final class WechatShare: SharePlatform { public init() {} - public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult { - guard WXApi.isWXAppInstalled() else { - return .notInstalled(platform: name) - } + public func share(_ content: ShareContent, scene: ShareScene) { + guard WXApi.isWXAppInstalled() else { return } + let wxScene: WeChatManager.ShareScene = (scene == .timeline) ? .timeline : .session - do { - switch content.type { - case "1": - // 链接分享(msext gameController.m:613-635) - try await WeChatManager.shared.shareLink( - .init( - url: content.webpageUrl, - title: content.title, - desc: content.desc, - thumbnailURL: nil - ), - scene: wxScene - ) - case "2": - // 截图分享(msext gameController.m:637-658) - let shot = try ImageProvider.captureScreenshot() - guard let imageData = shot.jpegData(compressionQuality: 0.6) else { - return .failed(reason: "screenshot JPEG encode failed") + let thumb = Self.appIconImage() + + Task { @MainActor in + do { + if content.type == "2" { + // 截图分享(msext WechatShareManager isScreenshotShare 分支) + 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), + scene: wxScene + ) + } else { + // 链接分享(msext else 分支,含 type=="1" 与任何其它值) + try await WeChatManager.shared.shareLink( + .init( + url: content.webpageUrl, + title: content.title, + desc: content.desc, + thumbnailURL: nil + ), + scene: wxScene + ) } - let thumb = ImageProvider.scaledThumb(shot) - try await WeChatManager.shared.shareImage( - .init(imageData: imageData, thumb: thumb), - scene: wxScene - ) - default: - // 远端图分享(msext gameController.m:659-680:type 是其它值就当 URL 远端图) - let remote = try await ImageProvider.downloadRemote(content.webpageUrl) - // msext 直接用原始 data 作为大图;这里 UIImage 已是 decode 后的, - // 用 jpegData 重新编码维持等价(避免持有未压缩的原始字节膨胀)。 - guard let imageData = remote.jpegData(compressionQuality: 0.9) else { - return .failed(reason: "remote image JPEG encode failed") - } - let thumb = ImageProvider.scaledThumb(remote) - try await WeChatManager.shared.shareImage( - .init(imageData: imageData, thumb: thumb), - scene: wxScene - ) + } catch { + // SDK 回包失败不向 H5 反馈(sharesuccess 已乐观发出,对齐原项目) } - return .success - } catch let WeChatError.shareFailed(errCode) { - if errCode == -2 { return .cancelled } - return .failed(reason: "WeChat errCode \(errCode)") - } catch { - return .failed(reason: "\(error)") } } + + /// app icon 缩略图。读 Info.plist `CFBundleIcons` → `CFBundlePrimaryIcon` + /// → `CFBundleIconFiles` 的最后一个(最高分辨率),与原 msext + /// WechatShareManager `getAppIconImage` 等价。失败兜底返回 1x1 透明图。 + private static func appIconImage() -> UIImage { + if let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any], + let primary = icons["CFBundlePrimaryIcon"] as? [String: Any], + let files = primary["CFBundleIconFiles"] as? [String], + let last = files.last, + let icon = UIImage(named: last) { + return icon + } + if let icon = UIImage(named: "AppIcon") { return icon } + let renderer = UIGraphicsImageRenderer(size: CGSize(width: 1, height: 1)) + return renderer.image { _ in } + } }