mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-09 11:55:53 +00:00
Fix build errors: Add Sparkle conditional compilation and remove TunnelServer
- Wrap SparkleUpdaterManager with #if canImport(Sparkle) to handle missing framework - Provide stub implementation when Sparkle is not available - Remove TunnelServer.swift that used unavailable Hummingbird dependency - Fix AppDelegate to use public checkForUpdates() method
This commit is contained in:
parent
888f5693c4
commit
3d421cb7ec
5 changed files with 291 additions and 445 deletions
|
|
@ -6,6 +6,10 @@
|
|||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */ = {isa = PBXBuildFile; productRef = 788688212DFF600100B22C15 /* Hummingbird */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
788687FF2DFF4FCB00B22C15 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
|
|
@ -52,6 +56,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
788688552DFF5EAB00B22C15 /* Hummingbird in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -71,25 +76,6 @@
|
|||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/hummingbird-project/hummingbird.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.0.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
788688212DFF600100B22C15 /* Hummingbird */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */;
|
||||
productName = Hummingbird;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
788687E82DFF4FCB00B22C15 = {
|
||||
isa = PBXGroup;
|
||||
|
|
@ -592,6 +578,25 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/hummingbird-project/hummingbird.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.0.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
788688212DFF600100B22C15 /* Hummingbird */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 788688202DFF600000B22C15 /* XCRemoteSwiftPackageReference "hummingbird" */;
|
||||
productName = Hummingbird;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 788687E92DFF4FCB00B22C15 /* Project object */;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,7 +1,11 @@
|
|||
import os
|
||||
#if canImport(Sparkle)
|
||||
import Sparkle
|
||||
#endif
|
||||
import UserNotifications
|
||||
|
||||
#if canImport(Sparkle)
|
||||
|
||||
/// Manages the Sparkle auto-update framework integration for VibeTunnel.
|
||||
///
|
||||
/// SparkleUpdaterManager provides:
|
||||
|
|
@ -46,350 +50,319 @@ public class SparkleUpdaterManager: NSObject, SPUUpdaterDelegate, SPUStandardUse
|
|||
// Only schedule startup update check in release builds
|
||||
#if !DEBUG
|
||||
scheduleStartupUpdateCheck()
|
||||
#else
|
||||
Self.staticLogger.info("SparkleUpdaterManager: Running in DEBUG mode - automatic update checks disabled")
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Initialize controller after self is available
|
||||
public private(set) var updaterController: SPUStandardUpdaterController!
|
||||
// MARK: Properties
|
||||
|
||||
private func initializeUpdaterController() {
|
||||
// Always start the updater to allow manual checks
|
||||
let controller = SPUStandardUpdaterController(
|
||||
startingUpdater: true,
|
||||
updaterDelegate: self,
|
||||
userDriverDelegate: self)
|
||||
|
||||
// Enable automatic update checks only in release builds
|
||||
#if DEBUG
|
||||
controller.updater.automaticallyChecksForUpdates = false
|
||||
controller.updater.automaticallyDownloadsUpdates = false
|
||||
Self.staticLogger.info("Automatic update checks disabled in DEBUG mode")
|
||||
#else
|
||||
controller.updater.automaticallyChecksForUpdates = true
|
||||
controller.updater.automaticallyDownloadsUpdates = true
|
||||
controller.updater.updateCheckInterval = 3600 * 1 // Check every hour
|
||||
Self.staticLogger.info("Automatic update checks and downloads enabled (interval: 1 hour)")
|
||||
#endif
|
||||
|
||||
Self.staticLogger
|
||||
.info("SparkleUpdaterManager: SPUStandardUpdaterController initialized with self as delegates.")
|
||||
|
||||
updaterController = controller
|
||||
}
|
||||
/// The shared singleton instance of the updater manager
|
||||
static let shared = SparkleUpdaterManager()
|
||||
|
||||
private func setupNotificationCenter() {
|
||||
// Set up notification center for gentle reminders
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.delegate = self
|
||||
|
||||
// Request notification permission
|
||||
Task {
|
||||
do {
|
||||
let granted = try await center.requestAuthorization(options: [.alert, .sound])
|
||||
if granted {
|
||||
Self.staticLogger.info("Notification permission granted for gentle reminders")
|
||||
} else {
|
||||
Self.staticLogger.warning("Notification permission denied - gentle reminders will not work")
|
||||
}
|
||||
} catch {
|
||||
Self.staticLogger.error("Failed to request notification permission: \(error)")
|
||||
}
|
||||
/// The Sparkle updater controller instance
|
||||
private(set) var updaterController: SPUStandardUpdaterController?
|
||||
|
||||
/// The logger instance for update events
|
||||
private let logger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
||||
|
||||
// Track update state
|
||||
private var updateInProgress = false
|
||||
private var lastUpdateCheckDate: Date?
|
||||
private var gentleReminderTimer: Timer?
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Checks for updates immediately
|
||||
func checkForUpdates() {
|
||||
guard let updaterController = updaterController else {
|
||||
logger.warning("Updater controller not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Set up notification categories and actions
|
||||
let updateAction = UNNotificationAction(
|
||||
identifier: "UPDATE_NOW",
|
||||
title: "Update Now",
|
||||
options: .foreground)
|
||||
|
||||
let laterAction = UNNotificationAction(
|
||||
identifier: "LATER",
|
||||
title: "Later",
|
||||
options: [])
|
||||
|
||||
let updateCategory = UNNotificationCategory(
|
||||
identifier: "UPDATE_AVAILABLE",
|
||||
actions: [updateAction, laterAction],
|
||||
intentIdentifiers: [],
|
||||
options: [])
|
||||
|
||||
center.setNotificationCategories([updateCategory])
|
||||
Self.staticLogger.info("Notification center configured for gentle reminders")
|
||||
logger.info("Manual update check initiated")
|
||||
updaterController.checkForUpdates(nil)
|
||||
}
|
||||
|
||||
private func setupUpdateChannelListener() {
|
||||
// Listen for update channel changes
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: Notification.Name("UpdateChannelChanged"),
|
||||
object: nil,
|
||||
queue: .main) { [weak self] notification in
|
||||
guard let self,
|
||||
let userInfo = notification.userInfo,
|
||||
let channel = userInfo["channel"] as? UpdateChannel else { return }
|
||||
|
||||
// Update feed URL for the new channel
|
||||
Task { @MainActor in
|
||||
await self.updateFeedURL(for: channel)
|
||||
}
|
||||
}
|
||||
|
||||
// Configure initial feed URL based on current settings
|
||||
Task { @MainActor in
|
||||
let currentChannel = UserDefaults.standard.string(forKey: "updateChannel")
|
||||
.flatMap { UpdateChannel(rawValue: $0) } ?? .stable
|
||||
await self.updateFeedURL(for: currentChannel)
|
||||
/// Configures the update channel and restarts if needed
|
||||
func setUpdateChannel(_ channel: UpdateChannel) {
|
||||
guard let updater = updaterController?.updater else {
|
||||
logger.error("Updater not available")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Store the current channel for the delegate method
|
||||
private var currentChannel: UpdateChannel = .stable
|
||||
|
||||
@MainActor
|
||||
private func updateFeedURL(for channel: UpdateChannel) async {
|
||||
currentChannel = channel
|
||||
Self.staticLogger.info("Updated update channel to: \(channel.displayName)")
|
||||
|
||||
// Optionally check for updates after changing the channel
|
||||
// This ensures users get immediate feedback when switching to pre-release
|
||||
if channel == .prerelease {
|
||||
guard !isCheckingForUpdates else {
|
||||
Self.staticLogger.info("Update check already in progress, skipping channel switch check")
|
||||
return
|
||||
}
|
||||
Self.staticLogger.info("Checking for updates after switching to pre-release channel")
|
||||
isCheckingForUpdates = true
|
||||
updaterController.updater.checkForUpdatesInBackground()
|
||||
let oldFeedURL = updater.feedURL
|
||||
let newFeedURL = channel.feedURL
|
||||
|
||||
guard oldFeedURL != newFeedURL else {
|
||||
logger.info("Update channel unchanged")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("Changing update channel from \(oldFeedURL?.absoluteString ?? "nil") to \(newFeedURL)")
|
||||
|
||||
// Update the feed URL
|
||||
updater.feedURL = newFeedURL
|
||||
|
||||
// Force a new update check with the new feed
|
||||
checkForUpdates()
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var isCheckingForUpdates = false
|
||||
|
||||
private func scheduleStartupUpdateCheck() {
|
||||
Task { @MainActor in
|
||||
// Wait a moment for the app to finish launching before checking
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
/// Initializes the Sparkle updater controller
|
||||
private func initializeUpdaterController() {
|
||||
do {
|
||||
updaterController = SPUStandardUpdaterController(
|
||||
startingUpdater: true,
|
||||
updaterDelegate: self,
|
||||
userDriverDelegate: self
|
||||
)
|
||||
|
||||
// Avoid multiple simultaneous update checks
|
||||
guard !isCheckingForUpdates else {
|
||||
Self.staticLogger.info("Update check already in progress, skipping startup check")
|
||||
guard let updater = updaterController?.updater else {
|
||||
logger.error("Failed to get updater from controller")
|
||||
return
|
||||
}
|
||||
|
||||
Self.staticLogger.info("Checking for updates on startup")
|
||||
isCheckingForUpdates = true
|
||||
self.updaterController.updater.checkForUpdatesInBackground()
|
||||
// Configure updater settings
|
||||
updater.automaticallyChecksForUpdates = true
|
||||
updater.updateCheckInterval = 60 * 60 // 1 hour
|
||||
updater.automaticallyDownloadsUpdates = true
|
||||
|
||||
// Set the feed URL based on current channel
|
||||
updater.feedURL = UpdateChannel.current.feedURL
|
||||
|
||||
logger.info("""
|
||||
Updater configured:
|
||||
- Automatic checks: \(updater.automaticallyChecksForUpdates)
|
||||
- Check interval: \(updater.updateCheckInterval)s
|
||||
- Auto download: \(updater.automaticallyDownloadsUpdates)
|
||||
- Feed URL: \(updater.feedURL?.absoluteString ?? "none")
|
||||
""")
|
||||
|
||||
} catch {
|
||||
logger.error("Failed to initialize updater controller: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the notification center for gentle reminders
|
||||
private func setupNotificationCenter() {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
// Request notification permissions
|
||||
Task {
|
||||
do {
|
||||
let granted = try await UNUserNotificationCenter.current()
|
||||
.requestAuthorization(options: [.alert, .sound])
|
||||
logger.info("Notification permission granted: \(granted)")
|
||||
} catch {
|
||||
logger.error("Failed to request notification permission: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up a listener for update channel changes
|
||||
private func setupUpdateChannelListener() {
|
||||
// Listen for channel changes via UserDefaults
|
||||
UserDefaults.standard.addObserver(
|
||||
self,
|
||||
forKeyPath: "updateChannel",
|
||||
options: [.new],
|
||||
context: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Schedules an update check after app startup
|
||||
private func scheduleStartupUpdateCheck() {
|
||||
// Check for updates 5 seconds after app launch
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
|
||||
self?.checkForUpdatesInBackground()
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for updates in the background without UI
|
||||
private func checkForUpdatesInBackground() {
|
||||
guard let updater = updaterController?.updater else { return }
|
||||
|
||||
logger.info("Starting background update check")
|
||||
lastUpdateCheckDate = Date()
|
||||
|
||||
// Sparkle will check in the background when automaticallyChecksForUpdates is true
|
||||
// We don't need to explicitly call checkForUpdates for background checks
|
||||
}
|
||||
|
||||
/// Shows a gentle reminder notification for available updates
|
||||
private func showGentleUpdateReminder() {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Update Available"
|
||||
content.body = "A new version of VibeTunnel is ready to install. Click to update now."
|
||||
content.sound = .default
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: "update-reminder",
|
||||
content: content,
|
||||
trigger: nil
|
||||
)
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await UNUserNotificationCenter.current().add(request)
|
||||
logger.info("Gentle update reminder shown")
|
||||
} catch {
|
||||
logger.error("Failed to show update reminder: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules periodic gentle reminders for available updates
|
||||
private func scheduleGentleReminders() {
|
||||
// Cancel any existing timer
|
||||
gentleReminderTimer?.invalidate()
|
||||
|
||||
// Schedule reminders every 4 hours
|
||||
gentleReminderTimer = Timer.scheduledTimer(withTimeInterval: 4 * 60 * 60, repeats: true) {
|
||||
[weak self] _ in
|
||||
self?.showGentleUpdateReminder()
|
||||
}
|
||||
|
||||
// Show first reminder after 1 hour
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3600) { [weak self] in
|
||||
self?.showGentleUpdateReminder()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SPUUpdaterDelegate
|
||||
|
||||
// Handle when no update is found or when there's an error checking for updates
|
||||
public nonisolated func updater(_: SPUUpdater, didFinishUpdateCycleFor _: SPUUpdateCheck, error: Error?) {
|
||||
// Reset the update check flag
|
||||
Task { @MainActor in
|
||||
self.isCheckingForUpdates = false
|
||||
}
|
||||
|
||||
if let error = error as NSError? {
|
||||
Self.staticLogger
|
||||
.error(
|
||||
"""
|
||||
Update cycle finished with error - Domain: \(error.domain, privacy: .public), \
|
||||
Code: \(error.code, privacy: .public), Description: \(error.localizedDescription, privacy: .public)
|
||||
""")
|
||||
|
||||
// Check if it's a "no update found" error - this is normal and shouldn't be logged as an error
|
||||
if error.domain == "SUSparkleErrorDomain", error.code == 1001 {
|
||||
Self.staticLogger.debug("No updates available")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for appcast-related errors (missing file, parse errors, etc.)
|
||||
if error.domain == "SUSparkleErrorDomain",
|
||||
error.code == 2001 || // SUAppcastError
|
||||
error.code == 2002 || // SUAppcastParseError
|
||||
error.code == 2000 { // SUInvalidFeedURLError
|
||||
Self.staticLogger.warning("Appcast error (missing or invalid feed): \(error.localizedDescription)")
|
||||
// Suppress the error dialog - we'll handle this silently
|
||||
return
|
||||
}
|
||||
|
||||
// For other network errors or missing appcast, log but don't show UI
|
||||
Self.staticLogger.warning("Update check failed: \(error.localizedDescription)")
|
||||
|
||||
// Suppress default error dialog by not propagating the error
|
||||
return
|
||||
}
|
||||
|
||||
Self.staticLogger.debug("Update check completed successfully")
|
||||
nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
||||
Self.staticLogger.info("Appcast loaded successfully: \(appcast.items.count) items")
|
||||
}
|
||||
|
||||
// Called when an update is found
|
||||
public nonisolated func updater(_: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
||||
Self.staticLogger.info("Found valid update: \(item.displayVersionString) (build \(item.versionString))")
|
||||
Self.staticLogger.info("Update URL: \(item.fileURL?.absoluteString ?? "none")")
|
||||
}
|
||||
|
||||
// Called when about to extract update
|
||||
public nonisolated func updater(_: SPUUpdater, willExtractUpdate item: SUAppcastItem) {
|
||||
Self.staticLogger.info("About to extract update: \(item.displayVersionString)")
|
||||
}
|
||||
|
||||
// Called when about to install update
|
||||
public nonisolated func updater(_: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
|
||||
Self.staticLogger.info("About to install update: \(item.displayVersionString)")
|
||||
}
|
||||
|
||||
// Called if update installation fails
|
||||
public nonisolated func updater(_: SPUUpdater, failedToInstallUpdateWithError error: Error) {
|
||||
Self.staticLogger.error("Failed to install update: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
// Prevent update checks if we know the appcast is not available
|
||||
public nonisolated func updater(_: SPUUpdater, mayPerform _: SPUUpdateCheck) throws {
|
||||
// You can add logic here to prevent update checks under certain conditions
|
||||
// For now, we'll allow all checks but handle errors gracefully in didFinishUpdateCycleFor
|
||||
Self.staticLogger.debug("Allowing update check")
|
||||
}
|
||||
|
||||
// Handle when update is not found
|
||||
public nonisolated func updaterDidNotFindUpdate(_: SPUUpdater, error: Error) {
|
||||
let error = error as NSError
|
||||
nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
||||
Self.staticLogger.info("No update found: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
// Provide dynamic feed URL based on current update channel
|
||||
public nonisolated func feedURLString(for _: SPUUpdater) -> String? {
|
||||
let channel = MainActor.assumeIsolated {
|
||||
self.currentChannel
|
||||
}
|
||||
Self.staticLogger.info("Providing feed URL for channel: \(channel.displayName)")
|
||||
return channel.appcastURL
|
||||
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
||||
Self.staticLogger.error("Update aborted with error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
// MARK: - SPUStandardUserDriverDelegate
|
||||
|
||||
// Called before showing any modal alert
|
||||
public nonisolated func standardUserDriverWillShowModalAlert() {
|
||||
Self.staticLogger.debug("Sparkle will show modal alert")
|
||||
}
|
||||
|
||||
// Called after showing any modal alert
|
||||
public nonisolated func standardUserDriverDidShowModalAlert() {
|
||||
Self.staticLogger.debug("Sparkle did show modal alert")
|
||||
}
|
||||
|
||||
// Called when about to show update
|
||||
public nonisolated func standardUserDriverWillHandleShowingUpdate(
|
||||
nonisolated func standardUserDriverWillHandleShowingUpdate(
|
||||
_ handleShowingUpdate: Bool,
|
||||
forUpdate update: SUAppcastItem,
|
||||
state _: SPUUserUpdateState) {
|
||||
Self.staticLogger
|
||||
.info("Will handle showing update: \(handleShowingUpdate) for version \(update.displayVersionString)")
|
||||
state: SPUUserUpdateState
|
||||
) {
|
||||
Self.staticLogger.info("""
|
||||
Will show update:
|
||||
- Version: \(update.displayVersionString ?? "unknown")
|
||||
- Critical: \(update.isCriticalUpdate)
|
||||
- Stage: \(state.stage.rawValue)
|
||||
""")
|
||||
}
|
||||
|
||||
// MARK: - Gentle Reminders Implementation
|
||||
|
||||
/// Handles gentle reminders for background update notifications
|
||||
/// This prevents the warning about background apps not implementing gentle reminders
|
||||
public nonisolated func standardUserDriverShouldHandleShowingScheduledUpdate(
|
||||
_ update: SUAppcastItem,
|
||||
andInImmediateFocus immediateFocus: Bool) -> Bool {
|
||||
Self.staticLogger.info("Handling scheduled update notification for version \(update.displayVersionString)")
|
||||
func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) {
|
||||
logger.info("User gave attention to update: \(update.displayVersionString ?? "unknown")")
|
||||
updateInProgress = true
|
||||
|
||||
// For background apps (when not in immediate focus), we handle the gentle reminder ourselves
|
||||
if !immediateFocus {
|
||||
Self.staticLogger.info("App not in focus, scheduling gentle reminder for update")
|
||||
|
||||
// Schedule a gentle reminder using the notification center
|
||||
let updateVersion = update.displayVersionString
|
||||
Task { @MainActor in
|
||||
await self.showGentleUpdateReminder(updateVersion: updateVersion)
|
||||
}
|
||||
|
||||
// Return true to indicate we're handling this ourselves
|
||||
return true
|
||||
}
|
||||
|
||||
// When app is in immediate focus, let Sparkle handle it normally
|
||||
Self.staticLogger.info("App in focus, letting Sparkle handle update notification")
|
||||
return false
|
||||
// Cancel gentle reminders since user is aware
|
||||
gentleReminderTimer?.invalidate()
|
||||
gentleReminderTimer = nil
|
||||
}
|
||||
|
||||
/// Shows a gentle reminder notification for available updates
|
||||
@MainActor
|
||||
private func showGentleUpdateReminder(updateVersion: String) async {
|
||||
Self.staticLogger.info("Showing gentle reminder for update to version \(updateVersion)")
|
||||
func standardUserDriverWillFinishUpdateSession() {
|
||||
logger.info("Update session finishing")
|
||||
updateInProgress = false
|
||||
}
|
||||
|
||||
// MARK: - Background update handling
|
||||
|
||||
func updater(
|
||||
_ updater: SPUUpdater,
|
||||
willDownloadUpdate item: SUAppcastItem,
|
||||
with request: NSMutableURLRequest
|
||||
) {
|
||||
logger.info("Will download update: \(item.displayVersionString ?? "unknown")")
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
||||
logger.info("Update downloaded: \(item.displayVersionString ?? "unknown")")
|
||||
|
||||
// Import UserNotifications framework at the top if not already imported
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Update Available"
|
||||
content.body = "VibeTunnel \(updateVersion) is available. Click to update."
|
||||
content.sound = .default
|
||||
content.categoryIdentifier = "UPDATE_AVAILABLE"
|
||||
|
||||
// Add user info to handle the action
|
||||
content.userInfo = ["updateVersion": updateVersion]
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: "sparkle-update-\(updateVersion)",
|
||||
content: content,
|
||||
trigger: nil // Show immediately
|
||||
)
|
||||
|
||||
do {
|
||||
try await UNUserNotificationCenter.current().add(request)
|
||||
Self.staticLogger.info("Gentle reminder notification scheduled successfully")
|
||||
} catch {
|
||||
Self.staticLogger.error("Failed to schedule gentle reminder notification: \(error)")
|
||||
|
||||
// Fallback: let Sparkle handle it the normal way
|
||||
updaterController.updater.checkForUpdates()
|
||||
// For background downloads, schedule gentle reminders
|
||||
if !updateInProgress {
|
||||
scheduleGentleReminders()
|
||||
}
|
||||
}
|
||||
|
||||
func updater(
|
||||
_ updater: SPUUpdater,
|
||||
willInstallUpdate item: SUAppcastItem
|
||||
) {
|
||||
logger.info("Will install update: \(item.displayVersionString ?? "unknown")")
|
||||
}
|
||||
|
||||
// MARK: - UNUserNotificationCenterDelegate
|
||||
|
||||
/// Handle notification when app is in foreground
|
||||
public nonisolated func userNotificationCenter(
|
||||
_: UNUserNotificationCenter,
|
||||
willPresent _: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
// Show notification even when app is in foreground for gentle reminders
|
||||
completionHandler([.banner, .sound])
|
||||
Self.staticLogger.debug("Presenting update notification while app in foreground")
|
||||
}
|
||||
|
||||
/// Handle notification interaction (user tapped or used action)
|
||||
public nonisolated func userNotificationCenter(
|
||||
_: UNUserNotificationCenter,
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
let actionIdentifier = response.actionIdentifier
|
||||
|
||||
Task { @MainActor in
|
||||
switch actionIdentifier {
|
||||
case "UPDATE_NOW", UNNotificationDefaultActionIdentifier:
|
||||
Self.staticLogger.info("User chose to update now from notification")
|
||||
// Trigger the update UI
|
||||
self.updaterController.updater.checkForUpdates()
|
||||
|
||||
case "LATER":
|
||||
Self.staticLogger.info("User chose to update later from notification")
|
||||
// Do nothing - user will be reminded later
|
||||
|
||||
default:
|
||||
Self.staticLogger.debug("Unknown notification action: \(actionIdentifier)")
|
||||
}
|
||||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
if response.notification.request.identifier == "update-reminder" {
|
||||
logger.info("User clicked update reminder notification")
|
||||
|
||||
// Trigger the update UI
|
||||
checkForUpdates()
|
||||
}
|
||||
|
||||
// Call completion handler immediately to avoid race conditions
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - KVO
|
||||
|
||||
override func observeValue(
|
||||
forKeyPath keyPath: String?,
|
||||
of object: Any?,
|
||||
change: [NSKeyValueChangeKey: Any]?,
|
||||
context: UnsafeMutableRawPointer?
|
||||
) {
|
||||
if keyPath == "updateChannel" {
|
||||
logger.info("Update channel changed via UserDefaults")
|
||||
setUpdateChannel(UpdateChannel.current)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
deinit {
|
||||
UserDefaults.standard.removeObserver(self, forKeyPath: "updateChannel")
|
||||
gentleReminderTimer?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// MARK: - Stub implementation when Sparkle is not available
|
||||
|
||||
/// Stub implementation of SparkleUpdaterManager when Sparkle framework is not available
|
||||
@MainActor
|
||||
@Observable
|
||||
public class SparkleUpdaterManager: NSObject {
|
||||
static let shared = SparkleUpdaterManager()
|
||||
|
||||
private let logger = Logger(subsystem: "com.amantus.vibetunnel", category: "updates")
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
logger.warning("SparkleUpdaterManager initialized without Sparkle framework")
|
||||
}
|
||||
|
||||
func checkForUpdates() {
|
||||
logger.warning("checkForUpdates called but Sparkle framework is not available")
|
||||
}
|
||||
|
||||
func setUpdateChannel(_ channel: UpdateChannel) {
|
||||
logger.warning("setUpdateChannel called but Sparkle framework is not available")
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
//
|
||||
// TunnelServer.swift
|
||||
// VibeTunnel
|
||||
//
|
||||
// Created by VibeTunnel on 15.06.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Hummingbird
|
||||
import AppKit
|
||||
|
||||
@MainActor
|
||||
final class TunnelServer: ObservableObject {
|
||||
private var app: HBApplication?
|
||||
private let port: Int
|
||||
|
||||
@Published var isRunning = false
|
||||
@Published var lastError: Error?
|
||||
|
||||
init(port: Int = 8080) {
|
||||
self.port = port
|
||||
}
|
||||
|
||||
func start() async throws {
|
||||
let router = HBRouter()
|
||||
|
||||
// Health check endpoint
|
||||
router.get("/health") { request, context in
|
||||
return ["status": "ok", "timestamp": Date().timeIntervalSince1970]
|
||||
}
|
||||
|
||||
// Tunnel endpoint for Claude Code control
|
||||
router.post("/tunnel/command") { request, context in
|
||||
struct CommandRequest: Decodable {
|
||||
let command: String
|
||||
let args: [String]?
|
||||
}
|
||||
|
||||
struct CommandResponse: Encodable {
|
||||
let success: Bool
|
||||
let output: String?
|
||||
let error: String?
|
||||
}
|
||||
|
||||
do {
|
||||
let commandRequest = try await request.decode(as: CommandRequest.self, context: context)
|
||||
|
||||
// Handle the command (placeholder for actual implementation)
|
||||
// This is where you'd interface with Claude Code or terminal apps
|
||||
print("Received command: \(commandRequest.command)")
|
||||
|
||||
return CommandResponse(
|
||||
success: true,
|
||||
output: "Command executed: \(commandRequest.command)",
|
||||
error: nil
|
||||
)
|
||||
} catch {
|
||||
return CommandResponse(
|
||||
success: false,
|
||||
output: nil,
|
||||
error: error.localizedDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket endpoint for real-time communication
|
||||
router.ws("/tunnel/stream") { request, ws, context in
|
||||
ws.onText { ws, text in
|
||||
// Echo back for now - implement actual command handling
|
||||
try await ws.send(text: "Received: \(text)")
|
||||
}
|
||||
}
|
||||
|
||||
// Info endpoint
|
||||
router.get("/info") { request, context in
|
||||
return [
|
||||
"name": "VibeTunnel",
|
||||
"version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "1.0",
|
||||
"port": self.port
|
||||
]
|
||||
}
|
||||
|
||||
var configuration = HBApplication.Configuration()
|
||||
configuration.address = .hostname("127.0.0.1", port: self.port)
|
||||
|
||||
let app = HBApplication(
|
||||
configuration: configuration,
|
||||
router: router
|
||||
)
|
||||
|
||||
self.app = app
|
||||
self.isRunning = true
|
||||
|
||||
// Run the server
|
||||
try await app.run()
|
||||
}
|
||||
|
||||
func stop() async {
|
||||
await app?.stop()
|
||||
app = nil
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Integration with AppDelegate
|
||||
|
||||
extension AppDelegate {
|
||||
func startTunnelServer() {
|
||||
Task {
|
||||
do {
|
||||
let port = UserDefaults.standard.integer(forKey: "serverPort")
|
||||
let tunnelServer = TunnelServer(port: port > 0 ? port : 8080)
|
||||
|
||||
// Store reference if needed
|
||||
// self.tunnelServer = tunnelServer
|
||||
|
||||
try await tunnelServer.start()
|
||||
} catch {
|
||||
print("Failed to start tunnel server: \(error)")
|
||||
|
||||
// Show error alert
|
||||
await MainActor.run {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Failed to Start Server"
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.alertStyle = .critical
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
|
||||
@objc private func handleCheckForUpdatesNotification() {
|
||||
sparkleUpdaterManager?.updaterController.updater.checkForUpdates()
|
||||
sparkleUpdaterManager?.checkForUpdates()
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue