Skip to content

Commit 949535c

Browse files
authored
Merge pull request #64 from macadmins/SCSwift
v2.1.0
2 parents 8606ba2 + 1b9bc79 commit 949535c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1644
-176
lines changed

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [2.1.0] - 2024-12-11
8+
### Changed
9+
- The tray menu has been changed to a custom menu that is an extension of the apps main UI. This allows for a more consistent look and feel between the tray menu and the main app. The tray menu now displays the same information as the main app, including device information, storage information and patching progress as well as actions. If you have custom actions configured using `Actions`, the first 6 actions will be displayed in the tray menu. If you have more than 6 actions, the rest can be run from the Self Service section in the main app.
10+
- If the app is launched using the URL scheme `supportcompanion://`, the tray menu will not be displayed.
11+
- Shadow for green text has been removed as it could make the text look blurry. Instead the green has been changed to a darker shade to make it more readable.
12+
- Copy device info button will now include additional information about the device, including battery and storage. Example output:
13+
```plaintext
14+
--------------------- Device ---------------------
15+
Host Name: AwesomeMac
16+
Serial Number: C0123456789
17+
Model: MacBook Pro (14-inch, Nov 2023)
18+
Processor: Apple M3 Pro
19+
Memory: 36 GB
20+
OS Version: 15.2.0
21+
OS Build: 24C98
22+
IP Address: 192.168.68.108
23+
Last Reboot: 4 days
24+
--------------------- Battery ---------------------
25+
Health: 94%
26+
Cycle Count: 35
27+
Temperature: 34.5°C
28+
--------------------- Storage ---------------------
29+
Used: 74.9%
30+
FileVault: Enabled
31+
```
32+
33+
### Added
34+
- Support for Japansese localization, thanks @kenchan0130 for the Japanese localization
35+
- A badge to the tray menu icon that visually indicates that the user has pending updates to install.
36+
737
## [2.0.2] - 2024-12-06
838
### Changed
939
- Added a softer shade of orange and red when light mode is enabled to improve visibility and readability.

SupportCompanion.xcodeproj/project.pbxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@
425425
de,
426426
fr,
427427
nb,
428+
ja,
428429
);
429430
mainGroup = F6F3BEB02CE1E7BA0036ADB9;
430431
packageReferences = (

SupportCompanion/AppDelegate.swift

Lines changed: 141 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import UserNotifications
1111
import SwiftUI
1212
import Combine
1313

14-
class AppDelegate: NSObject, NSApplicationDelegate {
14+
class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
15+
var popover: NSPopover!
1516
var statusItem: NSStatusItem?
1617
var windowController: NSWindowController?
1718
var transparentWindowController: TransparentWindowController?
@@ -21,14 +22,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2122
static var shouldExit = false
2223
private var notificationDelegate: NotificationDelegate?
2324
private var cancellables: Set<AnyCancellable> = []
25+
private var trayManager: TrayMenuManager { TrayMenuManager.shared }
26+
2427
@AppStorage("isDarkMode") private var isDarkMode: Bool = false
2528

29+
var hasUpdatesAvailable: Bool {
30+
appStateManager.pendingUpdatesCount > 0 || appStateManager.systemUpdateCache.count > 0
31+
}
2632

2733
func application(_ application: NSApplication, open urls: [URL]) {
2834
guard let url = urls.first else { return }
2935
switch url.host?.lowercased() {
3036
case nil:
3137
AppDelegate.shouldExit = true
38+
if let statusItem = statusItem {
39+
Logger.shared.logDebug("Removing status item")
40+
NSStatusBar.system.removeStatusItem(statusItem)
41+
self.statusItem = nil
42+
}
3243
default:
3344
AppDelegate.shouldExit = false
3445
}
@@ -38,14 +49,26 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3849
}
3950

4051
func applicationDidFinishLaunching(_ notification: Notification) {
41-
setupTrayMenu()
42-
let icon = NSImage(named: "MenuIcon")
43-
icon?.size = NSSize(width: 16, height: 16)
44-
statusItem?.button?.image = icon
45-
statusItem?.button?.image?.isTemplate = true
46-
47-
appStateManager.refreshAll()
52+
if !AppDelegate.shouldExit {
53+
setupTrayMenu()
54+
}
55+
56+
popover = NSPopover()
57+
popover.behavior = .transient // Closes when clicking outside
58+
popover.contentSize = NSSize(width: 500, height: 520)
59+
popover.contentViewController = NSHostingController(
60+
rootView: TrayMenuView(
61+
viewModel: CardGridViewModel(appState: AppStateManager.shared)
62+
)
63+
.environmentObject(AppStateManager.shared)
64+
)
65+
popover.delegate = self
66+
4867
configureAppUpdateNotificationCommand(mode: appStateManager.preferences.mode)
68+
69+
appStateManager.showWindowCallback = { [weak self] in
70+
self?.showWindow()
71+
}
4972

5073
if appStateManager.preferences.showDesktopInfo {
5174
// Initialize transparent window
@@ -63,67 +86,142 @@ class AppDelegate: NSObject, NSApplicationDelegate {
6386
notificationDelegate = NotificationDelegate()
6487
UNUserNotificationCenter.current().delegate = notificationDelegate
6588
appStateManager.startBackgroundTasks()
66-
67-
appStateManager.preferences.$actions
68-
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
69-
.sink { [weak self] _ in
70-
self?.setupTrayMenu()
71-
}
72-
.store(in: &cancellables)
73-
89+
appStateManager.refreshAll()
7490
}
7591

7692
private func setupTrayMenu() {
77-
// Initialize status item only if it doesn't already exist
93+
let trayManager = TrayMenuManager.shared
7894
if statusItem == nil {
7995
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
80-
if let button = statusItem?.button {
81-
let icon = NSImage(named: "MenuIcon")
82-
icon?.size = NSSize(width: 16, height: 16)
83-
button.image = icon
84-
button.image?.isTemplate = true
96+
97+
setupTrayMenuIconBinding()
98+
99+
if let button = trayManager.getStatusItem().button {
100+
button.action = #selector(togglePopover)
101+
button.target = self
102+
}
103+
}
104+
}
105+
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)
85114
}
115+
.store(in: &cancellables)
116+
}
117+
118+
class TrayMenuManager {
119+
static let shared = TrayMenuManager()
120+
121+
private var statusItem: NSStatusItem
122+
123+
private init() {
124+
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
125+
updateTrayIcon(hasUpdates: false) // Default state
86126
}
87127

88-
// Update the menu
89-
let menu = NSMenu()
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+
}
134+
135+
baseIcon.size = NSSize(width: 16, height: 16)
136+
baseIcon.isTemplate = true // Ensure base icon respects system appearance
137+
138+
if let button = statusItem.button {
139+
// Clear any existing layers
140+
button.layer?.sublayers?.forEach { $0.removeFromSuperlayer() }
141+
142+
// Set the base icon as the button's image
143+
button.image = baseIcon
144+
button.image?.isTemplate = true
145+
146+
if hasUpdates {
147+
Logger.shared.logDebug("Updates available, adding badge to tray icon")
148+
149+
// Add badge dynamically as a layer
150+
let badgeLayer = CALayer()
151+
badgeLayer.backgroundColor = NSColor.red.cgColor
152+
badgeLayer.frame = CGRect(
153+
x: button.bounds.width - 15, // Align to the lower-right corner
154+
y: 10, // Small offset from the bottom
155+
width: 8,
156+
height: 8
157+
)
158+
badgeLayer.cornerRadius = 4 // Make it circular
159+
160+
// Ensure button has a layer to add sublayers
161+
if button.layer == nil {
162+
button.wantsLayer = true
163+
button.layer = CALayer()
164+
}
165+
166+
button.layer?.addSublayer(badgeLayer)
167+
}
168+
}
169+
}
90170

91-
menu.addItem(NSMenuItem(title: Constants.TrayMenu.openApp, action: #selector(showWindow), keyEquivalent: "o"))
92-
menu.addItem(NSMenuItem.separator())
171+
func getStatusItem() -> NSStatusItem {
172+
return statusItem
173+
}
174+
}
93175

94-
// Create Actions submenu
95-
let actionsSubmenu = NSMenu()
96-
for action in appStateManager.preferences.actions {
97-
let actionItem = NSMenuItem(title: action.name, action: #selector(runAction), keyEquivalent: "")
98-
actionItem.representedObject = action
99-
actionsSubmenu.addItem(actionItem)
176+
@objc private func togglePopover() {
177+
guard let button = trayManager.getStatusItem().button else {
178+
print("Error: TrayMenuManager's statusItem.button is nil")
179+
return
100180
}
101181

102-
let actionsMenuItem = NSMenuItem(title: Constants.CardTitle.actions, action: nil, keyEquivalent: "")
103-
menu.setSubmenu(actionsSubmenu, for: actionsMenuItem)
104-
menu.addItem(actionsMenuItem)
182+
if popover.isShown {
183+
popover.performClose(nil)
184+
} else {
185+
// Dynamically set the popover content
186+
popover.contentViewController = NSHostingController(
187+
rootView: TrayMenuView(
188+
viewModel: CardGridViewModel(appState: AppStateManager.shared)
189+
)
190+
.environmentObject(AppStateManager.shared)
191+
)
192+
193+
// Anchor the popover to the status item's button
194+
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
105195

106-
menu.addItem(NSMenuItem.separator())
107-
menu.addItem(NSMenuItem(title: Constants.TrayMenu.quitApp, action: #selector(quitApp), keyEquivalent: "q"))
196+
// Ensure the popover window is brought to the front
197+
if let popoverWindow = popover.contentViewController?.view.window {
198+
popoverWindow.makeKeyAndOrderFront(nil)
199+
NSApp.activate(ignoringOtherApps: true)
200+
}
201+
}
202+
}
108203

109-
// Assign the updated menu to the status item
110-
statusItem?.menu = menu
204+
func popoverDidClose(_ notification: Notification) {
205+
Logger.shared.logDebug("Popover closed, cleaning up...")
206+
207+
// Cleanup logic: release the popover or its content
208+
popover.contentViewController = nil
111209
}
112210

113-
@objc private func showWindow() {
211+
@objc func showWindow() {
114212
if windowController == nil {
115213
NSApp.setActivationPolicy(.regular)
116214
let contentView = ContentView()
117215
.environmentObject(AppStateManager.shared)
118216
.environmentObject(Preferences())
119-
.frame(minWidth: 1500, minHeight: 900)
217+
.frame(minWidth: 1500, minHeight: 950)
120218

121219
let hostingController = NSHostingController(rootView: contentView)
122220

123221
let window = NSWindow(contentViewController: hostingController)
124-
window.setContentSize(NSSize(width: 1500, height: 900))
222+
window.setContentSize(NSSize(width: 1500, height: 950))
125223
window.styleMask = [.titled, .closable, .resizable]
126-
window.minSize = NSSize(width: 1500, height: 900)
224+
window.minSize = NSSize(width: 1500, height: 950)
127225
window.title = ""
128226
window.isReleasedWhenClosed = false
129227
window.backgroundColor = .clear

0 commit comments

Comments
 (0)