Files
Commilitia-Drop/ios/CDrop/MacApp/MacClipboardMonitor.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

119 lines
4.8 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 AppKit
import CryptoKit
import Foundation
// Mac NSPasteboard.general.changeCount~500ms
// desktop/platform/clipboard.go Monitor selfHash +
// lastHash clipboard_darwin.m org.nspasteboard.{Concealed,Transient,AutoGenerated}
// Handoff/shared-pasteboard RTF/RTFD beginActivity
// App Nap / JS +
// prod Mac 线 engine.html handoff
final class MacClipboardMonitor
{
static let shared = MacClipboardMonitor()
private init() {}
// app start engine.uploadClipboardText
private var upload: ((String) -> Void)?
private var timer: Timer?
private var activity: NSObjectProtocol?
private var lastChangeCount = 0
private var lastHash = "" //
private var selfHash = "" //
private static let pollInterval: TimeInterval = 0.5
var isRunning: Bool { timer != nil }
func start(upload: @escaping (String) -> Void)
{
self.upload = upload
guard timer == nil else { return }
lastChangeCount = NSPasteboard.general.changeCount //
// App Nap Timer activity
activity = ProcessInfo.processInfo.beginActivity(
options: .userInitiatedAllowingIdleSystemSleep, reason: "clipboard sync")
let t = Timer(timeInterval: Self.pollInterval, repeats: true)
{ [weak self] _ in self?.poll() }
RunLoop.main.add(t, forMode: .common)
timer = t
}
func stop()
{
timer?.invalidate()
timer = nil
if let activity { ProcessInfo.processInfo.endActivity(activity) }
activity = nil
}
// / 使
// changeCount Monitor.WillWrite
func armSelfWrite(_ text: String)
{
selfHash = Self.hash(text)
}
private func poll()
{
let cc = NSPasteboard.general.changeCount
guard cc != lastChangeCount else { return }
lastChangeCount = cc
guard let text = Self.readText() else { return } // / /
let h = Self.hash(text)
if h == selfHash { selfHash = ""; lastHash = h; return } //
if h == lastHash { return } //
lastHash = h
upload?(text)
}
// + clipboard_darwin.m
private static func readText() -> String?
{
let pb = NSPasteboard.general
let sensitive: Set<String> = [
"org.nspasteboard.ConcealedType",
"org.nspasteboard.TransientType",
"org.nspasteboard.AutoGeneratedType",
]
if let types = pb.types, types.contains(where: { sensitive.contains($0.rawValue) })
{
return nil
}
var s = pb.string(forType: .string)
if s == nil || isStagingPath(s!) { s = plainFromRich(pb) }
guard let out = s, !isStagingPath(out), !out.isEmpty else { return nil }
return out
}
// Universal Clipboard / Handoff .../shared-pasteboard/xxx.rtfd
//
private static func isStagingPath(_ s: String) -> Bool
{
s.contains("/shared-pasteboard/") || s.contains("com.apple.coreservices.useractivityd")
}
private static func plainFromRich(_ pb: NSPasteboard) -> String?
{
for (type, doc) in [ (NSPasteboard.PasteboardType.rtfd, NSAttributedString.DocumentType.rtfd),
(NSPasteboard.PasteboardType.rtf, NSAttributedString.DocumentType.rtf) ]
{
if let data = pb.data(forType: type),
let a = try? NSAttributedString(data: data,
options: [ .documentType: doc ],
documentAttributes: nil)
{
return a.string
}
}
return nil
}
private static func hash(_ s: String) -> String
{
SHA256.hash(data: Data(s.utf8)).map { String(format: "%02x", $0) }.joined()
}
}
#endif