Skip to content

Commit

Permalink
Merge pull request #64 from macadmins/SCSwift
Browse files Browse the repository at this point in the history
v2.1.0
  • Loading branch information
almenscorner authored Dec 11, 2024
2 parents 8606ba2 + 1b9bc79 commit 949535c
Show file tree
Hide file tree
Showing 43 changed files with 1,644 additions and 176 deletions.
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.1.0] - 2024-12-11
### Changed
- 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.
- If the app is launched using the URL scheme `supportcompanion://`, the tray menu will not be displayed.
- 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.
- Copy device info button will now include additional information about the device, including battery and storage. Example output:
```plaintext
--------------------- Device ---------------------
Host Name: AwesomeMac
Serial Number: C0123456789
Model: MacBook Pro (14-inch, Nov 2023)
Processor: Apple M3 Pro
Memory: 36 GB
OS Version: 15.2.0
OS Build: 24C98
IP Address: 192.168.68.108
Last Reboot: 4 days
--------------------- Battery ---------------------
Health: 94%
Cycle Count: 35
Temperature: 34.5°C
--------------------- Storage ---------------------
Used: 74.9%
FileVault: Enabled
```

### Added
- Support for Japansese localization, thanks @kenchan0130 for the Japanese localization
- A badge to the tray menu icon that visually indicates that the user has pending updates to install.

## [2.0.2] - 2024-12-06
### Changed
- Added a softer shade of orange and red when light mode is enabled to improve visibility and readability.
Expand Down
1 change: 1 addition & 0 deletions SupportCompanion.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@
de,
fr,
nb,
ja,
);
mainGroup = F6F3BEB02CE1E7BA0036ADB9;
packageReferences = (
Expand Down
184 changes: 141 additions & 43 deletions SupportCompanion/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import UserNotifications
import SwiftUI
import Combine

class AppDelegate: NSObject, NSApplicationDelegate {
class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
var popover: NSPopover!
var statusItem: NSStatusItem?
var windowController: NSWindowController?
var transparentWindowController: TransparentWindowController?
Expand All @@ -21,14 +22,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
static var shouldExit = false
private var notificationDelegate: NotificationDelegate?
private var cancellables: Set<AnyCancellable> = []
private var trayManager: TrayMenuManager { TrayMenuManager.shared }

@AppStorage("isDarkMode") private var isDarkMode: Bool = false

var hasUpdatesAvailable: Bool {
appStateManager.pendingUpdatesCount > 0 || appStateManager.systemUpdateCache.count > 0
}

func application(_ application: NSApplication, open urls: [URL]) {
guard let url = urls.first else { return }
switch url.host?.lowercased() {
case nil:
AppDelegate.shouldExit = true
if let statusItem = statusItem {
Logger.shared.logDebug("Removing status item")
NSStatusBar.system.removeStatusItem(statusItem)
self.statusItem = nil
}
default:
AppDelegate.shouldExit = false
}
Expand All @@ -38,14 +49,26 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

func applicationDidFinishLaunching(_ notification: Notification) {
setupTrayMenu()
let icon = NSImage(named: "MenuIcon")
icon?.size = NSSize(width: 16, height: 16)
statusItem?.button?.image = icon
statusItem?.button?.image?.isTemplate = true

appStateManager.refreshAll()
if !AppDelegate.shouldExit {
setupTrayMenu()
}

popover = NSPopover()
popover.behavior = .transient // Closes when clicking outside
popover.contentSize = NSSize(width: 500, height: 520)
popover.contentViewController = NSHostingController(
rootView: TrayMenuView(
viewModel: CardGridViewModel(appState: AppStateManager.shared)
)
.environmentObject(AppStateManager.shared)
)
popover.delegate = self

configureAppUpdateNotificationCommand(mode: appStateManager.preferences.mode)

appStateManager.showWindowCallback = { [weak self] in
self?.showWindow()
}

if appStateManager.preferences.showDesktopInfo {
// Initialize transparent window
Expand All @@ -63,67 +86,142 @@ class AppDelegate: NSObject, NSApplicationDelegate {
notificationDelegate = NotificationDelegate()
UNUserNotificationCenter.current().delegate = notificationDelegate
appStateManager.startBackgroundTasks()

appStateManager.preferences.$actions
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [weak self] _ in
self?.setupTrayMenu()
}
.store(in: &cancellables)

appStateManager.refreshAll()
}

private func setupTrayMenu() {
// Initialize status item only if it doesn't already exist
let trayManager = TrayMenuManager.shared
if statusItem == nil {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = statusItem?.button {
let icon = NSImage(named: "MenuIcon")
icon?.size = NSSize(width: 16, height: 16)
button.image = icon
button.image?.isTemplate = true

setupTrayMenuIconBinding()

if let button = trayManager.getStatusItem().button {
button.action = #selector(togglePopover)
button.target = self
}
}
}

func setupTrayMenuIconBinding() {
appStateManager.$pendingUpdatesCount
.combineLatest(appStateManager.$systemUpdateCache)
.map { pendingUpdatesCount, systemUpdateCache in
pendingUpdatesCount > 0 || systemUpdateCache.count > 0
}
.sink { hasUpdates in
TrayMenuManager.shared.updateTrayIcon(hasUpdates: hasUpdates)
}
.store(in: &cancellables)
}

class TrayMenuManager {
static let shared = TrayMenuManager()

private var statusItem: NSStatusItem

private init() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
updateTrayIcon(hasUpdates: false) // Default state
}

// Update the menu
let menu = NSMenu()
func updateTrayIcon(hasUpdates: Bool) {
let iconName = "MenuIcon"
guard let baseIcon = NSImage(named: iconName) else {
print("Error: \(iconName) not found")
return
}

baseIcon.size = NSSize(width: 16, height: 16)
baseIcon.isTemplate = true // Ensure base icon respects system appearance

if let button = statusItem.button {
// Clear any existing layers
button.layer?.sublayers?.forEach { $0.removeFromSuperlayer() }

// Set the base icon as the button's image
button.image = baseIcon
button.image?.isTemplate = true

if hasUpdates {
Logger.shared.logDebug("Updates available, adding badge to tray icon")

// Add badge dynamically as a layer
let badgeLayer = CALayer()
badgeLayer.backgroundColor = NSColor.red.cgColor
badgeLayer.frame = CGRect(
x: button.bounds.width - 15, // Align to the lower-right corner
y: 10, // Small offset from the bottom
width: 8,
height: 8
)
badgeLayer.cornerRadius = 4 // Make it circular

// Ensure button has a layer to add sublayers
if button.layer == nil {
button.wantsLayer = true
button.layer = CALayer()
}

button.layer?.addSublayer(badgeLayer)
}
}
}

menu.addItem(NSMenuItem(title: Constants.TrayMenu.openApp, action: #selector(showWindow), keyEquivalent: "o"))
menu.addItem(NSMenuItem.separator())
func getStatusItem() -> NSStatusItem {
return statusItem
}
}

// Create Actions submenu
let actionsSubmenu = NSMenu()
for action in appStateManager.preferences.actions {
let actionItem = NSMenuItem(title: action.name, action: #selector(runAction), keyEquivalent: "")
actionItem.representedObject = action
actionsSubmenu.addItem(actionItem)
@objc private func togglePopover() {
guard let button = trayManager.getStatusItem().button else {
print("Error: TrayMenuManager's statusItem.button is nil")
return
}

let actionsMenuItem = NSMenuItem(title: Constants.CardTitle.actions, action: nil, keyEquivalent: "")
menu.setSubmenu(actionsSubmenu, for: actionsMenuItem)
menu.addItem(actionsMenuItem)
if popover.isShown {
popover.performClose(nil)
} else {
// Dynamically set the popover content
popover.contentViewController = NSHostingController(
rootView: TrayMenuView(
viewModel: CardGridViewModel(appState: AppStateManager.shared)
)
.environmentObject(AppStateManager.shared)
)

// Anchor the popover to the status item's button
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)

menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: Constants.TrayMenu.quitApp, action: #selector(quitApp), keyEquivalent: "q"))
// Ensure the popover window is brought to the front
if let popoverWindow = popover.contentViewController?.view.window {
popoverWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
}
}

// Assign the updated menu to the status item
statusItem?.menu = menu
func popoverDidClose(_ notification: Notification) {
Logger.shared.logDebug("Popover closed, cleaning up...")

// Cleanup logic: release the popover or its content
popover.contentViewController = nil
}

@objc private func showWindow() {
@objc func showWindow() {
if windowController == nil {
NSApp.setActivationPolicy(.regular)
let contentView = ContentView()
.environmentObject(AppStateManager.shared)
.environmentObject(Preferences())
.frame(minWidth: 1500, minHeight: 900)
.frame(minWidth: 1500, minHeight: 950)

let hostingController = NSHostingController(rootView: contentView)

let window = NSWindow(contentViewController: hostingController)
window.setContentSize(NSSize(width: 1500, height: 900))
window.setContentSize(NSSize(width: 1500, height: 950))
window.styleMask = [.titled, .closable, .resizable]
window.minSize = NSSize(width: 1500, height: 900)
window.minSize = NSSize(width: 1500, height: 950)
window.title = ""
window.isReleasedWhenClosed = false
window.backgroundColor = .clear
Expand Down
Loading

0 comments on commit 949535c

Please sign in to comment.