#if os(macOS) import AppKit 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 quickSend = MacQuickSend.shared @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 已由 App 级 MacBackgroundController 挂在常驻隐藏窗口后台续跑(脱离本窗口 // 生命周期),故关主窗口引擎不销毁;本视图只呈现引擎状态、不再内嵌 WebView。剪贴板 monitor / // 全局热键 / App Nap 防护同样上移到 MacBackgroundController(按登录态启停,不随本视图显隐)。 // 瞬时通知横幅(吊销 / 改名 / 剪贴板 / 登录审批反馈)覆盖窗口顶部。同 iOS RootView。 .overlay(alignment: .top) { NoticeOverlay() } // Quick Send(热键 / 菜单栏触发):有待发文件即切到「传输」并弹发送表单。 .onChange(of: quickSend.pending) { _, urls in if !urls.isEmpty { selection = .transfers } } .sheet(isPresented: Binding(get: { !quickSend.pending.isEmpty }, set: { if !$0 { quickSend.pending = [] } })) { MacComposeSheet(urls: quickSend.pending, onCancel: { quickSend.pending = [] }, onSend: { device in for url in quickSend.pending { engine.sendFile(to: device, fileURL: url) } quickSend.pending = [] }) .environment(engine) } } @ViewBuilder private var detail: some View { switch selection { case .transfers: MacTransfersView() case .files: MacFilesView() case .messages: MacMessagesView() 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 } } // 设备:引擎经桥推来的真实 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 others.isEmpty { ContentUnavailableView("暂无其他设备", systemImage: "laptopcomputer.and.iphone", description: Text("同账户的其他设备将显示在此。")) } else { List(others) { dev in HStack(spacing: 10) { 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() } .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) 的登录,该设备需重新登录。") } } } // 设置:账号 + 本机设备名(可改)+ 云剪贴板 + 引擎状态 + 引擎日志 + 登出。 struct MacSettingsView: View { @Environment(EngineController.self) private var engine @Environment(AuthManager.self) private var auth @AppStorage("macClipboardAutoSync") private var autoSync = true @AppStorage("macDownloadDir") private var downloadDirPath = "" @AppStorage("macHideDock") private var hideDock = false @State private var nameDraft = DeviceNameStore.value private var signalingLabel: String { if engine.hubConnected { return "已连接" } if engine.hubReconnecting { return "重连中…" } return "未连接" } var body: some View { Form { Section("账号") { LabeledContent("用户", value: auth.session?.user.name ?? "—") HStack { TextField("本机设备名", text: $nameDraft) .onSubmit { commitRename() } Button("保存") { commitRename() } .disabled(nameDraft.trimmingCharacters(in: .whitespaces).isEmpty) } Button("登出", role: .destructive) { // 引擎复位由 App 级 MacBackgroundController 随登出(会话变 nil)统一处理,此处只登出。 auth.logout() } } Section("云剪贴板") { Toggle("自动同步本机复制(上行)", isOn: $autoSync) .onChange(of: autoSync) { MacBackgroundController.shared.refreshClipboardMonitor() } Button("上传本机剪贴板") { engine.uploadClipboard() } Button("拉取云剪贴板到本机") { engine.pullClipboard() } } Section("后台") { Toggle("隐藏 Dock 图标(仅菜单栏常驻)", isOn: $hideDock) .onChange(of: hideDock) { MacBackgroundController.shared.applyDockPolicy() } Text("关闭主窗口后仍在菜单栏后台运行、持续同步;点菜单栏图标可重开窗口。") .font(.caption) .foregroundStyle(.secondary) } Section("接收") { LabeledContent("保存到", value: downloadDirDisplay) Button("选择目录…") { pickDownloadDir() } } Section("引擎") { LabeledContent("状态", value: engine.status) LabeledContent("信令", value: signalingLabel) LabeledContent("在线设备", value: "\(engine.presenceCount)") } 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("设置") } private func commitRename() { let name = nameDraft.trimmingCharacters(in: .whitespacesAndNewlines) guard !name.isEmpty else { return } engine.renameSelf(to: name) } private var downloadDirDisplay: String { downloadDirPath.isEmpty ? "下载(默认)" : (downloadDirPath as NSString).abbreviatingWithTildeInPath } private func pickDownloadDir() { let panel = NSOpenPanel() panel.canChooseFiles = false panel.canChooseDirectories = true panel.allowsMultipleSelection = false panel.prompt = "选择" if panel.runModal() == .OK, let url = panel.url { downloadDirPath = url.path } } } #endif