Mac 原生五区视图(Phase 3b)

在共享引擎公开 API 之上写 Mac 原生视图(不复用 iOS TabView 视图,改按 Mac 惯用交互),接入 MacRootView 五区取代占位。引擎零改动——feature 方法皆 internal,Mac 视图同 target 可直调。

- 传输 MacTransfersView:NSOpenPanel 多选发送 + 拖放发送(.dropDestination,Mac 独有无 iOS 前例)+ 进行中列表(进度条 / 速度 / 直连·中继 / 右键取消·切中继)+ 历史(右键打开·在 Finder 显示·删记录)+ 清空历史;MacComposeSheet(待发文件列表 + 目标设备 radioGroup 选择)
- 文件 MacFilesView:engine.receivedFiles() 轮询(onAppear + history.count 变即刷)+ 双击 / 右键打开(NSWorkspace.open)+ 在 Finder 显示(activateFileViewerSelecting)+ 转发(confirmationDialog 选设备 → sendFile)+ 删除
- 消息 MacMessagesView:HSplitView 双栏(会话列表 + 线程),选中即 setActiveConversation + markConversationRead、离开置 nil,撰写栏 canMessage 门控,气泡 in/out 对齐 + 未读红点
- 设备 MacDevicesView 增强:macDeviceSymbol 类型图标 + 排除本机(selfDeviceID)+ 右键移除设备(confirmationDialog + revokeDevice)
- 设置 MacSettingsView 增强:设备名可改(TextField + renameSelf)+ 云剪贴板上传 / 拉取 + 引擎诊断(status / 信令 hubConnected·hubReconnecting / 在线设备数)+ 引擎日志
- NoticeOverlay 挂 MacRootView 窗口顶部(吊销 / 改名 / 剪贴板 / 登录审批反馈自动横幅);Sources/NotificationBanner.swift 纳入 Mac target
- deviceSymbol / fileByteSize 因所在 iOS 源文件(RootView / SendComposeSheet)未纳入 Mac target,按同语义在 MacFeatureViews 另置一份
- 验证:just mac-build(CommilitiaDropMac.app)+ iOS 模拟器(CommilitiaDrop.app)均 BUILD SUCCEEDED
This commit is contained in:
2026-07-07 22:48:22 +08:00
parent 512d7563ee
commit 5bee5b8942
3 changed files with 611 additions and 35 deletions
+77 -35
View File
@@ -49,6 +49,11 @@ struct MacRootView: View
.opacity(0)
.allowsHitTesting(false)
}
// / / / iOS RootView
.overlay(alignment: .top)
{
NoticeOverlay()
}
}
@ViewBuilder
@@ -57,11 +62,11 @@ struct MacRootView: View
switch selection
{
case .transfers:
MacPlaceholder(section: .transfers)
MacTransfersView()
case .files:
MacPlaceholder(section: .files)
MacFilesView()
case .messages:
MacPlaceholder(section: .messages)
MacMessagesView()
case .devices:
MacDevicesView()
case .settings:
@@ -128,66 +133,79 @@ enum MacSection: Hashable, Identifiable, CaseIterable
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
// presence + 线 +
struct MacDevicesView: View
{
@Environment(EngineController.self) private var engine
@State private var revokeTarget: DeviceItem?
private var others: [DeviceItem]
{
engine.devices.filter { $0.deviceID != engine.selfDeviceID }
}
var body: some View
{
Group
{
if engine.devices.isEmpty
if others.isEmpty
{
ContentUnavailableView("暂无其他设备", systemImage: "laptopcomputer.and.iphone",
description: Text("同账户的其他在线设备将显示在此。"))
description: Text("同账户的其他设备将显示在此。"))
}
else
{
List(engine.devices)
List(others)
{ dev in
HStack(spacing: 10)
{
Circle()
.fill(dev.online ? Color.green : Color.secondary)
.frame(width: 9, height: 9)
Text(dev.name)
Image(systemName: macDeviceSymbol(dev.type))
.foregroundStyle(dev.online ? Color.green : Color.secondary)
VStack(alignment: .leading, spacing: 2)
{
Text(dev.name)
Text(dev.online ? "在线" : "离线")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text(dev.type)
.font(.caption)
.foregroundStyle(.secondary)
}
.contextMenu
{
Button("移除设备", role: .destructive) { revokeTarget = dev }
}
}
}
}
.navigationTitle("设备")
.confirmationDialog("移除该设备?",
isPresented: Binding(get: { revokeTarget != nil },
set: { if !$0 { revokeTarget = nil } }),
presenting: revokeTarget)
{ dev in
Button("移除 \(dev.name)", role: .destructive) { engine.revokeDevice(dev.deviceID) }
Button("取消", role: .cancel) {}
}
message:
{ dev in
Text("将吊销 \(dev.name) 的登录,该设备需重新登录。")
}
}
}
// + + + 3a
// + + + + +
struct MacSettingsView: View
{
@Environment(EngineController.self) private var engine
@Environment(AuthManager.self) private var auth
@State private var nameDraft = DeviceNameStore.value
private var signalingLabel: String
{
if engine.hubConnected { return "已连接" }
if engine.hubReconnecting { return "重连中…" }
return "未连接"
}
var body: some View
{
@@ -196,13 +214,30 @@ struct MacSettingsView: View
Section("账号")
{
LabeledContent("用户", value: auth.session?.user.name ?? "")
LabeledContent("本机设备", value: engine.deviceName)
HStack
{
TextField("本机设备名", text: $nameDraft)
.onSubmit { commitRename() }
Button("保存") { commitRename() }
.disabled(nameDraft.trimmingCharacters(in: .whitespaces).isEmpty)
}
Button("登出", role: .destructive)
{
engine.reset()
auth.logout()
}
}
Section("云剪贴板")
{
Button("上传本机剪贴板") { engine.uploadClipboard() }
Button("拉取云剪贴板到本机") { engine.pullClipboard() }
}
Section("引擎")
{
LabeledContent("状态", value: engine.status)
LabeledContent("信令", value: signalingLabel)
LabeledContent("在线设备", value: "\(engine.presenceCount)")
}
Section("引擎日志")
{
if engine.logs.isEmpty
@@ -225,5 +260,12 @@ struct MacSettingsView: View
.formStyle(.grouped)
.navigationTitle("设置")
}
private func commitRename()
{
let name = nameDraft.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty else { return }
engine.renameSelf(to: name)
}
}
#endif