diff --git a/src-tauri/src/actions.rs b/src-tauri/src/actions.rs index 8e752e50b2..34b44bda34 100644 --- a/src-tauri/src/actions.rs +++ b/src-tauri/src/actions.rs @@ -524,7 +524,9 @@ impl ShortcutAction for TranscribeAction { let overlay_started = Instant::now(); match settings.overlay_style { OverlayStyle::Live if model_supports_streaming => utils::show_streaming_overlay(app), - OverlayStyle::Live | OverlayStyle::Minimal => show_recording_overlay(app), + OverlayStyle::Live | OverlayStyle::Minimal => { + show_recording_overlay(app) + } OverlayStyle::None => {} // show_overlay_state no-ops on None anyway } // Everything above runs before capture can begin, so each span here is diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index facc9786a0..e2c466a00e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -662,6 +662,7 @@ pub fn run(cli_args: CliArgs) { shortcut::change_selected_language_setting, shortcut::change_overlay_position_setting, shortcut::change_overlay_style_setting, + shortcut::change_monochrome_overlay_setting, shortcut::change_debug_mode_setting, shortcut::change_word_correction_threshold_setting, shortcut::change_extra_recording_buffer_setting, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 44363d4905..69c741348d 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -119,8 +119,8 @@ pub enum OverlayPosition { Bottom, } -/// Which recording overlay to display. `Minimal` and `Live` share one base -/// (the pill); `Live` grows into the panel that shows live transcription text. +/// Which recording overlay to display. `Minimal` and `Live` +/// share one pill; `Live` grows into the live transcription panel. /// `None` hides the overlay entirely. Decoupled from whether the model runs in /// streaming mode (that is driven purely by model capability). #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] @@ -514,6 +514,8 @@ pub struct AppSettings { /// `overlay_position` (position `none` → style `None`). #[serde(default = "default_overlay_style")] pub overlay_style: OverlayStyle, + #[serde(default)] + pub monochrome_overlay: bool, } fn default_model() -> String { @@ -970,6 +972,7 @@ pub fn get_default_settings() -> AppSettings { vad_enabled: default_vad_enabled(), vad_backend: VadBackend::default(), overlay_style: default_overlay_style(), + monochrome_overlay: false, } } diff --git a/src-tauri/src/shortcut/mod.rs b/src-tauri/src/shortcut/mod.rs index 8a603f4b74..00108e0303 100644 --- a/src-tauri/src/shortcut/mod.rs +++ b/src-tauri/src/shortcut/mod.rs @@ -685,6 +685,7 @@ pub fn change_overlay_style_setting(app: AppHandle, style: String) -> Result<(), }; settings.overlay_style = parsed; settings::write_settings(&app, settings); + let _ = app.emit_to("recording_overlay", "overlay-style-changed", parsed); // Keep the cached overlay-enabled flag in sync so emit_levels stops (or // resumes) emitting on the next audio callback. @@ -696,6 +697,16 @@ pub fn change_overlay_style_setting(app: AppHandle, style: String) -> Result<(), Ok(()) } +#[tauri::command] +#[specta::specta] +pub fn change_monochrome_overlay_setting(app: AppHandle, enabled: bool) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.monochrome_overlay = enabled; + settings::write_settings(&app, settings); + let _ = app.emit_to("recording_overlay", "overlay-monochrome-changed", enabled); + Ok(()) +} + #[tauri::command] #[specta::specta] pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> { diff --git a/src/bindings.ts b/src/bindings.ts index c6d3953515..5a359072ec 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -117,6 +117,14 @@ async changeOverlayStyleSetting(style: string) : Promise> { else return { status: "error", error: e as any }; } }, +async changeMonochromeOverlaySetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_monochrome_overlay_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async changeDebugModeSetting(enabled: boolean) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("change_debug_mode_setting", { enabled }) }; @@ -1004,7 +1012,7 @@ vad_backend?: VadBackend; * not gated on this — that follows model capability. Migrated from the old * `overlay_position` (position `none` → style `None`). */ -overlay_style?: OverlayStyle } +overlay_style?: OverlayStyle; monochrome_overlay?: boolean } export type AudioDevice = { index: string; name: string; is_default: boolean } export type AutoSubmitKey = "enter" | "ctrl_enter" | "cmd_enter" export type AvailableAccelerators = { transcribe: string[]; ort: string[]; gpu_devices: GpuDeviceOption[] } @@ -1067,8 +1075,8 @@ export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | " export type OrtAcceleratorSetting = "auto" | "cpu" | "cuda" | "directml" | "rocm" export type OverlayPosition = "top" | "bottom" /** - * Which recording overlay to display. `Minimal` and `Live` share one base - * (the pill); `Live` grows into the panel that shows live transcription text. + * Which recording overlay to display. `Minimal` and `Live` + * share one pill; `Live` grows into the live transcription panel. * `None` hides the overlay entirely. Decoupled from whether the model runs in * streaming mode (that is driven purely by model capability). */ diff --git a/src/components/settings/ShowOverlay.tsx b/src/components/settings/ShowOverlay.tsx index d5f2448813..564dbef735 100644 --- a/src/components/settings/ShowOverlay.tsx +++ b/src/components/settings/ShowOverlay.tsx @@ -2,6 +2,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; +import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; import type { OverlayPosition, OverlayStyle } from "@/bindings"; @@ -66,6 +67,18 @@ export const ShowOverlay: React.FC = React.memo( /> + {selectedStyle !== "none" && ( + updateSetting("monochrome_overlay", enabled)} + isUpdating={isUpdating("monochrome_overlay")} + label={t("settings.advanced.overlay.monochrome.label")} + description={t("settings.advanced.overlay.monochrome.description")} + descriptionMode={descriptionMode} + grouped={grouped} + /> + )} + {selectedStyle !== "none" && ( { // Overlay placement (top vs bottom of the screen). The Live panel grows downward // from a top overlay (oldest line under the pill) and upward from a bottom one. const [position, setPosition] = useState<"top" | "bottom">("bottom"); + const [monochrome, setMonochrome] = useState(false); // True once live text overflows the cap. A top overlay fades its top edge only // while overflowing, so the resting first line stays crisp flush under the pill. const [overflowing, setOverflowing] = useState(false); @@ -75,6 +76,7 @@ const RecordingOverlay: React.FC = () => { setPosition( settings.data.overlay_position === "top" ? "top" : "bottom", ); + setMonochrome(settings.data.monochrome_overlay ?? false); } } catch { // Keep the previous/default placement if settings can't be read. @@ -94,6 +96,11 @@ const RecordingOverlay: React.FC = () => { setCaptureReady(false); }); + const unlistenMonochrome = await listen( + "overlay-monochrome-changed", + (event) => setMonochrome(event.payload), + ); + const unlistenReady = await listen("recording-ready", () => { setElapsed(0); setCaptureReady(true); @@ -124,6 +131,7 @@ const RecordingOverlay: React.FC = () => { return () => { unlistenShow(); unlistenHide(); + unlistenMonochrome(); unlistenReady(); unlistenLevel(); unlistenStream(); @@ -240,7 +248,7 @@ const RecordingOverlay: React.FC = () => { const collapsed = working && !hasText; return ( -
+
{ return (
commands.changeLazyStreamCloseSetting(value as boolean), overlay_style: (value) => commands.changeOverlayStyleSetting(value as string), + monochrome_overlay: (value) => + commands.changeMonochromeOverlaySetting(value as boolean), vad_enabled: (value) => commands.changeVadEnabledSetting(value as boolean), vad_backend: async (value) => { const result = await commands.changeVadBackendSetting(value as VadBackend);