Skip to content

Commit fb78710

Browse files
committed
Add custom tray menu
1 parent d4974f1 commit fb78710

17 files changed

+731
-32
lines changed

SupportCompanion/AppDelegate.swift

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import SwiftUI
1212
import Combine
1313

1414
class AppDelegate: NSObject, NSApplicationDelegate {
15+
var popover: NSPopover!
1516
var statusItem: NSStatusItem?
1617
var windowController: NSWindowController?
1718
var transparentWindowController: TransparentWindowController?
@@ -43,8 +44,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4344
icon?.size = NSSize(width: 16, height: 16)
4445
statusItem?.button?.image = icon
4546
statusItem?.button?.image?.isTemplate = true
46-
47-
appStateManager.refreshAll()
47+
48+
popover = NSPopover()
49+
popover.behavior = .transient // Closes when clicking outside
50+
popover.contentSize = NSSize(width: 500, height: 520)
51+
popover.contentViewController = NSHostingController(
52+
rootView: CustomMenuView(
53+
viewModel: CardGridViewModel(appState: AppStateManager.shared)
54+
)
55+
.environmentObject(AppStateManager.shared)
56+
)
4857
configureAppUpdateNotificationCommand(mode: appStateManager.preferences.mode)
4958

5059
if appStateManager.preferences.showDesktopInfo {
@@ -73,7 +82,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
7382

7483
}
7584

76-
private func setupTrayMenu() {
85+
/*private func setupTrayMenu() {
7786
// Initialize status item only if it doesn't already exist
7887
if statusItem == nil {
7988
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
@@ -108,6 +117,40 @@ class AppDelegate: NSObject, NSApplicationDelegate {
108117

109118
// Assign the updated menu to the status item
110119
statusItem?.menu = menu
120+
}*/
121+
122+
private func setupTrayMenu() {
123+
if statusItem == nil {
124+
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
125+
126+
if let button = statusItem?.button {
127+
let icon = NSImage(named: "MenuIcon")
128+
icon?.size = NSSize(width: 16, height: 16)
129+
button.image = icon
130+
button.image?.isTemplate = true
131+
132+
// Connect the button action
133+
button.action = #selector(togglePopover)
134+
button.target = self
135+
}
136+
}
137+
}
138+
139+
@objc private func togglePopover() {
140+
guard let button = statusItem?.button else { return }
141+
142+
if popover.isShown {
143+
popover.performClose(nil)
144+
} else {
145+
// Show the popover relative to the status bar button
146+
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
147+
148+
// Bring the application and popover window to the front
149+
if let popoverWindow = popover.contentViewController?.view.window {
150+
popoverWindow.makeKeyAndOrderFront(nil)
151+
NSApp.activate(ignoringOtherApps: true) // Ensure app gets focus
152+
}
153+
}
111154
}
112155

113156
@objc private func showWindow() {

SupportCompanion/Components/CardData.swift

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ struct CardData: View {
1212
@Environment(\.colorScheme) var colorScheme
1313
let info: [(key: String, display: String, value: InfoValue)]
1414
let customContent: (String, InfoValue) -> AnyView
15+
let fontSize: CGFloat?
1516

1617
init(
1718
info: [(key: String, display: String, value: InfoValue)],
18-
customContent: @escaping (String, InfoValue) -> AnyView = { _, _ in AnyView(EmptyView()) }
19+
customContent: @escaping (String, InfoValue) -> AnyView = { _, _ in AnyView(EmptyView()) },
20+
fontSize: CGFloat? = 14
1921
) {
2022
self.info = info
2123
self.customContent = customContent
24+
self.fontSize = fontSize
2225
}
2326

2427
var body: some View {
@@ -40,7 +43,7 @@ struct CardData: View {
4043
HStack(alignment: .top) {
4144
Text(display)
4245
.fontWeight(.bold)
43-
.font(.system(size: 14))
46+
.font(.system(size: fontSize ?? 14))
4447

4548
switch key {
4649
case Constants.Battery.Keys.health:
@@ -69,37 +72,37 @@ struct CardData: View {
6972
let color = colorForValue(key: Constants.Battery.Keys.health, value: value)
7073
let isGreen = color == .green
7174

72-
return Group {
75+
return HStack(spacing: 0) {
7376
Text(value.displayValue)
7477
.foregroundColor(color)
75-
.font(.system(size: 14))
78+
.font(.system(size: fontSize ?? 14))
7679
.shadow(color: isGreen ? .black.opacity(0.4) : .clear, radius: 1, x: 0, y: 1)
7780
Text("%")
78-
.font(.system(size: 14))
81+
.font(.system(size: fontSize ?? 14))
7982
}
8083
}
8184

8285
private func temperatureContent(value: InfoValue) -> some View {
8386
let color = colorForValue(key: Constants.Battery.Keys.temperature, value: value)
8487
let isGreen = color == .green
8588

86-
return Group {
89+
return HStack(spacing: 0) {
8790
Text(value.displayValue)
8891
.foregroundColor(color)
89-
.font(.system(size: 14))
92+
.font(.system(size: fontSize ?? 14))
9093
.shadow(color: isGreen ? .black.opacity(0.4) : .clear, radius: 1, x: 0, y: 1)
9194
Text("°C")
92-
.font(.system(size: 14))
95+
.font(.system(size: fontSize ?? 14))
9396
}
9497
}
9598

9699
/// Displays generic content with a suffix (e.g., "days")
97100
private func daysContent(value: InfoValue, suffix: String, color: Color = .primary) -> some View {
98101
Text(value.displayValue)
99102
.foregroundColor(color)
100-
.font(.system(size: 14))
103+
.font(.system(size: fontSize ?? 14))
101104
+ Text(suffix)
102-
.font(.system(size: 14))
105+
.font(.system(size: fontSize ?? 14))
103106
}
104107

105108
private func pssoRegistrationContent(value: InfoValue) -> some View {
@@ -108,7 +111,7 @@ struct CardData: View {
108111

109112
return Text(value.displayValue)
110113
.foregroundColor(colorForValue(key: Constants.PlatformSSO.Keys.registrationCompleted, value: value))
111-
.font(.system(size: 14))
114+
.font(.system(size: fontSize ?? 14))
112115
.shadow(color: isGreen ? .black.opacity(0.4) : .clear, radius: 1, x: 0, y: 1)
113116
}
114117

@@ -124,14 +127,14 @@ struct CardData: View {
124127
.foregroundColor((colorScheme == .light ? .redLight : .red))
125128
}
126129
Text(value.displayValue)
127-
.font(.system(size: 14))
130+
.font(.system(size: fontSize ?? 14))
128131
}
129132
}
130133

131134
/// Displays default text with optional coloring
132135
private func defaultText(value: InfoValue, key: String) -> some View {
133136
Text(value.displayValue)
134-
.font(.system(size: 14))
137+
.font(.system(size: fontSize ?? 14))
135138
.foregroundColor(colorForValue(key: key, value: value))
136139
}
137140

SupportCompanion/Constants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ enum Constants {
258258
enum Keys {
259259
static let name = "StorageName"
260260
static let fileVault = "FileVault"
261+
static let usage = "Usage"
261262
}
262263
}
263264

SupportCompanion/Localizable.xcstrings

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,6 +1456,9 @@
14561456
}
14571457
}
14581458
}
1459+
},
1460+
"Done" : {
1461+
14591462
},
14601463
"Error.CommandFailed" : {
14611464
"comment" : "Error message when a command failed",
@@ -2430,6 +2433,9 @@
24302433
}
24312434
}
24322435
}
2436+
},
2437+
"Pending: " : {
2438+
24332439
},
24342440
"PlatformSSO.LoginFrequency" : {
24352441
"comment" : "Label for the login frequency",
@@ -2682,6 +2688,12 @@
26822688
}
26832689
}
26842690
}
2691+
},
2692+
"Progress: " : {
2693+
2694+
},
2695+
"Quick Actions" : {
2696+
26852697
},
26862698
"Realm:" : {
26872699

@@ -2716,6 +2728,12 @@
27162728
},
27172729
"Rings" : {
27182730

2731+
},
2732+
"Run" : {
2733+
2734+
},
2735+
"Select an action" : {
2736+
27192737
},
27202738
"Select an option" : {
27212739

SupportCompanion/Models/Battery.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ struct BatteryInfo: Identifiable {
2727
}
2828
}
2929

30-
func toKeyValuePairs() -> [(key: String, display: String, value: InfoValue)] {
31-
let health: Int
30+
private var healthPercentage: Int {
3231
if designCapacity > 0 {
33-
health = Int(round((Double(maxCapacity) / Double(designCapacity)) * 100))
32+
return Int(round((Double(maxCapacity) / Double(designCapacity)) * 100))
3433
} else {
35-
health = 0 // Handle invalid `designCapacity` gracefully
34+
return 0
3635
}
36+
}
37+
38+
func toKeyValuePairs() -> [(key: String, display: String, value: InfoValue)] {
39+
let health = healthPercentage
3740

3841
return [
3942
(
@@ -63,4 +66,26 @@ struct BatteryInfo: Identifiable {
6366
)
6467
]
6568
}
69+
70+
func toKeyValuePairsCompact() -> [(key: String, display: String, value: InfoValue)] {
71+
let health = healthPercentage
72+
73+
return [
74+
(
75+
key: Constants.Battery.Keys.health,
76+
display: Constants.Battery.Labels.health,
77+
value: .int(health)
78+
),
79+
(
80+
key: Constants.Battery.Keys.temperature,
81+
display: Constants.Battery.Labels.temperature,
82+
value: .double(temperature)
83+
),
84+
(
85+
key: Constants.Battery.Keys.timeToFull,
86+
display: Constants.Battery.Labels.timeToFull,
87+
value: .string(timeToFull)
88+
)
89+
]
90+
}
6691
}

SupportCompanion/Models/DeviceInfo.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,24 @@ struct DeviceInfo: Identifiable, Equatable {
8686
)
8787
]
8888
}
89+
90+
func toKeyValuePairsCompact() -> [(key: String, display: String, value: InfoValue)] {
91+
return [
92+
(
93+
key: Constants.DeviceInfo.Keys.osVersion,
94+
display: Constants.DeviceInfo.Labels.osVersion,
95+
value: .string(osVersion)
96+
),
97+
(
98+
key: Constants.DeviceInfo.Keys.osBuild,
99+
display: Constants.DeviceInfo.Labels.osBuild,
100+
value: .string(osBuild)
101+
),
102+
(
103+
key: Constants.DeviceInfo.Keys.lastRestart,
104+
display: Constants.DeviceInfo.Labels.lastRestart,
105+
value: .int(lastRestart)
106+
)
107+
]
108+
}
89109
}

SupportCompanion/ViewModels/CardGridViewModel.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ class CardGridViewModel: ObservableObject {
3232
]
3333
}
3434

35-
func createRestartIntuneAgentButton() -> CustomButton {
36-
CustomButton(Constants.Actions.restartIntuneAgent) {
35+
func createRestartIntuneAgentButton(fontSize: CGFloat? = nil) -> CustomButton {
36+
CustomButton(Constants.Actions.restartIntuneAgent, fontSize: fontSize) {
3737
ActionHelpers.restartIntuneAgent { result in
3838
ActionHelpers.handleResult(
3939
operationName: "Restart Intune Agent",
@@ -50,8 +50,8 @@ class CardGridViewModel: ObservableObject {
5050
}
5151
}
5252

53-
func createGatherLogsButton() -> CustomButton {
54-
CustomButton(Constants.Actions.gatherLogs) {
53+
func createGatherLogsButton(fontSize: CGFloat? = nil) -> CustomButton {
54+
CustomButton(Constants.Actions.gatherLogs, fontSize: fontSize) {
5555
ActionHelpers.gatherLogs(preferences: self.appState.preferences) { result in
5656
ActionHelpers.handleResult(
5757
operationName: Constants.Actions.gatherLogs,
@@ -86,8 +86,8 @@ class CardGridViewModel: ObservableObject {
8686
}
8787
}
8888

89-
func createChangePasswordButton() -> CustomButton {
90-
CustomButton(Constants.Actions.changePassword) {
89+
func createChangePasswordButton(fontSize: CGFloat? = nil) -> CustomButton {
90+
CustomButton(Constants.Actions.changePassword, fontSize: fontSize) {
9191
await ActionHelpers.openChangePassword(preferences: self.appState.preferences) { result in
9292
ActionHelpers.handleResult(
9393
operationName: Constants.Actions.changePassword,
@@ -108,7 +108,7 @@ class CardGridViewModel: ObservableObject {
108108
case `default`
109109
}
110110

111-
func createOpenManagementAppButton(type: ManagementAppURLType) -> CustomButton {
111+
func createOpenManagementAppButton(type: ManagementAppURLType, fontSize: CGFloat? = nil) -> CustomButton {
112112
let appName: String
113113
let appURL: String
114114

@@ -129,7 +129,7 @@ class CardGridViewModel: ObservableObject {
129129
appURL = ""
130130
}
131131

132-
return CustomButton("\(Constants.Actions.openManagementApp) \(appName)") {
132+
return CustomButton("\(Constants.Actions.openManagementApp) \(appName)", fontSize: fontSize) {
133133
ActionHelpers.openManagementApp(appURL: appURL)
134134
}
135135
}

SupportCompanion/Views/CardGrid.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct CardGrid: View {
1818
@State private var modalMessage = ""
1919

2020
var body: some View {
21-
let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
21+
let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), ]
2222
ZStack{
2323
ScrollView {
2424
LazyVGrid(
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
struct CompactBatteryCard: View {
5+
@EnvironmentObject var appState: AppStateManager
6+
7+
var body: some View {
8+
CustomCardCompact(
9+
title: "Battery",
10+
titleImageName: "battery.100percent",
11+
buttonImageName: "info.circle",
12+
buttonAction: {},
13+
imageSize: (20, 20),
14+
content: {
15+
CardData(info: appState.batteryInfoManager.batteryInfo.toKeyValuePairsCompact(), fontSize: 12)
16+
}
17+
)
18+
.onAppear() {
19+
appState.batteryInfoManager.refresh()
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)