From 512d7563eee51c8dfc24af25a0490f9ed2c1af61 Mon Sep 17 00:00:00 2001 From: commilitia Date: Tue, 7 Jul 2026 22:22:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=9F=E7=94=9F=20macOS=20=E5=BC=95=E6=93=8E?= =?UTF-8?q?=20+=20Broker=20=E7=99=BB=E5=BD=95=E6=8E=A5=E9=80=9A=EF=BC=88Ph?= =?UTF-8?q?ase=203a=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引擎 / 鉴权跨平台 in-place(iOS 路径 #if 守卫、逐字不变,仅加 macOS/AppKit 分支):EngineController(import 守卫 + 剪贴板 UIPasteboard→NSPasteboard 上下行两处 + device_type 平台条件)、EngineWebView(UIViewRepresentable→typealias 统一后分 NSViewRepresentable)、Theme(动态色 Color(nsColor:dynamicProvider) 同 RGB)、DeviceNameStore(Host.current().localizedName)、PushRegistry(NSApplication.registerForRemoteNotifications)、Auth/BrokerLogin(presentationAnchor→NSApplication.keyWindow)、Auth/AuthManager(死 import UIKit 守卫) - BackgroundTaskManager:整文件 #if os(iOS)(BGContinuedProcessingTask 为 API_UNAVAILABLE(macos)、编译级拒绝)+ macOS 同签名 no-op stub,使 EngineController.syncBackgroundTask 调用点无须改动 - device_type 修正(非机械):Mac 原生客户端 boot 注入 device_type=macos(与桌面 Wails 客户端一致,后端 / 各端已识别),不再硬编 ios——否则其他端设备列表图标错、且触发其 isWakeable 的 contains("ios") 误判本 Mac 可推送唤醒 - CommilitiaDropMac target 纳入「引擎 + 登录」最小集:Shared + Sources/Engine + Sources/Auth(除 iOS 专属 LoginView)+ RecordsStore / PushRegistry / LocalNetworkPermission / DeviceNameStore / BackgroundTaskManager + Resources/i18n;依赖 WebRTC;Info.plist 加 cdrop:// 回调 scheme(ASWebAuthenticationSession OAuth 回调)+ 本地网络(NSLocalNetworkUsageDescription / NSBonjourServices _cdrop._tcp) - Mac 壳接引擎:CommilitiaDropMacApp(@State auth + engine,MacAppRoot 登录门 + 会话激活分桶记录)、MacLoginView(Broker 账户登录,免相机)、MacRootView(背景挂离屏引擎 WebView 保活 + 设备真实 presence 列表 + 设置账号/登出/引擎日志)、MacMenuContent(账号 + 在线设备数)。传输/文件/消息详情暂占位,随 Phase 3b 复用 iOS 视图 - 验证:just mac-build(CommilitiaDropMac.app)+ iOS 模拟器(CommilitiaDrop.app)均 BUILD SUCCEEDED。运行期登录 / 引擎联通待真机自测(Broker OAuth 需账号交互) --- ios/CDrop/MacApp/CommilitiaDropMacApp.swift | 52 +++++++- ios/CDrop/MacApp/MacLoginView.swift | 54 +++++++++ ios/CDrop/MacApp/MacMenuContent.swift | 23 +++- ios/CDrop/MacApp/MacRootView.swift | 113 +++++++++++++++++- ios/CDrop/Shared/Theme.swift | 22 ++++ ios/CDrop/Sources/Auth/AuthManager.swift | 2 + ios/CDrop/Sources/Auth/BrokerLogin.swift | 8 ++ ios/CDrop/Sources/BackgroundTaskManager.swift | 13 ++ ios/CDrop/Sources/DeviceNameStore.swift | 6 + .../Sources/Engine/EngineController.swift | 25 +++- ios/CDrop/Sources/Engine/EngineWebView.swift | 24 +++- ios/CDrop/Sources/PushRegistry.swift | 12 +- ios/CDrop/Support/macOS/App-Info.plist | 17 +++ ios/CDrop/project.yml | 27 +++++ 14 files changed, 378 insertions(+), 20 deletions(-) create mode 100644 ios/CDrop/MacApp/MacLoginView.swift diff --git a/ios/CDrop/MacApp/CommilitiaDropMacApp.swift b/ios/CDrop/MacApp/CommilitiaDropMacApp.swift index 3b664a4..4a478b9 100644 --- a/ios/CDrop/MacApp/CommilitiaDropMacApp.swift +++ b/ios/CDrop/MacApp/CommilitiaDropMacApp.swift @@ -1,18 +1,21 @@ #if os(macOS) import SwiftUI -// 原生 macOS 客户端入口。单窗口 + 菜单栏常驻,桌面壳(MacRootView)替代 iOS 的标签栏。 -// 当前为脚手架:导航骨架 + 菜单栏占位;引擎(LibWebRtcEngine + 离屏 WebView)、复用视图与 -// 桌面专属功能(剪贴板自动同步 / 全局热键 / 原生通知)随后续阶段并入。iOS 入口另见 CDropApp。 +// 原生 macOS 客户端入口。单窗口 + 菜单栏常驻,桌面壳(MacRootView)替代 iOS 的标签栏。复用 iOS 引擎 +// (EngineController:离屏 WKWebView 跑 JS 引擎 + 原生 libwebrtc 数据面)与鉴权(AuthManager:Broker +// 登录);引擎 WebView 挂在 MacRootView 背景保活(登录后才启)。iOS 入口另见 CDropApp。桌面专属功能 +// (剪贴板自动同步 / 全局热键 / 原生通知)随后续阶段并入。 @main struct CommilitiaDropMacApp: App { + @State private var auth = AuthManager() + @State private var engine = EngineController() + var body: some Scene { - // 启动即打开的单主窗口。 Window("Commilitia Drop", id: "main") { - MacRootView() + MacAppRoot(auth: auth, engine: engine) .frame(minWidth: 820, minHeight: 560) } .defaultLaunchBehavior(.presented) @@ -23,10 +26,11 @@ struct CommilitiaDropMacApp: App CommandGroup(replacing: .newItem) {} } - // 菜单栏常驻:状态 + 快捷剪贴板 / Quick Send(当前占位)。 MenuBarExtra { MacMenuContent() + .environment(engine) + .environment(auth) } label: { @@ -35,4 +39,40 @@ struct CommilitiaDropMacApp: App .menuBarExtraStyle(.window) } } + +// 登录门:有会话进桌面壳(引擎随 MacRootView 背景 WebView 启动),无会话进原生 Broker 登录页。逻辑 +// 对齐 iOS 的 AppRoot(CDropApp):注入 auth 源、会话激活即按 userId 加载持久记录。 +struct MacAppRoot: View +{ + let auth: AuthManager + let engine: EngineController + + var body: some View + { + Group + { + if auth.session != nil + { + MacRootView() + .environment(engine) + .environment(auth) + } + else + { + MacLoginView() + .environment(auth) + } + } + .onAppear + { + engine.auth = auth + // 冷启已有会话:立即按 userId 加载该用户的持久记录(reset 只清内存不动盘)。 + if let uid = auth.session?.user.id { engine.activateRecords(userId: uid) } + } + .onChange(of: auth.session?.user.id) + { _, uid in + if let uid { engine.activateRecords(userId: uid) } + } + } +} #endif diff --git a/ios/CDrop/MacApp/MacLoginView.swift b/ios/CDrop/MacApp/MacLoginView.swift new file mode 100644 index 0000000..dfbf654 --- /dev/null +++ b/ios/CDrop/MacApp/MacLoginView.swift @@ -0,0 +1,54 @@ +#if os(macOS) +import SwiftUI + +// 原生 macOS 登录:以 Commilitia 账户(Auth Broker)登录为主,免相机——ASWebAuthenticationSession +// 弹系统授权窗,回调经 cdrop:// 回到 app(见 BrokerLogin / AuthManager.startBrokerLogin)。不复用 +// iOS 的 LoginView(其二维码 / UIImage 路径为 iOS 专属,且没人扫 Mac 屏幕来登录 Mac 自己)。 +struct MacLoginView: View +{ + @Environment(AuthManager.self) private var auth + @State private var busy = false + + var body: some View + { + VStack(spacing: 20) + { + Image(systemName: "paperplane.fill") + .font(.system(size: 52)) + .foregroundStyle(.tint) + Text("Commilitia Drop") + .font(.largeTitle.weight(.semibold)) + Text("跨设备直连传输与云剪贴板") + .font(.callout) + .foregroundStyle(.secondary) + + Button + { + busy = true + Task + { + await auth.startBrokerLogin() + busy = false + } + } + label: + { + Text(busy ? "登录中…" : "使用 Commilitia 账户登录") + .frame(maxWidth: 260) + } + .controlSize(.large) + .buttonStyle(.borderedProminent) + .disabled(busy) + + if !auth.statusText.isEmpty + { + Text(auth.statusText) + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding(40) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} +#endif diff --git a/ios/CDrop/MacApp/MacMenuContent.swift b/ios/CDrop/MacApp/MacMenuContent.swift index fe1eca9..d8eb18c 100644 --- a/ios/CDrop/MacApp/MacMenuContent.swift +++ b/ios/CDrop/MacApp/MacMenuContent.swift @@ -1,9 +1,11 @@ #if os(macOS) import SwiftUI -// 菜单栏常驻内容(占位):后续接入连接状态 + 一键剪贴板 push/pull + Quick Send。 +// 菜单栏常驻内容:账号 / 在线设备数 / 打开主窗口。后续接入一键剪贴板 push/pull + Quick Send。 struct MacMenuContent: View { + @Environment(EngineController.self) private var engine + @Environment(AuthManager.self) private var auth @Environment(\.openWindow) private var openWindow var body: some View @@ -12,9 +14,22 @@ struct MacMenuContent: View { Text("Commilitia Drop") .font(.headline) - Text("桌面功能适配中") - .font(.caption) - .foregroundStyle(.secondary) + + if let name = auth.session?.user.name + { + Text(name) + .font(.caption) + .foregroundStyle(.secondary) + Text("在线设备:\(engine.devices.filter(\.online).count)") + .font(.caption) + .foregroundStyle(.secondary) + } + else + { + Text("未登录") + .font(.caption) + .foregroundStyle(.secondary) + } Divider() Button("打开主窗口") { openWindow(id: "main") } diff --git a/ios/CDrop/MacApp/MacRootView.swift b/ios/CDrop/MacApp/MacRootView.swift index f0b92fe..cec9fd4 100644 --- a/ios/CDrop/MacApp/MacRootView.swift +++ b/ios/CDrop/MacApp/MacRootView.swift @@ -2,10 +2,13 @@ import SwiftUI // 桌面根界面:NavigationSplitView 侧栏(传输 / 文件 / 消息 / 设备 / 设置)+ 详情 NavigationStack, -// 替代 iOS 的标签栏。当前为脚手架:各区详情为占位(MacPlaceholder),随后续阶段接入复用视图 -// 与桌面专属功能。分区标签暂硬编中文;接入 i18n(Shared/I18n)后改走 t()(与 iOS 单一真源一致)。 +// 替代 iOS 的标签栏。离屏无头引擎 WebView 挂在背景保活(同 iOS RootView)。传输 / 文件 / 消息详情为 +// 占位(随 Phase 3b 复用 iOS 视图);设备 / 设置已接引擎真实态(presence / 账号 / 日志),作 3a 引擎 +// 联通证明。分区标签暂硬编中文;接入 i18n(t())待 Phase 3b 复用视图时统一。 struct MacRootView: View { + @Environment(EngineController.self) private var engine + @Environment(AuthManager.self) private var auth @State private var selection: MacSection = .transfers var body: some View @@ -23,6 +26,7 @@ struct MacRootView: View ForEach(MacSection.sidebar) { section in Label(section.title, systemImage: section.icon) + .badge(badge(for: section)) .tag(section) } } @@ -37,6 +41,14 @@ struct MacRootView: View detail } } + // 离屏无头引擎 WebView:零尺寸不可见,仅保持存活(登录后随本视图出现即 boot)。同 iOS RootView。 + .background + { + EngineWebView(controller: engine, auth: auth) + .frame(width: 0, height: 0) + .opacity(0) + .allowsHitTesting(false) + } } @ViewBuilder @@ -51,9 +63,9 @@ struct MacRootView: View case .messages: MacPlaceholder(section: .messages) case .devices: - MacPlaceholder(section: .devices) + MacDevicesView() case .settings: - MacPlaceholder(section: .settings) + MacSettingsView() } } @@ -69,6 +81,17 @@ struct MacRootView: View .lineLimit(1) } } + + // 未读红点:消息 / 传输(对齐 iOS RootView 的 badge)。 + private func badge(for section: MacSection) -> Int + { + switch section + { + case .messages: return engine.unreadMessages + case .transfers: return engine.unreadTransfers + default: return 0 + } + } } // 侧栏分区。与 iOS 标签一致:传输 / 文件 / 消息 / 设备 / 设置。 @@ -105,7 +128,7 @@ enum MacSection: Hashable, Identifiable, CaseIterable static var sidebar: [MacSection] { MacSection.allCases } } -// 阶段占位详情:标明该区将接入的功能,随后续阶段替换为实际视图。 +// 阶段占位详情(传输 / 文件 / 消息,随 Phase 3b 替换为复用 iOS 视图)。 struct MacPlaceholder: View { let section: MacSection @@ -123,4 +146,84 @@ struct MacPlaceholder: View .navigationTitle(section.title) } } + +// 设备:引擎经桥推来的真实 presence(在线绿点 + 名称 + 类型),证明登录后引擎已联通信令 / SSE。 +struct MacDevicesView: View +{ + @Environment(EngineController.self) private var engine + + var body: some View + { + Group + { + if engine.devices.isEmpty + { + ContentUnavailableView("暂无其他设备", systemImage: "laptopcomputer.and.iphone", + description: Text("同账户的其他在线设备将显示在此。")) + } + else + { + List(engine.devices) + { dev in + HStack(spacing: 10) + { + Circle() + .fill(dev.online ? Color.green : Color.secondary) + .frame(width: 9, height: 9) + Text(dev.name) + Spacer() + Text(dev.type) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + .navigationTitle("设备") + } +} + +// 设置:账号 + 本机设备名 + 登出 + 引擎日志(3a 联通与现场自检)。 +struct MacSettingsView: View +{ + @Environment(EngineController.self) private var engine + @Environment(AuthManager.self) private var auth + + var body: some View + { + Form + { + Section("账号") + { + LabeledContent("用户", value: auth.session?.user.name ?? "—") + LabeledContent("本机设备", value: engine.deviceName) + Button("登出", role: .destructive) + { + engine.reset() + auth.logout() + } + } + Section("引擎日志") + { + if engine.logs.isEmpty + { + Text("暂无日志").foregroundStyle(.secondary) + } + else + { + ForEach(engine.logs.prefix(30)) + { entry in + Text(entry.message) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + .lineLimit(2) + .textSelection(.enabled) + } + } + } + } + .formStyle(.grouped) + .navigationTitle("设置") + } +} #endif diff --git a/ios/CDrop/Shared/Theme.swift b/ios/CDrop/Shared/Theme.swift index 415fd5d..149230d 100644 --- a/ios/CDrop/Shared/Theme.swift +++ b/ios/CDrop/Shared/Theme.swift @@ -1,22 +1,44 @@ import SwiftUI +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit +#endif // 品牌主色单一真源(强约束③,与 web 主 accent 对齐):浅色 #644AC9 / 深色 #9580FF。放在 Shared,使主 // app、控件 / 小组件扩展共用同一定义(此前仅在主 app 的 RootView 内,扩展无法引用)。 extension Color { + #if os(iOS) static let cdropAccent = Color(uiColor: UIColor { trait in trait.userInterfaceStyle == .dark ? UIColor(red: 0.584, green: 0.502, blue: 1.0, alpha: 1) : UIColor(red: 0.392, green: 0.290, blue: 0.788, alpha: 1) }) + #else + static let cdropAccent = Color(nsColor: NSColor(name: nil, dynamicProvider: + { appearance in + appearance.bestMatch(from: [ .aqua, .darkAqua ]) == .darkAqua + ? NSColor(red: 0.584, green: 0.502, blue: 1.0, alpha: 1) + : NSColor(red: 0.392, green: 0.290, blue: 0.788, alpha: 1) + })) + #endif // 品牌琥珀色(字标里「Drop」用色,对齐 web 品牌资产):浅色 #DB9410 / 深色 #F8C637。 + #if os(iOS) static let cdropAmber = Color(uiColor: UIColor { trait in trait.userInterfaceStyle == .dark ? UIColor(red: 0.973, green: 0.776, blue: 0.216, alpha: 1) : UIColor(red: 0.859, green: 0.580, blue: 0.063, alpha: 1) }) + #else + static let cdropAmber = Color(nsColor: NSColor(name: nil, dynamicProvider: + { appearance in + appearance.bestMatch(from: [ .aqua, .darkAqua ]) == .darkAqua + ? NSColor(red: 0.973, green: 0.776, blue: 0.216, alpha: 1) + : NSColor(red: 0.859, green: 0.580, blue: 0.063, alpha: 1) + })) + #endif } diff --git a/ios/CDrop/Sources/Auth/AuthManager.swift b/ios/CDrop/Sources/Auth/AuthManager.swift index 85cddfd..5bbe802 100644 --- a/ios/CDrop/Sources/Auth/AuthManager.swift +++ b/ios/CDrop/Sources/Auth/AuthManager.swift @@ -1,7 +1,9 @@ import AuthenticationServices import Foundation import Observation +#if canImport(UIKit) import UIKit +#endif // 扫码登录(新设备侧)+ 会话持有。iOS 显示二维码,已登录的 web / 桌面设备用“扫码登录” // 扫码批准,本端轮询 /api/auth/qr/status 拿到自签会话。字段 / 流程见后端 internal/httpapi/ diff --git a/ios/CDrop/Sources/Auth/BrokerLogin.swift b/ios/CDrop/Sources/Auth/BrokerLogin.swift index e6d0dab..cebce47 100644 --- a/ios/CDrop/Sources/Auth/BrokerLogin.swift +++ b/ios/CDrop/Sources/Auth/BrokerLogin.swift @@ -1,7 +1,11 @@ import AuthenticationServices import CryptoKit import Foundation +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit +#endif // 应用内 Broker 登录(device-authorization + PKCE,对齐桌面 desktop/platform/oauth.go)。iOS 无法 // 跑 loopback 监听,故用 ASWebAuthenticationSession + 自定义 scheme 回调(cdrop://auth-callback): @@ -53,10 +57,14 @@ final class BrokerAuthFlow: NSObject, ASWebAuthenticationPresentationContextProv func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + #if os(iOS) let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } return scenes.flatMap { $0.windows }.first { $0.isKeyWindow } ?? scenes.first?.windows.first ?? ASPresentationAnchor() + #else + return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor() + #endif } } diff --git a/ios/CDrop/Sources/BackgroundTaskManager.swift b/ios/CDrop/Sources/BackgroundTaskManager.swift index 0773306..45236c5 100644 --- a/ios/CDrop/Sources/BackgroundTaskManager.swift +++ b/ios/CDrop/Sources/BackgroundTaskManager.swift @@ -1,3 +1,4 @@ +#if os(iOS) import BackgroundTasks import UIKit @@ -104,3 +105,15 @@ final class BackgroundTaskManager } } } +#else +// macOS 无 BackgroundTasks(BGContinuedProcessingTask 为 API_UNAVAILABLE(macos)):桌面无「切后台 +// 挂起」概念,续传任务无意义。提供同签名 no-op stub,使 EngineController.syncBackgroundTask 等调用点 +// 在 macOS 编译无须改动。 +final class BackgroundTaskManager +{ + static let shared = BackgroundTaskManager() + private init() {} + func registerOnce() {} + func sync(activeCount: Int, fraction: Double) {} +} +#endif diff --git a/ios/CDrop/Sources/DeviceNameStore.swift b/ios/CDrop/Sources/DeviceNameStore.swift index 3e8f5ad..e3b2886 100644 --- a/ios/CDrop/Sources/DeviceNameStore.swift +++ b/ios/CDrop/Sources/DeviceNameStore.swift @@ -1,5 +1,7 @@ import Foundation +#if canImport(UIKit) import UIKit +#endif // 用户可编辑的设备名,持久在 UserDefaults。用于扫码登录 qr/start 的初始 device_name。设备名 // 已与会话令牌解耦:登录后在设置页改名经 PATCH /api/devices/{device_id} 即时生效(后端按稳定 @@ -14,7 +16,11 @@ enum DeviceNameStore get { if let v = UserDefaults.standard.string(forKey: key), !v.isEmpty { return v } + #if os(iOS) let sys = UIDevice.current.name + #else + let sys = Host.current().localizedName ?? ProcessInfo.processInfo.hostName + #endif return sys.isEmpty ? "iPhone" : sys } set diff --git a/ios/CDrop/Sources/Engine/EngineController.swift b/ios/CDrop/Sources/Engine/EngineController.swift index d207e47..eb5f425 100644 --- a/ios/CDrop/Sources/Engine/EngineController.swift +++ b/ios/CDrop/Sources/Engine/EngineController.swift @@ -1,6 +1,10 @@ import Foundation import Observation +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit +#endif import WebKit // 引擎过桥推来的设备 presence(对应 web store 的 DeviceInfo)。lastSeen 是 ms epoch。 @@ -275,12 +279,18 @@ final class EngineController: NSObject let ucc = WKUserContentController() ucc.add(self, name: "cdropEngine") - // boot 注入:把登录后的真实会话注入引擎(无会话则 null)。device_type 标 ios 供后端 - // 登记;api_base 默认空(引擎与 /api 同源,从 prod 加载时相对 URL 即可)。 + // boot 注入:把登录后的真实会话注入引擎(无会话则 null)。device_type 标平台供后端登记 / + // 其他端设备图标 + isWakeable 推送判定;api_base 默认空(引擎与 /api 同源,从 prod 加载时相对 + // URL 即可)。iOS 为 "ios",原生 Mac 为 "macos"(与桌面 Wails 客户端一致,后端 / 各端已识别)。 let debug = ProcessInfo.processInfo.environment["CDROP_DEBUG_SESSION"] == "1" + #if os(macOS) + let deviceType = "macos" + #else + let deviceType = "ios" + #endif let boot = "window.__CDROP_BOOT__ = { session: \(sessionJSON()), " + "device_name: \(jsString(currentDeviceName())), " - + "api_base: \(jsString(apiBaseForBoot())), device_type: \"ios\", debug: \(debug) };" + + "api_base: \(jsString(apiBaseForBoot())), device_type: \(jsString(deviceType)), debug: \(debug) };" ucc.addUserScript(WKUserScript(source: boot, injectionTime: .atDocumentStart, forMainFrameOnly: true)) @@ -539,7 +549,11 @@ final class EngineController: NSObject // 剪贴板上行:读本机 UIPasteboard 文本交引擎上传云剪贴板(用户手势触发,前台读取合规)。 func uploadClipboard() { + #if os(macOS) + let content = NSPasteboard.general.string(forType: .string) ?? "" + #else let content = UIPasteboard.general.string ?? "" + #endif guard !content.isEmpty else { notify(t("ios.clipboard.empty"), kind: .info); return } sendCommand("clipboardUpload", payload: [ "content": content ]) } @@ -982,7 +996,12 @@ extension EngineController: WKScriptMessageHandler // 引擎拉回的云剪贴板内容 → 写入本机 UIPasteboard。 if let p = payload as? [String: Any], let content = p["content"] as? String, !content.isEmpty { + #if os(macOS) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(content, forType: .string) + #else UIPasteboard.general.string = content + #endif notify(t("ios.clipboard.pulled"), kind: .success) } else diff --git a/ios/CDrop/Sources/Engine/EngineWebView.swift b/ios/CDrop/Sources/Engine/EngineWebView.swift index d073e9e..2e2ce5b 100644 --- a/ios/CDrop/Sources/Engine/EngineWebView.swift +++ b/ios/CDrop/Sources/Engine/EngineWebView.swift @@ -1,9 +1,18 @@ import SwiftUI import WebKit +// 跨平台桥接:iOS 用 UIViewRepresentable,macOS 原生用 NSViewRepresentable,二者所需方法名 +// (makeUIView/updateUIView 与 makeNSView/updateNSView)不同,故经 typealias 统一协议名后, +// struct 体内仅方法实现按平台分支。 +#if os(iOS) +private typealias PlatformViewRepresentable = UIViewRepresentable +#else +private typealias PlatformViewRepresentable = NSViewRepresentable +#endif + // 把 EngineController 持有的离屏 WKWebView 桥进 SwiftUI 视图树(保持存活)。无可见 UI // 用途——引擎由 controller 经 JS 桥驱动(见 ios/PLAN.md arch A)。 -struct EngineWebView: UIViewRepresentable +struct EngineWebView: PlatformViewRepresentable { let controller: EngineController // 会话源:在构建 WebView(注入 __CDROP_BOOT__)之前确定性接好。冷启动重开时,本视图的 @@ -11,6 +20,7 @@ struct EngineWebView: UIViewRepresentable // session:null → 引擎报「missing injected session」整个 boot 死(#11)。故在此同步接好。 let auth: AuthManager + #if os(iOS) func makeUIView(context: Context) -> WKWebView { controller.auth = auth @@ -21,4 +31,16 @@ struct EngineWebView: UIViewRepresentable { // 无需更新。 } + #else + func makeNSView(context: Context) -> WKWebView + { + controller.auth = auth + return controller.makeWebView() + } + + func updateNSView(_ nsView: WKWebView, context: Context) + { + // 无需更新。 + } + #endif } diff --git a/ios/CDrop/Sources/PushRegistry.swift b/ios/CDrop/Sources/PushRegistry.swift index 58c8aff..bf2b59e 100644 --- a/ios/CDrop/Sources/PushRegistry.swift +++ b/ios/CDrop/Sources/PushRegistry.swift @@ -1,4 +1,8 @@ +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit +#endif import UserNotifications // APNs 设备令牌的交汇点:解耦 AppDelegate(经 UIKit 回调拿到令牌)与 EngineController(持 @@ -28,7 +32,13 @@ final class PushRegistry: NSObject UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in guard granted else { return } - Task { @MainActor in UIApplication.shared.registerForRemoteNotifications() } + Task { @MainActor in + #if os(iOS) + UIApplication.shared.registerForRemoteNotifications() + #else + NSApplication.shared.registerForRemoteNotifications() + #endif + } } } diff --git a/ios/CDrop/Support/macOS/App-Info.plist b/ios/CDrop/Support/macOS/App-Info.plist index 95d6468..ff6c112 100644 --- a/ios/CDrop/Support/macOS/App-Info.plist +++ b/ios/CDrop/Support/macOS/App-Info.plist @@ -24,9 +24,26 @@ APPL CFBundleShortVersionString 1.0 + CFBundleURLTypes + + + CFBundleURLName + net.commilitia.Commilitia-Drop + CFBundleURLSchemes + + cdrop + + + CFBundleVersion 1 LSMinimumSystemVersion 26.0 + NSBonjourServices + + _cdrop._tcp + + NSLocalNetworkUsageDescription + Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。 diff --git a/ios/CDrop/project.yml b/ios/CDrop/project.yml index 2aaf1d4..2621925 100644 --- a/ios/CDrop/project.yml +++ b/ios/CDrop/project.yml @@ -180,6 +180,24 @@ targets: deploymentTarget: "26.0" sources: - path: MacApp + - path: Shared + # 复用 iOS 引擎与鉴权(跨平台 in-place,UIKit 处 #if 守卫 / AppKit 分支)。仅纳入「引擎 + 登录」 + # 最小集:Engine 全部 + Auth(除 iOS 专属 LoginView,Mac 另写原生登录)+ 若干单例 + i18n。iOS UI + # 树(RootView / MessagesView / ScannerView / AppDelegate 等)暂不纳入,随 Phase 3b 复用。 + - path: Sources/Engine + - path: Sources/Auth + excludes: + - LoginView.swift + - path: Sources/RecordsStore.swift + - path: Sources/PushRegistry.swift + - path: Sources/LocalNetworkPermission.swift + - path: Sources/DeviceNameStore.swift + - path: Sources/BackgroundTaskManager.swift + - path: Sources/Resources/i18n + dependencies: + # 原生数据面 libwebrtc(LibWebRtcEngine)。macOS 切片缺子头,构建经 just mac-prep 补齐 + # (scripts/patch-macos-webrtc-headers.sh)。 + - package: WebRTC info: path: Support/macOS/App-Info.plist properties: @@ -191,6 +209,15 @@ targets: - zh-Hans - zh-Hant - en + # ASWebAuthenticationSession 的 OAuth 回调经 cdrop:// scheme 回到 app(同 iOS)。 + CFBundleURLTypes: + - CFBundleURLName: net.commilitia.Commilitia-Drop + CFBundleURLSchemes: + - cdrop + # 本地网络:libwebrtc 收集 host 候选、同内网直连须本地网络权限(macOS 15+ 亦弹窗)。 + NSLocalNetworkUsageDescription: "Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。" + NSBonjourServices: + - "_cdrop._tcp" settings: base: PRODUCT_BUNDLE_IDENTIFIER: net.commilitia.Commilitia-Drop