From 8359e4df45e9ce6cc0ffd288baa43f7701a8b184 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Fri, 8 May 2026 18:53:18 -0400 Subject: [PATCH] feat: live overlay wiring and backend click-through sync hooks Co-Authored-By: Claude Opus 4.7 (1M context) --- src/hooks/useClickThroughSync.ts | 48 +++++++++++++++++++++++++++++++ src/hooks/useLiveOverlayWiring.ts | 34 ++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/hooks/useClickThroughSync.ts create mode 100644 src/hooks/useLiveOverlayWiring.ts diff --git a/src/hooks/useClickThroughSync.ts b/src/hooks/useClickThroughSync.ts new file mode 100644 index 0000000..38fbdeb --- /dev/null +++ b/src/hooks/useClickThroughSync.ts @@ -0,0 +1,48 @@ +import { useEffect } from "react"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { useOverlayStore } from "../lib/store"; +import { applyOpacity } from "../lib/overlay"; +import { + EVT_CLICK_THROUGH_NO_OVERLAY, + EVT_CLICK_THROUGH_TOGGLED, + type ClickThroughToggledPayload, +} from "../types/overlay"; + +/** Subscribes to backend events about click-through state and toast about no-overlay attempts. */ +export function useClickThroughSync( + onNoOverlay: () => void +): void { + useEffect(() => { + let unlistenToggled: UnlistenFn | undefined; + let unlistenNoOverlay: UnlistenFn | undefined; + let cancelled = false; + + (async () => { + unlistenToggled = await listen( + EVT_CLICK_THROUGH_TOGGLED, + (e) => { + if (cancelled) return; + useOverlayStore.getState()._setClickThroughFromBackend( + e.payload.clickThrough + ); + // Restore the user's target opacity. Rust performed the dip-half of + // the pulse and emitted this event after sleeping ~180ms; we own + // the restore. Reading from the live store guarantees we restore + // to whatever the slider says *now*, not whatever it said when the + // hotkey was pressed. + void applyOpacity(useOverlayStore.getState().opacity); + } + ); + unlistenNoOverlay = await listen(EVT_CLICK_THROUGH_NO_OVERLAY, () => { + if (cancelled) return; + onNoOverlay(); + }); + })().catch((err) => console.warn("event listen failed", err)); + + return () => { + cancelled = true; + unlistenToggled?.(); + unlistenNoOverlay?.(); + }; + }, [onNoOverlay]); +} diff --git a/src/hooks/useLiveOverlayWiring.ts b/src/hooks/useLiveOverlayWiring.ts new file mode 100644 index 0000000..f44791b --- /dev/null +++ b/src/hooks/useLiveOverlayWiring.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef } from "react"; +import { useOverlayStore } from "../lib/store"; +import { + applyAlwaysOnTop, + applyClickThrough, + applyOpacity, +} from "../lib/overlay"; + +/** Pushes Zustand changes to the overlay window in real time. No-op when overlay is closed. */ +export function useLiveOverlayWiring(): void { + const opacity = useOverlayStore((s) => s.opacity); + const alwaysOnTop = useOverlayStore((s) => s.alwaysOnTop); + const clickThrough = useOverlayStore((s) => s.clickThrough); + const isOpen = useOverlayStore((s) => s.isOpen); + + const lastAppliedClickThrough = useRef(null); + + useEffect(() => { + if (!isOpen) return; + void applyOpacity(opacity); + }, [opacity, isOpen]); + + useEffect(() => { + if (!isOpen) return; + void applyAlwaysOnTop(alwaysOnTop); + }, [alwaysOnTop, isOpen]); + + useEffect(() => { + if (!isOpen) return; + if (lastAppliedClickThrough.current === clickThrough) return; + lastAppliedClickThrough.current = clickThrough; + void applyClickThrough(clickThrough); + }, [clickThrough, isOpen]); +}