原生 macOS 引擎 + Broker 登录接通(Phase 3a)
- 引擎 / 鉴权跨平台 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 需账号交互)
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import AuthenticationServices
|
||||
import Foundation
|
||||
import Observation
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
// 扫码登录(新设备侧)+ 会话持有。iOS 显示二维码,已登录的 web / 桌面设备用“扫码登录”
|
||||
// 扫码批准,本端轮询 /api/auth/qr/status 拿到自签会话。字段 / 流程见后端 internal/httpapi/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user