135 lines
4.6 KiB
Swift
135 lines
4.6 KiB
Swift
import AppKit
|
|
import ServiceManagement
|
|
import SwiftUI
|
|
|
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
static weak var shared: AppDelegate?
|
|
|
|
private var statusItem: NSStatusItem!
|
|
private var popover: NSPopover!
|
|
private var floatingWidget: FloatingWidgetWindow?
|
|
private var webWindow: NSWindow?
|
|
private let bugStore = BugStore.shared
|
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
Self.shared = self
|
|
NSApp.setActivationPolicy(.accessory)
|
|
|
|
setupStatusItem()
|
|
setupPopover()
|
|
|
|
if let config = AppStateService.shared.config {
|
|
if config.showFloatingWidget {
|
|
showFloatingWidget()
|
|
}
|
|
if config.launchAtLogin {
|
|
try? SMAppService.mainApp.register()
|
|
}
|
|
}
|
|
|
|
Task {
|
|
await PollerService.shared.startIfConfigured()
|
|
// Real-time change push is non-blocking; no-op if the subscribe
|
|
// base URL isn't configured (falls back to polling).
|
|
BitableEventService.shared.connect()
|
|
CalibrationService.shared.start()
|
|
}
|
|
}
|
|
|
|
private func setupStatusItem() {
|
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
if let button = statusItem.button {
|
|
button.image = NSImage(systemSymbolName: "ant.fill", accessibilityDescription: "Bugger")
|
|
button.action = #selector(togglePopover)
|
|
button.target = self
|
|
}
|
|
updateBadge(count: bugStore.activeBugs.count)
|
|
}
|
|
|
|
private func setupPopover() {
|
|
popover = NSPopover()
|
|
popover.contentSize = NSSize(width: 360, height: 500)
|
|
popover.behavior = .transient
|
|
popover.contentViewController = NSHostingController(rootView: BugListPopover())
|
|
}
|
|
|
|
func updateBadge(count: Int) {
|
|
if count > 0 {
|
|
statusItem.button?.title = " \(count)"
|
|
} else {
|
|
statusItem.button?.title = ""
|
|
}
|
|
}
|
|
|
|
@objc func togglePopover() {
|
|
guard let button = statusItem.button else { return }
|
|
|
|
if popover.isShown {
|
|
closePopover()
|
|
} else {
|
|
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
popover.contentViewController?.view.window?.makeKey()
|
|
}
|
|
}
|
|
|
|
func closePopover() {
|
|
popover.performClose(nil)
|
|
}
|
|
|
|
/// Opens the popover programmatically (e.g. to show the OAuth setup view
|
|
/// when a write fails due to a missing scope).
|
|
func openPopover() {
|
|
guard let button = statusItem?.button, !popover.isShown else { return }
|
|
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
popover.contentViewController?.view.window?.makeKey()
|
|
}
|
|
|
|
func showFloatingWidget() {
|
|
guard floatingWidget == nil else { return }
|
|
floatingWidget = FloatingWidgetWindow()
|
|
floatingWidget?.orderFrontRegardless()
|
|
}
|
|
|
|
func hideFloatingWidget() {
|
|
floatingWidget?.close()
|
|
floatingWidget = nil
|
|
}
|
|
|
|
/// Opens (or reuses) a window rendering the Feishu record page in an
|
|
/// embedded web view — for actions the API can't do, like advancing the
|
|
/// 流转状态 workflow.
|
|
func openWebWindow(url: URL, title: String) {
|
|
if let webWindow {
|
|
webWindow.title = title
|
|
(webWindow.contentViewController as? NSHostingController<FeishuWebWindowContent>)?
|
|
.rootView = FeishuWebWindowContent(url: url)
|
|
// Recover from a collapsed window frame.
|
|
if webWindow.frame.width < 400 {
|
|
webWindow.setContentSize(NSSize(width: 1100, height: 800))
|
|
webWindow.center()
|
|
}
|
|
} else {
|
|
let window = NSWindow(
|
|
contentRect: NSRect(x: 0, y: 0, width: 1100, height: 800),
|
|
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
window.title = title
|
|
window.contentViewController = NSHostingController(
|
|
rootView: FeishuWebWindowContent(url: url)
|
|
)
|
|
window.contentMinSize = NSSize(width: 600, height: 400)
|
|
window.isReleasedWhenClosed = false
|
|
// Accessory app: ensure the window lands on the active Space.
|
|
window.collectionBehavior.insert(.moveToActiveSpace)
|
|
webWindow = window
|
|
}
|
|
webWindow?.orderFrontRegardless()
|
|
webWindow?.makeKeyAndOrderFront(nil)
|
|
// Accessory app: pull the window to the front explicitly.
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
}
|
|
|
|
}
|