-
-
Notifications
You must be signed in to change notification settings - Fork 316
Add opt-in 'Lock input device' setting #489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,6 +53,10 @@ struct SettingsView: View { | |
| @State private var cachedDefaultInputName: String = "" | ||
| @State private var cachedDefaultOutputName: String = "" | ||
|
|
||
| // When enabled, FluidVoice always captures from the selected input device and ignores | ||
| // macOS system-default input changes / Bluetooth auto-switching. | ||
| @State private var lockInputDevice: Bool = SettingsStore.shared.lockInputDevice | ||
|
|
||
| // Analytics consent UI state (default ON; user can opt-out) | ||
| @State private var shareAnonymousAnalytics: Bool = SettingsStore.shared.shareAnonymousAnalytics | ||
| @State private var showAnalyticsPrivacy: Bool = false | ||
|
|
@@ -1083,8 +1087,10 @@ struct SettingsView: View { | |
| } | ||
|
|
||
| SettingsStore.shared.preferredInputDeviceUID = newUID | ||
| // Only change system default if sync is enabled | ||
| if SettingsStore.shared.syncAudioDevicesWithSystem { | ||
| // Only change the macOS system default if syncing is enabled and the | ||
| // input device is not locked. When locked we keep the system default | ||
| // untouched so FluidVoice can use this device independently. | ||
| if SettingsStore.shared.syncAudioDevicesWithSystem, !self.lockInputDevice { | ||
| _ = AudioDevice.setDefaultInputDevice(uid: newUID) | ||
| } | ||
| } | ||
|
|
@@ -1095,6 +1101,15 @@ struct SettingsView: View { | |
|
|
||
| // If selection is empty or not found in new list, select first available | ||
| if !newDevices.isEmpty { | ||
| // When locked, keep the preferred device selected as long as it's available. | ||
| if self.lockInputDevice, | ||
| let prefUID = SettingsStore.shared.preferredInputDeviceUID, | ||
| newDevices.contains(where: { $0.uid == prefUID }) | ||
| { | ||
| self.selectedInputUID = prefUID | ||
| return | ||
| } | ||
|
|
||
| let currentValid = newDevices.contains { $0.uid == self.selectedInputUID } | ||
| if !currentValid { | ||
| if let defaultUID = AudioDevice.getDefaultInputDevice()?.uid, | ||
|
|
@@ -1109,6 +1124,36 @@ struct SettingsView: View { | |
| } | ||
| } | ||
|
|
||
| // Lock input device: always capture from the selected mic regardless of | ||
| // the macOS system default or Bluetooth auto-switching. | ||
| HStack(alignment: .center, spacing: 12) { | ||
| VStack(alignment: .leading, spacing: 2) { | ||
| Text("Always use this input device when available") | ||
| .font(self.theme.typography.bodyStrong) | ||
| .foregroundStyle(self.settingsTitleText) | ||
| Text("Ignores macOS input changes. Falls back to the system default if unavailable.") | ||
| .font(self.theme.typography.bodySmall) | ||
| .foregroundStyle(self.settingsSecondaryText) | ||
| .fixedSize(horizontal: false, vertical: true) | ||
| } | ||
|
|
||
| Spacer(minLength: 0) | ||
|
|
||
| Toggle("", isOn: self.$lockInputDevice) | ||
| .labelsHidden() | ||
| .toggleStyle(.switch) | ||
| .tint(self.theme.palette.accent) | ||
| } | ||
| .disabled(self.asr.isRunning) | ||
| .padding(.bottom, 6) | ||
| .onChange(of: self.lockInputDevice) { _, locked in | ||
| SettingsStore.shared.lockInputDevice = locked | ||
| if locked, !self.selectedInputUID.isEmpty { | ||
| // Pin the currently selected device as the preferred input. | ||
| SettingsStore.shared.preferredInputDeviceUID = self.selectedInputUID | ||
| } | ||
| } | ||
|
|
||
| HStack { | ||
| Text("Output Device") | ||
| .font(self.theme.typography.bodyStrong) | ||
|
|
@@ -1556,16 +1601,27 @@ struct SettingsView: View { | |
|
|
||
| self.refreshDevices() | ||
|
|
||
| // Keep the lock toggle in sync with persisted state. | ||
| self.lockInputDevice = SettingsStore.shared.lockInputDevice | ||
|
|
||
| // Sync input device selection after refresh | ||
| if !self.inputDevices.isEmpty { | ||
| let inputValid = self.inputDevices.contains { $0.uid == self.selectedInputUID } | ||
| if !inputValid || self.selectedInputUID.isEmpty { | ||
| if let defaultUID = AudioDevice.getDefaultInputDevice()?.uid, | ||
| self.inputDevices.contains(where: { $0.uid == defaultUID }) | ||
| { | ||
| self.selectedInputUID = defaultUID | ||
| } else { | ||
| self.selectedInputUID = self.inputDevices.first?.uid ?? "" | ||
| // When locked, restore the pinned preferred device if it's currently available. | ||
| if self.lockInputDevice, | ||
| let prefUID = SettingsStore.shared.preferredInputDeviceUID, | ||
| self.inputDevices.contains(where: { $0.uid == prefUID }) | ||
| { | ||
| self.selectedInputUID = prefUID | ||
| } else { | ||
| let inputValid = self.inputDevices.contains { $0.uid == self.selectedInputUID } | ||
| if !inputValid || self.selectedInputUID.isEmpty { | ||
| if let defaultUID = AudioDevice.getDefaultInputDevice()?.uid, | ||
| self.inputDevices.contains(where: { $0.uid == defaultUID }) | ||
| { | ||
| self.selectedInputUID = defaultUID | ||
| } else { | ||
| self.selectedInputUID = self.inputDevices.first?.uid ?? "" | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1600,6 +1656,13 @@ struct SettingsView: View { | |
| .onChange(of: self.visualizerNoiseThreshold) { _, newValue in | ||
| SettingsStore.shared.visualizerNoiseThreshold = newValue | ||
| } | ||
| // Keep the "(System Default)" markers accurate even when only the system default changes | ||
| // (not the device list) — e.g. while the input device is locked and the picker selection | ||
| // no longer tracks the macOS default. Safe outside view-body layout, like the other handlers. | ||
| .onReceive(self.appServices.audioObserver.$changeTick) { _ in | ||
| self.cachedDefaultInputName = AudioDevice.getDefaultInputDevice()?.name ?? "" | ||
| self.cachedDefaultOutputName = AudioDevice.getDefaultOutputDevice()?.name ?? "" | ||
|
Comment on lines
+1662
to
+1664
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| private func refreshRollbackState() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This locked-mode guard only returns while the preferred device is present; when the locked mic disappears with Settings open, the handler falls through to the existing fallback that assigns
selectedInputUIDto the system default. That programmatic selection change still runs the pickeronChangeabove, which writes the fallback UID toSettingsStore.shared.preferredInputDeviceUID, so the original pinned microphone is forgotten and reconnecting it will not restore the lock target.Useful? React with 👍 / 👎.