512d7563ee
- 引擎 / 鉴权跨平台 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 需账号交互)
230 lines
6.9 KiB
Swift
230 lines
6.9 KiB
Swift
#if os(macOS)
|
||
import SwiftUI
|
||
|
||
// 桌面根界面:NavigationSplitView 侧栏(传输 / 文件 / 消息 / 设备 / 设置)+ 详情 NavigationStack,
|
||
// 替代 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
|
||
{
|
||
NavigationSplitView
|
||
{
|
||
List(selection: $selection)
|
||
{
|
||
brandHeader
|
||
.listRowInsets(EdgeInsets(top: 10, leading: 8, bottom: 14, trailing: 8))
|
||
.listRowSeparator(.hidden)
|
||
.selectionDisabled()
|
||
Section
|
||
{
|
||
ForEach(MacSection.sidebar)
|
||
{ section in
|
||
Label(section.title, systemImage: section.icon)
|
||
.badge(badge(for: section))
|
||
.tag(section)
|
||
}
|
||
}
|
||
}
|
||
.navigationSplitViewColumnWidth(min: 200, ideal: 220, max: 280)
|
||
.navigationTitle("Commilitia Drop")
|
||
}
|
||
detail:
|
||
{
|
||
NavigationStack
|
||
{
|
||
detail
|
||
}
|
||
}
|
||
// 离屏无头引擎 WebView:零尺寸不可见,仅保持存活(登录后随本视图出现即 boot)。同 iOS RootView。
|
||
.background
|
||
{
|
||
EngineWebView(controller: engine, auth: auth)
|
||
.frame(width: 0, height: 0)
|
||
.opacity(0)
|
||
.allowsHitTesting(false)
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var detail: some View
|
||
{
|
||
switch selection
|
||
{
|
||
case .transfers:
|
||
MacPlaceholder(section: .transfers)
|
||
case .files:
|
||
MacPlaceholder(section: .files)
|
||
case .messages:
|
||
MacPlaceholder(section: .messages)
|
||
case .devices:
|
||
MacDevicesView()
|
||
case .settings:
|
||
MacSettingsView()
|
||
}
|
||
}
|
||
|
||
private var brandHeader: some View
|
||
{
|
||
HStack(spacing: 10)
|
||
{
|
||
Image(systemName: "paperplane.fill")
|
||
.font(.title2)
|
||
.foregroundStyle(.tint)
|
||
Text("Commilitia Drop")
|
||
.font(.title3.weight(.semibold))
|
||
.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 标签一致:传输 / 文件 / 消息 / 设备 / 设置。
|
||
enum MacSection: Hashable, Identifiable, CaseIterable
|
||
{
|
||
case transfers, files, messages, devices, settings
|
||
|
||
var id: Self { self }
|
||
|
||
var title: String
|
||
{
|
||
switch self
|
||
{
|
||
case .transfers: return "传输"
|
||
case .files: return "文件"
|
||
case .messages: return "消息"
|
||
case .devices: return "设备"
|
||
case .settings: return "设置"
|
||
}
|
||
}
|
||
|
||
var icon: String
|
||
{
|
||
switch self
|
||
{
|
||
case .transfers: return "arrow.up.arrow.down"
|
||
case .files: return "folder"
|
||
case .messages: return "bubble.left.and.bubble.right"
|
||
case .devices: return "laptopcomputer.and.iphone"
|
||
case .settings: return "gearshape"
|
||
}
|
||
}
|
||
|
||
static var sidebar: [MacSection] { MacSection.allCases }
|
||
}
|
||
|
||
// 阶段占位详情(传输 / 文件 / 消息,随 Phase 3b 替换为复用 iOS 视图)。
|
||
struct MacPlaceholder: View
|
||
{
|
||
let section: MacSection
|
||
|
||
var body: some View
|
||
{
|
||
ContentUnavailableView
|
||
{
|
||
Label(section.title, systemImage: section.icon)
|
||
}
|
||
description:
|
||
{
|
||
Text("即将接入(原生数据面复用 iOS 引擎,桌面功能适配中)。")
|
||
}
|
||
.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
|