原生 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,18 +1,21 @@
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// 原生 macOS 客户端入口。单窗口 + 菜单栏常驻,桌面壳(MacRootView)替代 iOS 的标签栏。
|
// 原生 macOS 客户端入口。单窗口 + 菜单栏常驻,桌面壳(MacRootView)替代 iOS 的标签栏。复用 iOS 引擎
|
||||||
// 当前为脚手架:导航骨架 + 菜单栏占位;引擎(LibWebRtcEngine + 离屏 WebView)、复用视图与
|
// (EngineController:离屏 WKWebView 跑 JS 引擎 + 原生 libwebrtc 数据面)与鉴权(AuthManager:Broker
|
||||||
// 桌面专属功能(剪贴板自动同步 / 全局热键 / 原生通知)随后续阶段并入。iOS 入口另见 CDropApp。
|
// 登录);引擎 WebView 挂在 MacRootView 背景保活(登录后才启)。iOS 入口另见 CDropApp。桌面专属功能
|
||||||
|
// (剪贴板自动同步 / 全局热键 / 原生通知)随后续阶段并入。
|
||||||
@main
|
@main
|
||||||
struct CommilitiaDropMacApp: App
|
struct CommilitiaDropMacApp: App
|
||||||
{
|
{
|
||||||
|
@State private var auth = AuthManager()
|
||||||
|
@State private var engine = EngineController()
|
||||||
|
|
||||||
var body: some Scene
|
var body: some Scene
|
||||||
{
|
{
|
||||||
// 启动即打开的单主窗口。
|
|
||||||
Window("Commilitia Drop", id: "main")
|
Window("Commilitia Drop", id: "main")
|
||||||
{
|
{
|
||||||
MacRootView()
|
MacAppRoot(auth: auth, engine: engine)
|
||||||
.frame(minWidth: 820, minHeight: 560)
|
.frame(minWidth: 820, minHeight: 560)
|
||||||
}
|
}
|
||||||
.defaultLaunchBehavior(.presented)
|
.defaultLaunchBehavior(.presented)
|
||||||
@@ -23,10 +26,11 @@ struct CommilitiaDropMacApp: App
|
|||||||
CommandGroup(replacing: .newItem) {}
|
CommandGroup(replacing: .newItem) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单栏常驻:状态 + 快捷剪贴板 / Quick Send(当前占位)。
|
|
||||||
MenuBarExtra
|
MenuBarExtra
|
||||||
{
|
{
|
||||||
MacMenuContent()
|
MacMenuContent()
|
||||||
|
.environment(engine)
|
||||||
|
.environment(auth)
|
||||||
}
|
}
|
||||||
label:
|
label:
|
||||||
{
|
{
|
||||||
@@ -35,4 +39,40 @@ struct CommilitiaDropMacApp: App
|
|||||||
.menuBarExtraStyle(.window)
|
.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
|
#endif
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// 菜单栏常驻内容(占位):后续接入连接状态 + 一键剪贴板 push/pull + Quick Send。
|
// 菜单栏常驻内容:账号 / 在线设备数 / 打开主窗口。后续接入一键剪贴板 push/pull + Quick Send。
|
||||||
struct MacMenuContent: View
|
struct MacMenuContent: View
|
||||||
{
|
{
|
||||||
|
@Environment(EngineController.self) private var engine
|
||||||
|
@Environment(AuthManager.self) private var auth
|
||||||
@Environment(\.openWindow) private var openWindow
|
@Environment(\.openWindow) private var openWindow
|
||||||
|
|
||||||
var body: some View
|
var body: some View
|
||||||
@@ -12,9 +14,22 @@ struct MacMenuContent: View
|
|||||||
{
|
{
|
||||||
Text("Commilitia Drop")
|
Text("Commilitia Drop")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text("桌面功能适配中")
|
|
||||||
.font(.caption)
|
if let name = auth.session?.user.name
|
||||||
.foregroundStyle(.secondary)
|
{
|
||||||
|
Text(name)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text("在线设备:\(engine.devices.filter(\.online).count)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Text("未登录")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
Button("打开主窗口") { openWindow(id: "main") }
|
Button("打开主窗口") { openWindow(id: "main") }
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// 桌面根界面:NavigationSplitView 侧栏(传输 / 文件 / 消息 / 设备 / 设置)+ 详情 NavigationStack,
|
// 桌面根界面:NavigationSplitView 侧栏(传输 / 文件 / 消息 / 设备 / 设置)+ 详情 NavigationStack,
|
||||||
// 替代 iOS 的标签栏。当前为脚手架:各区详情为占位(MacPlaceholder),随后续阶段接入复用视图
|
// 替代 iOS 的标签栏。离屏无头引擎 WebView 挂在背景保活(同 iOS RootView)。传输 / 文件 / 消息详情为
|
||||||
// 与桌面专属功能。分区标签暂硬编中文;接入 i18n(Shared/I18n)后改走 t()(与 iOS 单一真源一致)。
|
// 占位(随 Phase 3b 复用 iOS 视图);设备 / 设置已接引擎真实态(presence / 账号 / 日志),作 3a 引擎
|
||||||
|
// 联通证明。分区标签暂硬编中文;接入 i18n(t())待 Phase 3b 复用视图时统一。
|
||||||
struct MacRootView: View
|
struct MacRootView: View
|
||||||
{
|
{
|
||||||
|
@Environment(EngineController.self) private var engine
|
||||||
|
@Environment(AuthManager.self) private var auth
|
||||||
@State private var selection: MacSection = .transfers
|
@State private var selection: MacSection = .transfers
|
||||||
|
|
||||||
var body: some View
|
var body: some View
|
||||||
@@ -23,6 +26,7 @@ struct MacRootView: View
|
|||||||
ForEach(MacSection.sidebar)
|
ForEach(MacSection.sidebar)
|
||||||
{ section in
|
{ section in
|
||||||
Label(section.title, systemImage: section.icon)
|
Label(section.title, systemImage: section.icon)
|
||||||
|
.badge(badge(for: section))
|
||||||
.tag(section)
|
.tag(section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +41,14 @@ struct MacRootView: View
|
|||||||
detail
|
detail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 离屏无头引擎 WebView:零尺寸不可见,仅保持存活(登录后随本视图出现即 boot)。同 iOS RootView。
|
||||||
|
.background
|
||||||
|
{
|
||||||
|
EngineWebView(controller: engine, auth: auth)
|
||||||
|
.frame(width: 0, height: 0)
|
||||||
|
.opacity(0)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -51,9 +63,9 @@ struct MacRootView: View
|
|||||||
case .messages:
|
case .messages:
|
||||||
MacPlaceholder(section: .messages)
|
MacPlaceholder(section: .messages)
|
||||||
case .devices:
|
case .devices:
|
||||||
MacPlaceholder(section: .devices)
|
MacDevicesView()
|
||||||
case .settings:
|
case .settings:
|
||||||
MacPlaceholder(section: .settings)
|
MacSettingsView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +81,17 @@ struct MacRootView: View
|
|||||||
.lineLimit(1)
|
.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 标签一致:传输 / 文件 / 消息 / 设备 / 设置。
|
// 侧栏分区。与 iOS 标签一致:传输 / 文件 / 消息 / 设备 / 设置。
|
||||||
@@ -105,7 +128,7 @@ enum MacSection: Hashable, Identifiable, CaseIterable
|
|||||||
static var sidebar: [MacSection] { MacSection.allCases }
|
static var sidebar: [MacSection] { MacSection.allCases }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阶段占位详情:标明该区将接入的功能,随后续阶段替换为实际视图。
|
// 阶段占位详情(传输 / 文件 / 消息,随 Phase 3b 替换为复用 iOS 视图)。
|
||||||
struct MacPlaceholder: View
|
struct MacPlaceholder: View
|
||||||
{
|
{
|
||||||
let section: MacSection
|
let section: MacSection
|
||||||
@@ -123,4 +146,84 @@ struct MacPlaceholder: View
|
|||||||
.navigationTitle(section.title)
|
.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
|
#endif
|
||||||
|
|||||||
@@ -1,22 +1,44 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
#if canImport(UIKit)
|
||||||
import UIKit
|
import UIKit
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
// 品牌主色单一真源(强约束③,与 web 主 accent 对齐):浅色 #644AC9 / 深色 #9580FF。放在 Shared,使主
|
// 品牌主色单一真源(强约束③,与 web 主 accent 对齐):浅色 #644AC9 / 深色 #9580FF。放在 Shared,使主
|
||||||
// app、控件 / 小组件扩展共用同一定义(此前仅在主 app 的 RootView 内,扩展无法引用)。
|
// app、控件 / 小组件扩展共用同一定义(此前仅在主 app 的 RootView 内,扩展无法引用)。
|
||||||
extension Color
|
extension Color
|
||||||
{
|
{
|
||||||
|
#if os(iOS)
|
||||||
static let cdropAccent = Color(uiColor: UIColor
|
static let cdropAccent = Color(uiColor: UIColor
|
||||||
{ trait in
|
{ trait in
|
||||||
trait.userInterfaceStyle == .dark
|
trait.userInterfaceStyle == .dark
|
||||||
? UIColor(red: 0.584, green: 0.502, blue: 1.0, alpha: 1)
|
? UIColor(red: 0.584, green: 0.502, blue: 1.0, alpha: 1)
|
||||||
: UIColor(red: 0.392, green: 0.290, blue: 0.788, 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。
|
// 品牌琥珀色(字标里「Drop」用色,对齐 web 品牌资产):浅色 #DB9410 / 深色 #F8C637。
|
||||||
|
#if os(iOS)
|
||||||
static let cdropAmber = Color(uiColor: UIColor
|
static let cdropAmber = Color(uiColor: UIColor
|
||||||
{ trait in
|
{ trait in
|
||||||
trait.userInterfaceStyle == .dark
|
trait.userInterfaceStyle == .dark
|
||||||
? UIColor(red: 0.973, green: 0.776, blue: 0.216, alpha: 1)
|
? UIColor(red: 0.973, green: 0.776, blue: 0.216, alpha: 1)
|
||||||
: UIColor(red: 0.859, green: 0.580, blue: 0.063, 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
|
#if canImport(UIKit)
|
||||||
import UIKit
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
// 扫码登录(新设备侧)+ 会话持有。iOS 显示二维码,已登录的 web / 桌面设备用“扫码登录”
|
// 扫码登录(新设备侧)+ 会话持有。iOS 显示二维码,已登录的 web / 桌面设备用“扫码登录”
|
||||||
// 扫码批准,本端轮询 /api/auth/qr/status 拿到自签会话。字段 / 流程见后端 internal/httpapi/
|
// 扫码批准,本端轮询 /api/auth/qr/status 拿到自签会话。字段 / 流程见后端 internal/httpapi/
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
#if canImport(UIKit)
|
||||||
import UIKit
|
import UIKit
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
// 应用内 Broker 登录(device-authorization + PKCE,对齐桌面 desktop/platform/oauth.go)。iOS 无法
|
// 应用内 Broker 登录(device-authorization + PKCE,对齐桌面 desktop/platform/oauth.go)。iOS 无法
|
||||||
// 跑 loopback 监听,故用 ASWebAuthenticationSession + 自定义 scheme 回调(cdrop://auth-callback):
|
// 跑 loopback 监听,故用 ASWebAuthenticationSession + 自定义 scheme 回调(cdrop://auth-callback):
|
||||||
@@ -53,10 +57,14 @@ final class BrokerAuthFlow: NSObject, ASWebAuthenticationPresentationContextProv
|
|||||||
|
|
||||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
|
||||||
{
|
{
|
||||||
|
#if os(iOS)
|
||||||
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
||||||
return scenes.flatMap { $0.windows }.first { $0.isKeyWindow }
|
return scenes.flatMap { $0.windows }.first { $0.isKeyWindow }
|
||||||
?? scenes.first?.windows.first
|
?? scenes.first?.windows.first
|
||||||
?? ASPresentationAnchor()
|
?? ASPresentationAnchor()
|
||||||
|
#else
|
||||||
|
return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#if os(iOS)
|
||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
import UIKit
|
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
|
import Foundation
|
||||||
|
#if canImport(UIKit)
|
||||||
import UIKit
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
// 用户可编辑的设备名,持久在 UserDefaults。用于扫码登录 qr/start 的初始 device_name。设备名
|
// 用户可编辑的设备名,持久在 UserDefaults。用于扫码登录 qr/start 的初始 device_name。设备名
|
||||||
// 已与会话令牌解耦:登录后在设置页改名经 PATCH /api/devices/{device_id} 即时生效(后端按稳定
|
// 已与会话令牌解耦:登录后在设置页改名经 PATCH /api/devices/{device_id} 即时生效(后端按稳定
|
||||||
@@ -14,7 +16,11 @@ enum DeviceNameStore
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
if let v = UserDefaults.standard.string(forKey: key), !v.isEmpty { return v }
|
if let v = UserDefaults.standard.string(forKey: key), !v.isEmpty { return v }
|
||||||
|
#if os(iOS)
|
||||||
let sys = UIDevice.current.name
|
let sys = UIDevice.current.name
|
||||||
|
#else
|
||||||
|
let sys = Host.current().localizedName ?? ProcessInfo.processInfo.hostName
|
||||||
|
#endif
|
||||||
return sys.isEmpty ? "iPhone" : sys
|
return sys.isEmpty ? "iPhone" : sys
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
|
#if canImport(UIKit)
|
||||||
import UIKit
|
import UIKit
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
import WebKit
|
import WebKit
|
||||||
|
|
||||||
// 引擎过桥推来的设备 presence(对应 web store 的 DeviceInfo)。lastSeen 是 ms epoch。
|
// 引擎过桥推来的设备 presence(对应 web store 的 DeviceInfo)。lastSeen 是 ms epoch。
|
||||||
@@ -275,12 +279,18 @@ final class EngineController: NSObject
|
|||||||
let ucc = WKUserContentController()
|
let ucc = WKUserContentController()
|
||||||
ucc.add(self, name: "cdropEngine")
|
ucc.add(self, name: "cdropEngine")
|
||||||
|
|
||||||
// boot 注入:把登录后的真实会话注入引擎(无会话则 null)。device_type 标 ios 供后端
|
// boot 注入:把登录后的真实会话注入引擎(无会话则 null)。device_type 标平台供后端登记 /
|
||||||
// 登记;api_base 默认空(引擎与 /api 同源,从 prod 加载时相对 URL 即可)。
|
// 其他端设备图标 + isWakeable 推送判定;api_base 默认空(引擎与 /api 同源,从 prod 加载时相对
|
||||||
|
// URL 即可)。iOS 为 "ios",原生 Mac 为 "macos"(与桌面 Wails 客户端一致,后端 / 各端已识别)。
|
||||||
let debug = ProcessInfo.processInfo.environment["CDROP_DEBUG_SESSION"] == "1"
|
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()), "
|
let boot = "window.__CDROP_BOOT__ = { session: \(sessionJSON()), "
|
||||||
+ "device_name: \(jsString(currentDeviceName())), "
|
+ "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,
|
ucc.addUserScript(WKUserScript(source: boot,
|
||||||
injectionTime: .atDocumentStart,
|
injectionTime: .atDocumentStart,
|
||||||
forMainFrameOnly: true))
|
forMainFrameOnly: true))
|
||||||
@@ -539,7 +549,11 @@ final class EngineController: NSObject
|
|||||||
// 剪贴板上行:读本机 UIPasteboard 文本交引擎上传云剪贴板(用户手势触发,前台读取合规)。
|
// 剪贴板上行:读本机 UIPasteboard 文本交引擎上传云剪贴板(用户手势触发,前台读取合规)。
|
||||||
func uploadClipboard()
|
func uploadClipboard()
|
||||||
{
|
{
|
||||||
|
#if os(macOS)
|
||||||
|
let content = NSPasteboard.general.string(forType: .string) ?? ""
|
||||||
|
#else
|
||||||
let content = UIPasteboard.general.string ?? ""
|
let content = UIPasteboard.general.string ?? ""
|
||||||
|
#endif
|
||||||
guard !content.isEmpty else { notify(t("ios.clipboard.empty"), kind: .info); return }
|
guard !content.isEmpty else { notify(t("ios.clipboard.empty"), kind: .info); return }
|
||||||
sendCommand("clipboardUpload", payload: [ "content": content ])
|
sendCommand("clipboardUpload", payload: [ "content": content ])
|
||||||
}
|
}
|
||||||
@@ -982,7 +996,12 @@ extension EngineController: WKScriptMessageHandler
|
|||||||
// 引擎拉回的云剪贴板内容 → 写入本机 UIPasteboard。
|
// 引擎拉回的云剪贴板内容 → 写入本机 UIPasteboard。
|
||||||
if let p = payload as? [String: Any], let content = p["content"] as? String, !content.isEmpty
|
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
|
UIPasteboard.general.string = content
|
||||||
|
#endif
|
||||||
notify(t("ios.clipboard.pulled"), kind: .success)
|
notify(t("ios.clipboard.pulled"), kind: .success)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WebKit
|
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
|
// 把 EngineController 持有的离屏 WKWebView 桥进 SwiftUI 视图树(保持存活)。无可见 UI
|
||||||
// 用途——引擎由 controller 经 JS 桥驱动(见 ios/PLAN.md arch A)。
|
// 用途——引擎由 controller 经 JS 桥驱动(见 ios/PLAN.md arch A)。
|
||||||
struct EngineWebView: UIViewRepresentable
|
struct EngineWebView: PlatformViewRepresentable
|
||||||
{
|
{
|
||||||
let controller: EngineController
|
let controller: EngineController
|
||||||
// 会话源:在构建 WebView(注入 __CDROP_BOOT__)之前确定性接好。冷启动重开时,本视图的
|
// 会话源:在构建 WebView(注入 __CDROP_BOOT__)之前确定性接好。冷启动重开时,本视图的
|
||||||
@@ -11,6 +20,7 @@ struct EngineWebView: UIViewRepresentable
|
|||||||
// session:null → 引擎报「missing injected session」整个 boot 死(#11)。故在此同步接好。
|
// session:null → 引擎报「missing injected session」整个 boot 死(#11)。故在此同步接好。
|
||||||
let auth: AuthManager
|
let auth: AuthManager
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
func makeUIView(context: Context) -> WKWebView
|
func makeUIView(context: Context) -> WKWebView
|
||||||
{
|
{
|
||||||
controller.auth = auth
|
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
|
import UIKit
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
// APNs 设备令牌的交汇点:解耦 AppDelegate(经 UIKit 回调拿到令牌)与 EngineController(持
|
// APNs 设备令牌的交汇点:解耦 AppDelegate(经 UIKit 回调拿到令牌)与 EngineController(持
|
||||||
@@ -28,7 +32,13 @@ final class PushRegistry: NSObject
|
|||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])
|
||||||
{ granted, _ in
|
{ granted, _ in
|
||||||
guard granted else { return }
|
guard granted else { return }
|
||||||
Task { @MainActor in UIApplication.shared.registerForRemoteNotifications() }
|
Task { @MainActor in
|
||||||
|
#if os(iOS)
|
||||||
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
|
#else
|
||||||
|
NSApplication.shared.registerForRemoteNotifications()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,26 @@
|
|||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>net.commilitia.Commilitia-Drop</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>cdrop</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>26.0</string>
|
<string>26.0</string>
|
||||||
|
<key>NSBonjourServices</key>
|
||||||
|
<array>
|
||||||
|
<string>_cdrop._tcp</string>
|
||||||
|
</array>
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>Commilitia Drop 需要访问本地网络以发现同内网设备并建立直连传输。</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -180,6 +180,24 @@ targets:
|
|||||||
deploymentTarget: "26.0"
|
deploymentTarget: "26.0"
|
||||||
sources:
|
sources:
|
||||||
- path: MacApp
|
- 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:
|
info:
|
||||||
path: Support/macOS/App-Info.plist
|
path: Support/macOS/App-Info.plist
|
||||||
properties:
|
properties:
|
||||||
@@ -191,6 +209,15 @@ targets:
|
|||||||
- zh-Hans
|
- zh-Hans
|
||||||
- zh-Hant
|
- zh-Hant
|
||||||
- en
|
- 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:
|
settings:
|
||||||
base:
|
base:
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: net.commilitia.Commilitia-Drop
|
PRODUCT_BUNDLE_IDENTIFIER: net.commilitia.Commilitia-Drop
|
||||||
|
|||||||
Reference in New Issue
Block a user