Mac Quick Send:全局热键 + 菜单栏(Phase 4c)

- 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 文件),不受影响
This commit is contained in:
2026-07-08 00:15:10 +08:00
parent 92e2d550d2
commit f273f2466e
4 changed files with 91 additions and 2 deletions
+38
View File
@@ -0,0 +1,38 @@
#if os(macOS)
import AppKit
import Carbon.HIToolbox
// Carbon RegisterEventHotKeyS 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
+1
View File
@@ -34,6 +34,7 @@ struct MacMenuContent: View
if auth.session != nil
{
Divider()
Button("快速发送文件…") { MacQuickSend.trigger() }
Button("上传本机剪贴板") { engine.uploadClipboard() }
Button("拉取云剪贴板") { engine.pullClipboard() }
}
+31
View File
@@ -0,0 +1,31 @@
#if os(macOS)
import AppKit
import Observation
// Quick SendMacHotkey 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
+21 -2
View File
@@ -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()