mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Better gentle Sparkle update driver
This commit is contained in:
parent
11911ebc8f
commit
788269c2ab
4 changed files with 257 additions and 251 deletions
|
|
@ -15,6 +15,7 @@ public final class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate {
|
||||||
public static let shared = SparkleUpdaterManager()
|
public static let shared = SparkleUpdaterManager()
|
||||||
|
|
||||||
fileprivate var updaterController: SPUStandardUpdaterController?
|
fileprivate var updaterController: SPUStandardUpdaterController?
|
||||||
|
private(set) var userDriverDelegate: SparkleUserDriverDelegate?
|
||||||
private let logger = os.Logger(
|
private let logger = os.Logger(
|
||||||
subsystem: Bundle.main.bundleIdentifier ?? "VibeTunnel",
|
subsystem: Bundle.main.bundleIdentifier ?? "VibeTunnel",
|
||||||
category: "SparkleUpdater"
|
category: "SparkleUpdater"
|
||||||
|
|
@ -46,19 +47,22 @@ public final class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create user driver delegate for gentle reminders
|
||||||
|
userDriverDelegate = SparkleUserDriverDelegate()
|
||||||
|
|
||||||
// Initialize Sparkle with standard configuration
|
// Initialize Sparkle with standard configuration
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// In debug mode, start the updater for testing
|
// In debug mode, start the updater for testing
|
||||||
updaterController = SPUStandardUpdaterController(
|
updaterController = SPUStandardUpdaterController(
|
||||||
startingUpdater: true,
|
startingUpdater: true,
|
||||||
updaterDelegate: self,
|
updaterDelegate: self,
|
||||||
userDriverDelegate: nil
|
userDriverDelegate: userDriverDelegate
|
||||||
)
|
)
|
||||||
#else
|
#else
|
||||||
updaterController = SPUStandardUpdaterController(
|
updaterController = SPUStandardUpdaterController(
|
||||||
startingUpdater: true,
|
startingUpdater: true,
|
||||||
updaterDelegate: self,
|
updaterDelegate: self,
|
||||||
userDriverDelegate: nil
|
userDriverDelegate: userDriverDelegate
|
||||||
)
|
)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,247 +0,0 @@
|
||||||
import AppKit
|
|
||||||
import Foundation
|
|
||||||
import Sparkle
|
|
||||||
import os.log
|
|
||||||
|
|
||||||
/// Custom user driver for Sparkle updates that implements gentle reminders
|
|
||||||
@MainActor
|
|
||||||
public final class SparkleUserDriver: NSObject, SPUUserDriverDelegate {
|
|
||||||
private let logger = os.Logger(
|
|
||||||
subsystem: Bundle.main.bundleIdentifier ?? "VibeTunnel",
|
|
||||||
category: "SparkleUserDriver"
|
|
||||||
)
|
|
||||||
|
|
||||||
private var updateItem: SUAppcastItem?
|
|
||||||
private var userDriver: SPUUserDriver?
|
|
||||||
private var reminderTimer: Timer?
|
|
||||||
private var lastReminderDate: Date?
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
private let initialReminderDelay: TimeInterval = 60 * 60 * 24 // 24 hours
|
|
||||||
private let subsequentReminderInterval: TimeInterval = 60 * 60 * 24 * 3 // 3 days
|
|
||||||
|
|
||||||
public override init() {
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - SPUUserDriverDelegate
|
|
||||||
|
|
||||||
public func showCanCheck(forUpdates updater: SPUUpdater) {
|
|
||||||
logger.info("User can check for updates")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showUpdatePermissionRequest(for updater: SPUUpdater, systemProfile: [String : Any], reply: @escaping (SPUUpdatePermissionResponse) -> Void) {
|
|
||||||
// Show permission dialog
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = "Check for Updates Automatically?"
|
|
||||||
alert.informativeText = "VibeTunnel can automatically check for updates. You can always check for updates manually from the menu."
|
|
||||||
alert.addButton(withTitle: "Check Automatically")
|
|
||||||
alert.addButton(withTitle: "Don't Check")
|
|
||||||
|
|
||||||
let response = alert.runModal()
|
|
||||||
|
|
||||||
reply(SPUUpdatePermissionResponse(
|
|
||||||
automaticUpdateChecks: response == .alertFirstButtonReturn,
|
|
||||||
sendSystemProfile: false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showUserInitiatedUpdateCheck(completion updateCheckStatusCompletion: @escaping (SPUUserInitiatedCheckStatus) -> Void) {
|
|
||||||
logger.info("User initiated update check")
|
|
||||||
updateCheckStatusCompletion(.checkEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUpdateState, reply: @escaping (SPUUpdateAlertChoice) -> Void) {
|
|
||||||
logger.info("Update found: \(appcastItem.displayVersionString ?? "Unknown version")")
|
|
||||||
|
|
||||||
// Store the update item for gentle reminders
|
|
||||||
self.updateItem = appcastItem
|
|
||||||
|
|
||||||
// Cancel any existing reminder timer
|
|
||||||
reminderTimer?.invalidate()
|
|
||||||
|
|
||||||
// Show immediate notification
|
|
||||||
showUpdateNotification(appcastItem: appcastItem, state: state, reply: reply, isReminder: false)
|
|
||||||
|
|
||||||
// Schedule the first gentle reminder
|
|
||||||
scheduleGentleReminder(appcastItem: appcastItem, state: state)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {
|
|
||||||
// Handle release notes display if needed
|
|
||||||
logger.info("Showing update release notes")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) {
|
|
||||||
logger.error("Failed to download release notes: \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) {
|
|
||||||
logger.info("No updates found")
|
|
||||||
acknowledgement()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) {
|
|
||||||
logger.error("Updater error: \(error)")
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = "Update Error"
|
|
||||||
alert.informativeText = error.localizedDescription
|
|
||||||
alert.alertStyle = .warning
|
|
||||||
alert.addButton(withTitle: "OK")
|
|
||||||
alert.runModal()
|
|
||||||
|
|
||||||
acknowledgement()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showDownloadInitiated(cancellation: @escaping () -> Void) {
|
|
||||||
logger.info("Download initiated")
|
|
||||||
// Could show a download progress indicator here
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
|
|
||||||
logger.info("Download expected content length: \(expectedContentLength)")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showDownloadDidReceiveData(ofLength length: UInt64) {
|
|
||||||
// Update download progress if showing progress UI
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showDownloadDidStartExtractingUpdate() {
|
|
||||||
logger.info("Extracting update")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showExtractionReceivedProgress(_ progress: Double) {
|
|
||||||
// Update extraction progress if showing progress UI
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showReady(toInstallAndRelaunch reply: @escaping (SPUUpdateAlertChoice) -> Void) {
|
|
||||||
logger.info("Ready to install and relaunch")
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = "Update Ready to Install"
|
|
||||||
alert.informativeText = "VibeTunnel is ready to install the update and relaunch."
|
|
||||||
alert.addButton(withTitle: "Install and Relaunch")
|
|
||||||
alert.addButton(withTitle: "Install Later")
|
|
||||||
|
|
||||||
let response = alert.runModal()
|
|
||||||
|
|
||||||
if response == .alertFirstButtonReturn {
|
|
||||||
reply(.installUpdateChoice)
|
|
||||||
} else {
|
|
||||||
reply(.installLaterChoice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
|
||||||
logger.info("Installing update, application terminated: \(applicationTerminated)")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showSendingTerminationSignal() {
|
|
||||||
logger.info("Sending termination signal")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showUpdateInstallationDidFinish(acknowledgement: @escaping () -> Void) {
|
|
||||||
logger.info("Update installation finished")
|
|
||||||
acknowledgement()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func dismissUpdateInstallation() {
|
|
||||||
logger.info("Dismissing update installation")
|
|
||||||
|
|
||||||
// Cancel any pending reminders
|
|
||||||
reminderTimer?.invalidate()
|
|
||||||
reminderTimer = nil
|
|
||||||
updateItem = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Gentle Reminders
|
|
||||||
|
|
||||||
private func scheduleGentleReminder(appcastItem: SUAppcastItem, state: SPUUpdateState) {
|
|
||||||
// Determine the delay for the next reminder
|
|
||||||
let delay: TimeInterval
|
|
||||||
if lastReminderDate == nil {
|
|
||||||
// First reminder
|
|
||||||
delay = initialReminderDelay
|
|
||||||
} else {
|
|
||||||
// Subsequent reminders
|
|
||||||
delay = subsequentReminderInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Scheduling gentle reminder in \(delay / 3600) hours")
|
|
||||||
|
|
||||||
reminderTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in
|
|
||||||
self?.showGentleReminder(appcastItem: appcastItem, state: state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showGentleReminder(appcastItem: SUAppcastItem, state: SPUUpdateState) {
|
|
||||||
logger.info("Showing gentle reminder for update")
|
|
||||||
|
|
||||||
lastReminderDate = Date()
|
|
||||||
|
|
||||||
// Show the update notification as a reminder
|
|
||||||
showUpdateNotification(appcastItem: appcastItem, state: state, reply: { [weak self] choice in
|
|
||||||
if choice == .installUpdateChoice {
|
|
||||||
// User chose to install, no more reminders needed
|
|
||||||
self?.reminderTimer?.invalidate()
|
|
||||||
self?.reminderTimer = nil
|
|
||||||
} else {
|
|
||||||
// Schedule the next reminder
|
|
||||||
self?.scheduleGentleReminder(appcastItem: appcastItem, state: state)
|
|
||||||
}
|
|
||||||
}, isReminder: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showUpdateNotification(appcastItem: SUAppcastItem, state: SPUUpdateState, reply: @escaping (SPUUpdateAlertChoice) -> Void, isReminder: Bool) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
// Create a more prominent alert for updates
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = isReminder ? "Update Reminder" : "Update Available"
|
|
||||||
|
|
||||||
let versionString = appcastItem.displayVersionString ?? "new version"
|
|
||||||
var informativeText = "VibeTunnel \(versionString) is available."
|
|
||||||
|
|
||||||
if isReminder {
|
|
||||||
informativeText += " You have a pending update ready to install."
|
|
||||||
}
|
|
||||||
|
|
||||||
if let releaseNotesURL = appcastItem.releaseNotesURL {
|
|
||||||
informativeText += " Would you like to download it now?"
|
|
||||||
}
|
|
||||||
|
|
||||||
alert.informativeText = informativeText
|
|
||||||
alert.alertStyle = .informational
|
|
||||||
|
|
||||||
// Add buttons
|
|
||||||
alert.addButton(withTitle: state.stage == .downloaded ? "Install Update" : "Download Update")
|
|
||||||
alert.addButton(withTitle: "Skip This Version")
|
|
||||||
alert.addButton(withTitle: "Remind Me Later")
|
|
||||||
|
|
||||||
// Make the window more prominent
|
|
||||||
if let window = alert.window {
|
|
||||||
window.level = .floating
|
|
||||||
window.center()
|
|
||||||
window.makeKeyAndOrderFront(nil)
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = alert.runModal()
|
|
||||||
|
|
||||||
switch response {
|
|
||||||
case .alertFirstButtonReturn:
|
|
||||||
reply(.installUpdateChoice)
|
|
||||||
case .alertSecondButtonReturn:
|
|
||||||
reply(.skipThisVersionChoice)
|
|
||||||
default:
|
|
||||||
reply(.installLaterChoice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
210
VibeTunnel/Core/Services/SparkleUserDriverDelegate.swift
Normal file
210
VibeTunnel/Core/Services/SparkleUserDriverDelegate.swift
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import Foundation
|
||||||
|
@preconcurrency import Sparkle
|
||||||
|
import UserNotifications
|
||||||
|
import os.log
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
/// Delegate for Sparkle's standard user driver that implements gentle update reminders
|
||||||
|
/// using local notifications for background apps.
|
||||||
|
@MainActor
|
||||||
|
final class SparkleUserDriverDelegate: NSObject, @preconcurrency SPUStandardUserDriverDelegate {
|
||||||
|
private let logger = os.Logger(
|
||||||
|
subsystem: Bundle.main.bundleIdentifier ?? "VibeTunnel",
|
||||||
|
category: "SparkleUserDriver"
|
||||||
|
)
|
||||||
|
|
||||||
|
private var pendingUpdate: SUAppcastItem?
|
||||||
|
private var reminderTimer: Timer?
|
||||||
|
private var lastReminderDate: Date?
|
||||||
|
private var notificationIdentifier: String?
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
private let initialReminderDelay: TimeInterval = 60 * 60 * 24 // 24 hours
|
||||||
|
private let subsequentReminderInterval: TimeInterval = 60 * 60 * 24 * 3 // 3 days
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
setupNotificationCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SPUStandardUserDriverDelegate
|
||||||
|
|
||||||
|
/// Required to eliminate the "no user driver delegate" warning for background apps
|
||||||
|
var supportsGentleScheduledUpdateReminders: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called to determine if Sparkle should handle showing the update
|
||||||
|
func standardUserDriverShouldHandleShowingScheduledUpdate(_ update: SUAppcastItem, andInImmediateFocus immediateFocus: Bool) -> Bool {
|
||||||
|
logger.info("Should handle showing update: \(update.displayVersionString), immediate: \(immediateFocus)")
|
||||||
|
|
||||||
|
// Store the pending update for reminders
|
||||||
|
pendingUpdate = update
|
||||||
|
|
||||||
|
// If it's not immediate focus and we have a pending update, schedule a reminder
|
||||||
|
if !immediateFocus {
|
||||||
|
scheduleGentleReminder(for: update)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let Sparkle handle showing the update UI
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called before an update is shown
|
||||||
|
func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) {
|
||||||
|
logger.info("Will show update: \(update.displayVersionString), userInitiated: \(state.userInitiated)")
|
||||||
|
|
||||||
|
// If this is a user-initiated check or the update is being shown, cancel reminders
|
||||||
|
if state.userInitiated || handleShowingUpdate {
|
||||||
|
cancelReminders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when user first interacts with the update
|
||||||
|
func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) {
|
||||||
|
logger.info("User gave attention to update: \(update.displayVersionString)")
|
||||||
|
|
||||||
|
// Cancel any pending reminders since user has seen the update
|
||||||
|
cancelReminders()
|
||||||
|
|
||||||
|
// Remove any existing notifications
|
||||||
|
if let identifier = notificationIdentifier {
|
||||||
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when update session ends
|
||||||
|
func standardUserDriverWillFinishUpdateSession() {
|
||||||
|
logger.info("Update session ending")
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
pendingUpdate = nil
|
||||||
|
cancelReminders()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called before showing a modal alert
|
||||||
|
func standardUserDriverWillShowModalAlert() {
|
||||||
|
logger.debug("Will show modal alert")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called after showing a modal alert
|
||||||
|
func standardUserDriverDidShowModalAlert() {
|
||||||
|
logger.debug("Did show modal alert")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Gentle Reminders
|
||||||
|
|
||||||
|
private func setupNotificationCategories() {
|
||||||
|
let updateAction = UNNotificationAction(
|
||||||
|
identifier: "UPDATE_ACTION",
|
||||||
|
title: "Update Now",
|
||||||
|
options: [.foreground]
|
||||||
|
)
|
||||||
|
|
||||||
|
let laterAction = UNNotificationAction(
|
||||||
|
identifier: "LATER_ACTION",
|
||||||
|
title: "Remind Me Later",
|
||||||
|
options: []
|
||||||
|
)
|
||||||
|
|
||||||
|
let category = UNNotificationCategory(
|
||||||
|
identifier: "UPDATE_REMINDER",
|
||||||
|
actions: [updateAction, laterAction],
|
||||||
|
intentIdentifiers: [],
|
||||||
|
options: []
|
||||||
|
)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().setNotificationCategories([category])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleGentleReminder(for update: SUAppcastItem) {
|
||||||
|
// Cancel any existing reminder
|
||||||
|
reminderTimer?.invalidate()
|
||||||
|
|
||||||
|
// Determine the delay for the next reminder
|
||||||
|
let delay: TimeInterval
|
||||||
|
if lastReminderDate == nil {
|
||||||
|
// First reminder
|
||||||
|
delay = initialReminderDelay
|
||||||
|
} else {
|
||||||
|
// Subsequent reminders
|
||||||
|
delay = subsequentReminderInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Scheduling gentle reminder in \(delay / 3600) hours for version \(update.displayVersionString)")
|
||||||
|
|
||||||
|
// Schedule the reminder
|
||||||
|
let versionString = update.displayVersionString
|
||||||
|
reminderTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in
|
||||||
|
Task { @MainActor in
|
||||||
|
self?.showReminderNotificationForVersion(versionString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showReminderNotificationForVersion(_ versionString: String) {
|
||||||
|
lastReminderDate = Date()
|
||||||
|
|
||||||
|
// Create notification content
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "Update Available"
|
||||||
|
content.body = "VibeTunnel \(versionString) is ready to install."
|
||||||
|
content.sound = .default
|
||||||
|
content.categoryIdentifier = "UPDATE_REMINDER"
|
||||||
|
|
||||||
|
// Add action button
|
||||||
|
content.userInfo = ["updateVersion": versionString]
|
||||||
|
|
||||||
|
// Create unique identifier
|
||||||
|
notificationIdentifier = "vibetunnel-update-\(versionString)-\(Date().timeIntervalSince1970)"
|
||||||
|
|
||||||
|
// Create the request
|
||||||
|
let request = UNNotificationRequest(
|
||||||
|
identifier: notificationIdentifier!,
|
||||||
|
content: content,
|
||||||
|
trigger: nil // Show immediately
|
||||||
|
)
|
||||||
|
|
||||||
|
// Schedule the notification
|
||||||
|
UNUserNotificationCenter.current().add(request) { [weak self] error in
|
||||||
|
if let error = error {
|
||||||
|
self?.logger.error("Failed to schedule notification: \(error)")
|
||||||
|
} else {
|
||||||
|
self?.logger.info("Scheduled reminder notification for version \(versionString)")
|
||||||
|
|
||||||
|
// Schedule the next reminder if we still have a pending update
|
||||||
|
Task { @MainActor in
|
||||||
|
if let pendingUpdate = self?.pendingUpdate {
|
||||||
|
self?.scheduleGentleReminder(for: pendingUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelReminders() {
|
||||||
|
reminderTimer?.invalidate()
|
||||||
|
reminderTimer = nil
|
||||||
|
lastReminderDate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Notification Handling
|
||||||
|
|
||||||
|
func handleNotificationAction(_ action: String, userInfo: [AnyHashable: Any]) {
|
||||||
|
switch action {
|
||||||
|
case "UPDATE_ACTION":
|
||||||
|
logger.info("User tapped 'Update Now' in notification")
|
||||||
|
// Bring app to foreground and trigger update check
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
// The SparkleUpdaterManager will handle the actual update check
|
||||||
|
SparkleUpdaterManager.shared.checkForUpdates()
|
||||||
|
|
||||||
|
case "LATER_ACTION":
|
||||||
|
logger.info("User tapped 'Remind Me Later' in notification")
|
||||||
|
// The next reminder is already scheduled
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import AppKit
|
import AppKit
|
||||||
import os.log
|
import os.log
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
/// Main entry point for the VibeTunnel macOS application
|
/// Main entry point for the VibeTunnel macOS application
|
||||||
@main
|
@main
|
||||||
|
|
@ -68,7 +69,7 @@ struct VibeTunnelApp: App {
|
||||||
|
|
||||||
/// Manages app lifecycle, single instance enforcement, and core services
|
/// Manages app lifecycle, single instance enforcement, and core services
|
||||||
@MainActor
|
@MainActor
|
||||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUserNotificationCenterDelegate {
|
||||||
private(set) var sparkleUpdaterManager: SparkleUpdaterManager?
|
private(set) var sparkleUpdaterManager: SparkleUpdaterManager?
|
||||||
private let serverManager = ServerManager.shared
|
private let serverManager = ServerManager.shared
|
||||||
private let sessionMonitor = SessionMonitor.shared
|
private let sessionMonitor = SessionMonitor.shared
|
||||||
|
|
@ -103,6 +104,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
// Initialize Sparkle updater manager
|
// Initialize Sparkle updater manager
|
||||||
sparkleUpdaterManager = SparkleUpdaterManager.shared
|
sparkleUpdaterManager = SparkleUpdaterManager.shared
|
||||||
|
|
||||||
|
// Set up notification center delegate
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
|
||||||
|
// Request notification permissions
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])
|
||||||
|
logger.info("Notification permission granted: \(granted)")
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to request notification permissions: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize dock icon visibility through DockIconManager
|
// Initialize dock icon visibility through DockIconManager
|
||||||
DockIconManager.shared.updateDockVisibility()
|
DockIconManager.shared.updateDockVisibility()
|
||||||
|
|
||||||
|
|
@ -289,4 +303,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UNUserNotificationCenterDelegate
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||||
|
didReceive response: UNNotificationResponse,
|
||||||
|
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
|
logger.info("Received notification response: \(response.actionIdentifier)")
|
||||||
|
|
||||||
|
// Handle update reminder actions
|
||||||
|
if response.notification.request.content.categoryIdentifier == "UPDATE_REMINDER" {
|
||||||
|
sparkleUpdaterManager?.userDriverDelegate?.handleNotificationAction(
|
||||||
|
response.actionIdentifier,
|
||||||
|
userInfo: response.notification.request.content.userInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||||
|
willPresent notification: UNNotification,
|
||||||
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
// Show notifications even when app is in foreground
|
||||||
|
completionHandler([.banner, .sound])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue