import { useEffect, useState, useRef } from "react"; import { toast, Toaster } from "sonner"; import { useTranslation } from "react-i18next"; import { listen } from "@tauri-apps/api/event"; import { platform } from "@tauri-apps/plugin-os"; import { checkAccessibilityPermission, checkMicrophonePermission, } from "tauri-plugin-macos-permissions-api"; import { ModelStateEvent, RecordingErrorEvent } from "./lib/types/events"; import "./App.css"; import AccessibilityPermissions from "./components/AccessibilityPermissions"; import Footer from "./components/footer"; import Onboarding, { AccessibilityOnboarding } from "./components/onboarding"; import { Sidebar, SidebarSection, SECTIONS_CONFIG } from "./components/Sidebar"; import { useSettings } from "./hooks/useSettings"; import { useSettingsStore } from "./stores/settingsStore"; import { commands } from "@/bindings"; import { getLanguageDirection, initializeRTL } from "@/lib/utils/rtl"; type OnboardingStep = "accessibility" | "model" | "done"; const renderSettingsContent = (section: SidebarSection) => { const ActiveComponent = SECTIONS_CONFIG[section]?.component || SECTIONS_CONFIG.general.component; return ; }; function App() { const { t, i18n } = useTranslation(); const [onboardingStep, setOnboardingStep] = useState( null, ); // Track if this is a returning user who just needs to grant permissions // (vs a new user who needs full onboarding including model selection) const [isReturningUser, setIsReturningUser] = useState(false); const [currentSection, setCurrentSection] = useState("general"); const { settings, updateSetting } = useSettings(); const direction = getLanguageDirection(i18n.language); const refreshAudioDevices = useSettingsStore( (state) => state.refreshAudioDevices, ); const refreshOutputDevices = useSettingsStore( (state) => state.refreshOutputDevices, ); const hasCompletedPostOnboardingInit = useRef(false); useEffect(() => { checkOnboardingStatus(); }, []); // Initialize RTL direction when language changes useEffect(() => { initializeRTL(i18n.language); }, [i18n.language]); // Initialize Enigo, shortcuts, and refresh audio devices when main app loads useEffect(() => { if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) { hasCompletedPostOnboardingInit.current = true; Promise.all([ commands.initializeEnigo(), commands.initializeShortcuts(), ]).catch((e) => { console.warn("Failed to initialize:", e); }); refreshAudioDevices(); refreshOutputDevices(); } }, [onboardingStep, refreshAudioDevices, refreshOutputDevices]); // Handle keyboard shortcuts for debug mode toggle useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // Check for Ctrl+Shift+D (Windows/Linux) or Cmd+Shift+D (macOS) const isDebugShortcut = event.shiftKey && event.key.toLowerCase() === "d" && (event.ctrlKey || event.metaKey); if (isDebugShortcut) { event.preventDefault(); const currentDebugMode = settings?.debug_mode ?? false; updateSetting("debug_mode", !currentDebugMode); } }; // Add event listener when component mounts document.addEventListener("keydown", handleKeyDown); // Cleanup event listener when component unmounts return () => { document.removeEventListener("keydown", handleKeyDown); }; }, [settings?.debug_mode, updateSetting]); // Listen for recording errors from the backend and show a toast useEffect(() => { const unlisten = listen("recording-error", (event) => { const { error_type, detail } = event.payload; if (error_type === "microphone_permission_denied") { const currentPlatform = platform(); const platformKey = `errors.micPermissionDenied.${currentPlatform}`; const description = t(platformKey, { defaultValue: t("errors.micPermissionDenied.generic"), }); toast.error(t("errors.micPermissionDeniedTitle"), { description }); } else { toast.error( t("errors.recordingFailed", { error: detail ?? "Unknown error" }), ); } }); return () => { unlisten.then((fn) => fn()); }; }, [t]); // Listen for model loading failures and show a toast useEffect(() => { const unlisten = listen("model-state-changed", (event) => { if (event.payload.event_type === "loading_failed") { toast.error( t("errors.modelLoadFailed", { model: event.payload.model_name || t("errors.modelLoadFailedUnknown"), }), { description: event.payload.error, }, ); } }); return () => { unlisten.then((fn) => fn()); }; }, [t]); const revealMainWindowForPermissions = async () => { try { await commands.showMainWindowCommand(); } catch (e) { console.warn("Failed to show main window for permission onboarding:", e); } }; const checkOnboardingStatus = async () => { try { // Check if they have any models available const result = await commands.hasAnyModelsAvailable(); const hasModels = result.status === "ok" && result.data; const currentPlatform = platform(); if (hasModels) { // Returning user - check if they need to grant permissions first setIsReturningUser(true); if (currentPlatform === "macos") { try { const [hasAccessibility, hasMicrophone] = await Promise.all([ checkAccessibilityPermission(), checkMicrophonePermission(), ]); if (!hasAccessibility || !hasMicrophone) { await revealMainWindowForPermissions(); setOnboardingStep("accessibility"); return; } } catch (e) { console.warn("Failed to check macOS permissions:", e); // If we can't check, proceed to main app and let them fix it there } } if (currentPlatform === "windows") { try { const microphoneStatus = await commands.getWindowsMicrophonePermissionStatus(); if ( microphoneStatus.supported && microphoneStatus.overall_access === "denied" ) { await revealMainWindowForPermissions(); setOnboardingStep("accessibility"); return; } } catch (e) { console.warn("Failed to check Windows microphone permissions:", e); // If we can't check, proceed to main app and let them fix it there } } setOnboardingStep("done"); } else { // New user - start full onboarding setIsReturningUser(false); setOnboardingStep("accessibility"); } } catch (error) { console.error("Failed to check onboarding status:", error); setOnboardingStep("accessibility"); } }; const handleAccessibilityComplete = () => { // Returning users already have models, skip to main app // New users need to select a model setOnboardingStep(isReturningUser ? "done" : "model"); }; const handleModelSelected = () => { // Transition to main app - user has started a download setOnboardingStep("done"); }; // Still checking onboarding status if (onboardingStep === null) { return null; } if (onboardingStep === "accessibility") { return ; } if (onboardingStep === "model") { return ; } return (
{/* Main content area that takes remaining space */}
{/* Scrollable content area */}
{renderSettingsContent(currentSection)}
{/* Fixed footer at bottom */}
); } export default App;