| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- import AppKit
- @MainActor
- final class StatusBarController: NSObject {
- private let authService = GoogleOAuthService.shared
- private let statusItem: NSStatusItem
- private var observers: [NSObjectProtocol] = []
- private var hasPremiumAccess = false
- override init() {
- statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
- super.init()
- if let icon = NSImage(named: "StatusBarIcon") {
- // Template so the system tints like other menu bar icons (adapts light/dark menu bar).
- icon.isTemplate = true
- statusItem.button?.image = icon
- } else if let icon = NSImage(systemSymbolName: "calendar.badge.clock", accessibilityDescription: AppBrand.name) {
- icon.isTemplate = true
- statusItem.button?.image = icon
- } else {
- statusItem.button?.title = AppBrand.name
- }
- statusItem.button?.setAccessibilityLabel("Meetings status menu".localized)
- rebuildMenu()
- let center = NotificationCenter.default
- observers.append(center.addObserver(forName: .meetingsSnapshotUpdated, object: nil, queue: .main) { [weak self] _ in
- self?.rebuildMenu()
- })
- observers.append(center.addObserver(forName: .statusBarSignOutRequested, object: nil, queue: .main) { [weak self] _ in
- self?.rebuildMenu()
- })
- observers.append(center.addObserver(forName: .widgetSignInRequested, object: nil, queue: .main) { [weak self] _ in
- self?.rebuildMenu()
- })
- observers.append(center.addObserver(forName: .statusBarPremiumAccessChanged, object: nil, queue: .main) { [weak self] notification in
- guard let self else { return }
- if let hasPremium = notification.userInfo?["hasPremiumAccess"] as? Bool {
- self.hasPremiumAccess = hasPremium
- }
- self.rebuildMenu()
- })
- }
- deinit {
- observers.forEach { NotificationCenter.default.removeObserver($0) }
- }
- private func rebuildMenu() {
- let menu = NSMenu()
- let openAppItem = NSMenuItem(title: "Open App".localized, action: #selector(openAppClicked), keyEquivalent: "")
- openAppItem.target = self
- menu.addItem(openAppItem)
- menu.addItem(.separator())
- addOpenSidebarItem(title: "Join Meetings".localized, pageRaw: 0, to: menu)
- addOpenSidebarItem(title: "Schedule".localized, pageRaw: 1, to: menu)
- addOpenSidebarItem(title: "Calendar".localized, pageRaw: 2, to: menu)
- addOpenSidebarItem(title: "Widgets".localized, pageRaw: 3, to: menu)
- addOpenSidebarItem(title: "AI Companion".localized, pageRaw: 5, to: menu)
- addOpenSidebarItem(title: "Settings".localized, pageRaw: 4, to: menu)
- let signedIn = authService.loadTokens() != nil
- if signedIn && hasPremiumAccess {
- let topMeetings = WidgetMeetingStore.load().prefix(3).map { $0 }
- menu.addItem(.separator())
- let header = NSMenuItem(title: "Top meetings".localized, action: nil, keyEquivalent: "")
- header.isEnabled = false
- menu.addItem(header)
- if topMeetings.isEmpty {
- let empty = NSMenuItem(title: "No upcoming meetings".localized, action: nil, keyEquivalent: "")
- empty.isEnabled = false
- menu.addItem(empty)
- } else {
- for meeting in topMeetings {
- let titleText = truncatedMeetingTitle(meeting.title)
- let title = "\(meeting.timeText) \(titleText)"
- let item = NSMenuItem(title: title, action: #selector(meetingClicked(_:)), keyEquivalent: "")
- item.target = self
- item.representedObject = meeting.joinLink
- menu.addItem(item)
- }
- }
- }
- if signedIn {
- menu.addItem(.separator())
- let signOutItem = NSMenuItem(title: "Sign Out".localized, action: #selector(signOutClicked), keyEquivalent: "")
- signOutItem.target = self
- menu.addItem(signOutItem)
- } else {
- menu.addItem(.separator())
- let signInItem = NSMenuItem(title: "Sign In".localized, action: #selector(signInClicked), keyEquivalent: "")
- signInItem.target = self
- menu.addItem(signInItem)
- }
- menu.addItem(.separator())
- let quitItem = NSMenuItem(title: "Quit".localized, action: #selector(quitClicked), keyEquivalent: "q")
- quitItem.target = self
- menu.addItem(quitItem)
- statusItem.menu = menu
- }
- @objc private func openAppClicked() {
- bringAppToFront()
- }
- @objc private func signInClicked() {
- bringAppToFront()
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
- NotificationCenter.default.post(name: .widgetSignInRequested, object: nil)
- }
- }
- @objc private func signOutClicked() {
- bringAppToFront()
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
- NotificationCenter.default.post(name: .statusBarSignOutRequested, object: nil)
- }
- }
- @objc private func openSidebarPageClicked(_ sender: NSMenuItem) {
- guard let pageRaw = sender.representedObject as? Int else { return }
- bringAppToFront()
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
- NotificationCenter.default.post(name: .statusBarOpenSidebarPage, object: nil, userInfo: ["page": pageRaw])
- }
- }
- @objc private func meetingClicked(_ sender: NSMenuItem) {
- guard let link = sender.representedObject as? String,
- let url = URL(string: link.trimmingCharacters(in: .whitespacesAndNewlines)) else { return }
- let routed = authService.urlForPreferredGoogleAccountIfPossible(url)
- NSWorkspace.shared.open(routed)
- }
- @objc private func quitClicked() {
- NSApp.terminate(nil)
- }
- private func bringAppToFront() {
- NSApp.unhide(nil)
- NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
- NSApp.activate(ignoringOtherApps: true)
- NSApp.arrangeInFront(nil)
- for window in NSApp.windows where window.contentViewController is ViewController {
- if window.isMiniaturized {
- window.deminiaturize(nil)
- }
- window.orderFrontRegardless()
- window.makeKeyAndOrderFront(nil)
- }
- }
- private func truncatedMeetingTitle(_ title: String) -> String {
- let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
- guard trimmed.count > 10 else { return trimmed }
- return "\(trimmed.prefix(10))..."
- }
- private func addOpenSidebarItem(title: String, pageRaw: Int, to menu: NSMenu) {
- let item = NSMenuItem(title: title, action: #selector(openSidebarPageClicked(_:)), keyEquivalent: "")
- item.target = self
- item.representedObject = pageRaw
- menu.addItem(item)
- }
- }
|