Mac 原生系统通知 + 接收目录可配(Phase 4b·1)
- 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
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 / 隐藏文件),最近的在前。
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user