add update notifications, only when new packages appear
Off by default. Enabling the checkbox requests notification permission (UNUserNotificationCenter); if denied, the checkbox reverts — the auth callback arrives on a background queue, so the UI update hops to main. notifyIfNeeded remembers the last notified set and stays silent while the outdated list is unchanged, so an hourly refresh doesn't re-ping about the same packages. The set resets as packages get upgraded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7c53413e40
commit
32d81423ab
|
|
@ -1,4 +1,5 @@
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@main
|
@main
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
@ -40,6 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
/// last known outdated list, shown in the Upgrade All confirmation
|
/// last known outdated list, shown in the Upgrade All confirmation
|
||||||
var cachedOutdated: [String] = []
|
var cachedOutdated: [String] = []
|
||||||
|
var lastNotifiedOutdated: Set<String> = []
|
||||||
|
|
||||||
// MARK: - Menu
|
// MARK: - Menu
|
||||||
|
|
||||||
|
|
@ -266,6 +268,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
self.updateOutdatedMenu(with: lines)
|
self.updateOutdatedMenu(with: lines)
|
||||||
self.updateStatus(count: lines.count)
|
self.updateStatus(count: lines.count)
|
||||||
|
self.notifyIfNeeded(outdated: lines)
|
||||||
if lines.isEmpty {
|
if lines.isEmpty {
|
||||||
self.setMenuBarIcon("brewbar-uptodate")
|
self.setMenuBarIcon("brewbar-uptodate")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -274,6 +277,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notifies only when packages appear that weren't in the last notified
|
||||||
|
/// set — an unchanged list stays silent instead of pinging every refresh.
|
||||||
|
func notifyIfNeeded(outdated: [String]) {
|
||||||
|
let current = Set(outdated)
|
||||||
|
defer { lastNotifiedOutdated = current }
|
||||||
|
|
||||||
|
guard Settings.notifyOnNewUpdates,
|
||||||
|
!current.subtracting(lastNotifiedOutdated).isEmpty else { return }
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "Homebrew updates available"
|
||||||
|
let shown = outdated.prefix(4).joined(separator: ", ")
|
||||||
|
content.body = outdated.count > 4 ? "\(shown) and \(outdated.count - 4) more" : shown
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
}
|
||||||
|
|
||||||
func runAutoUpgrade() {
|
func runAutoUpgrade() {
|
||||||
statusMenuItem.title = "Auto-upgrading..."
|
statusMenuItem.title = "Auto-upgrading..."
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import ServiceManagement
|
import ServiceManagement
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
class SettingsWindowController: NSWindowController {
|
class SettingsWindowController: NSWindowController {
|
||||||
weak var appDelegate: AppDelegate?
|
weak var appDelegate: AppDelegate?
|
||||||
|
|
@ -18,6 +19,7 @@ class SettingsWindowController: NSWindowController {
|
||||||
var greedyCheckbox: NSButton!
|
var greedyCheckbox: NSButton!
|
||||||
var autoUpgradeFormulaeCheckbox: NSButton!
|
var autoUpgradeFormulaeCheckbox: NSButton!
|
||||||
var autoUpgradeCasksCheckbox: NSButton!
|
var autoUpgradeCasksCheckbox: NSButton!
|
||||||
|
var notifyCheckbox: NSButton!
|
||||||
|
|
||||||
convenience init(appDelegate: AppDelegate) {
|
convenience init(appDelegate: AppDelegate) {
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
|
|
@ -108,6 +110,12 @@ class SettingsWindowController: NSWindowController {
|
||||||
let caskWarningRow = NSStackView(views: [spacer, caskWarning])
|
let caskWarningRow = NSStackView(views: [spacer, caskWarning])
|
||||||
caskWarningRow.orientation = .horizontal
|
caskWarningRow.orientation = .horizontal
|
||||||
|
|
||||||
|
notifyCheckbox = NSButton(
|
||||||
|
checkboxWithTitle: "Notify when new updates are found",
|
||||||
|
target: self,
|
||||||
|
action: #selector(toggleNotify(_:))
|
||||||
|
)
|
||||||
|
|
||||||
let stack = NSStackView(views: [
|
let stack = NSStackView(views: [
|
||||||
frequencyRow,
|
frequencyRow,
|
||||||
customRow,
|
customRow,
|
||||||
|
|
@ -117,6 +125,7 @@ class SettingsWindowController: NSWindowController {
|
||||||
autoUpgradeCasksCheckbox,
|
autoUpgradeCasksCheckbox,
|
||||||
caskWarningRow,
|
caskWarningRow,
|
||||||
separator(),
|
separator(),
|
||||||
|
notifyCheckbox,
|
||||||
launchAtLoginCheckbox,
|
launchAtLoginCheckbox,
|
||||||
])
|
])
|
||||||
stack.orientation = .vertical
|
stack.orientation = .vertical
|
||||||
|
|
@ -155,6 +164,7 @@ class SettingsWindowController: NSWindowController {
|
||||||
greedyCheckbox.state = Settings.includeGreedyCasks ? .on : .off
|
greedyCheckbox.state = Settings.includeGreedyCasks ? .on : .off
|
||||||
autoUpgradeFormulaeCheckbox.state = Settings.autoUpgradeFormulae ? .on : .off
|
autoUpgradeFormulaeCheckbox.state = Settings.autoUpgradeFormulae ? .on : .off
|
||||||
autoUpgradeCasksCheckbox.state = Settings.autoUpgradeCasks ? .on : .off
|
autoUpgradeCasksCheckbox.state = Settings.autoUpgradeCasks ? .on : .off
|
||||||
|
notifyCheckbox.state = Settings.notifyOnNewUpdates ? .on : .off
|
||||||
}
|
}
|
||||||
|
|
||||||
func separator() -> NSBox {
|
func separator() -> NSBox {
|
||||||
|
|
@ -200,6 +210,21 @@ class SettingsWindowController: NSWindowController {
|
||||||
Settings.autoUpgradeCasks = sender.state == .on
|
Settings.autoUpgradeCasks = sender.state == .on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func toggleNotify(_ sender: NSButton) {
|
||||||
|
guard sender.state == .on else {
|
||||||
|
Settings.notifyOnNewUpdates = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in
|
||||||
|
// callback arrives on a background queue; UI needs main
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Settings.notifyOnNewUpdates = granted
|
||||||
|
sender.state = granted ? .on : .off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func toggleLaunchAtLogin(_ sender: NSButton) {
|
@objc func toggleLaunchAtLogin(_ sender: NSButton) {
|
||||||
do {
|
do {
|
||||||
if sender.state == .on {
|
if sender.state == .on {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue