// // QQShare.swift // ylgamehall // // QQ 分享 SharePlatform 实现,对齐 daoqi msext QQShareManager // `shareWithContent:completion:`(按 type 分两个分支): // - type == "2" → 截图分享:复制截图到 UIPasteboard + 弹引导框("立即打开 QQ") // - else → 链接分享:mqqapi://share/to_fri?req_type=1&url=... // // **不依赖 QQ SDK**(CLAUDE.md ADR-006):原项目走 SDK 弹原生会话列表带 // 图片预览;无 SDK 路径下退化为"剪贴板 + 引导框",与 Douyin 同款 UX 模式。 // // encodeString 与 msext line 1262-1268 等价:unreserved 字符集(RFC 3986 // alphanumeric + "-._~"),其它全部 percent encode。 // // fire-and-forget:sharesuccess 已在 FriendsShareHandler 立即触发。 // 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" public var isInstalled: Bool { guard let url = URL(string: "mqqapi://") else { return false } return UIApplication.shared.canOpenURL(url) } public init() {} public func share(_ content: ShareContent, scene: ShareScene) { guard isInstalled else { return } if content.type == "2" { shareScreenshot() } else { shareLink(content, scene: scene) } } // MARK: - 截图分享 /// 对齐 msext QQShareManager.m:1876-1906 isScreenshotShare 分支: /// 原项目用 QQ SDK shareToQQFriend QQShareTypeImage 直接传 UIImage 给 SDK; /// 无 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)") 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: - 链接分享 /// 对齐 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 { parts.append("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: "&"))" guard let qqURL = URL(string: urlString), UIApplication.shared.canOpenURL(qqURL) else { return } UIApplication.shared.open(qqURL) } nonisolated static func encode(_ string: String) -> String { var allowed = CharacterSet.alphanumerics allowed.insert(charactersIn: "-._~") return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? "" } }