From 33d4df69c4bc54333000b9a43902daa6ea52e34c Mon Sep 17 00:00:00 2001 From: Beingpax Date: Sun, 11 May 2025 10:05:55 +0545 Subject: [PATCH] Integrate device selection in onboarding --- .../OnboardingPermissionsView.swift | 123 ++++++++++++++++-- 1 file changed, 111 insertions(+), 12 deletions(-) diff --git a/VoiceInk/Views/Onboarding/OnboardingPermissionsView.swift b/VoiceInk/Views/Onboarding/OnboardingPermissionsView.swift index f9f2251..6e5e87c 100644 --- a/VoiceInk/Views/Onboarding/OnboardingPermissionsView.swift +++ b/VoiceInk/Views/Onboarding/OnboardingPermissionsView.swift @@ -12,6 +12,7 @@ struct OnboardingPermission: Identifiable { enum PermissionType { case microphone + case audioDeviceSelection case accessibility case screenRecording case keyboardShortcut @@ -19,6 +20,7 @@ struct OnboardingPermission: Identifiable { var systemName: String { switch self { case .microphone: return "mic" + case .audioDeviceSelection: return "headphones" case .accessibility: return "accessibility" case .screenRecording: return "rectangle.inset.filled.and.person.filled" case .keyboardShortcut: return "keyboard" @@ -30,8 +32,9 @@ struct OnboardingPermission: Identifiable { struct OnboardingPermissionsView: View { @Binding var hasCompletedOnboarding: Bool @EnvironmentObject private var hotkeyManager: HotkeyManager + @ObservedObject private var audioDeviceManager = AudioDeviceManager.shared @State private var currentPermissionIndex = 0 - @State private var permissionStates: [Bool] = [false, false, false, false] + @State private var permissionStates: [Bool] = [false, false, false, false, false] @State private var showAnimation = false @State private var scale: CGFloat = 0.8 @State private var opacity: CGFloat = 0 @@ -44,6 +47,12 @@ struct OnboardingPermissionsView: View { icon: "waveform", type: .microphone ), + OnboardingPermission( + title: "Microphone Selection", + description: "Select the audio input device you want to use with VoiceInk.", + icon: "headphones", + type: .audioDeviceSelection + ), OnboardingPermission( title: "Accessibility Access", description: "Allow VoiceInk to help you type anywhere in your Mac.", @@ -122,6 +131,58 @@ struct OnboardingPermissionsView: View { .scaleEffect(scale) .opacity(opacity) + // Audio device selection (only shown for audio device selection step) + if permissions[currentPermissionIndex].type == .audioDeviceSelection { + VStack(spacing: 20) { + if audioDeviceManager.availableDevices.isEmpty { + VStack(spacing: 12) { + Image(systemName: "mic.slash.circle.fill") + .font(.system(size: 36)) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(.secondary) + + Text("No microphones found") + .font(.subheadline) + .foregroundStyle(.secondary) + } + .padding() + } else { + Picker("Select Microphone", selection: Binding( + get: { + audioDeviceManager.selectedDeviceID ?? + (audioDeviceManager.availableDevices.first?.id ?? 0) + }, + set: { newValue in + audioDeviceManager.selectDevice(id: newValue) + audioDeviceManager.selectInputMode(.custom) + withAnimation { + permissionStates[currentPermissionIndex] = true + showAnimation = true + } + } + )) { + ForEach(audioDeviceManager.availableDevices, id: \.id) { device in + Text(device.name).tag(device.id) + } + } + .pickerStyle(.menu) + .labelsHidden() + .frame(maxWidth: 300) + .padding(8) + .background(Color.white.opacity(0.1)) + .cornerRadius(8) + } + + Text("For best results, using your Mac's built-in microphone is recommended.") + .font(.caption) + .foregroundColor(.white.opacity(0.7)) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + .scaleEffect(scale) + .opacity(opacity) + } + // Keyboard shortcut recorder (only shown for keyboard shortcut step) if permissions[currentPermissionIndex].type == .keyboardShortcut { VStack(spacing: 16) { @@ -134,12 +195,16 @@ struct OnboardingPermissionsView: View { VStack(spacing: 16) { KeyboardShortcuts.Recorder("Set Shortcut:", name: .toggleMiniRecorder) { newShortcut in - if newShortcut != nil { - permissionStates[currentPermissionIndex] = true - } else { - permissionStates[currentPermissionIndex] = false + withAnimation { + if newShortcut != nil { + permissionStates[currentPermissionIndex] = true + showAnimation = true + } else { + permissionStates[currentPermissionIndex] = false + showAnimation = false + } + hotkeyManager.updateShortcutStatus() } - hotkeyManager.updateShortcutStatus() } .controlSize(.large) @@ -167,7 +232,9 @@ struct OnboardingPermissionsView: View { } .buttonStyle(ScaleButtonStyle()) - if !permissionStates[currentPermissionIndex] && permissions[currentPermissionIndex].type != .keyboardShortcut { + if !permissionStates[currentPermissionIndex] && + permissions[currentPermissionIndex].type != .keyboardShortcut && + permissions[currentPermissionIndex].type != .audioDeviceSelection { SkipButton(text: "Skip for now") { moveToNext() } @@ -187,6 +254,8 @@ struct OnboardingPermissionsView: View { .onAppear { checkExistingPermissions() animateIn() + // Ensure audio devices are loaded + audioDeviceManager.loadAvailableDevices() } } @@ -207,14 +276,17 @@ struct OnboardingPermissionsView: View { // Check microphone permission permissionStates[0] = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized + // Check if device is selected + permissionStates[1] = audioDeviceManager.selectedDeviceID != nil + // Check accessibility permission - permissionStates[1] = AXIsProcessTrusted() + permissionStates[2] = AXIsProcessTrusted() // Check screen recording permission - permissionStates[2] = CGPreflightScreenCaptureAccess() + permissionStates[3] = CGPreflightScreenCaptureAccess() // Check keyboard shortcut - permissionStates[3] = hotkeyManager.isShortcutConfigured + permissionStates[4] = hotkeyManager.isShortcutConfigured } private func requestPermission() { @@ -236,6 +308,29 @@ struct OnboardingPermissionsView: View { } } + case .audioDeviceSelection: + // If no device is selected, select the fallback device + if audioDeviceManager.availableDevices.isEmpty { + withAnimation { + permissionStates[currentPermissionIndex] = true + showAnimation = true + } + moveToNext() + return + } + + if audioDeviceManager.selectedDeviceID == nil { + if let firstDevice = audioDeviceManager.availableDevices.first { + audioDeviceManager.selectDevice(id: firstDevice.id) + audioDeviceManager.selectInputMode(.custom) + withAnimation { + permissionStates[currentPermissionIndex] = true + showAnimation = true + } + } + } + moveToNext() + case .accessibility: let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] AXIsProcessTrustedWithOptions(options) @@ -287,9 +382,13 @@ struct OnboardingPermissionsView: View { } private func getButtonTitle() -> String { - if permissions[currentPermissionIndex].type == .keyboardShortcut { + switch permissions[currentPermissionIndex].type { + case .keyboardShortcut: return permissionStates[currentPermissionIndex] ? "Continue" : "Set Shortcut" + case .audioDeviceSelection: + return "Continue" + default: + return permissionStates[currentPermissionIndex] ? "Continue" : "Enable Access" } - return permissionStates[currentPermissionIndex] ? "Continue" : "Enable Access" } }