Files
Commilitia-Drop/ios/CDrop/MacApp/MacRootView.swift
T
admin 492563ae44 Mac 剪贴板自动同步(上行)+ 菜单栏快捷(Phase 4a)
- MacClipboardMonitor:NSPasteboard changeCount 轮询 500ms + ProcessInfo.beginActivity(.userInitiatedAllowingIdleSystemSleep) 防 App Nap,本机复制的普通文本自动上传云剪贴板。移植桌面 desktop/platform/clipboard.go 的 Monitor 回环守卫(selfHash 自写回声消费一次并解除 + lastHash 去重)与 clipboard_darwin.m 过滤(org.nspasteboard.{Concealed,Transient,AutoGenerated} 敏感标记跳过 / Handoff shared-pasteboard 暂存路径跳过 / 富文本从 RTF·RTFD 派生纯文本)
- EngineController:加 uploadClipboardText(_:)(monitor 直传已解析文本,绕过再读 pasteboard 避免暂存路径等边角,静默不弹空提示);macOS 写剪贴板前 armSelfWrite 武装守卫(手动拉取写入的内容不被 monitor 再上传成回环)
- 设置加「自动同步本机复制(上行)」开关(@AppStorage macClipboardAutoSync,默认开);MacRootView 按开关 onAppear/onChange 启停、登出(视图消失)即停;菜单栏加剪贴板上传 / 拉取(登录态)
- 下行自动(远端 → 本机自动写剪贴板)仍待办:共享引擎 JS 仅在手动 clipboardPull 时回灌原生 "clipboard" 通知,远端 SSE clipboard:update 在 WebView 壳不落原生(桌面走 Wails 专路 ApplyRemoteClipboard);补齐须改 web/src 引擎 + prod 部署(Mac 从线上加载 engine.html)+ 按 device_type=macos 门控免误伤 iOS。手动拉取(设置 / 菜单栏)已可用
- 验证:just mac-build(CommilitiaDropMac.app)+ iOS 模拟器(CommilitiaDrop.app)均 BUILD SUCCEEDED
2026-07-07 23:53:52 +08:00

285 lines
9.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#if os(macOS)
import SwiftUI
// NavigationSplitView / / / / + NavigationStack
// iOS WebView iOS RootView / /
// Phase 3b iOS / presence / / 3a
// i18nt() Phase 3b
struct MacRootView: View
{
@Environment(EngineController.self) private var engine
@Environment(AuthManager.self) private var auth
@AppStorage("macClipboardAutoSync") private var autoSync = true
@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)
}
// / / / iOS RootView
.overlay(alignment: .top)
{
NoticeOverlay()
}
//
.onAppear { syncClipboardMonitor() }
.onChange(of: autoSync) { syncClipboardMonitor() }
.onDisappear { MacClipboardMonitor.shared.stop() }
}
private func syncClipboardMonitor()
{
if autoSync { MacClipboardMonitor.shared.start { engine.uploadClipboardText($0) } }
else { MacClipboardMonitor.shared.stop() }
}
@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
@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)
{
engine.reset()
auth.logout()
}
}
Section("云剪贴板")
{
Toggle("自动同步本机复制(上行)", isOn: $autoSync)
Button("上传本机剪贴板") { engine.uploadClipboard() }
Button("拉取云剪贴板到本机") { engine.pullClipboard() }
}
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)
}
}
#endif