#if os(macOS) import AppKit import Observation // 原生 Mac 后台运行时协调器(App 级单例,脱离窗口生命周期)。持有 engine + auth,把引擎离屏 WebView // 挂到常驻隐藏窗口——关主窗口引擎不销毁、SSE / presence / 传输 / 剪贴板持续;并按登录态 + 设置启停剪贴板 // 自动同步 monitor 与全局热键,登录期间持 App 级 beginActivity 防 App Nap。另管 Dock 图标显隐策略。 // 使「关窗后仍在菜单栏后台可靠刷新」成立(对齐桌面 Wails 的隐藏 Dock + 托盘 + 后台同步)。 // // engine / auth 在此持有(不再是 App 结构的 @State),因其生命周期须与 App 一致、不随窗口显隐;视图经 // .environment 注入观察它们(皆 @Observable)。iOS 侧不受影响(iOS 入口 CDropApp 仍用自身 @State)。 // @MainActor:engine / auth / NSApp / NSWindow 均主线程隔离,本协调器亦然(仅从 App 委托 / SwiftUI 主线程调用)。 @MainActor final class MacBackgroundController { static let shared = MacBackgroundController() let auth = AuthManager() let engine = EngineController() private var hostWindow: NSWindow? private var activity: NSObjectProtocol? private var started = false private init() {} // App 启动(applicationDidFinishLaunching)调用一次:接引擎 auth 源、加载已恢复会话的记录、开始 // 观察登录态并按当前态启停后台。若启动即有持久会话(AuthManager.init 从 Keychain 恢复),引擎即刻 // 后台启动、不必等主窗口出现。 func bootstrap() { engine.auth = auth if let uid = auth.session?.user.id { engine.activateRecords(userId: uid) } observeAuth() syncToAuthState() } // 观察 auth.session(Observation 桥到命令式):每次变化重扫状态并重新武装(withObservationTracking // 单次触发,须自 re-arm)。 private func observeAuth() { withObservationTracking { _ = auth.session?.user.id } onChange: { Task { @MainActor in if let uid = self.auth.session?.user.id { self.engine.activateRecords(userId: uid) } self.syncToAuthState() self.observeAuth() } } } private func syncToAuthState() { if auth.session != nil { start() } else { stop() } } private func start() { guard !started else { return } started = true // App 级 App Nap 防护:引擎 SSE / 计时器 / 剪贴板轮询在后台续跑。 activity = ProcessInfo.processInfo.beginActivity( options: [ .userInitiatedAllowingIdleSystemSleep ], reason: "Commilitia Drop background sync") // 引擎离屏 WebView 挂常驻隐藏窗口——始终在窗口层级内(避 WebKit 无窗口挂起),且脱离主窗口 // 生命周期,关主窗口不销毁。makeWebView 幂等:已建则复用。 engine.auth = auth let wv = engine.makeWebView() let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 480, height: 320), styleMask: [ .borderless ], backing: .buffered, defer: false) win.isReleasedWhenClosed = false win.contentView = wv win.orderOut(nil) hostWindow = win refreshClipboardMonitor() MacHotkey.shared.register() } private func stop() { guard started else { return } started = false MacClipboardMonitor.shared.stop() MacHotkey.shared.unregister() engine.reset() hostWindow?.contentView = nil hostWindow?.close() hostWindow = nil if let activity { ProcessInfo.processInfo.endActivity(activity) } activity = nil } // 剪贴板自动同步开关变化(设置切换)/ 登录后调用:登录态 + 开关开才跑 monitor。 func refreshClipboardMonitor() { let on = UserDefaults.standard.object(forKey: "macClipboardAutoSync") as? Bool ?? true if started, on { MacClipboardMonitor.shared.start { [weak self] text in self?.engine.uploadClipboardText(text) } } else { MacClipboardMonitor.shared.stop() } } // Dock 图标显隐(设置切换 / 启动时应用)。accessory=纯菜单栏、无 Dock 图标;regular=显示 Dock 图标。 // 单窗口应用默认 regular;用户可切 accessory 做成纯菜单栏后台常驻工具。 func applyDockPolicy() { let hide = UserDefaults.standard.bool(forKey: "macHideDock") NSApp.setActivationPolicy(hide ? .accessory : .regular) } } #endif