import SwiftUI struct MenuBarView: View { @StateObject private var viewModel = MenuBarViewModel() var body: some View { VStack(spacing: 0) { // Header HStack { Image(systemName: "bubble.left.and.bubble.right.fill") .foregroundColor(.accentColor) .accessibilityHidden(true) Text("Conversation Assistant") .font(.headline) Spacer() } .padding() .background(Color(.windowBackgroundColor)) Divider() if viewModel.isAuthenticated { authenticatedView } else { authenticationView } Divider() // Footer HStack { // Open Settings - use SettingsLink on macOS 14+, fallback to NSApp.sendAction if #available(macOS 14, *) { SettingsLink { Image(systemName: "gear") .accessibilityLabel("Settings") } .buttonStyle(.borderless) } else { Button { // Close popover first if let appDelegate = NSApp.delegate as? AppDelegate { appDelegate.closePopover() } NSApp.activate(ignoringOtherApps: true) NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) } label: { Image(systemName: "gear") .accessibilityLabel("Settings") } .buttonStyle(.borderless) } Spacer() Text(AppVersion.displayVersion) .font(.caption2) .foregroundStyle(.tertiary) Spacer() Button("Quit") { NSApplication.shared.terminate(nil) } .buttonStyle(.borderless) } .padding() } .frame(width: 320) } private var authenticatedView: some View { VStack(spacing: 12) { // Sync Status HStack { Circle() .fill(viewModel.syncStatus.color) .frame(width: 8, height: 8) Text(viewModel.syncStatus.text) .font(.subheadline) Spacer() } .padding(.horizontal) // Full Disk Access Warning if viewModel.needsFullDiskAccess { VStack(spacing: 8) { Image(systemName: "exclamationmark.shield.fill") .font(.system(size: 32)) .foregroundColor(.orange) .accessibilityLabel("Warning") Text("Full Disk Access Required") .font(.headline) Text("Grant access to read iMessage database") .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) HStack(spacing: 12) { Button("Open Settings") { viewModel.openFullDiskAccessSettings() } .buttonStyle(.borderedProminent) Button("Retry") { viewModel.retryConnection() } .buttonStyle(.bordered) } } .padding() .background(Color.orange.opacity(0.1)) .cornerRadius(8) .padding(.horizontal) } else { // Stats VStack(alignment: .leading, spacing: 8) { StatRow(label: "Messages Synced", value: "\(viewModel.messageCount)") StatRow(label: "Conversations", value: "\(viewModel.conversationCount)") StatRow(label: "Last Sync", value: viewModel.lastSyncText) } .padding(.horizontal) } Spacer() // Sync Buttons HStack(spacing: 8) { Button(action: viewModel.triggerSync) { HStack { if viewModel.isSyncing && !viewModel.isResetting { ProgressView() .scaleEffect(0.7) } Text(viewModel.isSyncing && !viewModel.isResetting ? "Syncing..." : "Sync Now") } .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .disabled(viewModel.isSyncing || viewModel.isResetting || viewModel.needsFullDiskAccess) Button(action: viewModel.forceSync) { HStack { if viewModel.isResetting { ProgressView() .scaleEffect(0.7) } Text(viewModel.isResetting ? "Resetting..." : "Force Sync") } .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .disabled(viewModel.isSyncing || viewModel.isResetting || viewModel.needsFullDiskAccess) } .padding() } .padding(.vertical) } private var authenticationView: some View { VStack(spacing: 16) { Image(systemName: "link.badge.plus") .font(.system(size: 48)) .foregroundColor(.accentColor) .accessibilityLabel("Connect device") Text("Connect This Device") .font(.headline) if viewModel.isRegistering { ProgressView("Registering...") } else if !viewModel.registrationCode.isEmpty { Text("Enter this code in your admin panel:") .font(.caption) .foregroundColor(.secondary) Text(viewModel.registrationCode) .font(.system(size: 32, weight: .bold, design: .monospaced)) .foregroundColor(.accentColor) .padding(.vertical, 8) .onTapGesture { NSPasteboard.general.clearContents() NSPasteboard.general.setString(viewModel.registrationCode, forType: .string) } Text("Tap to copy • Waiting for verification...") .font(.caption2) .foregroundColor(.secondary) Button("Get New Code") { viewModel.refreshCode() } .buttonStyle(.link) } else { Text("Unable to connect to server") .font(.caption) .foregroundColor(.secondary) Button("Retry") { viewModel.registerDevice() } .buttonStyle(.borderedProminent) } if let error = viewModel.authError { Text(error) .font(.caption) .foregroundColor(.red) } } .padding() } } struct StatRow: View { let label: String let value: String var body: some View { HStack { Text(label) .foregroundColor(.secondary) Spacer() Text(value) .fontWeight(.medium) } } } enum SyncStatus { case idle case syncing case error(String) var color: Color { switch self { case .idle: return .green case .syncing: return .orange case .error: return .red } } var text: String { switch self { case .idle: return "Connected" case .syncing: return "Syncing..." case .error(let message): return message } } } #Preview { MenuBarView() .frame(height: 400) }