"use client"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { getCurrent } from "@tauri-apps/plugin-deep-link"; import { useCallback, useEffect, useRef, useState } from "react"; import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog"; import { ChangeVersionDialog } from "@/components/change-version-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; import { GroupAssignmentDialog } from "@/components/group-assignment-dialog"; import { GroupBadges } from "@/components/group-badges"; import { GroupManagementDialog } from "@/components/group-management-dialog"; import HomeHeader from "@/components/home-header"; import { ImportProfileDialog } from "@/components/import-profile-dialog"; import { PermissionDialog } from "@/components/permission-dialog"; import { ProfilesDataTable } from "@/components/profile-data-table"; import { ProfileSelectorDialog } from "@/components/profile-selector-dialog"; import { ProxyManagementDialog } from "@/components/proxy-management-dialog"; import { ProxySettingsDialog } from "@/components/proxy-settings-dialog"; import { SettingsDialog } from "@/components/settings-dialog"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications"; import type { PermissionType } from "@/hooks/use-permissions"; import { usePermissions } from "@/hooks/use-permissions"; import { useUpdateNotifications } from "@/hooks/use-update-notifications"; import { showErrorToast } from "@/lib/toast-utils"; import type { BrowserProfile, CamoufoxConfig, GroupWithCount } from "@/types"; type BrowserTypeString = | "mullvad-browser" | "firefox" | "firefox-developer" | "chromium" | "brave" | "zen" | "tor-browser" | "camoufox"; interface PendingUrl { id: string; url: string; } export default function Home() { const [profiles, setProfiles] = useState([]); const [error, setError] = useState(null); const [proxyDialogOpen, setProxyDialogOpen] = useState(false); const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false); const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const [settingsDialogOpen, setSettingsDialogOpen] = useState(false); const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false); const [proxyManagementDialogOpen, setProxyManagementDialogOpen] = useState(false); const [camoufoxConfigDialogOpen, setCamoufoxConfigDialogOpen] = useState(false); const [groupManagementDialogOpen, setGroupManagementDialogOpen] = useState(false); const [groupAssignmentDialogOpen, setGroupAssignmentDialogOpen] = useState(false); const [selectedGroupId, setSelectedGroupId] = useState("default"); const [selectedProfilesForGroup, setSelectedProfilesForGroup] = useState< string[] >([]); const [selectedProfiles, setSelectedProfiles] = useState([]); const [pendingUrls, setPendingUrls] = useState([]); const [currentProfileForProxy, setCurrentProfileForProxy] = useState(null); const [currentProfileForVersionChange, setCurrentProfileForVersionChange] = useState(null); const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] = useState(null); const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false); const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); const [groups, setGroups] = useState([]); const [areGroupsLoading, setGroupsLoading] = useState(true); const [currentPermissionType, setCurrentPermissionType] = useState("microphone"); const [showBulkDeleteConfirmation, setShowBulkDeleteConfirmation] = useState(false); const [isBulkDeleting, setIsBulkDeleting] = useState(false); const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } = usePermissions(); const handleSelectGroup = useCallback((groupId: string) => { setSelectedGroupId(groupId); setSelectedProfiles([]); }, []); // Check for missing binaries and offer to download them const checkMissingBinaries = useCallback(async () => { try { const missingBinaries = await invoke<[string, string, string][]>( "check_missing_binaries", ); if (missingBinaries.length > 0) { console.log("Found missing binaries:", missingBinaries); // Group missing binaries by browser type to avoid concurrent downloads const browserMap = new Map(); for (const [profileName, browser, version] of missingBinaries) { if (!browserMap.has(browser)) { browserMap.set(browser, []); } const versions = browserMap.get(browser); if (versions) { versions.push(`${version} (for ${profileName})`); } } // Show a toast notification about missing binaries and auto-download them const missingList = Array.from(browserMap.entries()) .map(([browser, versions]) => `${browser}: ${versions.join(", ")}`) .join(", "); console.log(`Downloading missing binaries: ${missingList}`); try { // Download missing binaries sequentially by browser type to prevent conflicts const downloaded = await invoke( "ensure_all_binaries_exist", ); if (downloaded.length > 0) { console.log( "Successfully downloaded missing binaries:", downloaded, ); } } catch (downloadError) { console.error("Failed to download missing binaries:", downloadError); setError( `Failed to download missing binaries: ${JSON.stringify( downloadError, )}`, ); } } } catch (err: unknown) { console.error("Failed to check missing binaries:", err); } }, []); // Simple profiles loader without updates check (for use as callback) const loadProfiles = useCallback(async () => { try { const profileList = await invoke( "list_browser_profiles", ); setProfiles(profileList); // Check for missing binaries after loading profiles await checkMissingBinaries(); } catch (err: unknown) { console.error("Failed to load profiles:", err); setError(`Failed to load profiles: ${JSON.stringify(err)}`); } }, [checkMissingBinaries]); const [processingUrls, setProcessingUrls] = useState>(new Set()); const handleUrlOpen = useCallback( async (url: string) => { // Prevent duplicate processing of the same URL if (processingUrls.has(url)) { console.log("URL already being processed:", url); return; } setProcessingUrls((prev) => new Set(prev).add(url)); try { console.log("URL received for opening:", url); // Always show profile selector for manual selection - never auto-open // Replace any existing pending URL with the new one setPendingUrls([{ id: Date.now().toString(), url }]); } finally { // Remove URL from processing set after a short delay to prevent rapid duplicates setTimeout(() => { setProcessingUrls((prev) => { const next = new Set(prev); next.delete(url); return next; }); }, 1000); } }, [processingUrls], ); // Auto-update functionality - use the existing hook for compatibility const updateNotifications = useUpdateNotifications(loadProfiles); const { checkForUpdates, isUpdating } = updateNotifications; // Profiles loader with update check (for initial load and manual refresh) const loadProfilesWithUpdateCheck = useCallback(async () => { try { const profileList = await invoke( "list_browser_profiles", ); setProfiles(profileList); // Check for updates after loading profiles await checkForUpdates(); await checkMissingBinaries(); } catch (err: unknown) { console.error("Failed to load profiles:", err); setError(`Failed to load profiles: ${JSON.stringify(err)}`); } }, [checkForUpdates, checkMissingBinaries]); useAppUpdateNotifications(); // Check for startup URLs but only process them once const [hasCheckedStartupUrl, setHasCheckedStartupUrl] = useState(false); const checkCurrentUrl = useCallback(async () => { if (hasCheckedStartupUrl) return; try { const currentUrl = await getCurrent(); if (currentUrl && currentUrl.length > 0) { console.log("Startup URL detected:", currentUrl[0]); void handleUrlOpen(currentUrl[0]); } } catch (error) { console.error("Failed to check current URL:", error); } finally { setHasCheckedStartupUrl(true); } }, [handleUrlOpen, hasCheckedStartupUrl]); const checkStartupPrompt = useCallback(async () => { // Only check once during app startup to prevent reopening after dismissing notifications if (hasCheckedStartupPrompt) return; try { const shouldShow = await invoke( "should_show_settings_on_startup", ); if (shouldShow) { setSettingsDialogOpen(true); } } catch (error) { console.error("Failed to check startup prompt:", error); } finally { setHasCheckedStartupPrompt(true); } }, [hasCheckedStartupPrompt]); const checkAllPermissions = useCallback(async () => { try { // Wait for permissions to be initialized before checking if (!isInitialized) { return; } // Check if any permissions are not granted - prioritize missing permissions if (!isMicrophoneAccessGranted) { setCurrentPermissionType("microphone"); setPermissionDialogOpen(true); } else if (!isCameraAccessGranted) { setCurrentPermissionType("camera"); setPermissionDialogOpen(true); } } catch (error) { console.error("Failed to check permissions:", error); } }, [isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized]); const checkNextPermission = useCallback(() => { try { if (!isMicrophoneAccessGranted) { setCurrentPermissionType("microphone"); setPermissionDialogOpen(true); } else if (!isCameraAccessGranted) { setCurrentPermissionType("camera"); setPermissionDialogOpen(true); } else { setPermissionDialogOpen(false); } } catch (error) { console.error("Failed to check next permission:", error); } }, [isMicrophoneAccessGranted, isCameraAccessGranted]); const listenForUrlEvents = useCallback(async () => { try { // Listen for URL open events from the deep link handler (when app is already running) await listen("url-open-request", (event) => { console.log("Received URL open request:", event.payload); void handleUrlOpen(event.payload); }); // Listen for show profile selector events await listen("show-profile-selector", (event) => { console.log("Received show profile selector request:", event.payload); void handleUrlOpen(event.payload); }); // Listen for show create profile dialog events await listen("show-create-profile-dialog", (event) => { console.log( "Received show create profile dialog request:", event.payload, ); setError( "No profiles available. Please create a profile first before opening URLs.", ); setCreateProfileDialogOpen(true); }); // Listen for custom logo click events const handleLogoUrlEvent = (event: CustomEvent) => { console.log("Received logo URL event:", event.detail); void handleUrlOpen(event.detail); }; window.addEventListener( "url-open-request", handleLogoUrlEvent as EventListener, ); // Return cleanup function return () => { window.removeEventListener( "url-open-request", handleLogoUrlEvent as EventListener, ); }; } catch (error) { console.error("Failed to setup URL listener:", error); } }, [handleUrlOpen]); const openProxyDialog = useCallback((profile: BrowserProfile | null) => { setCurrentProfileForProxy(profile); setProxyDialogOpen(true); }, []); const openChangeVersionDialog = useCallback((profile: BrowserProfile) => { setCurrentProfileForVersionChange(profile); setChangeVersionDialogOpen(true); }, []); const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => { setCurrentProfileForCamoufoxConfig(profile); setCamoufoxConfigDialogOpen(true); }, []); const handleSaveCamoufoxConfig = useCallback( async (profile: BrowserProfile, config: CamoufoxConfig) => { setError(null); try { await invoke("update_camoufox_config", { profileName: profile.name, config, }); await loadProfiles(); setCamoufoxConfigDialogOpen(false); } catch (err: unknown) { console.error("Failed to update camoufox config:", err); setError(`Failed to update camoufox config: ${JSON.stringify(err)}`); throw err; } }, [loadProfiles], ); const handleSaveProxy = useCallback( async (proxyId: string | null) => { setProxyDialogOpen(false); setError(null); try { if (currentProfileForProxy) { await invoke("update_profile_proxy", { profileName: currentProfileForProxy.name, proxyId: proxyId, }); } await loadProfiles(); // Trigger proxy data reload in the table } catch (err: unknown) { console.error("Failed to update proxy settings:", err); setError(`Failed to update proxy settings: ${JSON.stringify(err)}`); } }, [currentProfileForProxy, loadProfiles], ); const handleCreateProfile = useCallback( async (profileData: { name: string; browserStr: BrowserTypeString; version: string; releaseType: string; proxyId?: string; camoufoxConfig?: CamoufoxConfig; }) => { setError(null); try { const _profile = await invoke( "create_browser_profile_new", { name: profileData.name, browserStr: profileData.browserStr, version: profileData.version, releaseType: profileData.releaseType, proxyId: profileData.proxyId, camoufoxConfig: profileData.camoufoxConfig, }, ); await loadProfiles(); // Trigger proxy data reload in the table } catch (error) { setError( `Failed to create profile: ${ error instanceof Error ? error.message : String(error) }`, ); throw error; } }, [loadProfiles], ); const [runningProfiles, setRunningProfiles] = useState>( new Set(), ); const runningProfilesRef = useRef>(new Set()); const checkBrowserStatus = useCallback(async (profile: BrowserProfile) => { try { const isRunning = await invoke("check_browser_status", { profile, }); const currentRunning = runningProfilesRef.current.has(profile.name); if (isRunning !== currentRunning) { console.log( `Profile ${profile.name} (${profile.browser}) status changed: ${currentRunning} -> ${isRunning}`, ); setRunningProfiles((prev) => { const next = new Set(prev); if (isRunning) { next.add(profile.name); } else { next.delete(profile.name); } runningProfilesRef.current = next; return next; }); } } catch (err) { console.error("Failed to check browser status:", err); } }, []); const launchProfile = useCallback( async (profile: BrowserProfile) => { setError(null); // Check if browser is disabled due to ongoing update try { const isDisabled = await invoke( "is_browser_disabled_for_update", { browser: profile.browser, }, ); if (isDisabled || isUpdating(profile.browser)) { setError( `${profile.browser} is currently being updated. Please wait for the update to complete.`, ); return; } } catch (err) { console.error("Failed to check browser update status:", err); } try { const updatedProfile = await invoke( "launch_browser_profile", { profile }, ); await loadProfiles(); await checkBrowserStatus(updatedProfile); } catch (err: unknown) { console.error("Failed to launch browser:", err); setError(`Failed to launch browser: ${JSON.stringify(err)}`); } }, [loadProfiles, checkBrowserStatus, isUpdating], ); const handleDeleteProfile = useCallback( async (profile: BrowserProfile) => { setError(null); console.log("Attempting to delete profile:", profile.name); try { // First check if the browser is running for this profile const isRunning = await invoke("check_browser_status", { profile, }); if (isRunning) { setError( "Cannot delete profile while browser is running. Please stop the browser first.", ); return; } // Attempt to delete the profile await invoke("delete_profile", { profileName: profile.name }); console.log("Profile deletion command completed successfully"); // Give a small delay to ensure file system operations complete await new Promise((resolve) => setTimeout(resolve, 500)); // Reload profiles to ensure UI is updated await loadProfiles(); console.log("Profile deleted and profiles reloaded successfully"); } catch (err: unknown) { console.error("Failed to delete profile:", err); const errorMessage = err instanceof Error ? err.message : String(err); setError(`Failed to delete profile: ${errorMessage}`); } }, [loadProfiles], ); const handleRenameProfile = useCallback( async (oldName: string, newName: string) => { setError(null); try { await invoke("rename_profile", { oldName, newName }); await loadProfiles(); } catch (err: unknown) { console.error("Failed to rename profile:", err); setError(`Failed to rename profile: ${JSON.stringify(err)}`); throw err; } }, [loadProfiles], ); const handleKillProfile = useCallback( async (profile: BrowserProfile) => { setError(null); try { await invoke("kill_browser_profile", { profile }); await loadProfiles(); } catch (err: unknown) { console.error("Failed to kill browser:", err); setError(`Failed to kill browser: ${JSON.stringify(err)}`); } }, [loadProfiles], ); const loadGroups = useCallback(async () => { setGroupsLoading(true); try { const groupsWithCounts = await invoke( "get_groups_with_profile_counts", ); setGroups(groupsWithCounts); } catch (err) { console.error("Failed to load groups with counts:", err); setGroups([]); } finally { setGroupsLoading(false); } }, []); const handleDeleteSelectedProfiles = useCallback( async (profileNames: string[]) => { setError(null); try { await invoke("delete_selected_profiles", { profileNames }); await loadProfiles(); await loadGroups(); } catch (err: unknown) { console.error("Failed to delete selected profiles:", err); setError(`Failed to delete selected profiles: ${JSON.stringify(err)}`); } }, [loadProfiles, loadGroups], ); const handleAssignProfilesToGroup = useCallback((profileNames: string[]) => { setSelectedProfilesForGroup(profileNames); setGroupAssignmentDialogOpen(true); }, []); const handleBulkDelete = useCallback(() => { if (selectedProfiles.length === 0) return; setShowBulkDeleteConfirmation(true); }, [selectedProfiles]); const confirmBulkDelete = useCallback(async () => { if (selectedProfiles.length === 0) return; setIsBulkDeleting(true); try { await invoke("delete_selected_profiles", { profileNames: selectedProfiles, }); await loadProfiles(); setSelectedProfiles([]); setShowBulkDeleteConfirmation(false); } catch (error) { console.error("Failed to delete selected profiles:", error); setError(`Failed to delete selected profiles: ${JSON.stringify(error)}`); } finally { setIsBulkDeleting(false); } }, [selectedProfiles, loadProfiles]); const handleBulkGroupAssignment = useCallback(() => { if (selectedProfiles.length === 0) return; handleAssignProfilesToGroup(selectedProfiles); setSelectedProfiles([]); }, [selectedProfiles, handleAssignProfilesToGroup]); const handleGroupAssignmentComplete = useCallback(async () => { await loadProfiles(); await loadGroups(); setGroupAssignmentDialogOpen(false); setSelectedProfilesForGroup([]); }, [loadProfiles, loadGroups]); const handleGroupManagementComplete = useCallback(async () => { await loadGroups(); }, [loadGroups]); useEffect(() => { void loadProfilesWithUpdateCheck(); void loadGroups(); // Check for startup default browser prompt void checkStartupPrompt(); // Listen for URL open events and get cleanup function const setupListeners = async () => { const cleanup = await listenForUrlEvents(); return cleanup; }; let cleanup: (() => void) | undefined; setupListeners().then((cleanupFn) => { cleanup = cleanupFn; }); // Check for startup URLs (when app was launched as default browser) void checkCurrentUrl(); // Set up periodic update checks (every 30 minutes) const updateInterval = setInterval( () => { void checkForUpdates(); }, 30 * 60 * 1000, ); return () => { clearInterval(updateInterval); if (cleanup) { cleanup(); } }; }, [ loadProfilesWithUpdateCheck, checkForUpdates, checkStartupPrompt, listenForUrlEvents, checkCurrentUrl, loadGroups, ]); useEffect(() => { if (profiles.length === 0) return; const interval = setInterval(() => { for (const profile of profiles) { void checkBrowserStatus(profile); } }, 500); return () => { clearInterval(interval); }; }, [profiles, checkBrowserStatus]); useEffect(() => { runningProfilesRef.current = runningProfiles; }, [runningProfiles]); useEffect(() => { if (error) { showErrorToast(error); setError(null); } }, [error]); // Check permissions when they are initialized useEffect(() => { if (isInitialized) { void checkAllPermissions(); } }, [isInitialized, checkAllPermissions]); return (
{ setProxyDialogOpen(false); }} onSave={handleSaveProxy} initialProxyId={currentProfileForProxy?.proxy_id} browserType={currentProfileForProxy?.browser} /> { setCreateProfileDialogOpen(false); }} onCreateProfile={handleCreateProfile} /> { setSettingsDialogOpen(false); }} /> { setChangeVersionDialogOpen(false); }} profile={currentProfileForVersionChange} onVersionChanged={() => void loadProfiles()} /> { setImportProfileDialogOpen(false); }} onImportComplete={() => void loadProfiles()} /> { setProxyManagementDialogOpen(false); }} /> {pendingUrls.map((pendingUrl) => ( { setPendingUrls((prev) => prev.filter((u) => u.id !== pendingUrl.id), ); }} url={pendingUrl.url} isUpdating={isUpdating} runningProfiles={runningProfiles} /> ))} { setPermissionDialogOpen(false); }} permissionType={currentPermissionType} onPermissionGranted={checkNextPermission} /> { setCamoufoxConfigDialogOpen(false); }} profile={currentProfileForCamoufoxConfig} onSave={handleSaveCamoufoxConfig} /> { setGroupManagementDialogOpen(false); }} onGroupManagementComplete={handleGroupManagementComplete} /> { setGroupAssignmentDialogOpen(false); }} selectedProfiles={selectedProfilesForGroup} onAssignmentComplete={handleGroupAssignmentComplete} /> setShowBulkDeleteConfirmation(false)} onConfirm={confirmBulkDelete} title="Delete Selected Profiles" description={`This action cannot be undone. This will permanently delete ${selectedProfiles.length} profile${selectedProfiles.length !== 1 ? "s" : ""} and all associated data.`} confirmButtonText={`Delete ${selectedProfiles.length} Profile${selectedProfiles.length !== 1 ? "s" : ""}`} isLoading={isBulkDeleting} profileNames={selectedProfiles} />
); }