bugger/Sources/AppDelegate.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)
}
}