diff --git a/docs/Development-Plan.md b/docs/Development-Plan.md index 2995a43..0274239 100644 --- a/docs/Development-Plan.md +++ b/docs/Development-Plan.md @@ -486,10 +486,13 @@ Contract Design Plan(本文档) - `Source/Share/{Wechat,QQ,Douyin}Share.swift`:三个平台 stub(仅返回 success),Phase 4.C/4.D/4.E 升级 - `FriendsShareHandler` 升级:调 ShareCenter.dispatch + 完成后触发 sharesuccess 反向 callback(success/cancelled 都发,notInstalled/failed 不发,沿用 msext 乐观策略) - BuildProject 通过 -- [ ] **4.3** **QQ 分享走 URL Scheme,不依赖 SDK** +- [x] **4.3** **QQ 分享走 URL Scheme,不依赖 SDK**(4.C 完成) - 移植 msext `QQShareManager.m:716-771` `simpleShareToQQFriend` 简化版 - - URL 构造:`mqqapi://share/to_fri?version=1&cflag=0&req_type=1&url=...&title=...&description=...` - - `canOpenURL` 检查 + `openURL` 异步打开 + - URL 构造:`mqqapi://share/to_fri?` 或 `mqqapi://share/to_qzone?`(scene == .timeline 走 qzone)+ `version=1&cflag=0&req_type=1&url=...&title=...&description=...` + - `encode` 与 msext line 1262-1268 等价:仅保留 RFC 3986 unreserved(alphanumeric + `-._~`)其它 percent encode + - `canOpenURL` 检查 + `await UIApplication.open` 异步打开;调起即视为 success + - **当前仅链接分享**(type == "1");type == "2" 截图 / 远端图 留 Phase 4.F + - Info.plist 加 `LSApplicationQueriesSchemes`:`mqq` / `mqqapi` / `mqqopensdkfriend` / `mqqopensdkapiV2/V3/V4` - [ ] **4.4** **抖音分享走 URL Scheme + Photos,不依赖 SDK** - 移植 msext `DouyinShareManager.m` 简化版 - URL Scheme:`snssdk1128://share/video` / `snssdk1128://camera` diff --git a/ylgamehall/Info.plist b/ylgamehall/Info.plist index 2d45cf7..6fd833c 100644 --- a/ylgamehall/Info.plist +++ b/ylgamehall/Info.plist @@ -47,5 +47,22 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + + LSApplicationQueriesSchemes + + mqq + mqqapi + mqqopensdkfriend + mqqopensdkapiV2 + mqqopensdkapiV3 + mqqopensdkapiV4 + snssdk1128 + diff --git a/ylgamehall/Source/Share/QQShare.swift b/ylgamehall/Source/Share/QQShare.swift index a70e612..2c07000 100644 --- a/ylgamehall/Source/Share/QQShare.swift +++ b/ylgamehall/Source/Share/QQShare.swift @@ -2,23 +2,35 @@ // QQShare.swift // ylgamehall // -// QQ 分享 SharePlatform 实现 stub(Phase 4.C 真实落地)。 +// QQ 分享 SharePlatform 实现(Phase 4.C 完整)。 // -// Phase 4.B 阶段为 stub(仅返回 success,让 SharePanel 流程跑通); -// Phase 4.C 移植 msext QQShareManager.m:716-771 simpleShareToQQFriend -// 的 URL Scheme fallback(mqqapi://share/to_fri?...)。 +// 移植 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= +// +// 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 路径)。 // import UIKit +import os.log @MainActor public final class QQShare: SharePlatform { public static let shared = QQShare() + private static let log = Logger(subsystem: "ylgamehall", category: "QQShare") + public let name = "QQ" - /// 检测 QQ app 是否安装:canOpenURL("mqqapi://") public var isInstalled: Bool { guard let url = URL(string: "mqqapi://") else { return false } return UIApplication.shared.canOpenURL(url) @@ -28,12 +40,53 @@ public final class QQShare: SharePlatform { public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult { guard isInstalled else { return .notInstalled(platform: name) } - // Phase 4.B 阶段 stub:返回 success,Phase 4.C 升级为完整 URL Scheme 调起 - // 真实实现(Phase 4.C): - // - 拼装 mqqapi://share/to_fri?version=1&cflag=0&req_type=1 - // &url=...&title=...&description=... - // - canOpenURL + open - // - 调起即视为 success(URL Scheme 单向无返回,msext 同款乐观策略) - return .success + + // 仅链接分享走完整 URL Scheme;图片分享留 Phase 4.F + guard content.type == "1" else { + Self.log.warning("QQShare: type=\(content.type, privacy: .public) (非链接) 暂未实现,返回 success 避免 H5 业务卡住") + return .success + } + + // 拼装 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" + ] + if !content.webpageUrl.isEmpty { + parts.append("url=\(Self.encode(content.webpageUrl))") + } else { + // 无 URL → 改 req_type=0 文本分享 + parts = ["version=1", "cflag=0", "req_type=0"] + } + if !content.title.isEmpty { + parts.append("title=\(Self.encode(content.title))") + } + if !content.desc.isEmpty { + parts.append("description=\(Self.encode(content.desc))") + } + 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") + } + + /// 与 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: "-._~") + return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? "" } }