This commit is contained in:
CJ Pais 2025-07-26 08:20:04 -07:00
parent 125d9b1c58
commit 8731881837
8 changed files with 157 additions and 2 deletions

View file

@ -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"

View file

@ -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();
}
}
}

View file

@ -10,6 +10,7 @@
"frontendDist": "../dist"
},
"app": {
"macOSPrivateApi": true,
"windows": [
{
"title": "Handy",

View file

@ -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;
}
}

View file

@ -0,0 +1,23 @@
import React from 'react';
import './RecordingOverlay.css';
const RecordingOverlay: React.FC = () => {
return (
<div className="recording-overlay">
<div className="bars-container">
{Array.from({ length: 8 }, (_, i) => (
<div
key={i}
className="bar"
style={{
animationDelay: `${i * 100}ms`,
animationDuration: `${800 + Math.random() * 400}ms`
}}
/>
))}
</div>
</div>
);
};
export default RecordingOverlay;

19
src/overlay/index.html Normal file
View file

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Recording Overlay</title>
<style>
html,
body {
margin: 0;
padding: 0;
background: transparent;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/overlay/main.tsx"></script>
</body>
</html>

9
src/overlay/main.tsx Normal file
View file

@ -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(
<React.StrictMode>
<RecordingOverlay />
</React.StrictMode>,
);

View file

@ -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