34f0db215c
实现②隐藏 Dock 图标 + ③可靠后台刷新,交付单窗口、可后台常驻的 App。 - 新增 MacBackgroundController(@MainActor App 级单例):持有 engine + auth(脱离窗口 @State,生命周期与 App 一致)。把引擎离屏 WebView 挂常驻隐藏 NSWindow(始终在窗口层级、避 WebKit 无窗口挂起,且脱离主窗口生命周期——关主窗口引擎不销毁,SSE / presence / 传输 / 剪贴板续跑)+ App 级 ProcessInfo.beginActivity 防 App Nap + 按登录态启停剪贴板 monitor / 全局热键。observeAuth(withObservationTracking 桥 Observation→命令式)观察 auth.session 驱动 start/stop - 新增 MacAppDelegate(@NSApplicationDelegateAdaptor):applicationDidFinishLaunching 应用 Dock 策略 + bootstrap 后台运行时;applicationShouldTerminateAfterLastWindowClosed=false——关最后一个窗口不退出,菜单栏常驻续跑 - CommilitiaDropMacApp:engine / auth 改由 MacBackgroundController 持有(不再窗口 @State);MacAppRoot 只做登录门界面切换、不挂引擎生命周期。MacRootView 移除内嵌 EngineWebView + 移除窗口 onAppear/onDisappear 的 monitor/hotkey(全上移 controller)——关窗引擎 / 同步 / 热键不再停 - 设置加「后台」区:隐藏 Dock 图标开关(@AppStorage macHideDock → NSApp.setActivationPolicy .accessory/.regular,默认 regular 显示)+ 后台常驻说明;剪贴板自动同步开关改调 controller.refreshClipboardMonitor;登出只 auth.logout(引擎复位交 controller 随会话变 nil 统一处理)。菜单栏「打开主窗口」先 NSApp.activate 再 openWindow(accessory / 关窗后窗口可前置可见) - 三项能力齐备:①菜单栏图标(既有 MenuBarExtra)②隐藏 Dock(新增,可设置切换)③可靠后台刷新(新增:引擎 / monitor / 热键 App 级持有 + 隐藏窗口托 WebView + App 级 App Nap 防护 + 关窗不退出续跑) - 验证:just mac-build(CommilitiaDropMac.app)+ iOS 模拟器(CommilitiaDrop.app)均 BUILD SUCCEEDED;ad-hoc 启动冒烟通过(进程存活、无崩溃报告、干净退出)。运行期后台同步须用户登录后自测(Broker OAuth 需账号)
121 lines
4.7 KiB
Swift
121 lines
4.7 KiB
Swift
#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
|