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()