From f273f2466e3a3b5db8b740d0e1c7d5d67a8ca70a Mon Sep 17 00:00:00 2001 From: commilitia Date: Wed, 8 Jul 2026 00:15:10 +0800 Subject: [PATCH] =?UTF-8?q?Mac=20Quick=20Send=EF=BC=9A=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E7=83=AD=E9=94=AE=20+=20=E8=8F=9C=E5=8D=95=E6=A0=8F=EF=BC=88Ph?= =?UTF-8?q?ase=204c=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MacQuickSend(@Observable 单例):激活 app + NSOpenPanel 多选文件 → 置 pending;MacRootView 观察 pending → 切到「传输」区 + 弹 MacComposeSheet 选目标设备发送(复用现有发送流程与 engine.sendFile) - MacHotkey:Carbon RegisterEventHotKey 全局热键 ⌃⌥⌘S(无须辅助功能权限)触发 Quick Send。事件处理器须 C 函数指针,用不捕获闭包(只调静态 MacQuickSend.trigger)+ dispatch 主队列再弹模态(避免 Carbon 回调内跑模态)。登录(MacRootView onAppear)注册、登出(onDisappear)注销 - 菜单栏加「快速发送文件…」(登录态) - 桌面 Wails 实际未实现全局热键(仅曾计划),此为原生 Mac 增益。键位暂固定 ⌃⌥⌘S,后续可做成设置项 - 验证:just mac-build(CommilitiaDropMac.app)BUILD SUCCEEDED;iOS 无关(仅改 macOS-only 文件),不受影响 --- ios/CDrop/MacApp/MacHotkey.swift | 38 +++++++++++++++++++++++++++ ios/CDrop/MacApp/MacMenuContent.swift | 1 + ios/CDrop/MacApp/MacQuickSend.swift | 31 ++++++++++++++++++++++ ios/CDrop/MacApp/MacRootView.swift | 23 ++++++++++++++-- 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 ios/CDrop/MacApp/MacHotkey.swift create mode 100644 ios/CDrop/MacApp/MacQuickSend.swift diff --git a/ios/CDrop/MacApp/MacHotkey.swift b/ios/CDrop/MacApp/MacHotkey.swift new file mode 100644 index 0000000..4e5b189 --- /dev/null +++ b/ios/CDrop/MacApp/MacHotkey.swift @@ -0,0 +1,38 @@ +#if os(macOS) +import AppKit +import Carbon.HIToolbox + +// 全局热键(Carbon RegisterEventHotKey,无须辅助功能权限):⌃⌥⌘S 触发 Quick Send。登录时注册、登出注销。 +// 键位暂固定,后续可做成设置项。RegisterEventHotKey 的事件处理器须 C 函数指针,故用不捕获闭包(只调 +// 静态 MacQuickSend.trigger),并 dispatch 到主队列再弹 NSOpenPanel(避免在 Carbon 事件回调内跑模态)。 +final class MacHotkey +{ + static let shared = MacHotkey() + private init() {} + + private var hotKeyRef: EventHotKeyRef? + private var handlerRef: EventHandlerRef? + + func register() + { + guard hotKeyRef == nil else { return } + var spec = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), + eventKind: OSType(kEventHotKeyPressed)) + InstallEventHandler(GetApplicationEventTarget(), + { _, _, _ in DispatchQueue.main.async { MacQuickSend.trigger() }; return noErr }, + 1, &spec, nil, &handlerRef) + var hotID = EventHotKeyID(signature: OSType(0x4344_5248), id: 1) // 'CDRH' + RegisterEventHotKey(UInt32(kVK_ANSI_S), + UInt32(controlKey | optionKey | cmdKey), + hotID, GetApplicationEventTarget(), 0, &hotKeyRef) + } + + func unregister() + { + if let hotKeyRef { UnregisterEventHotKey(hotKeyRef) } + hotKeyRef = nil + if let handlerRef { RemoveEventHandler(handlerRef) } + handlerRef = nil + } +} +#endif diff --git a/ios/CDrop/MacApp/MacMenuContent.swift b/ios/CDrop/MacApp/MacMenuContent.swift index 0d3f074..99f3f71 100644 --- a/ios/CDrop/MacApp/MacMenuContent.swift +++ b/ios/CDrop/MacApp/MacMenuContent.swift @@ -34,6 +34,7 @@ struct MacMenuContent: View if auth.session != nil { Divider() + Button("快速发送文件…") { MacQuickSend.trigger() } Button("上传本机剪贴板") { engine.uploadClipboard() } Button("拉取云剪贴板") { engine.pullClipboard() } } diff --git a/ios/CDrop/MacApp/MacQuickSend.swift b/ios/CDrop/MacApp/MacQuickSend.swift new file mode 100644 index 0000000..4916ae3 --- /dev/null +++ b/ios/CDrop/MacApp/MacQuickSend.swift @@ -0,0 +1,31 @@ +#if os(macOS) +import AppKit +import Observation + +// Quick Send:全局热键(MacHotkey)或菜单栏触发 → 激活 app + NSOpenPanel 选文件 → 主窗口切到「传输」区 +// 并弹发送表单选目标设备发送。pending 为待发文件,MacRootView 观察它、présente MacComposeSheet。 +@Observable +final class MacQuickSend +{ + static let shared = MacQuickSend() + private init() {} + + var pending: [URL] = [] + + // 激活 app + 选文件(主线程同步)。有选中即置 pending,触发主窗口的发送表单。未登录时 MacRootView + // 未挂载、表单不出现——Quick Send 仅登录态有意义(菜单项 / 热键均在登录后才提供 / 注册)。 + static func trigger() + { + NSApp.activate() + let panel = NSOpenPanel() + panel.canChooseFiles = true + panel.canChooseDirectories = false + panel.allowsMultipleSelection = true + panel.prompt = "发送" + if panel.runModal() == .OK, !panel.urls.isEmpty + { + shared.pending = panel.urls + } + } +} +#endif diff --git a/ios/CDrop/MacApp/MacRootView.swift b/ios/CDrop/MacApp/MacRootView.swift index c2e923c..0bc75cc 100644 --- a/ios/CDrop/MacApp/MacRootView.swift +++ b/ios/CDrop/MacApp/MacRootView.swift @@ -11,6 +11,7 @@ struct MacRootView: View @Environment(EngineController.self) private var engine @Environment(AuthManager.self) private var auth @AppStorage("macClipboardAutoSync") private var autoSync = true + @State private var quickSend = MacQuickSend.shared @State private var selection: MacSection = .transfers var body: some View @@ -57,9 +58,27 @@ struct MacRootView: View NoticeOverlay() } // 剪贴板自动同步(上行):登录进壳即按开关启停;开关变化即时生效;登出(本视图消失)停。 - .onAppear { syncClipboardMonitor() } + // 同时注册 / 注销 Quick Send 全局热键(⌃⌥⌘S)。 + .onAppear { syncClipboardMonitor(); MacHotkey.shared.register() } .onChange(of: autoSync) { syncClipboardMonitor() } - .onDisappear { MacClipboardMonitor.shared.stop() } + .onDisappear { MacClipboardMonitor.shared.stop(); MacHotkey.shared.unregister() } + // 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) + } } private func syncClipboardMonitor()