diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 79304c9..cbc444b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,7 +28,7 @@ tauri-build = { version = "2", features = [] } [dependencies] once_cell = "1" -tauri = { version = "2", features = ["tray-icon", 'image-png'] } +tauri = { version = "2", features = [ "macos-private-api", "tray-icon", 'image-png'] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/src/managers/audio.rs b/src-tauri/src/managers/audio.rs index e5c8994..8ef90ff 100644 --- a/src-tauri/src/managers/audio.rs +++ b/src-tauri/src/managers/audio.rs @@ -3,7 +3,7 @@ use crate::settings::get_settings; use log::{debug, info}; use std::sync::{Arc, Mutex}; use std::time::Instant; -use tauri::{App, Manager}; +use tauri::{App, LogicalSize, Manager, WebviewWindowBuilder}; const WHISPER_SAMPLE_RATE: usize = 16000; @@ -195,6 +195,7 @@ impl AudioRecordingManager { binding_id: binding_id.to_string(), }; debug!("Recording started for binding {binding_id}"); + self.show_overlay(); return true; } } @@ -239,6 +240,9 @@ impl AudioRecordingManager { *self.is_recording.lock().unwrap() = false; + // Hide the overlay + self.hide_overlay(); + // In on-demand mode turn the mic off again if matches!(*self.mode.lock().unwrap(), MicrophoneMode::OnDemand) { self.stop_microphone_stream(); @@ -258,4 +262,54 @@ impl AudioRecordingManager { _ => None, } } + + /* ---------- overlay window -------------------------------------------- */ + + fn show_overlay(&self) { + if let Some(overlay_window) = self.app_handle.get_webview_window("recording_overlay") { + let _ = overlay_window.show(); + let _ = overlay_window.set_focus(); + } else { + // Get screen dimensions for positioning + if let Ok(monitors) = self.app_handle.primary_monitor() { + if let Some(monitor) = monitors { + let size = monitor.size(); + let scale = monitor.scale_factor(); + let screen_width = (size.width as f64 / scale) as i32; + let screen_height = (size.height as f64 / scale) as i32; + + // Position at bottom center, 30px from bottom + let x = (screen_width - 150) / 2; + let y = screen_height - 50 - 30; + + match WebviewWindowBuilder::new(&self.app_handle, "recording_overlay") + .title("Recording") + .position(x as f64, y as f64) + .resizable(false) + .maximizable(false) + .minimizable(false) + .closable(false) + .decorations(false) + .always_on_top(true) + .skip_taskbar(true) + .transparent(true) + .build() + { + Ok(window) => { + debug!("Recording overlay window created successfully"); + } + Err(e) => { + debug!("Failed to create recording overlay window: {}", e); + } + } + } + } + } + } + + fn hide_overlay(&self) { + if let Some(overlay_window) = self.app_handle.get_webview_window("recording_overlay") { + let _ = overlay_window.hide(); + } + } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5a0f2eb..7d82073 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -10,6 +10,7 @@ "frontendDist": "../dist" }, "app": { + "macOSPrivateApi": true, "windows": [ { "title": "Handy", diff --git a/src/overlay/RecordingOverlay.css b/src/overlay/RecordingOverlay.css new file mode 100644 index 0000000..f66ff07 --- /dev/null +++ b/src/overlay/RecordingOverlay.css @@ -0,0 +1,38 @@ +.recording-overlay { + width: 150px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.7); + border-radius: 8px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.bars-container { + display: flex; + align-items: end; + justify-content: center; + gap: 3px; + height: 30px; +} + +.bar { + width: 4px; + background: linear-gradient(to top, #FAA2CA, #F28CBB); + border-radius: 2px; + animation: pulse infinite ease-in-out; + min-height: 4px; +} + +@keyframes pulse { + 0%, 100% { + height: 4px; + opacity: 0.6; + } + 50% { + height: 30px; + opacity: 1; + } +} diff --git a/src/overlay/RecordingOverlay.tsx b/src/overlay/RecordingOverlay.tsx new file mode 100644 index 0000000..a7629eb --- /dev/null +++ b/src/overlay/RecordingOverlay.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './RecordingOverlay.css'; + +const RecordingOverlay: React.FC = () => { + return ( +
+
+ {Array.from({ length: 8 }, (_, i) => ( +
+ ))} +
+
+ ); +}; + +export default RecordingOverlay; diff --git a/src/overlay/index.html b/src/overlay/index.html new file mode 100644 index 0000000..08db593 --- /dev/null +++ b/src/overlay/index.html @@ -0,0 +1,19 @@ + + + + + Recording Overlay + + + +
+ + + diff --git a/src/overlay/main.tsx b/src/overlay/main.tsx new file mode 100644 index 0000000..76053f3 --- /dev/null +++ b/src/overlay/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import RecordingOverlay from "./RecordingOverlay"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/vite.config.ts b/vite.config.ts index 8c63787..4f5869c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import { resolve } from "path"; // @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; @@ -9,6 +10,16 @@ const host = process.env.TAURI_DEV_HOST; export default defineConfig(async () => ({ plugins: [react(), tailwindcss()], + // Multiple entry points for main app and overlay + build: { + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + overlay: resolve(__dirname, "src/overlay/index.html"), + }, + }, + }, + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent vite from obscuring rust errors