From b55cb615a392beb7e5a41ba9cd9e7b800211a0c3 Mon Sep 17 00:00:00 2001 From: commilitia Date: Wed, 8 Jul 2026 00:07:59 +0800 Subject: [PATCH] =?UTF-8?q?Mac=20=E5=8E=9F=E7=94=9F=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=20+=20=E6=8E=A5=E6=94=B6=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=EF=BC=88Phase=204b=C2=B71=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MacNotifier:窗口非前台(!NSApp.isActive)时对「收到消息 / 收到文件」发原生系统通知(UNUserNotificationCenter,授权已由引擎启动的 PushRegistry.requestAuthorizationAndRegister 一并请求)。对齐桌面 app.ShowNotification——前台事件由应用内 NoticeOverlay / 未读红点负责,仅 app 非活跃才发系统通知,避免与应用内提示重复。引擎 handleNotify 的 "message"(新到对端消息,非当前会话)+ "transferDone"(incoming 且 completed)两处加 macOS 挂钩 - 接收目录可配:DownloadManager.downloadDir 在 macOS 读 UserDefaults macDownloadDir(设置页 NSOpenPanel 选目录),未配 / 目录已失效则回退系统「下载」目录(比「文稿」更符桌面直觉);iOS 仍固定沙盒 Documents。设置加「接收」区(显示当前保存目录 + 选择目录…)。非沙盒构建用普通路径即可;沙盒分发(Phase 6)须改安全作用域书签 - 验证:just mac-build(CommilitiaDropMac.app)+ iOS 模拟器(CommilitiaDrop.app)均 BUILD SUCCEEDED --- ios/CDrop/MacApp/MacNotifier.swift | 22 ++++++++++++++++ ios/CDrop/MacApp/MacRootView.swift | 25 +++++++++++++++++++ .../Sources/Engine/DownloadManager.swift | 13 ++++++++++ .../Sources/Engine/EngineController.swift | 11 ++++++++ 4 files changed, 71 insertions(+) create mode 100644 ios/CDrop/MacApp/MacNotifier.swift diff --git a/ios/CDrop/MacApp/MacNotifier.swift b/ios/CDrop/MacApp/MacNotifier.swift new file mode 100644 index 0000000..cc02489 --- /dev/null +++ b/ios/CDrop/MacApp/MacNotifier.swift @@ -0,0 +1,22 @@ +#if os(macOS) +import AppKit +import UserNotifications + +// 窗口非前台时的原生系统通知(收到消息 / 文件)。对齐桌面 app.ShowNotification:前台事件由应用内 +// NoticeOverlay / 未读红点负责,仅当 app 非活跃(NSApp.isActive == false)才发系统通知,避免与应用内 +// 提示重复。通知授权已由引擎启动时的 PushRegistry.requestAuthorizationAndRegister 一并请求。 +enum MacNotifier +{ + // 仅在 app 非活跃时发。title/body 由调用点给出(已是用户可读文本)。 + static func notifyIfInactive(title: String, body: String) + { + guard !NSApp.isActive else { return } + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = .default + let req = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(req) + } +} +#endif diff --git a/ios/CDrop/MacApp/MacRootView.swift b/ios/CDrop/MacApp/MacRootView.swift index cdd77c0..c2e923c 100644 --- a/ios/CDrop/MacApp/MacRootView.swift +++ b/ios/CDrop/MacApp/MacRootView.swift @@ -1,4 +1,5 @@ #if os(macOS) +import AppKit import SwiftUI // 桌面根界面:NavigationSplitView 侧栏(传输 / 文件 / 消息 / 设备 / 设置)+ 详情 NavigationStack, @@ -210,6 +211,7 @@ struct MacSettingsView: View @Environment(EngineController.self) private var engine @Environment(AuthManager.self) private var auth @AppStorage("macClipboardAutoSync") private var autoSync = true + @AppStorage("macDownloadDir") private var downloadDirPath = "" @State private var nameDraft = DeviceNameStore.value private var signalingLabel: String @@ -245,6 +247,11 @@ struct MacSettingsView: View Button("上传本机剪贴板") { engine.uploadClipboard() } Button("拉取云剪贴板到本机") { engine.pullClipboard() } } + Section("接收") + { + LabeledContent("保存到", value: downloadDirDisplay) + Button("选择目录…") { pickDownloadDir() } + } Section("引擎") { LabeledContent("状态", value: engine.status) @@ -280,5 +287,23 @@ struct MacSettingsView: View guard !name.isEmpty else { return } engine.renameSelf(to: name) } + + private var downloadDirDisplay: String + { + downloadDirPath.isEmpty ? "下载(默认)" : (downloadDirPath as NSString).abbreviatingWithTildeInPath + } + + private func pickDownloadDir() + { + let panel = NSOpenPanel() + panel.canChooseFiles = false + panel.canChooseDirectories = true + panel.allowsMultipleSelection = false + panel.prompt = "选择" + if panel.runModal() == .OK, let url = panel.url + { + downloadDirPath = url.path + } + } } #endif diff --git a/ios/CDrop/Sources/Engine/DownloadManager.swift b/ios/CDrop/Sources/Engine/DownloadManager.swift index b792784..c5a2cd5 100644 --- a/ios/CDrop/Sources/Engine/DownloadManager.swift +++ b/ios/CDrop/Sources/Engine/DownloadManager.swift @@ -16,7 +16,20 @@ final class DownloadManager private func downloadDir() -> URL { + #if os(macOS) + // macOS:可配置接收目录(UserDefaults macDownloadDir,设置页选)。未配 / 已失效则回退到系统 + // 「下载」目录(比「文稿」更符桌面直觉)。非沙盒构建用普通路径即可;沙盒分发(Phase 6)须改 + // 安全作用域书签。 + if let path = UserDefaults.standard.string(forKey: "macDownloadDir"), !path.isEmpty + { + let url = URL(fileURLWithPath: path, isDirectory: true) + if FileManager.default.fileExists(atPath: url.path) { return url } + } + return FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first + ?? FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + #else return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + #endif } // 列出已落盘的接收文件(Documents 顶层,排除流式临时 .part / 隐藏文件),最近的在前。 diff --git a/ios/CDrop/Sources/Engine/EngineController.swift b/ios/CDrop/Sources/Engine/EngineController.swift index 0cf4a74..51e1c74 100644 --- a/ios/CDrop/Sources/Engine/EngineController.swift +++ b/ios/CDrop/Sources/Engine/EngineController.swift @@ -969,6 +969,10 @@ extension EngineController: WKScriptMessageHandler if isNew, item.direction == "incoming", item.peerName != activeConversation { unreadByPeer[item.peerName, default: 0] += 1 + #if os(macOS) + // 窗口非前台时发系统通知(对齐桌面);前台由应用内未读红点负责。 + MacNotifier.notifyIfInactive(title: item.peerName, body: item.text) + #endif } } case "transferDone": @@ -976,6 +980,13 @@ extension EngineController: WKScriptMessageHandler if let p = payload as? [String: Any], let item = Self.parseTransfer(p) { noteIncomingTransfer(item) // 极快传输可能跳过活跃阶段,这里补计未读(#5,已 seen 则不重复) + #if os(macOS) + // 窗口非前台时,收到文件发系统通知(对齐桌面)。 + if item.direction == "incoming", item.state == "completed" + { + MacNotifier.notifyIfInactive(title: "收到文件", body: item.fileName) + } + #endif transfers.removeAll { $0.sessionId == item.sessionId } history.removeAll { $0.sessionId == item.sessionId } history.insert(item, at: 0)