add support for changing paste method (#187)
* add support for changing paste method * rename/refactor of methods
This commit is contained in:
parent
03289f84d7
commit
1432bac30f
8 changed files with 167 additions and 23 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::settings::{get_settings, PasteMethod};
|
||||||
use enigo::Enigo;
|
use enigo::Enigo;
|
||||||
use enigo::Key;
|
use enigo::Key;
|
||||||
use enigo::Keyboard;
|
use enigo::Keyboard;
|
||||||
|
|
@ -38,14 +39,29 @@ fn send_paste() -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
|
/// Pastes text directly using the enigo text method.
|
||||||
|
/// This tries to use system input methods if possible, otherwise simulates keystrokes one by one.
|
||||||
|
fn paste_via_direct_input(text: &str) -> Result<(), String> {
|
||||||
|
let mut enigo = Enigo::new(&Settings::default())
|
||||||
|
.map_err(|e| format!("Failed to initialize Enigo: {}", e))?;
|
||||||
|
|
||||||
|
enigo
|
||||||
|
.text(text)
|
||||||
|
.map_err(|e| format!("Failed to send text directly: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pastes text using the clipboard method (Ctrl+V/Cmd+V).
|
||||||
|
/// Saves the current clipboard, writes the text, sends paste command, then restores the clipboard.
|
||||||
|
fn paste_via_clipboard(text: &str, app_handle: &AppHandle) -> Result<(), String> {
|
||||||
let clipboard = app_handle.clipboard();
|
let clipboard = app_handle.clipboard();
|
||||||
|
|
||||||
// get the current clipboard content
|
// get the current clipboard content
|
||||||
let clipboard_content = clipboard.read_text().unwrap_or_default();
|
let clipboard_content = clipboard.read_text().unwrap_or_default();
|
||||||
|
|
||||||
clipboard
|
clipboard
|
||||||
.write_text(&text)
|
.write_text(text)
|
||||||
.map_err(|e| format!("Failed to write to clipboard: {}", e))?;
|
.map_err(|e| format!("Failed to write to clipboard: {}", e))?;
|
||||||
|
|
||||||
// small delay to ensure the clipboard content has been written to
|
// small delay to ensure the clipboard content has been written to
|
||||||
|
|
@ -62,3 +78,15 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
|
||||||
|
let settings = get_settings(&app_handle);
|
||||||
|
let paste_method = settings.paste_method;
|
||||||
|
|
||||||
|
println!("Using paste method: {:?}", paste_method);
|
||||||
|
|
||||||
|
match paste_method {
|
||||||
|
PasteMethod::CtrlV => paste_via_clipboard(&text, &app_handle),
|
||||||
|
PasteMethod::Direct => paste_via_direct_input(&text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ pub fn run() {
|
||||||
shortcut::change_overlay_position_setting,
|
shortcut::change_overlay_position_setting,
|
||||||
shortcut::change_debug_mode_setting,
|
shortcut::change_debug_mode_setting,
|
||||||
shortcut::change_word_correction_threshold_setting,
|
shortcut::change_word_correction_threshold_setting,
|
||||||
|
shortcut::change_paste_method_setting,
|
||||||
shortcut::update_custom_words,
|
shortcut::update_custom_words,
|
||||||
shortcut::suspend_binding,
|
shortcut::suspend_binding,
|
||||||
shortcut::resume_binding,
|
shortcut::resume_binding,
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,29 @@ pub enum ModelUnloadTimeout {
|
||||||
Sec5, // Debug mode only
|
Sec5, // Debug mode only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum PasteMethod {
|
||||||
|
CtrlV,
|
||||||
|
Direct,
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ModelUnloadTimeout {
|
impl Default for ModelUnloadTimeout {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ModelUnloadTimeout::Never
|
ModelUnloadTimeout::Never
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for PasteMethod {
|
||||||
|
fn default() -> Self {
|
||||||
|
// Default to CtrlV for macOS and Windows, Direct for Linux
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
return PasteMethod::Direct;
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
return PasteMethod::CtrlV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ModelUnloadTimeout {
|
impl ModelUnloadTimeout {
|
||||||
pub fn to_minutes(self) -> Option<u64> {
|
pub fn to_minutes(self) -> Option<u64> {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -93,6 +110,8 @@ pub struct AppSettings {
|
||||||
pub model_unload_timeout: ModelUnloadTimeout,
|
pub model_unload_timeout: ModelUnloadTimeout,
|
||||||
#[serde(default = "default_word_correction_threshold")]
|
#[serde(default = "default_word_correction_threshold")]
|
||||||
pub word_correction_threshold: f64,
|
pub word_correction_threshold: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub paste_method: PasteMethod,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model() -> String {
|
fn default_model() -> String {
|
||||||
|
|
@ -167,6 +186,7 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
custom_words: Vec::new(),
|
custom_words: Vec::new(),
|
||||||
model_unload_timeout: ModelUnloadTimeout::Never,
|
model_unload_timeout: ModelUnloadTimeout::Never,
|
||||||
word_correction_threshold: default_word_correction_threshold(),
|
word_correction_threshold: default_word_correction_threshold(),
|
||||||
|
paste_method: PasteMethod::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
|
||||||
|
|
||||||
use crate::actions::ACTION_MAP;
|
use crate::actions::ACTION_MAP;
|
||||||
use crate::settings::ShortcutBinding;
|
use crate::settings::ShortcutBinding;
|
||||||
use crate::settings::{self, get_settings, OverlayPosition};
|
use crate::settings::{self, get_settings, OverlayPosition, PasteMethod};
|
||||||
use crate::ManagedToggleState;
|
use crate::ManagedToggleState;
|
||||||
|
|
||||||
pub fn init_shortcuts(app: &App) {
|
pub fn init_shortcuts(app: &App) {
|
||||||
|
|
@ -210,6 +210,22 @@ pub fn change_word_correction_threshold_setting(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), String> {
|
||||||
|
let mut settings = settings::get_settings(&app);
|
||||||
|
let parsed = match method.as_str() {
|
||||||
|
"ctrl_v" => PasteMethod::CtrlV,
|
||||||
|
"direct" => PasteMethod::Direct,
|
||||||
|
other => {
|
||||||
|
eprintln!("Invalid paste method '{}', defaulting to ctrl_v", other);
|
||||||
|
PasteMethod::CtrlV
|
||||||
|
}
|
||||||
|
};
|
||||||
|
settings.paste_method = parsed;
|
||||||
|
settings::write_settings(&app, settings);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine whether a shortcut string contains at least one non-modifier key.
|
/// Determine whether a shortcut string contains at least one non-modifier key.
|
||||||
/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow
|
/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow
|
||||||
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").
|
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { TranslateToEnglish } from "./TranslateToEnglish";
|
||||||
import { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout";
|
import { ModelUnloadTimeoutSetting } from "./ModelUnloadTimeout";
|
||||||
import { CustomWords } from "./CustomWords";
|
import { CustomWords } from "./CustomWords";
|
||||||
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
||||||
|
import { PasteMethodSetting } from "./PasteMethod";
|
||||||
import { SettingsGroup } from "../ui/SettingsGroup";
|
import { SettingsGroup } from "../ui/SettingsGroup";
|
||||||
import { StartHidden } from "./StartHidden";
|
import { StartHidden } from "./StartHidden";
|
||||||
|
|
||||||
|
|
@ -13,6 +14,7 @@ export const AdvancedSettings: React.FC = () => {
|
||||||
<SettingsGroup title="Advanced">
|
<SettingsGroup title="Advanced">
|
||||||
<StartHidden descriptionMode="tooltip" grouped={true} />
|
<StartHidden descriptionMode="tooltip" grouped={true} />
|
||||||
<ShowOverlay descriptionMode="tooltip" grouped={true} />
|
<ShowOverlay descriptionMode="tooltip" grouped={true} />
|
||||||
|
<PasteMethodSetting descriptionMode="tooltip" grouped={true} />
|
||||||
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
|
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
|
||||||
<ModelUnloadTimeoutSetting descriptionMode="tooltip" grouped={true} />
|
<ModelUnloadTimeoutSetting descriptionMode="tooltip" grouped={true} />
|
||||||
<CustomWords descriptionMode="tooltip" grouped />
|
<CustomWords descriptionMode="tooltip" grouped />
|
||||||
|
|
|
||||||
43
src/components/settings/PasteMethod.tsx
Normal file
43
src/components/settings/PasteMethod.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Dropdown } from "../ui/Dropdown";
|
||||||
|
import { SettingContainer } from "../ui/SettingContainer";
|
||||||
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
import type { PasteMethod } from "../../lib/types";
|
||||||
|
|
||||||
|
interface PasteMethodProps {
|
||||||
|
descriptionMode?: "inline" | "tooltip";
|
||||||
|
grouped?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pasteMethodOptions = [
|
||||||
|
{ value: "ctrl_v", label: "Clipboard (Ctrl+V)" },
|
||||||
|
{ value: "direct", label: "Direct" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PasteMethodSetting: React.FC<PasteMethodProps> = React.memo(({
|
||||||
|
descriptionMode = "tooltip",
|
||||||
|
grouped = false,
|
||||||
|
}) => {
|
||||||
|
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||||
|
|
||||||
|
const selectedMethod = (getSetting("paste_method") ||
|
||||||
|
"ctrl_v") as PasteMethod;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingContainer
|
||||||
|
title="Paste Method"
|
||||||
|
description="Clipboard (Ctrl+V) simulates Ctrl/Cmd+V keystrokes to paste from your clipboard. Direct tries to use system input methods if possible, otherwise inputs keystrokes one by one into the text field."
|
||||||
|
descriptionMode={descriptionMode}
|
||||||
|
grouped={grouped}
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
options={pasteMethodOptions}
|
||||||
|
selectedValue={selectedMethod}
|
||||||
|
onSelect={(value) =>
|
||||||
|
updateSetting("paste_method", value as PasteMethod)
|
||||||
|
}
|
||||||
|
disabled={isUpdating("paste_method")}
|
||||||
|
/>
|
||||||
|
</SettingContainer>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -34,6 +34,9 @@ export const ModelUnloadTimeoutSchema = z.enum([
|
||||||
]);
|
]);
|
||||||
export type ModelUnloadTimeout = z.infer<typeof ModelUnloadTimeoutSchema>;
|
export type ModelUnloadTimeout = z.infer<typeof ModelUnloadTimeoutSchema>;
|
||||||
|
|
||||||
|
export const PasteMethodSchema = z.enum(["ctrl_v", "direct"]);
|
||||||
|
export type PasteMethod = z.infer<typeof PasteMethodSchema>;
|
||||||
|
|
||||||
export const SettingsSchema = z.object({
|
export const SettingsSchema = z.object({
|
||||||
bindings: ShortcutBindingsMapSchema,
|
bindings: ShortcutBindingsMapSchema,
|
||||||
push_to_talk: z.boolean(),
|
push_to_talk: z.boolean(),
|
||||||
|
|
@ -50,6 +53,7 @@ export const SettingsSchema = z.object({
|
||||||
custom_words: z.array(z.string()).optional().default([]),
|
custom_words: z.array(z.string()).optional().default([]),
|
||||||
model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"),
|
model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"),
|
||||||
word_correction_threshold: z.number().optional().default(0.18),
|
word_correction_threshold: z.number().optional().default(0.18),
|
||||||
|
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BindingResponseSchema = z.object({
|
export const BindingResponseSchema = z.object({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { create } from 'zustand';
|
import { create } from "zustand";
|
||||||
import { subscribeWithSelector } from 'zustand/middleware';
|
import { subscribeWithSelector } from "zustand/middleware";
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { Settings, AudioDevice } from '../lib/types';
|
import { Settings, AudioDevice } from "../lib/types";
|
||||||
|
|
||||||
interface SettingsStore {
|
interface SettingsStore {
|
||||||
settings: Settings | null;
|
settings: Settings | null;
|
||||||
|
|
@ -12,7 +12,10 @@ interface SettingsStore {
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
initialize: () => Promise<void>;
|
initialize: () => Promise<void>;
|
||||||
updateSetting: <K extends keyof Settings>(key: K, value: Settings[K]) => Promise<void>;
|
updateSetting: <K extends keyof Settings>(
|
||||||
|
key: K,
|
||||||
|
value: Settings[K],
|
||||||
|
) => Promise<void>;
|
||||||
resetSetting: (key: keyof Settings) => Promise<void>;
|
resetSetting: (key: keyof Settings) => Promise<void>;
|
||||||
refreshSettings: () => Promise<void>;
|
refreshSettings: () => Promise<void>;
|
||||||
refreshAudioDevices: () => Promise<void>;
|
refreshAudioDevices: () => Promise<void>;
|
||||||
|
|
@ -47,7 +50,7 @@ const DEFAULT_SETTINGS: Partial<Settings> = {
|
||||||
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
||||||
index: "default",
|
index: "default",
|
||||||
name: "Default",
|
name: "Default",
|
||||||
is_default: true
|
is_default: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSettingsStore = create<SettingsStore>()(
|
export const useSettingsStore = create<SettingsStore>()(
|
||||||
|
|
@ -63,7 +66,7 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
setLoading: (isLoading) => set({ isLoading }),
|
setLoading: (isLoading) => set({ isLoading }),
|
||||||
setUpdating: (key, updating) =>
|
setUpdating: (key, updating) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
isUpdating: { ...state.isUpdating, [key]: updating }
|
isUpdating: { ...state.isUpdating, [key]: updating },
|
||||||
})),
|
})),
|
||||||
setAudioDevices: (audioDevices) => set({ audioDevices }),
|
setAudioDevices: (audioDevices) => set({ audioDevices }),
|
||||||
setOutputDevices: (outputDevices) => set({ outputDevices }),
|
setOutputDevices: (outputDevices) => set({ outputDevices }),
|
||||||
|
|
@ -114,10 +117,14 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
// Load audio devices
|
// Load audio devices
|
||||||
refreshAudioDevices: async () => {
|
refreshAudioDevices: async () => {
|
||||||
try {
|
try {
|
||||||
const devices: AudioDevice[] = await invoke("get_available_microphones");
|
const devices: AudioDevice[] = await invoke(
|
||||||
|
"get_available_microphones",
|
||||||
|
);
|
||||||
const devicesWithDefault = [
|
const devicesWithDefault = [
|
||||||
DEFAULT_AUDIO_DEVICE,
|
DEFAULT_AUDIO_DEVICE,
|
||||||
...devices.filter((d) => d.name !== "Default" && d.name !== "default"),
|
...devices.filter(
|
||||||
|
(d) => d.name !== "Default" && d.name !== "default",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
set({ audioDevices: devicesWithDefault });
|
set({ audioDevices: devicesWithDefault });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -129,10 +136,14 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
// Load output devices
|
// Load output devices
|
||||||
refreshOutputDevices: async () => {
|
refreshOutputDevices: async () => {
|
||||||
try {
|
try {
|
||||||
const devices: AudioDevice[] = await invoke("get_available_output_devices");
|
const devices: AudioDevice[] = await invoke(
|
||||||
|
"get_available_output_devices",
|
||||||
|
);
|
||||||
const devicesWithDefault = [
|
const devicesWithDefault = [
|
||||||
DEFAULT_AUDIO_DEVICE,
|
DEFAULT_AUDIO_DEVICE,
|
||||||
...devices.filter((d) => d.name !== "Default" && d.name !== "default"),
|
...devices.filter(
|
||||||
|
(d) => d.name !== "Default" && d.name !== "default",
|
||||||
|
),
|
||||||
];
|
];
|
||||||
set({ outputDevices: devicesWithDefault });
|
set({ outputDevices: devicesWithDefault });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -142,7 +153,10 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
},
|
},
|
||||||
|
|
||||||
// Update a specific setting
|
// Update a specific setting
|
||||||
updateSetting: async <K extends keyof Settings>(key: K, value: Settings[K]) => {
|
updateSetting: async <K extends keyof Settings>(
|
||||||
|
key: K,
|
||||||
|
value: Settings[K],
|
||||||
|
) => {
|
||||||
const { settings, setUpdating, refreshSettings } = get();
|
const { settings, setUpdating, refreshSettings } = get();
|
||||||
const updateKey = String(key);
|
const updateKey = String(key);
|
||||||
const originalValue = settings?.[key];
|
const originalValue = settings?.[key];
|
||||||
|
|
@ -171,20 +185,30 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
break;
|
break;
|
||||||
case "selected_microphone":
|
case "selected_microphone":
|
||||||
const micDeviceName = value === "Default" ? "default" : value;
|
const micDeviceName = value === "Default" ? "default" : value;
|
||||||
await invoke("set_selected_microphone", { deviceName: micDeviceName });
|
await invoke("set_selected_microphone", {
|
||||||
|
deviceName: micDeviceName,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "selected_output_device":
|
case "selected_output_device":
|
||||||
const outputDeviceName = value === "Default" ? "default" : value;
|
const outputDeviceName = value === "Default" ? "default" : value;
|
||||||
await invoke("set_selected_output_device", { deviceName: outputDeviceName });
|
await invoke("set_selected_output_device", {
|
||||||
|
deviceName: outputDeviceName,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "translate_to_english":
|
case "translate_to_english":
|
||||||
await invoke("change_translate_to_english_setting", { enabled: value });
|
await invoke("change_translate_to_english_setting", {
|
||||||
|
enabled: value,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "selected_language":
|
case "selected_language":
|
||||||
await invoke("change_selected_language_setting", { language: value });
|
await invoke("change_selected_language_setting", {
|
||||||
|
language: value,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "overlay_position":
|
case "overlay_position":
|
||||||
await invoke("change_overlay_position_setting", { position: value });
|
await invoke("change_overlay_position_setting", {
|
||||||
|
position: value,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "debug_mode":
|
case "debug_mode":
|
||||||
await invoke("change_debug_mode_setting", { enabled: value });
|
await invoke("change_debug_mode_setting", { enabled: value });
|
||||||
|
|
@ -193,7 +217,12 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
await invoke("update_custom_words", { words: value });
|
await invoke("update_custom_words", { words: value });
|
||||||
break;
|
break;
|
||||||
case "word_correction_threshold":
|
case "word_correction_threshold":
|
||||||
await invoke("change_word_correction_threshold_setting", { threshold: value });
|
await invoke("change_word_correction_threshold_setting", {
|
||||||
|
threshold: value,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "paste_method":
|
||||||
|
await invoke("change_paste_method_setting", { method: value });
|
||||||
break;
|
break;
|
||||||
case "bindings":
|
case "bindings":
|
||||||
case "selected_model":
|
case "selected_model":
|
||||||
|
|
@ -293,12 +322,13 @@ export const useSettingsStore = create<SettingsStore>()(
|
||||||
|
|
||||||
// Initialize everything
|
// Initialize everything
|
||||||
initialize: async () => {
|
initialize: async () => {
|
||||||
const { refreshSettings, refreshAudioDevices, refreshOutputDevices } = get();
|
const { refreshSettings, refreshAudioDevices, refreshOutputDevices } =
|
||||||
|
get();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
refreshSettings(),
|
refreshSettings(),
|
||||||
refreshAudioDevices(),
|
refreshAudioDevices(),
|
||||||
refreshOutputDevices(),
|
refreshOutputDevices(),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue