Pārlūkot izejas kodu

Add menu bar status icon with top meetings

Creates a macOS status item menu for opening the app, signing in/out, and launching the top 3 upcoming meeting links. Keeps the menu in sync with auth state and the widget meetings snapshot.

Made-with: Cursor
huzaifahayat12 1 mēnesi atpakaļ
vecāks
revīzija
bb9a2bf4a5

+ 2 - 0
meetings_app/AppDelegate.swift

@@ -11,6 +11,7 @@ import UserNotifications
11 11
 @main
12 12
 class AppDelegate: NSObject, NSApplicationDelegate {
13 13
     private let darkModeDefaultsKey = "settings.darkModeEnabled"
14
+    private var statusBarController: StatusBarController?
14 15
 
15 16
     func applicationDidFinishLaunching(_ aNotification: Notification) {
16 17
         // Always sync to current macOS appearance on launch.
@@ -21,6 +22,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
21 22
         UNUserNotificationCenter.current().delegate = self
22 23
         MeetingReminderManager.shared.requestPermissionIfNeeded()
23 24
         DesktopWidgetWindowManager.shared.restore()
25
+        statusBarController = StatusBarController()
24 26
     }
25 27
 
26 28
     func applicationWillTerminate(_ aNotification: Notification) {

+ 129 - 0
meetings_app/StatusBar/StatusBarController.swift

@@ -0,0 +1,129 @@
1
+import AppKit
2
+
3
+@MainActor
4
+final class StatusBarController: NSObject {
5
+    private let authService = GoogleOAuthService.shared
6
+    private let statusItem: NSStatusItem
7
+    private var observers: [NSObjectProtocol] = []
8
+
9
+    override init() {
10
+        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
11
+        super.init()
12
+
13
+        if let icon = NSImage(systemSymbolName: "calendar.badge.clock", accessibilityDescription: "Meetings") {
14
+            icon.isTemplate = true
15
+            statusItem.button?.image = icon
16
+        } else {
17
+            statusItem.button?.title = "Meetings"
18
+        }
19
+        statusItem.button?.setAccessibilityLabel("Meetings status menu")
20
+        rebuildMenu()
21
+
22
+        let center = NotificationCenter.default
23
+        observers.append(center.addObserver(forName: .meetingsSnapshotUpdated, object: nil, queue: .main) { [weak self] _ in
24
+            self?.rebuildMenu()
25
+        })
26
+        observers.append(center.addObserver(forName: .statusBarSignOutRequested, object: nil, queue: .main) { [weak self] _ in
27
+            self?.rebuildMenu()
28
+        })
29
+        observers.append(center.addObserver(forName: .widgetSignInRequested, object: nil, queue: .main) { [weak self] _ in
30
+            self?.rebuildMenu()
31
+        })
32
+    }
33
+
34
+    deinit {
35
+        observers.forEach { NotificationCenter.default.removeObserver($0) }
36
+    }
37
+
38
+    private func rebuildMenu() {
39
+        let menu = NSMenu()
40
+
41
+        let openAppItem = NSMenuItem(title: "Open App", action: #selector(openAppClicked), keyEquivalent: "")
42
+        openAppItem.target = self
43
+        menu.addItem(openAppItem)
44
+
45
+        let signedIn = authService.loadTokens() != nil
46
+        if signedIn {
47
+            let topMeetings = WidgetMeetingStore.load().prefix(3).map { $0 }
48
+            if topMeetings.isEmpty {
49
+                let empty = NSMenuItem(title: "No upcoming meetings", action: nil, keyEquivalent: "")
50
+                empty.isEnabled = false
51
+                menu.addItem(.separator())
52
+                menu.addItem(empty)
53
+            } else {
54
+                menu.addItem(.separator())
55
+                let header = NSMenuItem(title: "Top meetings", action: nil, keyEquivalent: "")
56
+                header.isEnabled = false
57
+                menu.addItem(header)
58
+
59
+                for meeting in topMeetings {
60
+                    let title = "\(meeting.timeText)  \(meeting.title)"
61
+                    let item = NSMenuItem(title: title, action: #selector(meetingClicked(_:)), keyEquivalent: "")
62
+                    item.target = self
63
+                    item.representedObject = meeting.joinLink
64
+                    menu.addItem(item)
65
+                }
66
+            }
67
+
68
+            menu.addItem(.separator())
69
+            let signOutItem = NSMenuItem(title: "Sign Out", action: #selector(signOutClicked), keyEquivalent: "")
70
+            signOutItem.target = self
71
+            menu.addItem(signOutItem)
72
+        } else {
73
+            menu.addItem(.separator())
74
+            let signInItem = NSMenuItem(title: "Sign In", action: #selector(signInClicked), keyEquivalent: "")
75
+            signInItem.target = self
76
+            menu.addItem(signInItem)
77
+        }
78
+
79
+        menu.addItem(.separator())
80
+        let quitItem = NSMenuItem(title: "Quit", action: #selector(quitClicked), keyEquivalent: "q")
81
+        quitItem.target = self
82
+        menu.addItem(quitItem)
83
+
84
+        statusItem.menu = menu
85
+    }
86
+
87
+    @objc private func openAppClicked() {
88
+        bringAppToFront()
89
+    }
90
+
91
+    @objc private func signInClicked() {
92
+        bringAppToFront()
93
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
94
+            NotificationCenter.default.post(name: .widgetSignInRequested, object: nil)
95
+        }
96
+    }
97
+
98
+    @objc private func signOutClicked() {
99
+        bringAppToFront()
100
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
101
+            NotificationCenter.default.post(name: .statusBarSignOutRequested, object: nil)
102
+        }
103
+    }
104
+
105
+    @objc private func meetingClicked(_ sender: NSMenuItem) {
106
+        guard let link = sender.representedObject as? String,
107
+              let url = URL(string: link.trimmingCharacters(in: .whitespacesAndNewlines)) else { return }
108
+        NSWorkspace.shared.open(url)
109
+    }
110
+
111
+    @objc private func quitClicked() {
112
+        NSApp.terminate(nil)
113
+    }
114
+
115
+    private func bringAppToFront() {
116
+        NSApp.unhide(nil)
117
+        NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
118
+        NSApp.activate(ignoringOtherApps: true)
119
+        NSApp.arrangeInFront(nil)
120
+        for window in NSApp.windows where window.contentViewController is ViewController {
121
+            if window.isMiniaturized {
122
+                window.deminiaturize(nil)
123
+            }
124
+            window.orderFrontRegardless()
125
+            window.makeKeyAndOrderFront(nil)
126
+        }
127
+    }
128
+}
129
+

+ 4 - 0
meetings_app/ViewController.swift

@@ -696,6 +696,7 @@ private extension ViewController {
696 696
         NotificationCenter.default.addObserver(self, selector: #selector(widgetRefreshRequested), name: .widgetRefreshRequested, object: nil)
697 697
         NotificationCenter.default.addObserver(self, selector: #selector(widgetOpenMeetWebRequested), name: .widgetOpenMeetWebRequested, object: nil)
698 698
         NotificationCenter.default.addObserver(self, selector: #selector(widgetOpenMeetingLinkRequested(_:)), name: .widgetOpenMeetingLinkRequested, object: nil)
699
+        NotificationCenter.default.addObserver(self, selector: #selector(statusBarSignOutRequested), name: .statusBarSignOutRequested, object: nil)
699 700
     }
700 701
 
701 702
     @objc private func widgetOpenJoinMeetingsPageRequested() { showSidebarPage(.joinMeetings) }
@@ -716,6 +717,8 @@ private extension ViewController {
716 717
         openInDefaultBrowser(url: url)
717 718
     }
718 719
 
720
+    @objc private func statusBarSignOutRequested() { performGoogleSignOut() }
721
+
719 722
     @objc private func joinMeetClicked(_ sender: Any?) {
720 723
         let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
721 724
 
@@ -6870,6 +6873,7 @@ private extension ViewController {
6870 6873
         } else {
6871 6874
             UserDefaults.standard.removeObject(forKey: WidgetMeetingStore.key)
6872 6875
         }
6876
+        NotificationCenter.default.post(name: .meetingsSnapshotUpdated, object: nil)
6873 6877
     }
6874 6878
 
6875 6879
     func showScheduleHelp() {

+ 2 - 0
meetings_app/Widgets/DesktopWidgetView.swift

@@ -358,4 +358,6 @@ extension Notification.Name {
358 358
     static let widgetOpenMeetWebRequested = Notification.Name("widgetOpenMeetWebRequested")
359 359
     static let widgetRefreshRequested = Notification.Name("widgetRefreshRequested")
360 360
     static let widgetOpenMeetingLinkRequested = Notification.Name("widgetOpenMeetingLinkRequested")
361
+    static let statusBarSignOutRequested = Notification.Name("statusBarSignOutRequested")
362
+    static let meetingsSnapshotUpdated = Notification.Name("meetingsSnapshotUpdated")
361 363
 }