import Combine import SwiftUI @MainActor class MenuBarViewModel: ObservableObject { @Published var isAuthenticated = false @Published var syncStatus: SyncStatus = .idle @Published var messageCount = 0 @Published var conversationCount = 0 @Published var lastSyncText = "Never" @Published var isSyncing = false @Published var isResetting = false @Published var needsFullDiskAccess = false @Published var syncErrorMessage: String? @Published var registrationCode = "" @Published var isRegistering = false @Published var authError: String? private let apiClient = APIClient.shared private let syncManager = SyncManager.shared private var cancellables = Set() private var pollingTimer: Timer? init() { isAuthenticated = apiClient.isAuthenticated syncManager.$isSyncing .receive(on: DispatchQueue.main) .sink { [weak self] syncing in self?.isSyncing = syncing self?.syncStatus = syncing ? .syncing : .idle } .store(in: &cancellables) syncManager.$lastSync .receive(on: DispatchQueue.main) .sink { [weak self] date in NSLog("MenuBarViewModel: received lastSync update: \(String(describing: date))") self?.updateLastSyncText(date) } .store(in: &cancellables) syncManager.$stats .receive(on: DispatchQueue.main) .sink { [weak self] stats in self?.messageCount = stats.messageCount self?.conversationCount = stats.conversationCount } .store(in: &cancellables) syncManager.$syncError .receive(on: DispatchQueue.main) .sink { [weak self] error in switch error { case .none: self?.needsFullDiskAccess = false self?.syncErrorMessage = nil self?.syncStatus = .idle case .fullDiskAccessRequired: self?.needsFullDiskAccess = true self?.syncErrorMessage = error.message self?.syncStatus = .error(error.message) default: self?.needsFullDiskAccess = false self?.syncErrorMessage = error.message self?.syncStatus = .error(error.message) } } .store(in: &cancellables) syncManager.$isResetting .receive(on: DispatchQueue.main) .sink { [weak self] resetting in self?.isResetting = resetting } .store(in: &cancellables) // Register device on startup if not authenticated if !isAuthenticated { registerDevice() } } func registerDevice() { isRegistering = true authError = nil Task { do { let (_, code) = try await apiClient.registerDevice() registrationCode = code startPollingForVerification() } catch { authError = error.localizedDescription } isRegistering = false } } private func startPollingForVerification() { // Poll every 3 seconds to check if device has been verified pollingTimer?.invalidate() pollingTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in Task { @MainActor in await self?.checkVerificationStatus() } } } private func checkVerificationStatus() async { do { let verified = try await apiClient.checkVerification() if verified { pollingTimer?.invalidate() pollingTimer = nil isAuthenticated = true registrationCode = "" syncManager.startSync() } } catch { // Silently continue polling } } func refreshCode() { registrationCode = "" registerDevice() } func triggerSync() { syncManager.syncNow() } func forceSync() { syncManager.resetAndResync() } func openFullDiskAccessSettings() { syncManager.openFullDiskAccessSettings() } func retryConnection() { syncManager.retryConnection() } /// Opens the Settings window, closing the popover first and activating the app func openSettings() { // Close the popover first if let appDelegate = NSApp.delegate as? AppDelegate { appDelegate.closePopover() } // Activate the app (required for menu bar apps to show settings) NSApp.activate(ignoringOtherApps: true) // Open settings window - macOS 14+ renamed to showSettingsWindow, older versions use showPreferencesWindow // Note: Using double-paren selector format to suppress warnings about private Objective-C selectors if #available(macOS 14, *) { NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) } else { NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) } } private func updateLastSyncText(_ date: Date?) { guard let date = date else { NSLog("MenuBarViewModel: updateLastSyncText - date is nil, setting to 'Never'") lastSyncText = "Never" return } let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .abbreviated let text = formatter.localizedString(for: date, relativeTo: Date()) NSLog("MenuBarViewModel: updateLastSyncText - date: \(date), text: \(text)") lastSyncText = text } }