@@ -11,7 +11,7 @@ import UserNotifications
11
11
import SwiftUI
12
12
import Combine
13
13
14
- class AppDelegate : NSObject , NSApplicationDelegate {
14
+ class AppDelegate : NSObject , NSApplicationDelegate , NSPopoverDelegate {
15
15
var popover : NSPopover !
16
16
var statusItem : NSStatusItem ?
17
17
var windowController : NSWindowController ?
@@ -22,8 +22,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
22
22
static var shouldExit = false
23
23
private var notificationDelegate : NotificationDelegate ?
24
24
private var cancellables : Set < AnyCancellable > = [ ]
25
+ private var trayManager : TrayMenuManager { TrayMenuManager . shared }
26
+
25
27
@AppStorage ( " isDarkMode " ) private var isDarkMode : Bool = false
26
28
29
+ var hasUpdatesAvailable : Bool {
30
+ appStateManager. pendingUpdatesCount > 0 || appStateManager. systemUpdateCache. count > 0
31
+ }
32
+
27
33
func application( _ application: NSApplication , open urls: [ URL ] ) {
28
34
guard let url = urls. first else { return }
29
35
switch url. host? . lowercased ( ) {
@@ -46,10 +52,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
46
52
if !AppDelegate. shouldExit {
47
53
setupTrayMenu ( )
48
54
}
49
- let icon = NSImage ( named: " MenuIcon " )
50
- icon? . size = NSSize ( width: 16 , height: 16 )
51
- statusItem? . button? . image = icon
52
- statusItem? . button? . image? . isTemplate = true
53
55
54
56
popover = NSPopover ( )
55
57
popover. behavior = . transient // Closes when clicking outside
@@ -60,6 +62,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
60
62
)
61
63
. environmentObject ( AppStateManager . shared)
62
64
)
65
+ popover. delegate = self
66
+
63
67
configureAppUpdateNotificationCommand ( mode: appStateManager. preferences. mode)
64
68
65
69
appStateManager. showWindowCallback = { [ weak self] in
@@ -83,87 +87,106 @@ class AppDelegate: NSObject, NSApplicationDelegate {
83
87
UNUserNotificationCenter . current ( ) . delegate = notificationDelegate
84
88
appStateManager. startBackgroundTasks ( )
85
89
appStateManager. refreshAll ( )
86
-
87
- /*appStateManager.preferences.$actions
88
- .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
89
- .sink { [weak self] _ in
90
- self?.setupTrayMenu()
91
- }
92
- .store(in: &cancellables)*/
93
-
94
90
}
95
91
96
- /* private func setupTrayMenu() {
97
- // Initialize status item only if it doesn't already exist
92
+ private func setupTrayMenu( ) {
93
+ let trayManager = TrayMenuManager . shared
98
94
if statusItem == nil {
99
95
statusItem = NSStatusBar . system. statusItem ( withLength: NSStatusItem . variableLength)
100
- if let button = statusItem?.button {
101
- let icon = NSImage(named: "MenuIcon")
102
- icon?.size = NSSize(width: 16, height: 16)
103
- button.image = icon
104
- button.image?.isTemplate = true
96
+
97
+ setupTrayMenuIconBinding ( )
98
+
99
+ if let button = trayManager. getStatusItem ( ) . button {
100
+ button. action = #selector( togglePopover)
101
+ button. target = self
105
102
}
106
103
}
104
+ }
107
105
108
- // Update the menu
109
- let menu = NSMenu()
106
+ func setupTrayMenuIconBinding( ) {
107
+ appStateManager. $pendingUpdatesCount
108
+ . combineLatest ( appStateManager. $systemUpdateCache)
109
+ . map { pendingUpdatesCount, systemUpdateCache in
110
+ pendingUpdatesCount > 0 || systemUpdateCache. count > 0
111
+ }
112
+ . sink { hasUpdates in
113
+ TrayMenuManager . shared. updateTrayIcon ( hasUpdates: hasUpdates)
114
+ }
115
+ . store ( in: & cancellables)
116
+ }
110
117
111
- menu.addItem(NSMenuItem(title: Constants.TrayMenu.openApp, action: #selector(showWindow), keyEquivalent: "o"))
112
- menu.addItem(NSMenuItem.separator())
118
+ class TrayMenuManager {
119
+ static let shared = TrayMenuManager ( )
120
+
121
+ private var statusItem : NSStatusItem
113
122
114
- // Create Actions submenu
115
- let actionsSubmenu = NSMenu()
116
- for action in appStateManager.preferences.actions {
117
- let actionItem = NSMenuItem(title: action.name, action: #selector(runAction), keyEquivalent: "")
118
- actionItem.representedObject = action
119
- actionsSubmenu.addItem(actionItem)
123
+ private init ( ) {
124
+ statusItem = NSStatusBar . system. statusItem ( withLength: NSStatusItem . variableLength)
125
+ updateTrayIcon ( hasUpdates: false ) // Default state
120
126
}
121
127
122
- let actionsMenuItem = NSMenuItem(title: Constants.CardTitle.actions, action: nil, keyEquivalent: "")
123
- menu.setSubmenu(actionsSubmenu, for: actionsMenuItem)
124
- menu.addItem(actionsMenuItem)
125
-
126
- menu.addItem(NSMenuItem.separator())
127
- menu.addItem(NSMenuItem(title: Constants.TrayMenu.quitApp, action: #selector(quitApp), keyEquivalent: "q"))
128
-
129
- // Assign the updated menu to the status item
130
- statusItem?.menu = menu
131
- }*/
128
+ func updateTrayIcon( hasUpdates: Bool ) {
129
+ let iconName = " MenuIcon "
130
+ guard let baseIcon = NSImage ( named: iconName) else {
131
+ print ( " Error: \( iconName) not found " )
132
+ return
133
+ }
132
134
133
- private func setupTrayMenu( ) {
134
- if statusItem == nil {
135
- statusItem = NSStatusBar . system. statusItem ( withLength: NSStatusItem . variableLength)
136
-
137
- if let button = statusItem? . button {
138
- let icon = NSImage ( named: " MenuIcon " )
139
- icon? . size = NSSize ( width: 16 , height: 16 )
140
- button. image = icon
141
- button. image? . isTemplate = true
142
-
143
- // Connect the button action
144
- button. action = #selector( togglePopover)
145
- button. target = self
135
+ if hasUpdates {
136
+ // Add a red dot badge
137
+ Logger . shared. logDebug ( " Updates available, adding badge to tray icon " )
138
+ let iconWithBadge = baseIcon. compositeWithBadge ( color: . red, badgeSize: 8 )
139
+ statusItem. button? . image = iconWithBadge
140
+ baseIcon. size = NSSize ( width: 16 , height: 16 )
141
+ } else {
142
+ // Use the base icon as is
143
+ Logger . shared. logDebug ( " No updates available, showing base tray icon " )
144
+ baseIcon. isTemplate = true
145
+ baseIcon. size = NSSize ( width: 16 , height: 16 )
146
+ statusItem. button? . image = baseIcon
146
147
}
147
148
}
149
+
150
+ func getStatusItem( ) -> NSStatusItem {
151
+ return statusItem
152
+ }
148
153
}
149
154
150
155
@objc private func togglePopover( ) {
151
- guard let button = statusItem? . button else { return }
156
+ guard let button = trayManager. getStatusItem ( ) . button else {
157
+ print ( " Error: TrayMenuManager's statusItem.button is nil " )
158
+ return
159
+ }
152
160
153
161
if popover. isShown {
154
162
popover. performClose ( nil )
155
163
} else {
156
- // Show the popover relative to the status bar button
164
+ // Dynamically set the popover content
165
+ popover. contentViewController = NSHostingController (
166
+ rootView: TrayMenuView (
167
+ viewModel: CardGridViewModel ( appState: AppStateManager . shared)
168
+ )
169
+ . environmentObject ( AppStateManager . shared)
170
+ )
171
+
172
+ // Anchor the popover to the status item's button
157
173
popover. show ( relativeTo: button. bounds, of: button, preferredEdge: . minY)
158
174
159
- // Bring the application and popover window to the front
175
+ // Ensure the popover window is brought to the front
160
176
if let popoverWindow = popover. contentViewController? . view. window {
161
177
popoverWindow. makeKeyAndOrderFront ( nil )
162
- NSApp . activate ( ignoringOtherApps: true ) // Ensure app gets focus
178
+ NSApp . activate ( ignoringOtherApps: true )
163
179
}
164
180
}
165
181
}
166
182
183
+ func popoverDidClose( _ notification: Notification ) {
184
+ Logger . shared. logDebug ( " Popover closed, cleaning up... " )
185
+
186
+ // Cleanup logic: release the popover or its content
187
+ popover. contentViewController = nil
188
+ }
189
+
167
190
@objc func showWindow( ) {
168
191
if windowController == nil {
169
192
NSApp . setActivationPolicy ( . regular)
0 commit comments