import { IconChevronRight, IconChevronDown, IconCircleCheck, IconCircleOff, IconRefresh, } from "@tabler/icons-react"; import { invoke } from "@tauri-apps/api/core"; import { useState, useCallback, useEffect } from "react"; import { isMacPlatform, isWindowsPlatform } from "../lib/platform"; type CaptureMode = "screen" | "screen-camera" | "camera"; type MacosPrivacyPane = | "camera" | "microphone" | "screen" | "speech" | "accessibility" | "input-monitoring"; type PermissionStatuses = { screen: boolean; camera: boolean; microphone: boolean; speech: boolean; accessibility: boolean; inputMonitoring: boolean; }; type ReadinessItem = { label: string; detail: string; pane: MacosPrivacyPane; active: boolean; macosOnly?: boolean; }; function readinessItems({ mode, cameraOn, micOn, includeFnMonitoring, includeVoicePaste, }: { mode: CaptureMode; cameraOn: boolean; micOn: boolean; includeFnMonitoring: boolean; includeVoicePaste: boolean; }): ReadinessItem[] { const mac = isMacPlatform(); const items: ReadinessItem[] = [ { label: "Screen Recording", detail: "Needed for screen or window capture.", pane: "screen", active: mode !== "camera", }, { label: "Microphone", detail: "Needed when the mic is on.", pane: "microphone", active: micOn, }, { label: "Speech Recognition", detail: "Used for native transcripts.", pane: "speech", active: micOn, macosOnly: true, }, { label: "Camera", detail: "Needed when camera is on.", pane: "camera", active: mode !== "screen" && cameraOn, }, { label: "Accessibility", detail: "Needed to paste dictated text into other apps.", pane: "accessibility", active: includeVoicePaste, macosOnly: true, }, { label: "Input Monitoring", detail: "Only needed for the Fn dictation shortcut.", pane: "input-monitoring", active: includeFnMonitoring, macosOnly: true, }, ]; return items.filter((item) => item.active && (!item.macosOnly || mac)); } function statusForPane( pane: MacosPrivacyPane, statuses: PermissionStatuses | null, ): boolean | null { if (!statuses) return null; const map: Record = { screen: statuses.screen, camera: statuses.camera, microphone: statuses.microphone, speech: statuses.speech, accessibility: statuses.accessibility, "input-monitoring": statuses.inputMonitoring, }; return map[pane]; } export function ReadinessPanel({ mode, cameraOn, micOn, includeFnMonitoring, includeVoicePaste, open, onOpenChange, onOpenPermission, }: { mode: CaptureMode; cameraOn: boolean; micOn: boolean; includeFnMonitoring: boolean; includeVoicePaste: boolean; open: boolean; onOpenChange: (open: boolean) => void; onOpenPermission: (pane: MacosPrivacyPane) => void; }) { const mac = isMacPlatform(); const canOpenPrivacySettings = mac || isWindowsPlatform(); const items = readinessItems({ mode, cameraOn, micOn, includeFnMonitoring, includeVoicePaste, }); const [statuses, setStatuses] = useState(null); const [checking, setChecking] = useState(false); const checkStatuses = useCallback(async () => { setChecking(true); try { const result = await invoke( "check_permission_statuses", ); setStatuses(result); } catch { // Non-macOS or command not available — leave statuses null } finally { setChecking(false); } }, []); const requestOrOpenPermission = useCallback( async (pane: MacosPrivacyPane) => { if (mac && pane === "screen") { try { const granted = await invoke( "system_audio_request_permission", ); await checkStatuses(); if (granted) return; } catch { // Fall through to the dedicated privacy pane. The request API may // be unavailable on an older macOS build or the user may already // have denied the prompt once. } } onOpenPermission(pane); }, [checkStatuses, mac, onOpenPermission], ); useEffect(() => { if (open && !statuses && mac) checkStatuses(); }, [open, statuses, mac, checkStatuses]); return (
{open ? (
{items.length ? ( items.map((item) => { const granted = statusForPane(item.pane, statuses); return (
{item.label} {item.detail}
{mac ? ( ) : null} {mac && granted !== null ? ( {granted ? ( ) : ( )} ) : null} {canOpenPrivacySettings ? ( ) : ( System prompt )}
); }) ) : (
Turn on camera or mic when you need them.
)}
) : null}
); }