Files
XStats/Packages/XStatsKit/Sources/XStatsUI/Calendar/CalendarMenuBarController.swift
T
缘生 aa8228fac8 Codex/calendar (#4)
* feat(calendar): 新增菜单栏日历与黄历详情
2026-09-23 09:00:05 +08:00

137 lines
5.1 KiB
Swift

// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import AppKit
import Localization
import SwiftUI
/// 独立日历入口:只按分钟刷新日期文本,关闭后移除定时器,不参与 MetricsHub 的秒级采样。
@MainActor
final class CalendarMenuBarController: NSObject {
private let model: AppModel
private var item: NSStatusItem?
private var panel: StatusPanel?
private var timer: Timer?
private var observers: [(NotificationCenter, NSObjectProtocol)] = []
init(model: AppModel) {
self.model = model
super.init()
}
func start() {
update()
observeSettings()
for (center, name) in [
(NotificationCenter.default, Notification.Name.NSCalendarDayChanged),
(NotificationCenter.default, Notification.Name.NSSystemTimeZoneDidChange),
(NSWorkspace.shared.notificationCenter, NSWorkspace.didWakeNotification),
] {
let token = center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.refreshTitle() }
}
observers.append((center, token))
}
}
func stop() {
timer?.invalidate()
timer = nil
for (center, token) in observers { center.removeObserver(token) }
observers.removeAll()
dismiss()
if let item { NSStatusBar.system.removeStatusItem(item) }
item = nil
}
private func observeSettings() {
withObservationTracking {
_ = model.settings.calendarEnabled
_ = model.settings.calendarFeatures
_ = model.settings.calendarFirstWeekday
_ = model.settings.language
_ = model.settings.appearance
} onChange: { [weak self] in
Task { @MainActor in
self?.update()
self?.observeSettings()
}
}
}
func update() {
guard model.settings.calendarEnabled else {
dismiss()
if let item { NSStatusBar.system.removeStatusItem(item) }
item = nil
timer?.invalidate()
timer = nil
return
}
if item == nil {
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
item.autosaveName = "XStats.calendar"
item.button?.target = self
item.button?.action = #selector(clicked(_:))
item.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
item.button?.image = NSImage(systemSymbolName: "calendar", accessibilityDescription: tr("日历"))
item.button?.imagePosition = .imageLeading
item.button?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
self.item = item
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
MainActor.assumeIsolated { self?.refreshTitle() }
}
timer?.tolerance = 2
}
refreshTitle()
panel?.refreshHeight()
}
private func refreshTitle() {
guard let button = item?.button else { return }
let formatter = DateFormatter()
formatter.locale = L10n.locale
formatter.calendar = CalendarEngine.gregorian()
formatter.timeZone = .autoupdatingCurrent
formatter.setLocalizedDateFormatFromTemplate(model.settings.calendarFeatures.contains(.weekdays) ? "MdEEE" : "Md")
button.title = " " + formatter.string(from: Date())
formatter.dateStyle = .full
button.toolTip = formatter.string(from: Date())
}
@objc private func clicked(_ sender: NSStatusBarButton) {
if NSApp.currentEvent?.type == .rightMouseUp, let item {
let menu = NSMenu()
menu.addItem(withTitle: tr("日历设置…"), action: #selector(openSettings), keyEquivalent: "").target = self
menu.addItem(withTitle: tr("退出 XStats"), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
item.menu = menu
sender.performClick(nil)
item.menu = nil
} else {
show()
}
}
func show() {
guard let button = item?.button, let window = button.window else { return }
if panel == nil {
let model = self.model
let panel = StatusPanel(width: CalendarPopover.width, minHeight: 380,
content: { CalendarPopover().environment(model) },
measuring: { CalendarPopover().environment(model).environment(\.isSnapshot, true) })
panel.onVisibilityChange = { [weak self] visible in self?.item?.button?.highlight(visible) }
self.panel = panel
}
refreshTitle()
let anchor = window.convertToScreen(button.convert(button.bounds, to: nil))
panel?.toggle(below: anchor, on: window.screen)
}
func dismiss() { panel?.dismiss() }
@objc private func openSettings() {
dismiss()
model.openMainWindow(.settingsMenuBar)
}
}