Handy/src/overlay/RecordingOverlay.tsx
Anton Sotkov 82d5c759ad
Add “Processing…” overlay state when post-processing (#740)
* feat: add “Processing…” overlay state

* format

* missing translations

---------

Co-authored-by: CJ Pais <cj@cjpais.com>
2026-02-09 10:36:00 +08:00

120 lines
3.6 KiB
TypeScript

import { listen } from "@tauri-apps/api/event";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
MicrophoneIcon,
TranscriptionIcon,
CancelIcon,
} from "../components/icons";
import "./RecordingOverlay.css";
import { commands } from "@/bindings";
import i18n, { syncLanguageFromSettings } from "@/i18n";
import { getLanguageDirection } from "@/lib/utils/rtl";
type OverlayState = "recording" | "transcribing" | "processing";
const RecordingOverlay: React.FC = () => {
const { t } = useTranslation();
const [isVisible, setIsVisible] = useState(false);
const [state, setState] = useState<OverlayState>("recording");
const [levels, setLevels] = useState<number[]>(Array(16).fill(0));
const smoothedLevelsRef = useRef<number[]>(Array(16).fill(0));
const direction = getLanguageDirection(i18n.language);
useEffect(() => {
const setupEventListeners = async () => {
// Listen for show-overlay event from Rust
const unlistenShow = await listen("show-overlay", async (event) => {
// Sync language from settings each time overlay is shown
await syncLanguageFromSettings();
const overlayState = event.payload as OverlayState;
setState(overlayState);
setIsVisible(true);
});
// Listen for hide-overlay event from Rust
const unlistenHide = await listen("hide-overlay", () => {
setIsVisible(false);
});
// Listen for mic-level updates
const unlistenLevel = await listen<number[]>("mic-level", (event) => {
const newLevels = event.payload as number[];
// Apply smoothing to reduce jitter
const smoothed = smoothedLevelsRef.current.map((prev, i) => {
const target = newLevels[i] || 0;
return prev * 0.7 + target * 0.3; // Smooth transition
});
smoothedLevelsRef.current = smoothed;
setLevels(smoothed.slice(0, 9));
});
// Cleanup function
return () => {
unlistenShow();
unlistenHide();
unlistenLevel();
};
};
setupEventListeners();
}, []);
const getIcon = () => {
if (state === "recording") {
return <MicrophoneIcon />;
} else {
return <TranscriptionIcon />;
}
};
return (
<div
dir={direction}
className={`recording-overlay ${isVisible ? "fade-in" : ""}`}
>
<div className="overlay-left">{getIcon()}</div>
<div className="overlay-middle">
{state === "recording" && (
<div className="bars-container">
{levels.map((v, i) => (
<div
key={i}
className="bar"
style={{
height: `${Math.min(20, 4 + Math.pow(v, 0.7) * 16)}px`, // Cap at 20px max height
transition: "height 60ms ease-out, opacity 120ms ease-out",
opacity: Math.max(0.2, v * 1.7), // Minimum opacity for visibility
}}
/>
))}
</div>
)}
{state === "transcribing" && (
<div className="transcribing-text">{t("overlay.transcribing")}</div>
)}
{state === "processing" && (
<div className="transcribing-text">{t("overlay.processing")}</div>
)}
</div>
<div className="overlay-right">
{state === "recording" && (
<div
className="cancel-button"
onClick={() => {
commands.cancelOperation();
}}
>
<CancelIcon />
</div>
)}
</div>
</div>
);
};
export default RecordingOverlay;