feat(overlay): replace show_overlay toggle with position selector
This commit is contained in:
parent
53874110b1
commit
091b392aa6
10 changed files with 149 additions and 144 deletions
|
|
@ -181,7 +181,7 @@ pub fn run() {
|
||||||
shortcut::change_audio_feedback_setting,
|
shortcut::change_audio_feedback_setting,
|
||||||
shortcut::change_translate_to_english_setting,
|
shortcut::change_translate_to_english_setting,
|
||||||
shortcut::change_selected_language_setting,
|
shortcut::change_selected_language_setting,
|
||||||
shortcut::change_show_overlay_setting,
|
shortcut::change_overlay_position_setting,
|
||||||
shortcut::change_debug_mode_setting,
|
shortcut::change_debug_mode_setting,
|
||||||
shortcut::suspend_binding,
|
shortcut::suspend_binding,
|
||||||
shortcut::resume_binding,
|
shortcut::resume_binding,
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ pub struct AppSettings {
|
||||||
pub translate_to_english: bool,
|
pub translate_to_english: bool,
|
||||||
#[serde(default = "default_selected_language")]
|
#[serde(default = "default_selected_language")]
|
||||||
pub selected_language: String,
|
pub selected_language: String,
|
||||||
#[serde(default = "default_show_overlay")]
|
#[serde(default = "default_overlay_position")]
|
||||||
pub show_overlay: bool,
|
pub overlay_position: String,
|
||||||
#[serde(default = "default_debug_mode")]
|
#[serde(default = "default_debug_mode")]
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -58,9 +58,9 @@ fn default_selected_language() -> String {
|
||||||
"auto".to_string()
|
"auto".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_show_overlay() -> bool {
|
fn default_overlay_position() -> String {
|
||||||
// Default to true - users expect visual feedback by default
|
// Default to "bottom" - less intrusive position
|
||||||
true
|
"bottom".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_debug_mode() -> bool {
|
fn default_debug_mode() -> bool {
|
||||||
|
|
@ -114,7 +114,7 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
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(),
|
||||||
show_overlay: true,
|
overlay_position: "bottom".to_string(),
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,10 +135,14 @@ pub fn change_selected_language_setting(app: AppHandle, language: String) -> Res
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn change_show_overlay_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Result<(), String> {
|
||||||
let mut settings = settings::get_settings(&app);
|
let mut settings = settings::get_settings(&app);
|
||||||
settings.show_overlay = enabled;
|
settings.overlay_position = position;
|
||||||
settings::write_settings(&app, settings);
|
settings::write_settings(&app, settings);
|
||||||
|
|
||||||
|
// Update overlay position without recreating window
|
||||||
|
crate::utils::update_overlay_position(&app);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -338,53 +338,66 @@ fn play_audio_file(
|
||||||
/* OVERLAY MANAGEMENT */
|
/* OVERLAY MANAGEMENT */
|
||||||
/* ──────────────────────────────────────────────────────────────── */
|
/* ──────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
/// Creates the recording overlay window and keeps it hidden by default
|
const OVERLAY_WIDTH: f64 = 172.0;
|
||||||
pub fn create_recording_overlay(app_handle: &AppHandle) {
|
const OVERLAY_HEIGHT: f64 = 36.0;
|
||||||
// Get work area dimensions for positioning (respects taskbars, docks, etc.)
|
const OVERLAY_OFFSET: f64 = 46.0;
|
||||||
|
|
||||||
|
fn calculate_overlay_position(app_handle: &AppHandle) -> Option<(f64, f64)> {
|
||||||
if let Ok(monitors) = app_handle.primary_monitor() {
|
if let Ok(monitors) = app_handle.primary_monitor() {
|
||||||
if let Some(monitor) = monitors {
|
if let Some(monitor) = monitors {
|
||||||
const OVERLAY_WIDTH: f64 = 172.0;
|
|
||||||
const OVERLAY_HEIGHT: f64 = 36.0;
|
|
||||||
const OVERLAY_TOP_OFFSET: f64 = 46.0;
|
|
||||||
|
|
||||||
let work_area = monitor.work_area();
|
let work_area = monitor.work_area();
|
||||||
let scale = monitor.scale_factor();
|
let scale = monitor.scale_factor();
|
||||||
let work_area_width = work_area.size.width as f64 / scale;
|
let work_area_width = work_area.size.width as f64 / scale;
|
||||||
|
let work_area_height = work_area.size.height as f64 / scale;
|
||||||
let work_area_x = work_area.position.x as f64 / scale;
|
let work_area_x = work_area.position.x as f64 / scale;
|
||||||
let work_area_y = work_area.position.y as f64 / scale;
|
let work_area_y = work_area.position.y as f64 / scale;
|
||||||
|
|
||||||
// Position at top center of work area
|
let settings = settings::get_settings(app_handle);
|
||||||
let x = work_area_x + (work_area_width - OVERLAY_WIDTH) / 2.0;
|
let overlay_position = settings.overlay_position.as_str();
|
||||||
let y = work_area_y + OVERLAY_TOP_OFFSET;
|
|
||||||
|
|
||||||
match WebviewWindowBuilder::new(
|
let x = work_area_x + (work_area_width - OVERLAY_WIDTH) / 2.0;
|
||||||
app_handle,
|
let y = match overlay_position {
|
||||||
"recording_overlay",
|
"top" => work_area_y + OVERLAY_OFFSET,
|
||||||
tauri::WebviewUrl::App("src/overlay/index.html".into()),
|
"bottom" => work_area_y + work_area_height - OVERLAY_HEIGHT - OVERLAY_OFFSET,
|
||||||
)
|
_ => work_area_y + work_area_height - OVERLAY_HEIGHT - OVERLAY_OFFSET,
|
||||||
.title("Recording")
|
};
|
||||||
.position(x, y)
|
|
||||||
.resizable(false)
|
return Some((x, y));
|
||||||
.inner_size(OVERLAY_WIDTH, OVERLAY_HEIGHT)
|
}
|
||||||
.shadow(false)
|
}
|
||||||
.maximizable(false)
|
None
|
||||||
.minimizable(false)
|
}
|
||||||
.closable(false)
|
|
||||||
.accept_first_mouse(true)
|
/// Creates the recording overlay window and keeps it hidden by default
|
||||||
.decorations(false)
|
pub fn create_recording_overlay(app_handle: &AppHandle) {
|
||||||
.always_on_top(true)
|
if let Some((x, y)) = calculate_overlay_position(app_handle) {
|
||||||
.skip_taskbar(true)
|
match WebviewWindowBuilder::new(
|
||||||
.transparent(true)
|
app_handle,
|
||||||
.focused(false)
|
"recording_overlay",
|
||||||
.visible(false) // Start hidden
|
tauri::WebviewUrl::App("src/overlay/index.html".into()),
|
||||||
.build()
|
)
|
||||||
{
|
.title("Recording")
|
||||||
Ok(_window) => {
|
.position(x, y)
|
||||||
debug!("Recording overlay window created successfully (hidden)");
|
.resizable(false)
|
||||||
}
|
.inner_size(OVERLAY_WIDTH, OVERLAY_HEIGHT)
|
||||||
Err(e) => {
|
.shadow(false)
|
||||||
debug!("Failed to create recording overlay window: {}", e);
|
.maximizable(false)
|
||||||
}
|
.minimizable(false)
|
||||||
|
.closable(false)
|
||||||
|
.accept_first_mouse(true)
|
||||||
|
.decorations(false)
|
||||||
|
.always_on_top(true)
|
||||||
|
.skip_taskbar(true)
|
||||||
|
.transparent(true)
|
||||||
|
.focused(false)
|
||||||
|
.visible(false)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(_window) => {
|
||||||
|
debug!("Recording overlay window created successfully (hidden)");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to create recording overlay window: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -392,9 +405,9 @@ pub fn create_recording_overlay(app_handle: &AppHandle) {
|
||||||
|
|
||||||
/// Shows the recording overlay window with fade-in animation
|
/// Shows the recording overlay window with fade-in animation
|
||||||
pub fn show_recording_overlay(app_handle: &AppHandle) {
|
pub fn show_recording_overlay(app_handle: &AppHandle) {
|
||||||
// Check if show_overlay is enabled in settings
|
// Check if overlay should be shown based on position setting
|
||||||
let settings = settings::get_settings(app_handle);
|
let settings = settings::get_settings(app_handle);
|
||||||
if !settings.show_overlay {
|
if settings.overlay_position == "none" {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,9 +420,9 @@ pub fn show_recording_overlay(app_handle: &AppHandle) {
|
||||||
|
|
||||||
/// Shows the transcribing overlay window
|
/// Shows the transcribing overlay window
|
||||||
pub fn show_transcribing_overlay(app_handle: &AppHandle) {
|
pub fn show_transcribing_overlay(app_handle: &AppHandle) {
|
||||||
// Check if show_overlay is enabled in settings
|
// Check if overlay should be shown based on position setting
|
||||||
let settings = settings::get_settings(app_handle);
|
let settings = settings::get_settings(app_handle);
|
||||||
if !settings.show_overlay {
|
if settings.overlay_position == "none" {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -420,10 +433,19 @@ pub fn show_transcribing_overlay(app_handle: &AppHandle) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the overlay window position based on current settings
|
||||||
|
pub fn update_overlay_position(app_handle: &AppHandle) {
|
||||||
|
if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") {
|
||||||
|
if let Some((x, y)) = calculate_overlay_position(app_handle) {
|
||||||
|
let _ = overlay_window.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Hides the recording overlay window with fade-out animation
|
/// Hides the recording overlay window with fade-out animation
|
||||||
pub fn hide_recording_overlay(app_handle: &AppHandle) {
|
pub fn hide_recording_overlay(app_handle: &AppHandle) {
|
||||||
// Check if show_overlay is enabled in settings - if disabled, the overlay shouldn't be shown anyway
|
// Always hide the overlay regardless of settings - if setting was changed while recording,
|
||||||
// but we still want to hide it in case the setting was changed while recording
|
// we still want to hide it properly
|
||||||
if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") {
|
if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") {
|
||||||
// Emit event to trigger fade-out animation
|
// Emit event to trigger fade-out animation
|
||||||
let _ = overlay_window.emit("hide-overlay", ());
|
let _ = overlay_window.emit("hide-overlay", ());
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,11 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = ({
|
||||||
console.log("Microphone reset to default");
|
console.log("Microphone reset to default");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const microphoneOptions = audioDevices.map(device => ({
|
||||||
|
value: device.name,
|
||||||
|
label: device.name
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer
|
<SettingContainer
|
||||||
title="Microphone"
|
title="Microphone"
|
||||||
|
|
@ -64,12 +69,12 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = ({
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
devices={audioDevices}
|
options={microphoneOptions}
|
||||||
selectedDevice={selectedMicrophone}
|
selectedValue={selectedMicrophone}
|
||||||
onSelect={handleMicrophoneSelect}
|
onSelect={handleMicrophoneSelect}
|
||||||
placeholder={isLoading ? "Loading..." : "Select microphone..."}
|
placeholder={isLoading ? "Loading..." : "Select microphone..."}
|
||||||
disabled={isUpdating("selected_microphone") || isLoading}
|
disabled={isUpdating("selected_microphone") || isLoading}
|
||||||
refreshDevices={refreshAudioDevices}
|
onRefresh={refreshAudioDevices}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="px-2 py-1 hover:bg-logo-primary/30 active:bg-logo-primary/50 active:scale-95 rounded fill-text hover:cursor-pointer hover:border-logo-primary border border-transparent transition-all duration-150"
|
className="px-2 py-1 hover:bg-logo-primary/30 active:bg-logo-primary/50 active:scale-95 rounded fill-text hover:cursor-pointer hover:border-logo-primary border border-transparent transition-all duration-150"
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ export const OutputDeviceSelector: React.FC<OutputDeviceSelectorProps> = ({
|
||||||
console.log("Output device reset to default");
|
console.log("Output device reset to default");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const outputDeviceOptions = outputDevices.map(device => ({
|
||||||
|
value: device.name,
|
||||||
|
label: device.name
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer
|
<SettingContainer
|
||||||
title="Output Device"
|
title="Output Device"
|
||||||
|
|
@ -47,12 +52,12 @@ export const OutputDeviceSelector: React.FC<OutputDeviceSelectorProps> = ({
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
devices={outputDevices}
|
options={outputDeviceOptions}
|
||||||
selectedDevice={selectedOutputDevice}
|
selectedValue={selectedOutputDevice}
|
||||||
onSelect={handleOutputDeviceSelect}
|
onSelect={handleOutputDeviceSelect}
|
||||||
placeholder={isLoading ? "Loading..." : "Select output device..."}
|
placeholder={isLoading ? "Loading..." : "Select output device..."}
|
||||||
disabled={isUpdating("selected_output_device") || isLoading}
|
disabled={isUpdating("selected_output_device") || isLoading}
|
||||||
refreshDevices={refreshOutputDevices}
|
onRefresh={refreshOutputDevices}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="px-2 py-1 hover:bg-logo-primary/30 active:bg-logo-primary/50 active:scale-95 rounded fill-text hover:cursor-pointer hover:border-logo-primary border border-transparent transition-all duration-150"
|
className="px-2 py-1 hover:bg-logo-primary/30 active:bg-logo-primary/50 active:scale-95 rounded fill-text hover:cursor-pointer hover:border-logo-primary border border-transparent transition-all duration-150"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ToggleSwitch } from "../ui/ToggleSwitch";
|
import { Dropdown } from "../ui/Dropdown";
|
||||||
|
import { SettingContainer } from "../ui/SettingContainer";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
|
||||||
interface ShowOverlayProps {
|
interface ShowOverlayProps {
|
||||||
|
|
@ -7,23 +8,33 @@ interface ShowOverlayProps {
|
||||||
grouped?: boolean;
|
grouped?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const overlayOptions = [
|
||||||
|
{ value: "none", label: "Do not show" },
|
||||||
|
{ value: "bottom", label: "On bottom" },
|
||||||
|
{ value: "top", label: "On top" }
|
||||||
|
];
|
||||||
|
|
||||||
export const ShowOverlay: React.FC<ShowOverlayProps> = ({
|
export const ShowOverlay: React.FC<ShowOverlayProps> = ({
|
||||||
descriptionMode = "tooltip",
|
descriptionMode = "tooltip",
|
||||||
grouped = false,
|
grouped = false
|
||||||
}) => {
|
}) => {
|
||||||
const { getSetting, updateSetting, isUpdating } = useSettings();
|
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||||
|
|
||||||
const showOverlayEnabled = getSetting("show_overlay") ?? true;
|
const selectedPosition = getSetting("overlay_position") || "bottom";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleSwitch
|
<SettingContainer
|
||||||
checked={showOverlayEnabled}
|
title="Show Overlay"
|
||||||
onChange={(enabled) => updateSetting("show_overlay", enabled)}
|
|
||||||
isUpdating={isUpdating("show_overlay")}
|
|
||||||
label="Show Overlay"
|
|
||||||
description="Display visual feedback overlay during recording and transcription"
|
description="Display visual feedback overlay during recording and transcription"
|
||||||
descriptionMode={descriptionMode}
|
descriptionMode={descriptionMode}
|
||||||
grouped={grouped}
|
grouped={grouped}
|
||||||
/>
|
>
|
||||||
|
<Dropdown
|
||||||
|
options={overlayOptions}
|
||||||
|
selectedValue={selectedPosition}
|
||||||
|
onSelect={(value) => updateSetting("overlay_position", value)}
|
||||||
|
disabled={isUpdating("overlay_position")}
|
||||||
|
/>
|
||||||
|
</SettingContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,50 @@
|
||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import { AudioDevice } from "../../lib/types";
|
|
||||||
|
export interface DropdownOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
devices: AudioDevice[];
|
options: DropdownOption[];
|
||||||
selectedDevice: string | null;
|
selectedValue: string | null;
|
||||||
onSelect: (deviceName: string) => void;
|
onSelect: (value: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
refreshDevices?: () => void;
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Dropdown: React.FC<DropdownProps> = ({
|
export const Dropdown: React.FC<DropdownProps> = ({
|
||||||
devices,
|
options,
|
||||||
selectedDevice,
|
selectedValue,
|
||||||
onSelect,
|
onSelect,
|
||||||
placeholder = "Select a microphone...",
|
placeholder = "Select an option...",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
refreshDevices,
|
onRefresh,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
dropdownRef.current &&
|
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => {
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Find the selected device name with proper default handling
|
const selectedOption = options.find(option => option.value === selectedValue);
|
||||||
const selectedDeviceName = selectedDevice
|
|
||||||
? (() => {
|
|
||||||
// First try exact match
|
|
||||||
let device = devices.find((d) => d.name === selectedDevice);
|
|
||||||
|
|
||||||
// If no exact match and selected is "default" or "Default", try both variants
|
const handleSelect = (value: string) => {
|
||||||
if (!device && selectedDevice.toLowerCase() === "default") {
|
onSelect(value);
|
||||||
device = devices.find((d) => d.name.toLowerCase() === "default");
|
|
||||||
}
|
|
||||||
|
|
||||||
return device?.name || "Unknown Device";
|
|
||||||
})()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const handleSelect = (deviceName: string) => {
|
|
||||||
onSelect(deviceName);
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
|
if (!isOpen && onRefresh) onRefresh();
|
||||||
// Refresh devices when opening the dropdown
|
|
||||||
if (!isOpen && refreshDevices) {
|
|
||||||
refreshDevices();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,52 +53,31 @@ export const Dropdown: React.FC<DropdownProps> = ({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded min-w-[200px] text-left flex items-center justify-between transition-all duration-150 ${
|
className={`px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded min-w-[200px] text-left flex items-center justify-between transition-all duration-150 ${
|
||||||
disabled
|
disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary"
|
||||||
? "opacity-50 cursor-not-allowed"
|
|
||||||
: "hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary"
|
|
||||||
}`}
|
}`}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<span className="truncate">{selectedDeviceName || placeholder}</span>
|
<span className="truncate">{selectedOption?.label || placeholder}</span>
|
||||||
<svg
|
<svg className={`w-4 h-4 ml-2 transition-transform duration-200 ${isOpen ? "transform rotate-180" : ""}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
className={`w-4 h-4 ml-2 transition-transform duration-200 ${
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
isOpen ? "transform rotate-180" : ""
|
|
||||||
}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && !disabled && (
|
{isOpen && !disabled && (
|
||||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background border border-mid-gray/80 rounded shadow-lg z-50 max-h-60 overflow-y-auto">
|
<div className="absolute top-full left-0 right-0 mt-1 bg-background border border-mid-gray/80 rounded shadow-lg z-50 max-h-60 overflow-y-auto">
|
||||||
{devices.length === 0 ? (
|
{options.length === 0 ? (
|
||||||
<div className="px-2 py-1 text-sm text-mid-gray">
|
<div className="px-2 py-1 text-sm text-mid-gray">No options found</div>
|
||||||
No microphones found
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
devices.map((device) => (
|
options.map((option) => (
|
||||||
<button
|
<button
|
||||||
key={device.index}
|
key={option.value}
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-full px-2 py-1 text-sm text-left hover:bg-logo-primary/10 transition-colors duration-150 ${
|
className={`w-full px-2 py-1 text-sm text-left hover:bg-logo-primary/10 transition-colors duration-150 ${
|
||||||
selectedDevice === device.name
|
selectedValue === option.value ? "bg-logo-primary/20 text-logo-primary font-semibold" : ""
|
||||||
? "bg-logo-primary/20 text-logo-primary font-semibold"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelect(device.name)}
|
onClick={() => handleSelect(option.value)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<span className="truncate">{option.label}</span>
|
||||||
<span className="truncate">{device.name}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -199,8 +199,8 @@ export const useSettings = (): UseSettingsReturn => {
|
||||||
language: value,
|
language: value,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "show_overlay":
|
case "overlay_position":
|
||||||
await invoke("change_show_overlay_setting", { enabled: 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 });
|
||||||
|
|
@ -249,7 +249,7 @@ export const useSettings = (): UseSettingsReturn => {
|
||||||
selected_output_device: "Default",
|
selected_output_device: "Default",
|
||||||
translate_to_english: false,
|
translate_to_english: false,
|
||||||
selected_language: "auto",
|
selected_language: "auto",
|
||||||
show_overlay: true,
|
overlay_position: "bottom",
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const SettingsSchema = z.object({
|
||||||
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(),
|
||||||
show_overlay: z.boolean(),
|
overlay_position: z.string(),
|
||||||
debug_mode: z.boolean(),
|
debug_mode: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue