feat: add fallback microphone for clamshell/desktop mode (#329)
* feat: add fallback microphone for clamshell/desktop mode * refactor: moved "Closed Laptop Microphone" setting to Debug * cleanup * run format * add check back --------- Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
25f4a564fd
commit
45eec57226
12 changed files with 279 additions and 24 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -145,7 +145,7 @@ jobs:
|
||||||
$outFile = "vulkan_sdk_arm.exe"
|
$outFile = "vulkan_sdk_arm.exe"
|
||||||
Write-Host "Downloading Vulkan SDK binaries from $url"
|
Write-Host "Downloading Vulkan SDK binaries from $url"
|
||||||
Invoke-WebRequest -Uri $url -OutFile $outFile
|
Invoke-WebRequest -Uri $url -OutFile $outFile
|
||||||
|
|
||||||
- name: Extract Vulkan SDK binaries (Windows ARM64)
|
- name: Extract Vulkan SDK binaries (Windows ARM64)
|
||||||
if: contains(inputs.platform, 'windows') && contains(inputs.target, 'aarch64')
|
if: contains(inputs.platform, 'windows') && contains(inputs.target, 'aarch64')
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
|
||||||
|
|
@ -159,3 +159,23 @@ pub fn play_test_sound(app: AppHandle, sound_type: String) {
|
||||||
};
|
};
|
||||||
audio_feedback::play_test_sound(&app, sound);
|
audio_feedback::play_test_sound(&app, sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_clamshell_microphone(app: AppHandle, device_name: String) -> Result<(), String> {
|
||||||
|
let mut settings = get_settings(&app);
|
||||||
|
settings.clamshell_microphone = if device_name == "default" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(device_name)
|
||||||
|
};
|
||||||
|
write_settings(&app, settings);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_clamshell_microphone(app: AppHandle) -> Result<String, String> {
|
||||||
|
let settings = get_settings(&app);
|
||||||
|
Ok(settings
|
||||||
|
.clamshell_microphone
|
||||||
|
.unwrap_or_else(|| "default".to_string()))
|
||||||
|
}
|
||||||
|
|
|
||||||
94
src-tauri/src/helpers/clamshell.rs
Normal file
94
src-tauri/src/helpers/clamshell.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Checks if the MacBook is in clamshell mode (lid closed with external display)
|
||||||
|
///
|
||||||
|
/// This queries the macOS IORegistry for the AppleClamshellState key.
|
||||||
|
/// Returns true if the lid is closed, false if open.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn is_clamshell() -> Result<bool, String> {
|
||||||
|
let output = Command::new("ioreg")
|
||||||
|
.args(["-r", "-k", "AppleClamshellState", "-d", "4"])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to execute ioreg: {}", e))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"ioreg command failed with status: {}",
|
||||||
|
output.status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
// Look for "AppleClamshellState" = Yes in the output
|
||||||
|
Ok(stdout.contains("\"AppleClamshellState\" = Yes"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the Mac has a built-in display (i.e., is a laptop)
|
||||||
|
///
|
||||||
|
/// This queries the macOS IORegistry for built-in displays.
|
||||||
|
/// Returns true if a built-in display is found (MacBook), false otherwise (Mac Mini, Mac Studio, etc.)
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn has_builtin_display() -> Result<bool, String> {
|
||||||
|
let output = Command::new("ioreg")
|
||||||
|
.args(["-l", "-w", "0", "-r", "-c", "IODisplayConnect"])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to execute ioreg: {}", e))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"ioreg command failed with status: {}",
|
||||||
|
output.status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
// Look for built-in display indicators
|
||||||
|
// Built-in displays typically have AppleDisplay or AppleBacklightDisplay
|
||||||
|
Ok(stdout.contains("AppleBacklightDisplay")
|
||||||
|
|| (stdout.contains("built-in") && stdout.contains("IODisplayConnect")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stub implementation for non-macOS platforms
|
||||||
|
/// Always returns false since clamshell mode is macOS-specific
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn is_clamshell() -> Result<bool, String> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stub implementation for non-macOS platforms
|
||||||
|
/// Always returns false since built-in display detection is macOS-specific
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn has_builtin_display() -> Result<bool, String> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn test_clamshell_check() {
|
||||||
|
// This will run on macOS and should not panic
|
||||||
|
let result = is_clamshell();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
println!("Clamshell state: {:?}", result.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn test_has_builtin_display() {
|
||||||
|
let result = has_builtin_display();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
if let Ok(has_builtin) = result {
|
||||||
|
println!("Has built-in display (is laptop): {}", has_builtin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src-tauri/src/helpers/mod.rs
Normal file
1
src-tauri/src/helpers/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod clamshell;
|
||||||
|
|
@ -3,6 +3,7 @@ mod audio_feedback;
|
||||||
pub mod audio_toolkit;
|
pub mod audio_toolkit;
|
||||||
mod clipboard;
|
mod clipboard;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod helpers;
|
||||||
mod llm_client;
|
mod llm_client;
|
||||||
mod managers;
|
mod managers;
|
||||||
mod overlay;
|
mod overlay;
|
||||||
|
|
@ -274,6 +275,10 @@ pub fn run() {
|
||||||
commands::audio::get_selected_output_device,
|
commands::audio::get_selected_output_device,
|
||||||
commands::audio::play_test_sound,
|
commands::audio::play_test_sound,
|
||||||
commands::audio::check_custom_sounds,
|
commands::audio::check_custom_sounds,
|
||||||
|
commands::audio::set_clamshell_microphone,
|
||||||
|
commands::audio::get_clamshell_microphone,
|
||||||
|
helpers::clamshell::is_clamshell,
|
||||||
|
helpers::clamshell::has_builtin_display,
|
||||||
commands::transcription::set_model_unload_timeout,
|
commands::transcription::set_model_unload_timeout,
|
||||||
commands::transcription::get_model_load_status,
|
commands::transcription::get_model_load_status,
|
||||||
commands::transcription::unload_model_manually,
|
commands::transcription::unload_model_manually,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::audio_toolkit::{list_input_devices, vad::SmoothedVad, AudioRecorder, SileroVad};
|
use crate::audio_toolkit::{list_input_devices, vad::SmoothedVad, AudioRecorder, SileroVad};
|
||||||
use crate::settings::get_settings;
|
use crate::helpers::clamshell;
|
||||||
|
use crate::settings::{get_settings, AppSettings};
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
@ -180,6 +181,35 @@ impl AudioRecordingManager {
|
||||||
Ok(manager)
|
Ok(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- helper methods --------------------------------------------- */
|
||||||
|
|
||||||
|
fn get_effective_microphone_device(&self, settings: &AppSettings) -> Option<cpal::Device> {
|
||||||
|
// Check if we're in clamshell mode and have a clamshell microphone configured
|
||||||
|
let use_clamshell_mic = if let Ok(is_clamshell) = clamshell::is_clamshell() {
|
||||||
|
is_clamshell && settings.clamshell_microphone.is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let device_name = if use_clamshell_mic {
|
||||||
|
settings.clamshell_microphone.as_ref().unwrap()
|
||||||
|
} else {
|
||||||
|
settings.selected_microphone.as_ref()?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the device by name
|
||||||
|
match list_input_devices() {
|
||||||
|
Ok(devices) => devices
|
||||||
|
.into_iter()
|
||||||
|
.find(|d| d.name == *device_name)
|
||||||
|
.map(|d| d.device),
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to list devices, using default: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- microphone life-cycle -------------------------------------- */
|
/* ---------- microphone life-cycle -------------------------------------- */
|
||||||
|
|
||||||
/// Applies mute if mute_while_recording is enabled and stream is open
|
/// Applies mute if mute_while_recording is enabled and stream is open
|
||||||
|
|
@ -234,23 +264,9 @@ impl AudioRecordingManager {
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the selected device from settings
|
// Get the selected device from settings, considering clamshell mode
|
||||||
let settings = get_settings(&self.app_handle);
|
let settings = get_settings(&self.app_handle);
|
||||||
let selected_device = if let Some(device_name) = settings.selected_microphone {
|
let selected_device = self.get_effective_microphone_device(&settings);
|
||||||
// Find the device by name
|
|
||||||
match list_input_devices() {
|
|
||||||
Ok(devices) => devices
|
|
||||||
.into_iter()
|
|
||||||
.find(|d| d.name == device_name)
|
|
||||||
.map(|d| d.device),
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to list devices, using default: {}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(rec) = recorder_opt.as_mut() {
|
if let Some(rec) = recorder_opt.as_mut() {
|
||||||
rec.open(selected_device)
|
rec.open(selected_device)
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,8 @@ pub struct AppSettings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub selected_microphone: Option<String>,
|
pub selected_microphone: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub clamshell_microphone: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub selected_output_device: Option<String>,
|
pub selected_output_device: Option<String>,
|
||||||
#[serde(default = "default_translate_to_english")]
|
#[serde(default = "default_translate_to_english")]
|
||||||
pub translate_to_english: bool,
|
pub translate_to_english: bool,
|
||||||
|
|
@ -350,6 +352,7 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
selected_model: "".to_string(),
|
selected_model: "".to_string(),
|
||||||
always_on_microphone: false,
|
always_on_microphone: false,
|
||||||
selected_microphone: None,
|
selected_microphone: None,
|
||||||
|
clamshell_microphone: None,
|
||||||
selected_output_device: None,
|
selected_output_device: None,
|
||||||
translate_to_english: false,
|
translate_to_english: false,
|
||||||
selected_language: "auto".to_string(),
|
selected_language: "auto".to_string(),
|
||||||
|
|
|
||||||
98
src/components/settings/ClamshellMicrophoneSelector.tsx
Normal file
98
src/components/settings/ClamshellMicrophoneSelector.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { Dropdown } from "../ui/Dropdown";
|
||||||
|
import { SettingContainer } from "../ui/SettingContainer";
|
||||||
|
import { ResetButton } from "../ui/ResetButton";
|
||||||
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
|
||||||
|
interface ClamshellMicrophoneSelectorProps {
|
||||||
|
descriptionMode?: "inline" | "tooltip";
|
||||||
|
grouped?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClamshellMicrophoneSelector: React.FC<ClamshellMicrophoneSelectorProps> =
|
||||||
|
React.memo(({ descriptionMode = "tooltip", grouped = false }) => {
|
||||||
|
const {
|
||||||
|
getSetting,
|
||||||
|
updateSetting,
|
||||||
|
resetSetting,
|
||||||
|
isUpdating,
|
||||||
|
isLoading,
|
||||||
|
audioDevices,
|
||||||
|
refreshAudioDevices,
|
||||||
|
} = useSettings();
|
||||||
|
|
||||||
|
const [hasBuiltinDisplay, setHasBuiltinDisplay] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if the device has a built-in display (i.e., is a laptop)
|
||||||
|
const checkBuiltinDisplay = async () => {
|
||||||
|
try {
|
||||||
|
const result = await invoke<boolean>("has_builtin_display");
|
||||||
|
setHasBuiltinDisplay(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check for built-in display:", error);
|
||||||
|
setHasBuiltinDisplay(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkBuiltinDisplay();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Only render on devices with built-in displays (laptops)
|
||||||
|
if (!hasBuiltinDisplay) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedClamshellMicrophone =
|
||||||
|
getSetting("clamshell_microphone") === "default"
|
||||||
|
? "Default"
|
||||||
|
: getSetting("clamshell_microphone") || "Default";
|
||||||
|
|
||||||
|
const handleClamshellMicrophoneSelect = async (deviceName: string) => {
|
||||||
|
await updateSetting("clamshell_microphone", deviceName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
await resetSetting("clamshell_microphone");
|
||||||
|
};
|
||||||
|
|
||||||
|
const microphoneOptions = audioDevices.map((device) => ({
|
||||||
|
value: device.name,
|
||||||
|
label: device.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingContainer
|
||||||
|
title="Clamshell Microphone"
|
||||||
|
description="Choose a different microphone to use when your laptop lid is closed"
|
||||||
|
descriptionMode={descriptionMode}
|
||||||
|
grouped={grouped}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Dropdown
|
||||||
|
options={microphoneOptions}
|
||||||
|
selectedValue={selectedClamshellMicrophone}
|
||||||
|
onSelect={handleClamshellMicrophoneSelect}
|
||||||
|
placeholder={
|
||||||
|
isLoading || audioDevices.length === 0
|
||||||
|
? "Loading..."
|
||||||
|
: "Select microphone..."
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
isUpdating("clamshell_microphone") ||
|
||||||
|
isLoading ||
|
||||||
|
audioDevices.length === 0
|
||||||
|
}
|
||||||
|
onRefresh={refreshAudioDevices}
|
||||||
|
/>
|
||||||
|
<ResetButton
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={isUpdating("clamshell_microphone") || isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingContainer>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClamshellMicrophoneSelector.displayName = "ClamshellMicrophoneSelector";
|
||||||
|
|
@ -6,6 +6,7 @@ import { AlwaysOnMicrophone } from "../AlwaysOnMicrophone";
|
||||||
import { SoundPicker } from "../SoundPicker";
|
import { SoundPicker } from "../SoundPicker";
|
||||||
import { PostProcessingToggle } from "../PostProcessingToggle";
|
import { PostProcessingToggle } from "../PostProcessingToggle";
|
||||||
import { MuteWhileRecording } from "../MuteWhileRecording";
|
import { MuteWhileRecording } from "../MuteWhileRecording";
|
||||||
|
import { ClamshellMicrophoneSelector } from "../ClamshellMicrophoneSelector";
|
||||||
|
|
||||||
export const DebugSettings: React.FC = () => {
|
export const DebugSettings: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -18,6 +19,7 @@ export const DebugSettings: React.FC = () => {
|
||||||
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
|
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
|
||||||
<HistoryLimit descriptionMode="tooltip" grouped={true} />
|
<HistoryLimit descriptionMode="tooltip" grouped={true} />
|
||||||
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
|
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
|
||||||
|
<ClamshellMicrophoneSelector descriptionMode="tooltip" grouped={true} />
|
||||||
<PostProcessingToggle descriptionMode="tooltip" grouped={true} />
|
<PostProcessingToggle descriptionMode="tooltip" grouped={true} />
|
||||||
<MuteWhileRecording descriptionMode="tooltip" grouped={true} />
|
<MuteWhileRecording descriptionMode="tooltip" grouped={true} />
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export { PostProcessingSettings } from "./post-processing/PostProcessingSettings
|
||||||
|
|
||||||
// Individual setting components
|
// Individual setting components
|
||||||
export { MicrophoneSelector } from "./MicrophoneSelector";
|
export { MicrophoneSelector } from "./MicrophoneSelector";
|
||||||
|
export { ClamshellMicrophoneSelector } from "./ClamshellMicrophoneSelector";
|
||||||
export { OutputDeviceSelector } from "./OutputDeviceSelector";
|
export { OutputDeviceSelector } from "./OutputDeviceSelector";
|
||||||
export { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
export { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
||||||
export { PushToTalk } from "./PushToTalk";
|
export { PushToTalk } from "./PushToTalk";
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ export const SettingsSchema = z.object({
|
||||||
selected_model: z.string(),
|
selected_model: z.string(),
|
||||||
always_on_microphone: z.boolean(),
|
always_on_microphone: z.boolean(),
|
||||||
selected_microphone: z.string().nullable().optional(),
|
selected_microphone: z.string().nullable().optional(),
|
||||||
|
clamshell_microphone: z.string().nullable().optional(),
|
||||||
selected_output_device: z.string().nullable().optional(),
|
selected_output_device: z.string().nullable().optional(),
|
||||||
translate_to_english: z.boolean(),
|
translate_to_english: z.boolean(),
|
||||||
selected_language: z.string(),
|
selected_language: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ const DEFAULT_SETTINGS: Partial<Settings> = {
|
||||||
autostart_enabled: false,
|
autostart_enabled: false,
|
||||||
push_to_talk: false,
|
push_to_talk: false,
|
||||||
selected_microphone: "Default",
|
selected_microphone: "Default",
|
||||||
|
clamshell_microphone: "Default",
|
||||||
selected_output_device: "Default",
|
selected_output_device: "Default",
|
||||||
translate_to_english: false,
|
translate_to_english: false,
|
||||||
selected_language: "auto",
|
selected_language: "auto",
|
||||||
|
|
@ -102,6 +103,10 @@ const settingUpdaters: {
|
||||||
invoke("set_selected_microphone", {
|
invoke("set_selected_microphone", {
|
||||||
deviceName: value === "Default" ? "default" : value,
|
deviceName: value === "Default" ? "default" : value,
|
||||||
}),
|
}),
|
||||||
|
clamshell_microphone: (value) =>
|
||||||
|
invoke("set_clamshell_microphone", {
|
||||||
|
deviceName: value === "Default" ? "default" : value,
|
||||||
|
}),
|
||||||
selected_output_device: (value) =>
|
selected_output_device: (value) =>
|
||||||
invoke("set_selected_output_device", {
|
invoke("set_selected_output_device", {
|
||||||
deviceName: value === "Default" ? "default" : value,
|
deviceName: value === "Default" ? "default" : value,
|
||||||
|
|
@ -166,12 +171,17 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
const settings = (await store.get("settings")) as Settings;
|
const settings = (await store.get("settings")) as Settings;
|
||||||
|
|
||||||
// Load additional settings that come from invoke calls
|
// Load additional settings that come from invoke calls
|
||||||
const [microphoneMode, selectedMicrophone, selectedOutputDevice] =
|
const [
|
||||||
await Promise.allSettled([
|
microphoneMode,
|
||||||
invoke("get_microphone_mode"),
|
selectedMicrophone,
|
||||||
invoke("get_selected_microphone"),
|
clamshellMicrophone,
|
||||||
invoke("get_selected_output_device"),
|
selectedOutputDevice,
|
||||||
]);
|
] = await Promise.allSettled([
|
||||||
|
invoke("get_microphone_mode"),
|
||||||
|
invoke("get_selected_microphone"),
|
||||||
|
invoke("get_clamshell_microphone"),
|
||||||
|
invoke("get_selected_output_device"),
|
||||||
|
]);
|
||||||
|
|
||||||
// Merge all settings
|
// Merge all settings
|
||||||
const mergedSettings: Settings = {
|
const mergedSettings: Settings = {
|
||||||
|
|
@ -184,6 +194,10 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
selectedMicrophone.status === "fulfilled"
|
selectedMicrophone.status === "fulfilled"
|
||||||
? (selectedMicrophone.value as string)
|
? (selectedMicrophone.value as string)
|
||||||
: "Default",
|
: "Default",
|
||||||
|
clamshell_microphone:
|
||||||
|
clamshellMicrophone.status === "fulfilled"
|
||||||
|
? (clamshellMicrophone.value as string)
|
||||||
|
: "Default",
|
||||||
selected_output_device:
|
selected_output_device:
|
||||||
selectedOutputDevice.status === "fulfilled"
|
selectedOutputDevice.status === "fulfilled"
|
||||||
? (selectedOutputDevice.value as string)
|
? (selectedOutputDevice.value as string)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue