From a702834034fa888734dba60e086adb1d69c93489 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 26 Aug 2026 01:33:42 +0200 Subject: [PATCH] Audio config --- src-tauri/src/commands/audio.rs | 28 +-- src-tauri/src/commands/config.rs | 17 +- src-tauri/src/lib.rs | 1 + .../settings/AudioVideoSettings.tsx | 125 ++++++++++- src/components/ui/select.tsx | 199 ++++++++++++++++++ src/components/ui/slider.tsx | 52 +++++ src/lib/config.ts | 5 +- 7 files changed, 406 insertions(+), 21 deletions(-) create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/slider.tsx diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index 9e32d8c..1b0eabd 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -1,14 +1,7 @@ use cpal::traits::{DeviceTrait, HostTrait}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AudioDevice { - pub name: String, -} #[tauri::command] -pub fn list_input_devices() -> Result, String> { +pub fn list_input_devices() -> Result, String> { let host = cpal::default_host(); let devices = host @@ -16,10 +9,19 @@ pub fn list_input_devices() -> Result, String> { .map_err(|e| format!("Failed to enumerate input devices: {e}"))?; Ok(devices - .filter_map(|d| { - Some(AudioDevice { - name: d.description().ok()?.name().to_string(), - }) - }) + .filter_map(|d| Some(d.description().ok()?.name().to_string())) + .collect()) +} + +#[tauri::command] +pub fn list_output_devices() -> Result, String> { + let host = cpal::default_host(); + + let devices = host + .output_devices() + .map_err(|e| format!("Failed to enumerate output devices: {e}"))?; + + Ok(devices + .filter_map(|d| Some(d.description().ok()?.name().to_string())) .collect()) } diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 961b2da..cc7cc22 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -1,3 +1,4 @@ +use cpal::traits::{DeviceTrait, HostTrait}; use serde::{Deserialize, Serialize}; use std::fs; use std::sync::Mutex; @@ -6,13 +7,25 @@ use tauri::{AppHandle, Manager, State}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Config { - pub audio_device_name: Option, + pub input_device_name: Option, + pub output_device_name: Option, + pub input_volume: u8, + pub output_volume: u8, } impl Default for Config { fn default() -> Self { + let host = cpal::default_host(); + Self { - audio_device_name: None, + input_device_name: host + .default_input_device() + .and_then(|d| Some(d.description().ok()?.name().to_string())), + output_device_name: host + .default_output_device() + .and_then(|d| Some(d.description().ok()?.name().to_string())), + input_volume: 0, + output_volume: 0, } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5309958..c7845d6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,6 +16,7 @@ pub fn run() { commands::accounts::save_accounts, commands::accounts::get_accounts, commands::audio::list_input_devices, + commands::audio::list_output_devices, commands::config::update_config, commands::config::save_config, commands::config::get_config, diff --git a/src/components/settings/AudioVideoSettings.tsx b/src/components/settings/AudioVideoSettings.tsx index 11b75c2..a474f92 100644 --- a/src/components/settings/AudioVideoSettings.tsx +++ b/src/components/settings/AudioVideoSettings.tsx @@ -1,14 +1,129 @@ -import { Volume2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Volume2, Mic, Speaker } from "lucide-react"; +import { invoke } from "@tauri-apps/api/core"; +import { Label } from "@/components/ui/label"; +import { Slider } from "@/components/ui/slider"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { getConfig, updateConfig, saveConfig, Config } from "@/lib/config"; export function AudioVideoSettings() { + const [config, setConfig] = useState(null); + const [inputDevices, setInputDevices] = useState([]); + const [outputDevices, setOutputDevices] = useState([]); + + useEffect(() => { + getConfig().then(setConfig); + invoke("list_input_devices").then(setInputDevices); + invoke("list_output_devices").then(setOutputDevices); + }, []); + + async function handleChange(patch: Partial) { + if (!config) return; + const updated = { ...config, ...patch }; + setConfig(updated); + await updateConfig(updated); + await saveConfig(); + } + + if (!config) return null; + return ( -
+

Audio & Video

-

- Microphone/camera device selection goes here. -

+ +
+
+ + Input +
+ +
+ + +
+ +
+
+ + + {config.inputVolume}% + +
+ + handleChange({ inputVolume: value as any }) + } + max={100} + step={1} + /> +
+
+ +
+
+ + Output +
+ +
+ + +
+ +
+
+ + + {config.outputVolume}% + +
+ + handleChange({ outputVolume: value as any }) + } + max={100} + step={1} + /> +
+
); } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..56a7734 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,199 @@ +import * as React from "react" +import { Select as SelectPrimitive } from "@base-ui/react/select" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" + +const Select = SelectPrimitive.Root + +function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { + return ( + + ) +} + +function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { + return ( + + ) +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: SelectPrimitive.Trigger.Props & { + size?: "sm" | "default" +}) { + return ( + + {children} + + } + /> + + ) +} + +function SelectContent({ + className, + children, + side = "bottom", + sideOffset = 4, + align = "center", + alignOffset = 0, + alignItemWithTrigger = true, + ...props +}: SelectPrimitive.Popup.Props & + Pick< + SelectPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" + >) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: SelectPrimitive.GroupLabel.Props) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: SelectPrimitive.Item.Props) { + return ( + + + {children} + + + } + > + + + + ) +} + +function SelectSeparator({ + className, + ...props +}: SelectPrimitive.Separator.Props) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx new file mode 100644 index 0000000..691fff3 --- /dev/null +++ b/src/components/ui/slider.tsx @@ -0,0 +1,52 @@ +import { Slider as SliderPrimitive } from "@base-ui/react/slider" + +import { cn } from "@/lib/utils" + +function Slider({ + className, + defaultValue, + value, + min = 0, + max = 100, + ...props +}: SliderPrimitive.Root.Props) { + const _values = Array.isArray(value) + ? value + : Array.isArray(defaultValue) + ? defaultValue + : [min, max] + + return ( + + + + + + {Array.from({ length: _values.length }, (_, index) => ( + + ))} + + + ) +} + +export { Slider } diff --git a/src/lib/config.ts b/src/lib/config.ts index a7194e8..270e766 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,7 +1,10 @@ import { invoke } from "@tauri-apps/api/core"; export type Config = { - audioDeviceName: string | null; + outputDeviceName: string | null; + inputDeviceName: string | null; + inputVolume: number; + outputVolume: number; }; export async function updateConfig(config: Config): Promise {